您的位置:首页 > 其它

区间DP入门——石子合并问题

2017-05-03 15:58 281 查看
石子合并问题是最经典的DP问题。首先它有如下3种题型:



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

分析:当然这种情况是最简单的情况,合并的是任意两堆,直接贪心即可,每次选择最小的两堆合并。本问题实际上就是哈夫曼的变形。





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



 

分析:我们熟悉矩阵连乘,知道矩阵连乘也是每次合并相邻的两个矩阵,那么石子合并可以用矩阵连乘的方式来解决。

设dp[i][j]表示第i到第j堆石子合并的最优值,sum[i][j]表示第i到第j堆石子的总数量。那么就有状态转移公式:



[cpp] view
plain copy

 





#include <iostream>  

#include <string.h>  

#include <stdio.h>  

  

using namespace std;  

const int INF = 1 << 30;  

const int N = 205;  

  

int dp

;  

int sum
;  

int a
;  

  

int getMinval(int a[],int n)  

{  

    for(int i=0;i<n;i++)  

        dp[i][i] = 0;  

    for(int v=1;v<n;v++)  

    {  

        for(int i=0;i<n-v;i++)  

        {  

            int j = i + v;  

            dp[i][j] = INF;  

            int tmp = sum[j] - (i > 0 ? sum[i-1]:0);  

            for(int k=i;k<j;k++)  

                dp[i][j] = min(dp[i][j],dp[i][k]+dp[k+1][j] + tmp);  

        }  

    }  

    return dp[0][n-1];  

}  

  

int main()  

{  

    int n;  

    while(scanf("%d",&n)!=EOF)  

    {  

        for(int i=0;i<n;i++)  

            scanf("%d",&a[i]);  

        sum[0] = a[0];  

        for(int i=1;i<n;i++)  

            sum[i] = sum[i-1] + a[i];  

        printf("%d\n",getMinval(a,n));  

    }  

    return 0;  

}  

直线取石子问题的平行四边形优化:

[cpp] view
plain copy

 





#include <iostream>  

#include <string.h>  

#include <stdio.h>  

  

using namespace std;  

const int INF = 1 << 30;  

const int N = 1005;  

  

int dp

;  

int p

;  

int sum
;  

int n;  

  

int getMinval()  

{  

    for(int i=1; i<=n; i++)  

    {  

        dp[i][i] = 0;  

        p[i][i] = i;  

    }  

    for(int len=1; len<n; len++)  

    {  

        for(int i=1; i+len<=n; i++)  

        {  

            int end = i+len;  

            int tmp = INF;  

            int k = 0;  

            for(int j=p[i][end-1]; j<=p[i+1][end]; j++)  

            {  

                if(dp[i][j] + dp[j+1][end] + sum[end] - sum[i-1] < tmp)  

                {  

                    tmp = dp[i][j] + dp[j+1][end] + sum[end] - sum[i-1];  

                    k = j;  

                }  

            }  

            dp[i][end] = tmp;  

            p[i][end] = k;  

        }  

    }  

    return dp[1]
;  

}  

  

int main()  

{  

    while(scanf("%d",&n)!=EOF)  

    {  

        sum[0] = 0;  

        for(int i=1; i<=n; i++)  

        {  

            int val;  

            scanf("%d",&val);  

            sum[i] = sum[i-1] + val;  

        }  

        printf("%d\n",getMinval());  

    }  

    return 0;  

}  

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

分析:状态转移方程为:



其中有:



[cpp] view
plain copy

 





#include <iostream>  

#include <string.h>  

#include <stdio.h>  

  

using namespace std;  

const int INF = 1 << 30;  

const int N = 205;  

  

int mins

;  

int maxs

;  

int sum
,a
;  

int minval,maxval;  

int n;  

  

int getsum(int i,int j)  

{  

    if(i+j >= n) return getsum(i,n-i-1) + getsum(0,(i+j)%n);  

    else return sum[i+j] - (i>0 ? sum[i-1]:0);  

}  

  

void Work(int a[],int n)  

