动态规划中的石子归并问题
一.有N堆石子,每堆的重量是w[i],可以任意选两堆合并,每次合并的花费为w[i]+w[j],问把所有石子合并成为一堆后的最小花费是多少。
因为是可以任意合并,所以每次合并的时候选最小的两堆合并,贪心即可。
二.有N堆石子,每堆的重量是a[i],排成一条直线,每次只能合并相邻的两堆,直到合成一堆为止,问最后的最小花费是多少。
分析:因为规定了只能合并相邻的两堆,显然不能使用贪心法。
分成子问题来考虑,定义dp[i][j]表示从第i的石子合并到第j个石子的最小花费,那么dp[1][N]就是问题的解。
可以推出dp[i][j] = min(dp[i][k]+dp[k+1][j]) k∈(i,j)
初始时dp[i][j] = INF(i!=j) dp[i][i] = INF
1 #include <iostream> 2 #include <cstring> 3 #include <cstdio> 4 #include <string> 5 #include <algorithm> 6 using namespace std; 7 int T, n; 8 int a[210], dp[210][210], sum[210], s[210][210]; 9 //这里是数据量比较小 10 int INF = 99999999; 11 int main(){ 12 while(scanf("%d", &n) != EOF){ 13 memset(sum, 0, sizeof(sum)); 14 for(int i = 1; i <= n; i++){ 15 cin>>a[i]; 16 sum[i] = sum[i-1] + a[i]; 17 } 18 for(int i = 1; i <= n; i++){ 19 for(int j = 1; j <= n; j++) 20 dp[i][j] = INF; 21 } 22 for(int i = 1; i <= n; i++) dp[i][i] = 0; 23 24 for(int len = 2; len <= n; len++){ //表示归并的长度 25 for(int i = 1; i <= n-len+1; i++){ //归并的第一位 26 int j = i+len-1; //归并的最后一位 27 for(int k = i; k < j; k++){ 28 dp[i][j] = min(dp[i][j], dp[i][k] + dp[k+1][j] + sum[j] - sum[i-1]); 29 } 30 } 31 } 32 cout<<dp[1][n]<<endl; 33 } 34 return 0; 35 }
这样复杂度是O(N^3)
可以利用四边形不等式优化到O(N^2)
四边形不等式: 如果对于任意的a≤b≤c≤d,有
m[a,c] + m[b,d] <= m[a,d] + m[b,c]
那么m[i,j]满足四边形不等式。
对于转移方程形如下形式的动态规划问题:m[i,j] = opt{m[i,k] + m[k,j] + w[i,j]} (其中w[i,j]是m的附属量)
首先证明w满足四边形不等式,然后再证明m满足四边形不等式。最后证明s[i,j-1] ≤ s[i,j] ≤ s[i+1,j]这条性质来优化转移变量k的枚举量。
1 #include <iostream> 2 #include <cstring> 3 #include <cstdio> 4 #include <string> 5 #include <algorithm> 6 using namespace std; 7 int T, n; 8 int a[210], dp[210][210], sum[210], s[210][210]; 9 //这里是数据量比较小 10 //时间复杂度是N^3 11 int INF = 99999999; 12 int main(){ 13 while(scanf("%d", &n) != EOF){ 14 memset(sum, 0, sizeof(sum)); 15 memset(s,0,sizeof(s)); 16 for(int i = 1; i <= n; i++){ 17 cin>>a[i]; 18 s[i][i] = i; //只有一个的时候k当然等于i和j 19 sum[i] = sum[i-1] + a[i]; 20 } 21 for(int i = 1; i <= n; i++){ 22 for(int j = 1; j <= n; j++) 23 dp[i][j] = INF; 24 } 25 for(int i = 1; i <= n; i++) dp[i][i] = 0; 26 27 for(int len = 2; len <= n; len++){ //表示归并的长度 28 for(int i = 1; i <= n-len+1; i++){ //归并的第一位 29 int j = i+len-1; //归并的最后一位 30 for(int k = s[i][j-1]; k <= s[i+1][j]; k++){ 31 if(dp[i][j] > dp[i][k] + dp[k+1][j] + sum[j] - sum[i-1]){ 32 dp[i][j] = dp[i][k] + dp[k+1][j] + sum[j] - sum[i-1]; 33 s[i][j] = k; 34 } 35 } 36 37 } 38 } 39 cout<<dp[1][n]<<endl; 40 } 41 return 0; 42 }
三.环形石子归并问题
参考:http://www.cnblogs.com/SCAU_que/articles/1893979.html
在一个圆形操场的四周摆放着n 堆石子。现要将石子有次序地合并成一堆。
规定每次只能选相邻的2 堆石子合并成新的一堆,并将新的一堆石子数记为该次合并的得分。
试设计一个算法,计算出将n堆石子合并成一堆的最小得分和最大得分。
求最小的分和最大得分类似,下面只求最小得分。
分析:这种情况下可以看成两个直线的石子进行合并,但是需要修改一下dp的定义
dp[i][j]表示的是从i堆石子开始合并j堆的得分。
这样状态转移方程就是dp[i][j] = min(dp[i][k] + dp[(i+k-1)%n+1][j-k] + sum[i][j]) 这里取余的时候需要注意一下
sum[i][j]也表示从第i堆石子加后面j堆石子的总量。
网上没有找到题目,以上面的题目变成环形的情况写了一个代码
1 #include <iostream> 2 #include <cstring> 3 #include <cstdio> 4 #include <string> 5 #include <algorithm> 6 using namespace std; 7 #define maxn 210 8 #define INF 99999999 9 int a[maxn], dp[maxn][maxn], sum[maxn][maxn]; 10 int T, n; 11 int main(){ 12 scanf("%d", &T); 13 while(T--){ 14 scanf("%d", &n); 15 for(int i = 1;i <= n; i++){ 16 scanf("%d", &a[i]); 17 } 18 memset(sum, 0, sizeof(sum)); 19 for(int i = 1; i <= n; i++){ 20 for(int j = 1; j <= n; j++){ 21 sum[i][j] = sum[i][j-1] + a[(i+j-2)%n+1]; 22 } 23 } 24 for(int i = 0; i <= n; i++){ 25 for(int j = 0; j <= n; j++){ 26 dp[i][j] = INF; 27 } 28 } 29 for(int i = 1; i <= n; i++) dp[i][1] = 0; 30 for(int j = 2; j <= n; j++){ 31 for(int i = 1; i <= n; i++){ 32 for(int k = 1; k < j; k++){ 33 dp[i][j] = min(dp[i][j], dp[i][k] + dp[(i+k-1)%n+1][j-k] + sum[i][j]); 34 } 35 } 36 } 37 int ans = INF; 38 for(int i = 1; i <= n; i++){ 39 if(dp[i][n] <= ans) ans = dp[i][n]; 40 } 41 printf("%d\n", ans); 42 } 43 return 0; 44 }
http://poj.org/problem?id=1179 这道题数据量很大,还没有写,标记一下。