目录
1. 合并石子
https://www.nowcoder.com/questionTerminal/6d3ccbc5b6ad4f12b8fe4c97eaf969e0
有 N 堆金币排成一排,第 i 堆中有 C[i] 块金币。每次合并都会将相邻的两堆金币合并为一堆,成本为这两堆金币块数之和。经过N-1次合并,最终将所有金币合并为一堆。请找出将金币合并为一堆的最低成本。
其中,1 <= N <= 30,1 <= C[i] <= 100
输入描述:
第一行输入一个数字 N 表示有 N 堆金币
第二行输入 N 个数字表示每堆金币的数量 C[i]
输出描述:
输出一个数字 S 表示最小的合并成一堆的成本
输入
4 3 2 4 1
输出
20
经典区间DP问题。如果用 dp[i][j] 表示将 [i...j] 的石子合并成一堆的最小成本,最后要求的就是 dp[0][N-1]。显然固有成本是 [i ... j] 的数量和,因为不管怎么移动每一堆的数量至少都要计算一次,能优化的就是选择哪一堆作为分界点,然后将左右两边分别合并的决策。所以有状态转移方程
观察状态转移方程, 发现dp[i][j] 同时依赖于左边和下边的值,因此不能通过常规的遍历方式来填表。注意到 dp[i][k] 和 dp[k+1][j]的区间长度都小于 dp[i][j],所以应当使用区间长度作为DP的阶段,对每一个 i ,根据区间长度len 得到对应的右端点 j = i + len - 1。 len 从 2 遍历到 N。
#include <bits/stdc++.h>
using namespace std;
int main()
{
int n;
scanf("%d", &n);
vector<int> c(n);
vector<int> sum(n + 1, 0);
// [i...j]的和:sum[j+1] - sum[i]
for(int i = 0; i < n; i++)
{
scanf("%d", &c[i]);
sum[i+1] = c[i] + sum[i];
}
vector<vector<int>> dp(n, vector<int>(n, 1e9));
// dp[i][j]: 将[i...j]进行合并,最低成本
// dp[i][j] = min{dp[i][k] + dp[k+1][j]} + sum[i..j], i <= k < j;
for (int i = 0; i < n; ++i)
{
dp[i][i] = 0;
}
for (int len = 2; len <= n; ++len)
{
for (int i = 0; i < n; ++i)
{
int j = i + len - 1;
if(j > n - 1) break;
for (int k = i; k < j; ++k)
{
dp[i][j] = min(dp[i][k]+dp[k+1][j] + sum[j+1] - sum[i], dp[i][j]);
}
}
}
printf("%d\n", dp[0][n-1]);
return 0;
}
把合并的决策过程(也就是填表的过程)打印出来,可能更方便理解:
-----len = 2-----
dp[0][1] = dp[0][0] + dp[1][1] + 5 = 5
dp[0][1] = 5
dp[1][2] = dp[1][1] + dp[2][2] + 6 = 6
dp[1][2] = 6
dp[2][3] = dp[2][2] + dp[3][3] + 5 = 5
dp[2][3] = 5
-----len = 3-----
dp[0][2] = dp[0][0] + dp[1][2] + 9 = 15
dp[0][2] = dp[0][1] + dp[2][2] + 9 = 14
dp[0][2] = 14
dp[1][3] = dp[1][1] + dp[2][3] + 7 = 12
dp[1][3] = dp[1][2] + dp[3][3] + 7 = 13
dp[1][3] = 12
-----len = 4-----
dp[0][3] = dp[0][0] + dp[1][3] + 10 = 22
dp[0][3] = dp[0][1] + dp[2][3] + 10 = 20
dp[0][3] = dp[0][2] + dp[3][3] + 10 = 24
dp[0][3] = 20
相同的题目:1547. 切棍子的最小成本
2. 合并石子(环形)
将 n堆石子绕圆形操场排放,现要将石子有序地合并成一堆。规定每次只能选相邻的两堆合并成新的一堆,并将新的一堆的石子数记做该次合并的得分。
请编写一个程序,读入堆数n(1 <= n <= 200) 及每堆的石子数,并进行如下计算:
- 选择一种合并石子的方案,使得做 n-1次合并得分总和最大。
- 选择一种合并石子的方案,使得做 n-1次合并得分总和最小
输出共两行:
第一行为合并得分总和最小值,
第二行为合并得分总和最大值。
样例输入
4 4 5 9 4
样例输出
43 54
几乎与第一题合并金币一模一样,不过这里是环形,显然要变成链形才方便处理。在每个点都可以断开,所以有n种断开的方法,如果枚举断开的端点,再依次采用第一题的做法的话,复杂度是O(n^4),不是很合适。处理环的一个简单方法是,将相同的数组复制一份接在后面,在这个长度为2n的链形数组里使用第一题的解法,找到合并[i...j]的最大得分dp[i][j] (0 <= i < j <= 2*n-1),复杂度是O(n^3)。之后再找到每一个长度是n的(i,j)对,dp[i][j]的最大值就是所求的环形数组能得到的最大值。最小值同理。
#include <bits/stdc++.h>
using namespace std;
int main()
{
int n;
scanf("%d", &n);
vector<int> c(2*n), sum(2*n+1, 0);
for (int i = 0; i < n; ++i)
{
scanf("%d", &c[i]);
c[i + n] = c[i];
}
n*= 2;
for(int i = 0; i < n; i++) sum[i+1] = sum[i] + c[i];
// sum:前缀和, [i...j]的总和 sum[j+1] - sum[i];
vector<vector<int>> mindp(n, vector<int>(n, 1e9));
vector<vector<int>> maxdp(n, vector<int>(n, -1e9));
// dp[i][j]: 合并[i...j]的最大(小)得分
// dp[i][j] = max{dp[i][k] + dp[k+1][j] + sum(i...j)}, i <= k < j;
for (int i = 0; i < n; ++i)
{
mindp[i][i] = maxdp[i][i] = 0;
}
for (int len = 2; len < n; ++len)
{
for (int i = 0; i < n; ++i)
{
int j = i + len - 1;
if(j > n - 1) break;
for (int k = i; k < j; ++k)
{
mindp[i][j] = min(mindp[i][j], mindp[i][k] + mindp[k+1][j]);
maxdp[i][j] = max(maxdp[i][j], maxdp[i][k] + maxdp[k+1][j]);
}
mindp[i][j] += sum[j+1] - sum[i];
maxdp[i][j] += sum[j+1] - sum[i];
}
}
//环形数组,对所有长度是n的(i, j)对进行遍历,找到最大和最小值
int minval = INT32_MAX, maxval = INT32_MIN;
n /= 2;
for (int i = 0; i < n; ++i)
{
int j = i + n - 1;
minval = min(minval, mindp[i][j]);
maxval = max(maxval, maxdp[i][j]);
}
printf("%d\n%d\n", minval, maxval);
return 0;
}
3. 矩阵链乘
给定n个矩阵:A1,A2,...,An,其中Ai与Ai+1是可乘的,i=1,2...,n-1。确定计算矩阵连乘积的计算次序,使得依此次序计算矩阵连乘积需要的数乘次数最少。
矩阵链乘是算法导论里的经典DP问题,也是区间DP问题。
首先有这样一个事实:Ai 的列数应该等于Ai+1 的行数。那么,如果用表示Ai的列数,Ai+1的行数也是。
计算A[ i:j ]的数乘次数,可以先找一个分割点k,左边的数乘次数是A[ i:k ]的最小数乘次数,右边的数乘次数是A[ k+1:j ]的最小数乘次数。最后剩下两个矩阵:左边剩下的矩阵的行数是Ai的行数(也就是 Ai-1的列数),列数是Ak的列数;右边剩下的矩阵行数是Ak+1的行数(也就是Ak的列数),列数是Aj的列数。那么这两个矩阵相乘的数乘次数是 Ai-1的列数 * Ak的列数 * Aj的列数 = 。
那么,状态转移方程就很明显了:
和上面的问题一样,也是从len = 2...N进行枚举,对每个 0 <= i < N 枚举右端点j = i + len - 1。 最后得到dp[0][N-1]就是最少的数乘次数。
下面是一个套壳的环形矩阵链乘问题:
题目描述很长,大意如下:
有一串珠子,每个珠子上有两个数,记为head和tail。前一颗珠子的tail等于后一颗珠子的head。如果将相邻的两颗珠子合并,将会得到 a.head * a.tail * (a+1).tail 的得分,求出将所有珠子合并能得到的最大得分。
首先考虑链形的问题,其实可以把每个珠子看成 head * tail 的矩阵,合并两个珠子的得分也就是 两个矩阵相乘的数乘次数。那么对于链形问题来说,这道题就是给出了每个矩阵的规模,求出最大的数乘次数。
转移方程:
其中 p[i] 存放的应该是矩阵 i 的列大小(第i颗珠子的tail),也就是第i+1颗珠子的 head,因为题目是按顺序给出 head值,因此实际上应该采用head[i+1]进行计算。
解决了链形问题,环形问题的处理方法就和第二题一样,复制相同的head数组接在后面,做规模是2n * 2n的DP矩阵,之后,找到长度是n的(i, j)对中,dp[i][j]的最大值。
#include <bits/stdc++.h>
using namespace std;
int main()
{
int n;
scanf("%d", &n);
vector<int> head(2*n);
for (int i = 0; i < n; ++i)
{
scanf("%d", &head[i]);
head[i+n] = head[i];
}
vector<vector<int>> dp(2*n, vector<int>(2*n, 0));
// dp[i][j]: [i...j]释放的最大能量,看成矩阵乘法,head[i]是第i个矩阵的行,tail[i]是第i个矩阵的列
// dp[i][j] = max(dp[i][k] + dp[k+1][j] + head[i] * tail[k] * tail[j]) i <= k < j
// tail[k] = head[k+1], tail[j] = head[j+1];
for (int len = 2; len <= n; ++len)
{
for (int i = 0; i + len - 1< 2*n - 1; ++i)