{  

    for(int i=0;i<n;i++)  

        mins[i][0] = maxs[i][0] = 0;  

    for(int j=1;j<n;j++)  

    {  

        for(int i=0;i<n;i++)  

        {  

            mins[i][j] = INF;  

            maxs[i][j] = 0;  

            for(int k=0;k<j;k++)  

            {  

                mins[i][j] = min(mins[i][j],mins[i][k] + mins[(i+k+1)%n][j-k-1] + getsum(i,j));  

                maxs[i][j] = max(maxs[i][j],maxs[i][k] + maxs[(i+k+1)%n][j-k-1] + getsum(i,j));  

            }  

        }  

    }  

    minval = mins[0][n-1];  

    maxval = maxs[0][n-1];  

    for(int i=0;i<n;i++)  

    {  

        minval = min(minval,mins[i][n-1]);  

        maxval = max(maxval,maxs[i][n-1]);  

    }  

}  

  

int main()  

{  

    while(scanf("%d",&n)!=EOF)  

    {  

        for(int i=0;i<n;i++)  

            scanf("%d",&a[i]);  

        sum[0] = a[0];  

        for(int i=1;i<n;i++)  

            sum[i] = sum[i-1] + a[i];  

        Work(a,n);  

        printf("%d %d\n",minval,maxval);  

    }  

    return 0;  

}  

可以看出,上面的(2)(3)问题的时间复杂度都是O(n^3),由于过程满足平行四边形法则,故可以进一步优化到O(n^2)。

对于石子合并问题,有一个最好的算法,那就是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
是正无穷的。





举个例子:
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
等于正无穷大)234 186,最后得到420。最后的答案呢?就是各次合并的重量之和,即420+234+131+67=852。


基本思想是通过树的最优性得到一个节点间深度的约束,之后证明操作一次之后的解可以和原来的解一一对应,并保证节点移动之后他所在的深度不会改变。具体实现这个算法需要一点技巧,精髓在于不停快速寻找最小的k,即维护一个“2-递减序列”朴素的实现的时间复杂度是O(n*n),但可以用一个平衡树来优化,使得最终复杂度为O(nlogn)。


题目:http://poj.org/problem?id=1738

[cpp] view
plain copy

 





#include <iostream>  

#include <string.h>  

#include <stdio.h>  

  

using namespace std;  

const int N = 50005;  

  

int stone
;  

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;  

}  

本人的:

