当前位置:   article > 正文

石子合并问题(动态规划)_石子合并问题动态规划

石子合并问题动态规划

石子合并问题是一个经典的动态规划问题,应用了最优子结构和重复子问题的思想。

有如下3种题型:

不加限制的合并

(1)有N堆石子,现要将石子有序的合并成一堆,规定如下:每次只能移动任意的2堆石子合并,合并花费为新合成的一堆石子的数量。求将这N堆石子合并成

(动态规划)O(n^3)
设dp[i][j]表示将i至j之间的石子合并成一堆的最小花费。
初始时,对于任意i,都有dp[i][i]=0,因为合并一堆石子不需要花费。
对于区间[i,j],枚举合并点k,则该区间合并的最小花费为: dp[i][k]+ dp[k+1][j]+sum[i][j],其中 sum[i][j]表示区间[i,j]中石子数量的和。最终答案即为dp[1][n]。

线性(相邻)合并问题

(2)有N堆石子,现要将石子有序的合并成一堆,规定如下:每次只能移动相邻的2堆石子合并,合并花费为新合成的一堆石子的数量。求将这N堆石子合并成一堆的总花费最小(或最大)。

具体思路如下:

  1. 确定状态

设dp[i][j]表示合并第i到j个石子的最小代价。

     2.确定状态转移方程

对于第i到第j个石子的合并,可以选择在任意一个位置k断开,将问题分成合并i到k之间的石子和合并k+1到j之间的石子两个子问题。

因此,可以得到状态转移方程:

dp[i][j] = min(dp[i][j], dp[i][k] + dp[k+1][j] + sum[i][j]) (i <= k < j)

其中sum[i][j]表示第i到第j个石子的重量和,即需要合并的代价。

      3.确定边界

当只有一个石子时,代价为0,因此dp[i][i] = 0。

      4.最终结果

最终的结果为dp[1][n],表示合并全部石子的最小代价。

  1. for (int len = 1; len < n; len++) // 区间长度
  2. {
  3. for (int i = 1; i + len <= n; i++) //区间起点
  4. {
  5. int j = i + len; //区间终点
  6. for (int k = i; k < j; k++)
  7. {
  8. sum[i][j] = sum[i][k] + sum[k + 1][j];
  9. dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j] + sum[i][j]);
  10. }
  11. }
  12. }

我的代码

我在下面的代码中,对sum数组进行了优化(前缀和优化),用s数组表示

  1. #include <iostream>
  2. #include <cstring>
  3. using namespace std;
  4. const int N = 310;
  5. int n;
  6. int a[N], s[N]; // s数组用于前缀和优化
  7. int f[N][N]; // f[i][j]表示合并第i~j堆石子的最小代价
  8. int main() {
  9. cin >> n;
  10. for (int i = 1; i <= n; i ++ )
  11. cin >> a[i];
  12. // 前缀和优化
  13. for (int i = 1; i <= n; i ++ )
  14. s[i] = s[i - 1] + a[i];
  15. memset(f, 0x3f, sizeof f); // 初值无穷大
  16. for (int i = 1; i <= n; i ++ )
  17. f[i][i] = 0; // 一堆石子不需要合并
  18. // 枚举区间长度
  19. for (int len = 2; len <= n; len ++ )
  20. {
  21. // 枚举区间起点
  22. for (int i = 1; i + len - 1 <= n; i ++ )
  23. {
  24. int j = i + len - 1; // 区间终点
  25. // 枚举划分位置
  26. for (int k = i; k < j; k ++ )
  27. {
  28. f[i][j] = min(f[i][j], f[i][k] + f[k + 1][j] + s[j] - s[i - 1]);
  29. }
  30. }
  31. }
  32. cout << f[1][n] << endl;
  33. return 0;
  34. }

环形合并

(3)问题(2)的是在石子排列是直线情况下的解法,如果把石子改为环形排列,又怎么做呢?

核心思想:

将环形转换为直线
通过将数量变为 2n来转换成直线问题。 比如数组a【1,2,3】,但是环形的要求是1也可以和3连上,所以我们可以把数组a当成 【1,2,3,1,2,3】。这样,我们就可以算出 【2,3,1】的,【3,1,2】的。

我的代码:

  1. #include <iostream>
  2. #include <cstring>
  3. using namespace std;
  4. const int N = 310;
  5. int n;
  6. int a[2*N+1], s[2*N+1]; // s数组用于前缀和优化
  7. int f[2*N+1][2*N+1]; // f[i][j]表示合并第i~j堆石子的最小代价
  8. int main() {
  9. cin >> n;
  10. for (int i = 1; i <= n; i ++ )
  11. cin >> a[i];
  12. for (int i = n + 1; i <= 2 * n; i++)
  13. a[i] = a[i - n];
  14. // 前缀和优化
  15. for (int i = 1; i <= 2 * n; i ++ )
  16. s[i] = s[i - 1] + a[i];
  17. memset(f, 0x3f, sizeof f); // 初值无穷大
  18. for (int i = 1; i <= 2 * n; i ++ )
  19. f[i][i] = 0; // 一堆石子不需要合并
  20. // 枚举区间长度
  21. for (int len = 2; len <= n; len ++ )
  22. {
  23. // 枚举区间起点
  24. for (int i = 1; i + len - 1 <= 2 * n; i ++ )
  25. {
  26. int j = i + len - 1; // 区间终点
  27. // 枚举划分位置
  28. for (int k = i; k < j; k ++ )
  29. {
  30. f[i][j] = min(f[i][j], f[i][k] + f[k + 1][j] + s[j] - s[i - 1]);
  31. }
  32. }
  33. }
  34. cout << f[1][n] << endl;
  35. return 0;
  36. }

通过上述的状态转移方程,可以将问题分解成两个子问题,并且可以通过最优子结构来推导出最终的结果。同时,由于每个子问题都有重复的子问题,因此可以通过动态规划算法来避免重复计算,提高算法效率。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/2023面试高手/article/detail/509936
推荐阅读
相关标签
  

闽ICP备14008679号