首页 > 代码库 > 区间dp总结

区间dp总结

最经典的一个区间dp问题是矩阵链乘问题,算导和一些算法书上都有介绍,

给出N个矩阵和他们的规格,满足相邻的矩阵都能合法的进行矩阵乘法的运算,我们定义一个(a*b)和一个(b*c)的矩阵做乘法,乘法次数为b*b*a*c

求解最少的能将所有矩阵乘在一起的次数。

第一次见这个问题是cj同学随手拍给我的一道题,当时就感觉像以前写过的一道区间dp,然后纸上推了半天最后成功过了样例!

我们把要求解的这个问题看做是f(1,N),最后的一次操作肯定会剩下两个矩阵然后乘一起,我们不妨枚举一下这个分割点,

会发现 f(i,j)=MIN{f[i][k]+f[k+1][j]+r[i]*w[k]*r[k+1]*w[j]},方程的意义就是将矩阵从第k个分开,只要我们知道前k个和后(N-k)个矩阵相乘的最优次数,此时得最优次数也能得出。我们还发现这个转移方程的依赖关系不是像一些线性的只有知道前面才能推导后面,而是由长度较小的区间慢慢的推导出长度较大的区间,最后得出答案,这也是区间dp的奥妙所在。

 与之类似的问题有经典的石子合并问题:  http://acm.nyist.net/JudgeOnline/problem.php?pid=737

这个和上面的就是同一种做法,dp[i][j]=MIN{dp[i][k]+dp[k+1][j]+sum(i,j)}

 1  
 2 #include<bits/stdc++.h>
 3 using namespace std;
 4 #define inf 0x3f3f3f3f
 5 int a[205],dp[205][205];
 6 int sum[205][205];
 7 int SUM(int s,int e)
 8 {
 9     if(sum[s][e]!=-1) return sum[s][e];
10     int sumn=0,i;
11     for(i=s;i<=e;++i) sumn+=a[i];
12     return sum[s][e]=sumn;
13 }
14 int main()
15 {
16     int n,m,i,j,k;
17     while(cin>>n){memset(dp,inf,sizeof(dp));memset(sum,-1,sizeof(sum));
18         for(i=1;i<=n;++i) scanf("%d",&a[i]),dp[i][i]=0;
19         for(k=2;k<=n;++k)
20             for(i=1;i+k-1<=n;++i){
21     int minn=inf;
22     for(j=i;j<=i+k-1;++j)  if(dp[i][j]+dp[j+1][i+k-1]<minn) minn=dp[i][j]+dp[j+1][i+k-1];//j表示分割点
23     dp[i][i+k-1]=minn+SUM(i,i+k-1);
24             }
25         cout<<dp[1][n]<<endl;
26     }
27     return 0;
28 }
29 
30 
31  

有些问题会在此类问题的基础上做一些变化,可能会限制区间大小或是限制区间的起始点,处理起来可能没那么容易。

http://www.lydsy.com/JudgeOnline/problem.php?id=1996

上面就是一道区间dp的问题,是不是没那么推导方程了,题目说的看起来好像也很复杂。

我们还是从区间来看待问题,大问题的解就是[1,N]之间初始队形的方案数,如何由小区间推导呢,如果单纯的用dp[i][j]表示[i,j]之间的方案数得话,由于我们不知道

最后一个元素前面是哪个元素推导起来似乎没那么容易。比如目标序列是(1,2,4,3),在知道(1,2,4)的初始方案中,只有末尾是1和2的才能顺利推出目标序列,

通过观察我们发现最后一个元素的位置只会出现在头部或者尾部,不妨多加一维表示他的位置,这样方程就可以写出来了,

dp[i][j][0]=MIN{ dp[i][j-1][1] | a[j]>a[j-1]  ,   dp[i][j-1][0]  | a[j]>a[i]}

dp[i][j][1]=MIN{ dp[i+1][j][1] | a[i]<a[j]    ,   dp[i+1][j][0] | a[i]<a[i+1]}