/*

用T[i][j]代表从i到j连续的所有石堆合并后的整体 i<=j

dp[i][j]代表形成T[i][j]所需要的最小花费 

i==j时:

dp[i][j] = 0;因为不需要合并 因为dp数组是全局所以可以省去

i<j时:

T[i][j]肯定是由某两个【相邻】子堆组合而成 假设这个分界点是k,那么这两个子堆是T[i,k]和T[k+1][j],其中i<=k<j;

T[i][j]最小总花费是由两部分组成:

1) T[i][k]和T[k+1][j]各自的最小花费之和所形成的集合里的最小值(用k=i...j-1循环求最小和)

2) 组合T[i,k]和T[k+1][j]成为T[i][j]的动作需要的花费,其实就是这两个堆的总重量,也就是T[i][j]的重量,记录为w[i][j]

得到那么得到方程:dp[i][j] = min{dp[i][k]+dp[k+1][j]}+w[i][j];

w[i][j]一种简便求法:

我们使用w[i]来存储w[1]~w[i]所有堆重量之和

那么w[i][j]就是 w[j] - w[i-1];

自底向上构造:

注意到

所有长度为2的T[i][j]需要使用所有长度为1的T[i][j];

所有长度为3的T[i][j]需要使用所有长度为1/2的T[i][j];

所有长度为4的T[i][j]需要使用所有长度为1/2/3/4的T[i][j];

.....

所有长度为n的T[i][j]需要使用所有长度为1/2/3.....n-2/n-1的T[i][j];

用l代表长度,l->2~(n);i,j代表某一段的左右坐标

dp[i][j]代表形成T[i][j]所需要的最小花费 原问题dp[1]

复杂度O(n^3)

*/
#include <iostream>
#include <algorithm>
#include <climits>
using namespace std;
int w[105];
int dp[105][105];
int main()
{
int n;
cin>>n;
for(int i= 1;i<=n;i++)
{
cin>>w[i];
w[i]+=w[i-1];
}
for(int l = 2;l<=n;l++)
for(int i= 1;i<n;i++)
{
int j = i+l-1;
int amin = INT_MAX;
for(int k = i;k<j;k++)
{
amin = min(amin,dp[i][k]+dp[k+1][j]);
}
dp[i][j] = amin + w[j]-w[i-1];\
}
cout<<dp[1]
<<endl;
}
/**
#include<iostream>
using namespace std;
int N;//石子的堆数
int num[100]={0};//每堆石子个数

int sum(int begin,int n)
{
int total=0;
for (int i=begin;i<=begin+n-1;i++)
{ if(i==N)
total=total+num
;//取代num[0]
else
total=total+num[i%N];
}
return total;
}
int stone_merge()
{
int score[100][100];//score[i][j]:从第i堆石子开始的j堆石子合并后最小得分
int n,i,k,temp;
for (i=1;i<=N;i++)
score[i][1]=0;//一堆石子,合并得分为0

//num[0]=num
;//重要:sum()函数中i=N时,取num[0]
for (n=2;n<=N;n++)//合并的石子的堆数
{
for (i=1;i<=N;i++)//合并起始位置
{
score[i]
=score[i][1]+score[(i+1-1)%N+1][n-1];
for (k=2;k<=n-1;k++)//截断位置
{
temp=score[i][k]+score[(i+k-1)%N+1][n-k];
if(temp <score[i]
)
score[i]
= temp;//从第i开始的k堆是:第i+0堆到第(i+k-1)%N堆
}
score[i]
+=sum(i,n);
}
}
int min=2147483647;
for (i=1;i<=N;i++)
{ if (min>score[i]
)
min=score[i]
;//取从第i堆开始的N堆的最小者
}
return min;
}

int main()
{
int min_count=0;
cin>>N;//石子的堆数
for (int i=1;i<=N;i++)
cin>>num[i];//每堆石子的数量//从1开始,num[0]不用
min_count=stone_merge();
cout<<min_count<<endl;
return 0;
}
**/

#include<stdio.h>
int N;//最多100堆石子:N=100
int num[200]={0};
int max=-0x3f3f3f3f;
int stone_merge()
{
int score[200][101]={0};//l[i][j]:从第i堆石子起合并n堆石子的最小得分
int score2[200][101]={0};
int n,i,k,temp,t2;
for(i=0;i<2*N;i++)
{
score[i][1]=0;//一堆石子合并得分为0
score2[i][1]=0;
}
for(n=2;n<=N;n++)//合并n堆石子
{
for(i=0;i<=2*N-n;i++)//从第i对开始合并(有一次重复运算,但省去了循环取数,简化了程序)
{
score[i]
=score[i][1]+score[i+1][n-1];
score2[i]
=score2[i][1]+score2[i+1][n-1];
for(k=2;k<n;k++)//划分
{ temp=score[i][k]+score[k+i][n-k];
t2=score2[i][k]+score2[k+i][n-k];
if(temp<score[i]
)
score[i]
=temp;//取(i,n)划分两部分的得分
if(t2>score2[i]
)
score2[i]
=t2;
}
for(k=i;k<i+n;k++)
{
score[i]
+=num[k];//加上此次合并得分
score2[i]
+=num[k];
}
}
}
int min=2147483647;//int(4位)最大值为2147483647
for(i=0;i<N;i++)
{
if(score[i]
<min)
min=score[i]
;//从第i堆开始取N堆石子,的最小合并得分
if(score2[i]
>max)
max=score2[i]
;
}
return min;
}

int main()
{
int min_count;
scanf("%d",&N);//N堆石子
for(int i=0;i<N;i++)
scanf("%d",&num[i]);//每堆石子的数量
for(int i=N;i<2*N;i++)
num[i]=num[i-N];//复制一倍,化简环形计算(N堆石子是围成一个环的)
if(N==1) min_count=0;
else if(N==2) min_count=num[0]+num[1];
else min_count=stone_merge();
printf("%d\n",min_count);
printf("%d\n",max);
return 0;
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: