石子合并(一)
最基础的区间dp
有N
堆石子排成一排(n<=100
),现要将石子有次序地合并成一堆,规定每次只能选相邻的两堆合并成一堆,并将新的一堆的石子数,记为改次合并的得分,编一程序,由文件读入堆数n
及每堆石子数(<=200
);
选择一种合并石子的方案,使得做n-1
次合并,得分的总和最少/最多。
我们首先可以算出两两合并的得分, 通过两两合并的得分我们可以知道相邻三个合并的得分,然后相邻四个,五个······以此类推
举个栗子,对于 1 2 3 4 5,要把他们合并,假如我们已经知道相邻2个,3个,4个的得分,我们可以枚举两堆石子之间的断点k
dp[i][j] = min(dp[i][j],dp[i][k]+dp[k+1][j]+sum[j]-sum[i-1]); (sum为前缀和, dp[ i ] [ j ] 表示合并第 i 到 j 堆的最大的分)
这就是区间dp了,我们可以从小到大枚举区间长度,当然也可以枚举起点终点
#include<cstdio> #include<algorithm> #include<cstring> #include<iostream> using namespace std; const int maxn =6e3; const int inf = 0x7fffffff; int a[maxn], dp[maxn][maxn], sum[maxn], f[maxn][maxn]; int main(){ int n; scanf("%d", &n); for(int i=1; i<=n; i++){ scanf("%d", &a[i]); sum[i] = sum[i-1] + a[i];//维护一下前缀和便于计算得分 } for(int d=2; d<=n; d++){ //枚举区间长度 for(int i=1,j; (j=i+d-1)<=n; i++){//区间起点i, 终点j,且保证j不能超出范围 dp[i][j] = inf; //保存最小值,初始化 f[i][j] = 0; //保存最大值,初始化 for(int k=i; k<j; k++){//枚举 dp[i][j] = min(dp[i][j],dp[i][k]+dp[k+1][j]+sum[j]-sum[i-1]);//最大 f[i][j] = max(f[i][j],f[i][k]+f[k+1][j]+sum[j]-sum[i-1]);//最小 } } } printf("%d\n%d\n", dp[1][n],f[1][n]); return 0; }
石子合并(二)
在一个园形操场的四周摆放N
堆石子,现要将石子有次序地合并成一堆.规定每次只能选相邻的2
堆合并成新的一堆,并将新的一堆的石子数,记为该次合并的得分。
试设计出1
个算法,计算出将N
堆石子合并成1
堆的最小得分和最大得分.
比(一)多了一个条件:圆形操场
乍一想很复杂,其实我们可以把这个圆拆成一条链,链的长度为原本的二倍(也就是把(一)的一堆石子复制一下再接到后面)
然后按照(一)中的做法对这条链求长度为n的区间的最大得分,可以覆盖圆中的所有情况,也就是等效的
#include<cstdio> #include<algorithm> #include<cstring> #include<iostream> using namespace std; const int maxn =6e3; const int inf = 0x3f3f3f3f; int a[maxn], dp[maxn][maxn], sum[maxn], f[maxn][maxn]; int main(){ int n; scanf("%d", &n); for(int i=1; i<=n; i++){ scanf("%d", &a[i]); a[i+n] = a[i]; } for(int i=1; i<=n*2; i++){ sum[i] = sum[i-1] + a[i]; } int maxx = 0; int minn = inf; for(int i=2*n-1; i>=1; i--){ for(int j=i+1; j<i+n; j++){//这里我直接枚举了起点和终点 dp[i][j] = inf; for(int k=i; k<j; k++){ dp[i][j] = min(dp[i][j], dp[i][k]+dp[k+1][j]+sum[j] - sum[i-1]); f[i][j] = max(f[i][j], f[i][k]+f[k+1][j]+sum[j] - sum[i-1]); } } } for(int i=1; i<=n; i++){ minn = min(minn, dp[i][i+n-1]);//限制区间长度n maxx = max(maxx, f[i][i+n-1]);//并求最大/最小值 } printf("%d\n%d\n", minn, maxx); return 0; }
石子合并(三)
题目和(二)完全一致,但数据范围大得多,我们就要考虑时间复杂度了
可以用四边不等式优化,还有一种GarsiaWachs算法(强大又玄学,不会)
四边不等式
对于一个数组 f[][] , a < b <= c < d, 若它满足四边不等式,则
f[a][c]+f[b][d]<=f[b][c]+f[a][d]
设 w[ i ][ j ] 表示 i 到 j 的石子数,我们现在证明它满足四边不等式
令 b = a + k, d = c + k, 即 a < a+k <= c < c+k
我们证明 w[a][c] + w[a+k][c+k] <= w[a+k][c] + w[a][c+k];
移项,得 w[a][c] - w[a+k][c] <= w[a][c+k] = w[a+k][c+k];
问题转化成了证明函数f(j)= w[i][j] - w[i+1][j] 递增
然后再证 f[] 满足四边不等式(以后再写)
#include<cstdio> #include<algorithm> #include<cstring> #include<iostream> using namespace std; const int maxn =6e3; const int inf = 0x3f3f3f3f; int a[maxn], dp[maxn][maxn], sum[maxn], s[maxn][maxn]; int main(){ int n; scanf("%d", &n); for(int i=1; i<=n; i++){ scanf("%d", &a[i]); a[i+n] = a[i]; } for(int i=1; i<=n*2; i++){ sum[i] = sum[i-1] + a[i]; } for(int i=1; i<=n; i++){ s[i][i] = i; s[i+n][i+n] = i+n; } for(int d=2; d<=n; d++){ for(int i=1,j; (j=i+d-1)<=2*n-1; i++){ for(int k=s[i][j-1]; k<=s[i+1][j]; k++){ if(dp[i][j]<dp[i][k]+dp[k+1][j]+sum[j]-sum[i-1]){ dp[i][j] = max(dp[i+1][j],dp[i][j-1])+sum[j]-sum[i-1]; } } } } int maxx = 0; for(int i=1; i<=n; i++){ maxx = max(maxx, dp[i][i+n-1]); } printf("%d\n",maxx); return 0; }