DP专题八–石子合并
任取石子
有N堆石子,现要将石子有序的合并成一堆,规定如下:每次只能移动任意的2堆石子合并,合并花费为新合成的一堆石子的数量。求将这N堆石子合并成一堆的总花费
最小(或最大)。
分析
当然这种情况是最简单的情况,合并的是任意两堆,直接贪心即可,每次选择最小的两堆合并。本问题实际上就是哈夫曼的变形。
蓝桥杯–合并石子
参考90分代码(未优化)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| using namespace std; int dp[maxn][maxn]; int n; int a[maxn],sum[maxn]; void get_min_value() { for(int r=2;r<=n;r++) { for(int i=1;i<=n-r+1;i++) { int j=i+r-1; int temp=sum[j]-sum[i-1]; dp[i][j]=dp[i+1][j]+temp; cout<<"dp["<<i<<"]["<<j<<"] = "<<dp[i][j]<<endl; for(int k=i+1;k<j;k++) { int ans=dp[i][k]+dp[k+1][j]+temp; if(ans<dp[i][j]) dp[i][j]=ans; cout<<"dp["<<i<<"]["<<j<<"] = "<<dp[i][j]<<endl; } } } } int main() { freopen("input.txt","r",stdin); while(cin>>n) { for(int i=1;i<=n;i++) cin>>a[i]; sum[0]=0; for(int i=1;i<=n;i++) sum[i]=sum[i-1]+a[i]; for(int i=1;i<=n;i++) dp[i][i]=0; get_min_value(); cout<<dp[1][n]<<endl; } return 0; }
|
参考AC代码(四边形不等式优化)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| using namespace std; int dp[maxn][maxn]; int s[maxn][maxn]; int n; int a[maxn],sum[maxn]; void get_min_value() { for(int r=2;r<=n;r++) { for(int i=1;i<=n-r+1;i++) { int j=i+r-1; int temp=sum[j]-sum[i-1]; dp[i][j]=dp[i+1][j]+temp; s[i][j]=i; /*cout<<"dp["<<i<<"]["<<j<<"] = "<<dp[i][j]<<endl;*/ for(int k=s[i][j-1];k<=s[i+1][j];k++) { int ans=dp[i][k]+dp[k+1][j]+temp; if(ans<dp[i][j]) { dp[i][j]=ans; s[i][j]=k; } /*cout<<"dp["<<i<<"]["<<j<<"] = "<<dp[i][j]<<endl;*/ } } } } int main() { /*freopen("input.txt","r",stdin);*/ while(cin>>n) { for(int i=1;i<=n;i++) cin>>a[i]; sum[0]=0; for(int i=1;i<=n;i++) sum[i]=sum[i-1]+a[i]; for(int i=1;i<=n;i++) { dp[i][i]=0; s[i][i]=i; } get_min_value(); cout<<dp[1][n]<<endl; } return 0; }
|
思路
我们熟悉矩阵连乘,知道矩阵连乘也是每次合并相邻的两个矩阵,那么石子合并可以用矩阵连乘的方式来解决。
设dp[i][j]表示第i到第j堆石子合并的最优值,sum[i][j]表示第i到第j堆石子的总数量。那么就有状态转移公式:
四边形不等式优化从k的取值从i-j-1优化到s[i][j-1]-s[i+1][j].
POJ 1738–GarsiaWachs算法
Problem Description
Input
Output
Sample Input
Sample Output
参考AC代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| using namespace std; const int N = 50005; int stone[N]; int n,t,ans; void combine(int k) { int tmp = stone[k] + stone[k-1]; ans += tmp; for(int i=k;i<t-1;i++) stone[i] = stone[i+1]; t--; int j = 0; for(j=k-1;j>0 && stone[j-1] < tmp;j--) stone[j] = stone[j-1]; stone[j] = tmp; while(j >= 2 && stone[j] >= stone[j-2]) { int d = t - j; combine(j-1); j = t - d; } } int main() { while(scanf("%d",&n)!=EOF) { if(n == 0) break; for(int i=0;i<n;i++) scanf("%d",stone+i); t = 1; ans = 0; for(int i=1;i<n;i++) { stone[t++] = stone[i]; while(t >= 3 && stone[t-3] <= stone[t-1]) combine(t-2); } while(t > 1) combine(t-1); printf("%d\n",ans); } return 0; }
|
思路
对于石子合并问题,有一个最好的算法,那就是GarsiaWachs算法。时间复杂度为O(n^2)。
它的步骤如下:
设序列是stone[],从左往右,找一个满足stone[k-1] <= stone[k+1]的k,找到后合并stone[k]和stone[k-1],再从当前位置开始向左找最大的j,使其满足
stone[j] > stone[k]+stone[k-1],插到j的后面就行。一直重复,直到只剩下一堆石子就可以了。在这个过程中,可以假设stone[-1]和stone[n]是正无穷的。
举个例子:
186 64 35 32 103
因为35<103,所以最小的k是3,我们先把35和32删除,得到他们的和67,并向前寻找一个第一个超过67的数,把67插入到他后面,得到:186 67 64 103,现在由5个
数变为4个数了,继续:186 131 103,现在k=2(别忘了,设A[-1]和A[n]等于正无穷大)234 186,最后得到420。最后的答案呢?就是各次合并的重量之和,即
420+234+131+67=852。
基本思想是通过树的最优性得到一个节点间深度的约束,之后证明操作一次之后的解可以和原来的解一一对应,并保证节点移动之后他所在的深度不会改变。具体
实现这个算法需要一点技巧,精髓在于不停快速寻找最小的k,即维护一个“2-递减序列”朴素的实现的时间复杂度是O(n*n),但可以用一个平衡树来优化,使得
最终复杂度为O(nlogn)。
HDU 3506–环形石子合并
Problem Description
Input
Output
Sample Input
Sample Output
参考AC代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| using namespace std; int dp[maxn][maxn],a[maxn],sum[maxn],s[maxn][maxn]; int n; void get_min_value() { for(int r=2;r<=n;r++) { for(int i=1;i<=2*n-r+1;i++) { int j=i+r-1; int temp=sum[j]-sum[i-1]; dp[i][j]=dp[i+1][j]+temp; s[i][j]=i; /*cout<<"dp["<<i<<"]["<<j<<"] = "<<dp[i][j]<<endl;*/ for(int k=s[i][j-1];k<=s[i+1][j];k++) { int ans=dp[i][k]+dp[k+1][j]+temp; if(ans<dp[i][j]) { dp[i][j]=ans; s[i][j]=k; } /*cout<<"dp["<<i<<"]["<<j<<"] = "<<dp[i][j]<<endl;*/ } } } int ans=INT_MAX; for(int i=1;i<=n;i++) ans=min(ans,dp[i][i+n-1]); cout<<ans<<endl; } int main() { /*freopen("input.txt","r",stdin);*/ while(cin>>n) { sum[0]=0; for(int i=1;i<=n;i++) cin>>a[i]; for(int i=1;i<=n;i++) sum[i]=sum[i-1]+a[i]; for(int i=n+1;i<=(2*n);i++) a[i]=a[i-n]; for(int i=n+1;i<=(2*n);i++) sum[i]=sum[i-1]+a[i]; for(int i=1;i<=(2*n);i++) { dp[i][i]=0; s[i][i]=i; } get_min_value(); } return 0; }
|
思路
与直线型类似 只需把直线扩大两倍即可变为环形
注意更新a和sum数组即可
同意需要注意函数里的j取值为2×n-r+1 非n