区间dp
Posted stungyep
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了区间dp相关的知识,希望对你有一定的参考价值。
定义:区间dp就是在区间上进行动态规划,求解一段区间上的最优解。其主要思想就是现在小区间进行dp得到最优解,然后再利用小区间的最优解结合并大区间的最优解。
区间dp经典问题:
1.石子合并问题
有N堆石子排成一排,每堆石子有一定的数量。现要将N堆石子并成为一堆。合并的过程只能每次将相邻的两堆石子堆成一堆,每次合并花费的代价为这两堆石子的和,经过N-1次合并后成为一堆。求出总的代价最小值。
样例: 3堆石子 1 2 3 输出9(1+2+1+2+3=9)
我们假设dp[i][j]
表示取第 i~j 堆的最小代价;由此我们可以得出状态转移方程:(dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]+w[i][j]));得出状态转移方程后我们只需枚举k即可。(其中w[i][j]为取 i~j 石子的代价,即sum[j] - sum[i-1])。
show code(有一点要注意的,要先枚举长度,因为后面的状态的长度要用到前一个状态的长度):
#include <iostream>
#include <cstdlib>
#include <algorithm>
#include <cstring>
#include <stdio.h>
using namespace std;
const int maxn=105;
const int inf=0x3f3f3f3f;
int arr[maxn],sum[maxn];
int dp[maxn][maxn];
int main()
{
ios::sync_with_stdio(false);
int n,T;
cin>>T;
while(T--)
{
cin>>n;
memset(dp,inf,sizeof(dp));
memset(sum,0,sizeof(sum));
sum[0]=0;
for(int i=1;i<=n;++i){
cin>>arr[i];
sum[i]=sum[i-1]+arr[i];
dp[i][i]=0; //不移动则无代价
}
for(int len=2;len<=n;++len) //先枚举长度len
{
for(int i=1;i+len-1<=n;++i)
{
int j=i+len-1;
for(int k=i;k+1<=j;++k)
dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]+sum[j]-sum[i-1]);
//cout<<"dp["<<i<<"]["<<j<<"] is:"<<dp[i][j]<<endl;
}
}
cout<<dp[1][n]<<endl;
}
system("pause");
return 0;
}
2.括号匹配问题
问题描述:给出一串的只有
(
)
[
]
四种括号组成的串,让你求解需要最少添加括号数让串中的所有括号完全匹配。分析:求出最大匹配数,用总长度-最大匹配数就是答案。
最大匹配数可以用LCP求,最长公共子序列的最大值*2就是最大匹配数,但用dp求LCP复杂度太高(其实可以用后缀树组),所以我们假装不知道后缀数组,考虑另一种方法:通过dp让它满足子结构求解;
定义dp[i][j]
为串中第 i 个到第 j 个个括号的最大匹配数目;加入我们已近知道了i 到 j 的最大匹配数目,那么i+1 到 j+1 的区间也可以很简单的得到。假如第 i 个和第 j 个是一对匹配的括号那么 dp[i][j]=dp[i+1][j-1]+2
(这个很重要也很难确定); 然后再更新最大值:(dp[i][j]=max(dp[i][j],dp[i][k]+dp[k+1][j]))
show code:
#include <iostream>
#include <cstdlib>
#include <algorithm>
#include <cstring>
#include <stdio.h>
using namespace std;
const int maxn=105;
char s[maxn];
int dp[maxn][maxn];
int main()
{
ios::sync_with_stdio(false);
while(cin>>s+1)
{
if(s[1]=='e') break;
int n=strlen(s+1);
memset(dp,0,sizeof(dp)); //dp[i][j]为从i到j匹配到的最大匹配数
for(int len=2;len<=n;++len)
{
for(int i=1;i+len-1<=n;++i)
{
int j=i+len-1;
if( (s[i]=='('&&s[j]==')') || (s[i]=='['&&s[j]==']') )
dp[i][j]=dp[i+1][j-1]+2;
for(int k=i;k+1<=j;++k)
dp[i][j]=max(dp[i][j],dp[i][k]+dp[k+1][j]);
}
}
cout<<dp[1][n]<<endl;
}
system("pause");
return 0;
}
3.整数划分问题
题目描述:现在给你两个数 n 和 m ,要求在数字 n 的数位中插入 m-1 个乘号,使得这个n最大。
样例:n=111, m=2→(11×1=11)输出11;n=1111, m=2→(11×11) 输出121
解题思路:设 dp[i][j]
代表从第一位到第 i 为插入 j 个乘号得到的乘积的最大值,所以我们只需要枚举放第 j 号乘号的位置即可。很容易得到状态转移方程:(dp[i][j]=max(dp[i][j],dp[i][k]*num[k+1][j]));其中
num[i][j]
代表从arr[i] 到 arr[j]这段连续区间代表的数值。
show code:
#include <iostream>
#include <cstdlib>
#include <algorithm>
#include <cstring>
#include <stdio.h>
using namespace std;
typedef long long ll; //注意范围,乘积可能会爆Int
const int maxn=105;
char s[maxn];
ll dp[maxn][maxn],val[maxn][maxn]; //val为从i到j的数值
int m;
inline int id(char s)
{
return s-'0';
}
int main()
{
ios::sync_with_stdio(false);
cin>>s+1>>m;
int len=strlen(s+1);
memset(dp,0,sizeof(dp));
for(int i=1;i<=len;++i){ //求val数组
val[i][i]=id(s[i]);
for(int j=i+1;j<=len;++j)
val[i][j]=val[i][j-1]*10+id(s[j]);
}
for(int i=1;i<=len;++i)
dp[i][0]=val[1][i];
for(int num=1;num<=m-1;++num) //先枚举乘号的个数
{
for(int i=num+1;i<=len;++i){ //从可能最小位乘号后面一位开始枚举位数
for(int k=num;k<i;++k) //从可能的最小乘号那一位开始枚举
dp[i][num]=max(dp[i][num],dp[k][num-1]*val[k+1][i]);
}
}
cout<<dp[len][m-1]<<endl;
system("pause");
return 0;
}
以上是关于区间dp的主要内容,如果未能解决你的问题,请参考以下文章