@myecho
2019-04-16T10:34:41.000000Z
字数 5858
阅读 735
动态规划
区间DP一般都可以使用记忆化搜索或者直接DP的方法写。
区间DP在枚举状态时由于每次都是区间减少,所以可以使用区间l作为一维进行枚举。
由矩阵连乘拓展出来的几道习题
1. POJ 1651 Multiplication Puzzle
题意:一排牌/卡片(一串数字),每次从这些牌中拿走一张牌(首尾两张不能拿),把前一张,这一张,后一张牌上的数字相乘的结果累加,直到只剩下两张牌为止。问所能得到的最小结果是多少。
#include<iostream>
#include<algorithm>
#include<string>
using namespace std;
int a[101];
int m[101][101];
#define INF 0x3f3f3f3f
int main(){
int n;
cin >> n;
for(int i = 1; i <= n; ++i){
cin >> a[i];
}
for(int i = 1; i <= n; ++i){
m[i][i] = 0;
m[i][i+1] = 0; //边界还是挺重要的!
}
for(int l = 3; l <= n; ++l){
for(int i = 1; i <= n-l+1; ++i){
int j = i+l-1;
m[i][j] = INF;
for(int k = i+1; k <= j-1; ++k){ //注意k的范围
m[i][j] = min(m[i][j], m[i][k]+m[k][j]+a[k]*a[i]*a[j]); //not a[k+1]*a[k]*a[k-1]
}
}
}
cout << m[1][n] << endl;
return 0;
} //http://www.cnblogs.com/hoodlum1980/archive/2012/06/07/2540150.html
所以我们必须在木棍的做左边和左右边分别添加一个点,那么得到的就是整个区间。(套模板)
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
using namespace std;
const int MAXN = 50 + 10;
const int INF = ~0U >> 2;
int w[MAXN],dp[MAXN][MAXN];
int L,n;
int solve(){
n++;
for(int i = 0;i <= n;++i)
for(int j = i+1;j <= n;++j)
dp[i][j] = (i+1 == j ? 0 : INF);
w[0] = 0; w[n] = L;
for(int p = 1;p <= n;++p){ //区间长度
for(int s = 0;s <= n-p;++s){ // 起始位置
int e = s + p; //终点
for(int k = s+1;k < e;++k){ //状态转移
dp[s][e] = min(dp[s][e],dp[s][k] + dp[k][e] + w[e] - w[s]);
}
}
}
return dp[0][n];
}
int main()
{
while(scanf("%d",&L),L){
scanf("%d",&n);
for(int i = 1; i <= n; ++i){
scanf("%d",&w[i]);
}
printf("The minimum cutting is %d.\n",solve());
}
return 0;
}
其实dp很难逃出3种思路:
1、一维线性dp:每次考虑i时,选择最优子问题要么在i-1,要么在1...i-1里;
2、二维线性dp:考虑(i,j)子问题时,选择最优子问题要么在(i+1,j)、(i,j-1),要么在i<= k <=j里;
3、树形dp:考虑i节点最优时,选择子节点最优,一般融合了01背包dp的双重dp。
4. LightOJ 1422 Halloween Costumes
此题比较难考虑到区间dp上来,但是题目中通过推理可以判断出类似"断点"的结构。
题目链接;http://acm.hust.edu.cn/vjudge/problem/viewProblem.action?id=27130
5. poj2955 Brackets(寻求最大的括号匹配数)
题目链接:http://poj.org/problem?id=2955
递推公式:dp[i][j]= max(dp[i+1][j], dp[i+1][k-1]+dp[k][j]+2)其中i+1<=k<=j
我们可以看到从递推公式来看,和上式基本上是同一个问题。还是先找一个最差的情况,在这个题目中,dp[i+1][j]就是最差情况,然后再通过寻找一个k来划分子问题进而看是否可以优化子问题。
6. hdu 2476
题意:给出两个字符串s1,s2,通过刷这个操作,可以将s1中的某个区间内的字符全部改变为另一个字符,求问s1抓换到s2所需的最小的操作数?
需要进行两次dp操作,一次dp[i][j]统计从空串转换到s2所需的最小操作。最后一次ans[j]统计从s1转换到s2的最小的操作数。
题解:http://www.cnblogs.com/ACMDoli/articles/4676197.html
代码:#include <stdio.h>
#include <string.h>
#include <algorithm>
using namespace std;
char s1[105],s2[105];
int dp[105][105];//dp[i][j]为i~j的刷法
int ans[105],i,j,k,len;
int main()
{
while(~scanf("%s%s",s1,s2))
{
len = strlen(s1);
memset(dp,0,sizeof(dp));
for(j = 0; j<len; j++)
{
for(i = j; i>=0; i--)//j为尾,i为头
{
dp[i][j] = dp[i+1][j]+1;//先每个单独刷
for(k = i+1; k<=j; k++)//i到j中间所有的刷法
{
if(s2[i]==s2[k])
dp[i][j] = min(dp[i][j],(dp[i+1][k]+dp[k+1][j]));//i与k相同,寻找i刷到k的最优方案
}
}
}
for(i = 0; i<len; i++)
ans[i] = dp[0][i];//根据ans的定义先初始化
for(i = 0; i<len; i++)
{
if(s1[i] == s2[i])
{
if(i==0)
ans[i] = 0;
else
ans[i] = ans[i-1];//如果对应位置相等,这个位置可以不刷
}
else
{
for(j = 0; j<i; j++)
ans[i] = min(ans[i],ans[j]+dp[j+1][i]);//寻找j来分割区间得到最优解
}
}
printf("%d\n",ans[len-1]);
}
return 0;
}
#include<iostream>
#include<cstdio>
#include<cstring>
#define N 110
#define inf 0x7fffffff
using namespace std;
char str[N];
struct node
{
int num; //储存的是压缩后的字符串的长度
char s[N];
}dp[N][N];
int pos;
char ts[N];
void Int2S(int num)
{
int i,j;
pos=-1;
char cc;
while(num)
{
ts[++pos]=(num%10)+'0';
num/=10;
}
for(j=0;j<=pos/2;j++)
{
cc=ts[j];
ts[j]=ts[pos-j];
ts[pos-j]=cc;
}
}
int main()
{
int i,j,k,len,p,q,f,nt,t;
scanf("%s",str);
len=strlen(str);
for(i=0;i<len;i++)
{
dp[i][i].s[0]=str[i];
dp[i][i].s[1]='\0';
dp[i][i].num=1;
}
for(p=1;p<len;p++)
for(i=0;i<len-p;i++)
{
j=i+p;
dp[i][j].num=inf;
//开始处理i->j片段内的字符串如何进行打包操作
for(k=1;k<p;k++)
{
if((p+1)%k)//以k为长度进行分割
continue;
q=i+k,t=i;
while(q<=j)
{
if(str[q]!=str[t])
break;
q++;
t++;
if(t==i+k)
t=i;
}
if(q>j)
{
nt=(p+1)/k;
Int2S(nt);
strcpy(dp[i][j].s,ts); //最好不直接拷贝,长度无法确定
dp[i][j].s[++pos]='(';
for(t=0;dp[i][i+k-1].s[t];t++)//将子结构中的最优片段拷贝过来 不能直接拷贝原来的字符串,因为子结构可能完成了压缩
dp[i][j].s[++pos]=dp[i][i+k-1].s[t];
dp[i][j].s[++pos]=')';
dp[i][j].num=pos+1;
dp[i][j].s[++pos]='\0';
break;
}
}
//递推子结构中的最优结果
for(k=i;k<j;k++)
{
if(dp[i][j].num>dp[i][k].num+dp[k+1][j].num)
{
dp[i][j].num=dp[i][k].num+dp[k+1][j].num;
strcpy(dp[i][j].s,dp[i][k].s);
strcat(dp[i][j].s,dp[k+1][j].s);//会将前边的'\0'覆盖
}
}
}
printf("%s\n",dp[0][len-1].s);
return 0;
}
其共同特点是若A,若B,则AB,都可以将区间的问题通过划分为更小的区间来进行解决,这里需要注意不同问题之间的区别和练习,以及边界条件的处理。
二维DP(较难)
例题 URAL 1900 Brainwashing Device
#include<iostream>
#include<vector>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=555;
const int INF=1e9;
int dp[N][N], pt[N][N];
int c[N][N], g[N][N];
int n, k;
/*
输入[i][j]即为从i上车,从j下车的乘客数量,从i到j的乘客是需要途径i与j之间的中间车站的
关键在于认识到是如何划分成子问题的!
*/
void pts(int r, int st) //二维DP的路径一般也要使用二维数组进行保存
{
if(st == 0) return;
pts(pt[r][st], st - 1);
cout<<r-1;
if(st == k) cout<<"";
else cout<<" ";
}
int main(){
memset(c,0,sizeof(c));
cin>>n>>k;
for(int i=1;i<n;++i){
for(int j=i+1;j<=n;++j){
cin>>c[i][j];
c[i][j]+=c[i][j-1]; //相当于输入的每一行前缀之和
}
}
for(int i=1;i<n;++i){
for(int j=i+1;j<=n;++j){
g[i][j]=0; //g[i][j]代表第i条边到第j条边且在j-1到j之间安装装置产生的happy人数
for(int t=i;t<j;t++){
g[i][j]+=c[t][n]-c[t][j-1];//依次添加的是 从i到j j+1 ... n车站的乘客人数
}
}
}
memset(dp,-1,sizeof(dp));//非负数,数据可能为0
dp[1][0]=0;//初始状态
for(int i=1;i<=n;++i){
for(int j=i+1;j<=n;++j){
for(int t=1;t<=min(i,k);++t){ //min(i,k)要注意
if(dp[j][t]<dp[i][t-1]+g[i][j]){
dp[j][t]=dp[i][t-1]+g[i][j];
pt[j][t]=i;
}
}
}
}
int ans=-1,id;
for(int i=k+1;i<=n;i++){
if(dp[i][k]>ans){
ans=dp[i][k];
id=i;
}
}
cout<<ans<<endl;
pts(id,k);
return 0;
}
区间dp做的时候都会在for i 与for j 之后for k 之前添加dp[i][j]= INF(取最小值)或者0(需要取最大值时)之类的东西,防止无法开始比较。得不到初值.
还要注意的一点是题目中的循环顺序,我们可以发现当[i,j]时一定要求[i,k] [k,j]其中子问题的两个问题区间都比原问题的区间要小,因此最外层的循环一般是设置l,l为长度,第二层循环为i(0~n-l),j直接由l和i计算得出。从而保证所有的长度小的子问题已经得到了解决。
矩阵中最大的1
http://www.voidcn.com/article/p-vpbybrua-qz.html
s[i][j] <- s[i-1][j],s[i][j-1],s[i-1][j-1] 得到当前的最优解