注意只有一个元素的区间 dp[i][i][0]和dp[i][i][1]只要有一个是1就好了,不然会重复。

 1 #include<bits/stdc++.h>
 2 using namespace std;
 3 #define LL long long
 4 #define mod 19650827
 5 int dp[1005][1005][2];
 6 int a[1005];
 7 int f(int l,int r,int x)
 8 {
 9     if(dp[l][r][x]!=-1) return dp[l][r][x];
10     if(l==r){
11         if(x){
12           return dp[l][r][x]=1;
13         }
14         else{
15           return dp[l][r][x]=0;
16         }
17     }
18     int res=0;
19     if(x){
20         if(a[r]>a[r-1]) res=(res+f(l,r-1,1));
21         if(a[r]>a[l])   res=(res+f(l,r-1,0));
22     }
23     else{
24         if(a[l]<a[r]) res=(res+f(l+1,r,1));
25         if(a[l]<a[l+1]) res=(res+f(l+1,r,0));
26     }
27     return dp[l][r][x]=res%mod;
28 }
29 int main()
30 {
31     int N,i,j,k,s;
32     scanf("%d",&N);
33     for(i=1;i<=N;++i) scanf("%d",a+i);
34     memset(dp,-1,sizeof(dp));
35     printf("%d\n",(f(1,N,1)+f(1,N,0))%mod);
36     return 0;
37 }

 

还有一道与这个类似的是 http://acm.nyist.net/JudgeOnline/problem.php?pid=304

一道HN省赛题目,第一次没做出来,后来补上的。

注意这道题限制了起点V,必须从这个点开始走,每一次的决策无非就是往左或是往右,只要路过灯一定会关,因为这不消耗时间,

我们想知道[1,N]的灯全部关闭最小消耗的能源,如果我们知道了[1,N-1],和[2,N]的最小花费,是不是简单了许多呢,我们就从这里入手,

接着会发现即使知道了那两个子区间,可是并不知道此时机器人是在区间的左端右端还是中间啊,没法计算,我们不妨就令机器人每次都停在左端点或者右端点,

这样dp[i][j][0]和dp[i][j][1]就分别表示出来了上面的两种状态,知道了关闭区间利用前缀和求出来开启区间的单位花费在乘上距离就能推导出新的状态,具体细节留给大家思考,附上我的代码。

 1 #include<bits/stdc++.h>
 2 using namespace std;
 3 #define inf 0x3f3f3f3f
 4 int dp[1005][1005][2];
 5 int pre[1005]={0};
 6 int main()
 7 {
 8     //freopen("in.txt","r",stdin);
 9     int N,i,j,k,s;
10     int D[1005],W[1005],V;
11     while(cin>>N){
12         scanf("%d",&V);
13         for(i=1;i<=N;++i)
14         {
15           scanf("%d%d",D+i,W+i);
16           pre[i]=pre[i-1]+W[i];
17         }
18         memset(dp,inf,sizeof(dp));
19         dp[V][V][0]=dp[V][V][1]=0;
20         for(int len=2;len<=N;++len)
21         {
22             for(i=1,j=len;j<=N;++i,++j)
23             {
24                 dp[i][j][0]=min(dp[i+1][j][0]+(D[i+1]-D[i])*(pre[N]+pre[i]-pre[j]),dp[i+1][j][1]+(D[j]-D[i])*(pre[N]+pre[i]-pre[j]));
25                 dp[i][j][1]=min(dp[i][j-1][0]+(D[j]-D[i])*(pre[N]+pre[i-1]-pre[j-1]),dp[i][j-1][1]+(D[j]-D[j-1])*((pre[N]+pre[i-1]-pre[j-1])));
26             }
27 
28         }
29         printf("%d\n",min(dp[1][N][0],dp[1][N][1]));
30     }
31     return 0;
32 }

区间dp还有一种优化的方法叫做四边形不等式优化,具体证明参考集训队论文 https://wenku.baidu.com/view/60f88d40336c1eb91a375d88.html

只要w满足 w(i,j)+w(i1,j1)<=w(i1,j)+w(i,j1)  (i<=i1<=j<=j1)  称w满足四边形不等式

当w  满足   w(i1,j)<=w(i,j1)  (i<=i1<=j<=j1) 称w关于区间包含关系单调

设k=s(i,j)为w(i,j)中最优解的分割点,只要w满足上式,则有   s(i,j-1)<=s(i,j)<=s(i+1,j),可以降低一维的复杂度,减小了k的选择范围。

大家可以在一些区间dp里尝试使用这个优化技巧,但注意不一定题目中的w满足四边形不等式。

优化的题目参见这两篇博文, http://www.cnblogs.com/zzqc/p/7298025.html   http://www.cnblogs.com/zzqc/p/7300949.html

简言之,区间dp能解决的问题就是通过小区间更新大区间,最后得出指定区间的最优解,这只是初学了点皮毛,日后若发现更神秘的东西再来记下。

区间dp总结