从寿司拼盘到环形赛道:一文吃透区间与环形动态规划

你是否曾在自助餐的寿司台前纠结,怎样才能用最短的时间夹走所有想吃的寿司?或是在环形赛道上,思考如何规划路线才能最快跑完全程?这些生活中的小困惑,其实藏着计算机科学中重要的算法思想 ——区间与环形动态规划。今天,就让我们化身算法大厨,用代码翻炒出美味的解题思路!​

一、开胃小菜:动态规划基础回顾​

动态规划(Dynamic Programming,简称 DP)就像一份 “烹饪秘籍”,它把复杂的问题分解成一个个小任务,每个小任务的结果可以被重复利用,避免了重复劳动。比如计算斐波那契数列,我们可以通过记录前面计算的结果,快速得出后面的数值,而不是每次都从头开始计算。​

动态规划有三个核心要素:​

  1. 状态定义:把问题拆解成可描述的状态,比如斐波那契数列中第 n 项的值就是一个状态。​
  1. 状态转移方程:描述如何从已知状态推导出新状态,如F(n) = F(n-1) + F(n-2)。​
  1. 边界条件:问题最基础的起点,比如F(0) = 0,F(1) = 1。​

二、主菜登场:区间动态规划详解​

1. 什么是区间动态规划?​

想象你面前有一排寿司,编号从 1 到 n。你想知道从第 i 个寿司到第 j 个寿司(i <= j)的最佳组合方式,这就是区间动态规划的典型场景。它的核心思想是将问题划分为若干个区间,通过合并小区间的解来得到大区间的解。​

2. 状态定义与转移方程​

  • 状态定义:dp[i][j]表示从区间[i, j]内元素得到的最优解。​
  • 状态转移方程:dp[i][j] = min/max{dp[i][k] + dp[k+1][j] + cost(i, j)},其中k是区间[i, j]内的一个分割点,cost(i, j)是合并区间[i, k]和[k+1, j]的额外代价。​

3. 经典例题:石子合并​

题目描述:有 n 堆石子排成一排,每堆石子有一个重量。每次合并相邻两堆石子,合并的代价为两堆石子重量之和。求将所有石子合并成一堆的最小总代价。​

解题思路:​

  1. 状态定义:dp[i][j]表示将区间[i, j]内的石子合并成一堆的最小代价。​
  1. 状态转移方程:dp[i][j] = min{dp[i][k] + dp[k+1][j] + sum[i][j]},其中sum[i][j]是区间[i, j]内石子的总重量,k从i遍历到j-1。​
  1. 边界条件:dp[i][i] = 0,因为只有一堆石子时,合并代价为 0。​

C++ 代码实现:​

#include 
#include 
#include 
using namespace std;

const int INF = 1e9;

int main() {
    int n;
    cin >> n;
    vector stones(n);
    vector sum(n + 1, 0);
    for (int i = 0; i < n; ++i) {
        cin >> stones[i];
        sum[i + 1] = sum[i] + stones[i];
    }

    vector> dp(n, vector(n, INF));
    for (int i = 0; i < n; ++i) {
        dp[i][i] = 0;
    }

    for (int len = 2; len <= n; ++len) {
        for (int i = 0; i + len - 1 < n; ++i) {
            int j = i + len - 1;
            for (int k = i; k < j; ++k) {
                dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j] + sum[j + 1] - sum[i]);
            }
        }
    }

    cout << dp[0][n - 1] << endl;
    return 0;
}

三、甜品时间:环形动态规划进阶​

1. 环形动态规划的独特之处​

环形动态规划与区间动态规划类似,但问题场景从线性的 “一排寿司” 变成了环形的 “寿司拼盘”。由于首尾相连,我们需要额外处理边界情况,通常的做法是将环形问题展开成线性问题。​

2. 经典例题:环形石子合并​

题目描述:有 n 堆石子围成一圈,每堆石子有一个重量。每次合并相邻两堆石子,合并的代价为两堆石子重量之和。求将所有石子合并成一堆的最小总代价。​

解题思路:​

  1. 展开环形:将长度为 n 的环形石子数组复制一份,拼接在原数组后面,形成长度为 2n 的线性数组。​
  1. 区间动态规划:对新数组进行区间动态规划,计算所有长度为 n 的区间的最小合并代价。​
  1. 取最小值:在所有长度为 n 的区间中,找到最小的合并代价。​

C++ 代码实现:​

#include 
#include 
#include 
using namespace std;

const int INF = 1e9;

int main() {
    int n;
    cin >> n;
    vector stones(n);
    vector sum(2 * n + 1, 0);
    for (int i = 0; i < n; ++i) {
        cin >> stones[i];
        stones[i + n] = stones[i];
    }
    for (int i = 1; i <= 2 * n; ++i) {
        sum[i] = sum[i - 1] + stones[i - 1];
    }

    vector> dp(2 * n, vector(2 * n, INF));
    for (int i = 0; i < 2 * n; ++i) {
        dp[i][i] = 0;
    }

    for (int len = 2; len <= n; ++len) {
        for (int i = 0; i + len - 1 < 2 * n; ++i) {
            int j = i + len - 1;
            for (int k = i; k < j; ++k) {
                dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j] + sum[j + 1] - sum[i]);
            }
        }
    }

    int ans = INF;
    for (int i = 0; i < n; ++i) {
        ans = min(ans, dp[i][i + n - 1]);
    }

    cout << ans << endl;
    return 0;
}

四、餐后总结:动态规划的魅力​

区间与环形动态规划通过巧妙的状态定义和转移方程,将复杂问题化繁为简。无论是解决寿司拼盘的最优组合,还是环形赛道的最短路径,这些算法思想都能帮助我们找到最优解。下次遇到类似问题时,不妨试着用动态规划的 “烹饪秘籍”,炒出属于你的精彩答案!​

希望这篇文章能让你对区间与环形动态规划有更深刻的理解。如果你在实践中遇到有趣的题目,或者对代码有更好的优化思路,欢迎在评论区分享,让我们一起把算法玩出花样!​

你可能感兴趣的:(动态规划,动态规划,算法)