[kuangbin带你飞]专题二十二 区间DP

时间:2022-03-27 03:03:39
 
 
   
ID
Origin
Title
  17 / 60 Problem A ZOJ 3537 Cake
  54 / 105 Problem B LightOJ 1422 Halloween Costumes
  59 / 90 Problem C POJ 2955 Brackets
  26 / 51 Problem D CodeForces 149D Coloring Brackets
  47 / 58 Problem E POJ 1651 Multiplication Puzzle
  31 / 115 Problem F ZOJ 3469 Food Delivery
  38 / 65 Problem G HDU 4283 You Are the One
  34 / 60 Problem H HDU 2476 String painter
 
 
 
 
 
 
 
 
 
 
 
 

17 / 60 Problem A ZOJ 3537 Cake

54 / 105 Problem B LightOJ 1422 Halloween Costumes

给你n天分别要穿的衣服,可以套着穿,但是一旦脱下来就不能再穿了,问这n天要准备几件衣服。

很简单的区间DP的入门题。一开始这题想了很久就是想不出来。直到做了后面几道区间DP回过来终于想明白了。
区间DP可以使用记忆化搜索和直接DP的方法写。
这题的状态转移方程:
dp[i][j]=min(1+dp[i+1][j],dp[i+1][k-1]+dp[k][j]) ( a[i]==a[k] i<k<=j )
注意初始化。

/*
* Light OJ 1422 - Halloween Costumes
* http://lightoj.com/volume_showproblem.php?problem=1422
* 区间DP的思想。
* 比如要求解i到j,dp[i][j].
* 就是考虑i的衣服,一种是i的衣服只有在i使用,那么就是dp[i+1][j]+1件
* 然后再i+1~j中枚举k,如果a[i]==a[k].那么dp[i][j]=min(dp[i][j],dp[i+1][k-1]+dp[k][j])
* 注意因为i的衣服是可以使用多次的,所以不需要加1,是dp[k][j]
* 思想很妙
*/ #include <iostream>
#include <stdio.h>
#include <algorithm>
#include <string.h>
using namespace std;
const int MAXN=;
int a[MAXN];
int dp[MAXN][MAXN]; int main()
{
int T;
int n;
scanf("%d",&T);
int iCase=;
while(T--)
{
iCase++;
scanf("%d",&n);
for(int i=;i<=n;i++)
scanf("%d",&a[i]);
memset(dp,,sizeof(dp));
for(int i=;i<=n;i++)
for(int j=i;j<=n;j++)
dp[i][j]=j-i+;
for(int i=n-;i>=;i--)//注意DP的顺序
for(int j=i+;j<=n;j++)
{
dp[i][j]=dp[i+][j]+;//这个表示第i个的衣服在后面没有利用了
for(int k=i;k<=j;k++)
if(a[i]==a[k])//用同一件
dp[i][j]=min(dp[i][j],dp[i+][k-]+dp[k][j]);
}
printf("Case %d: %d\n",iCase,dp[][n]);
}
return ;
}

59 / 90 Problem C POJ 2955 Brackets

给一个括号序列,问序列中合法的括号最多有多少个,若A合法,则[A],(A)均合法,若A,B合法则AB也合法

合法序列就是括号可以两两匹配的。
思路就是区间DP的思想了。
我的代码是采用记忆化搜索写出来的。

状态转移方程dp[i][j]=max(dp[i+1][j],2+dp[i+1][k-1]+dp[k+1][j]) i和j是一对括号 && i<k<=j
其实就是看第i个括号的情况。
舍弃第i个括号,就是dp[i+1][j],或者是往后找和i匹配的,然后就分成了两部分了。

//============================================================================
// Name : POJ.cpp
// Author :
// Version :
// Copyright : Your copyright notice
// Description : Hello World in C++, Ansi-style
//============================================================================ #include <iostream>
#include <string.h>
#include <stdio.h>
#include <algorithm>
using namespace std;
const int MAXN=;
char str[MAXN];
int dp[MAXN][MAXN];
int solve(int i,int j)
{
if(dp[i][j]!=-)return dp[i][j];
if(j<=i)return dp[i][j]=;
else if(j==i+)
{
if( (str[i]=='('&&str[j]==')')||(str[i]=='['&&str[j]==']') )return dp[i][j]=;
else return dp[i][j]=;
}
dp[i][j]=solve(i+,j);
for(int k=i+;k<=j;k++)
if( (str[i]=='('&&str[k]==')')||(str[i]=='['&&str[k]==']') )
dp[i][j]=max(dp[i][j],+solve(i+,k-)+solve(k+,j));
return dp[i][j];
} int main()
{
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
while(scanf("%s",str)==)
{
if(strcmp(str,"end")==)break;
memset(dp,-,sizeof(dp));
int n=strlen(str);
printf("%d\n",solve(,n-));
}
return ;
}
#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;
char s[];
int dp[][];
int main()
{
int i, j, k, x;
while(gets(s)!=NULL)
{
if(s[]=='e')break;
memset(dp,,sizeof(dp));
int len= strlen(s);
for(k=;k<len;k++) //表示区间长度,从0-len更新
{
for(i=,j=k;j<len;i++,j++)
{
if(s[i]=='('&&s[j]==')'||s[i]=='['&&s[j]==']') //匹配
dp[i][j]=dp[i+][j-]+;
for(x=i;x<j;x++) //区间最值合并
dp[i][j]=max(dp[i][j],dp[i][x]+dp[x+][j]);
//printf("dp[%d][%d]=%d ",i,j,dp[i][j]);
}
//puts("");
}
printf("%d\n",dp[][len-]);
}
return ;
}

26 / 51 Problem D CodeForces 149D Coloring Brackets

给一个给定括号序列,给该括号上色,上色有三个要求

1、只有三种上色方案,不上色,上红色,上蓝色

2、每对括号必须只能给其中的一个上色

3、相邻的两个不能上同色,可以都不上色

求0-len-1这一区间内有多少种上色方案,很明显的区间DP

dp[l][r][i][j]表示l-r区间两端颜色分别是i,j的方案数

0代表不上色,1代表上红色,2代表上蓝色

对于l-r区间,有3种情况

1、if(l+1==r) 说明就只有一对,那么dp[l][r][0][1]=1;
        dp[l][r][1][0]=1;
        dp[l][r][0][2]=1;
        dp[l][r][2][0]=1;

2、if(l与r是配对的)

递归(l+1,r-1)

状态转移dp[l][r][0][1]=(dp[l][r][0][1]+dp[l+1][r-1][i][j])%mod; dp[l][r][1][0]=(dp[l][r][1][0]+dp[l+1][r-1][i][j])%mod;

dp[l][r][0][2]=(dp[l][r][0][2]+dp[l+1][r-1][i][j])%mod; dp[l][r][2][0]=(dp[l][r][2][0]+dp[l+1][r-1][i][j])%mod;

3、if(l与r不配对)

dp[l][r][i][j]=(dp[l][r][i][j]+(dp[l][p][i][k]*dp[p+1][r][q][j])%mod)%mod;

#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;
#define N 705
#define mod 1000000007
char s[N];
int match[N];
int tmp[N];
long long dp[N][N][][];
void getmatch(int len)
{
int p=;
for(int i=; i<len; i++)
{
if(s[i]=='(')
tmp[p++]=i;
else
{
match[i]=tmp[p-];
match[tmp[p-]]=i;
p--;
}
}
}
void dfs(int l,int r)
{
if(l+==r)
{
dp[l][r][][]=;
dp[l][r][][]=;
dp[l][r][][]=;
dp[l][r][][]=;
return ;
}
if(match[l]==r)
{
dfs(l+,r-);
for(int i=;i<;i++)
{
for(int j=;j<;j++)
{
if(j!=)
dp[l][r][][]=(dp[l][r][][]+dp[l+][r-][i][j])%mod;
if(i!=)
dp[l][r][][]=(dp[l][r][][]+dp[l+][r-][i][j])%mod;
if(j!=)
dp[l][r][][]=(dp[l][r][][]+dp[l+][r-][i][j])%mod;
if(i!=)
dp[l][r][][]=(dp[l][r][][]+dp[l+][r-][i][j])%mod;
}
}
return ;
}
else
{
int p=match[l];
dfs(l,p);
dfs(p+,r);
for(int i=;i<;i++)
{
for(int j=;j<;j++)
{
for(int k=;k<;k++)
{
for(int q=;q<;q++)
{
if(!((k== && q==) || (k== && q==)))
dp[l][r][i][j]=(dp[l][r][i][j]+(dp[l][p][i][k]*dp[p+][r][q][j])%mod)%mod;
}
}
}
}
}
}
int main()
{
while(scanf("%s",s)!=EOF)
{
int len=strlen(s);
getmatch(len);
memset(dp,,sizeof(dp));
dfs(,len-);
long long ans=;
for(int i=;i<;i++)
{
for(int j=;j<;j++)
{
ans=(ans+dp[][len-][i][j])%mod;
}
}
printf("%ld\n",ans);
}
return ;
}

47 / 58 Problem E POJ 1651 Multiplication Puzzle

题意:一系列的数字,除了头尾不能动,每次取出一个数字,这个数字与左右相邻数字的乘积为其价值,最后将所有价值加起来,要求最小值

区间DP
dp[0][n-1]表示答案。
求解dp[i][j]的时候,就是枚举[i+1,j-1]中最后删除的元素。
dp[i][j]=min(a[k]*a[i]*a[j]+dp[i][k]+dp[k][j]) i<k<j

//============================================================================
// Name : POJ.cpp
// Author :
// Version :
// Copyright : Your copyright notice
// Description : Hello World in C++, Ansi-style
//============================================================================ #include <iostream>
#include <stdio.h>
#include <algorithm>
#include <string.h>
using namespace std;
const int MAXN=;
const int INF=0x3f3f3f3f;
int a[MAXN];
int dp[MAXN][MAXN];
int solve(int i,int j)
{
if(dp[i][j]!=INF)return dp[i][j];
if(j==i+)return dp[i][j]=;
for(int k=i+;k<j;k++)
dp[i][j]=min(dp[i][j],a[k]*a[i]*a[j]+solve(i,k)+solve(k,j));
return dp[i][j];
}
int main()
{
//freopen("in.txt","r",stdin);
//freopen("out.txt","w",stdout);
int n;
while(scanf("%d",&n)==)
{
for(int i=;i<n;i++)
scanf("%d",&a[i]);
for(int i=;i<n;i++)
for(int j=;j<n;j++)
dp[i][j]=INF;
printf("%d\n",solve(,n-));
}
return ;
}

题意:一系列的数字,除了头尾不能动,每次取出一个数字,这个数字与左右相邻数字的乘积为其价值,

最后将所有价值加起来,要求最小值。

这题容易会想到贪心就是先把最大的数先取出这样就能满足剩下的总价值尽可能的小,如果出现多个一样

的数时优先取走价值小的,但是如果有出现多个价值一样的话就不好处理了。

于是可以考虑一下用区间解决,区间转移大致是这样的

dp[j][j + i] = min(dp[j][j + i] , dp[j][k] + dp[k][j + i] + a[k] * a[j] * a[j + i]);

(i表示区间长度,j表示区间起点,k表示区间中枚举的点)

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
int a[] , dp[][];
int main() {
int n;
cin >> n;
for(int i = ; i <= n ; i++)
cin >> a[i];
memset(dp , 0X3f3f3f3f , sizeof(dp));
for(int i = ; i < n ; i++) {
dp[i][i + ] = ;
}
for(int i = ; i < n ; i++) {
for(int j = ; j <= n && j + i <= n ; j++) {
for(int k = j + ; k < i + j ; k++) {
dp[j][j + i] = min(dp[j][j + i] , dp[j][k] + dp[k][j + i] + a[k] * a[j] * a[j + i]);
}
}
}
cout << dp[][n] << endl;
return ;
}

31 / 115 Problem F ZOJ 3469 Food Delivery

有一家快餐店送外卖,现在同时有n个家庭打进电话订购,送货员得以V-1的速度一家一家的运送,但是每一个家庭都有一个不开心的值,每分钟都会增加一倍,值达到一定程度,该家庭将不会再订购外卖了,现在为了以后有更多的家庭订购,要将外卖送到的情况下使得所有用户的不开心值总和达到最小

很明显,每多走一分钟,没送到的家庭的不开心值都会加倍,

假设是这样的顺序123X456,从X出发先往左右中间靠近的送,再往两边送省时间

dp[i][j][0]表示从i到j用户送到最小不开心值,此时送货员停留在左边即i位置

dp[i][j][1]表示从i到j用户送到最小不开心值,此时送货员停留在右边即j位置

状态有四种,

dp[i][j][0]=min(dp[i][j][0],dp[i+1][j][0]+(a[i+1].x-a[i].x)*(delay+a[i].v));
            dp[i][j][0]=min(dp[i][j][0],dp[i+1][j][1]+(a[j].x-a[i].x)*(delay+a[i].v));
            dp[i][j][1]=min(dp[i][j][1],dp[i][j-1][0]+(a[j].x-a[i].x)*(delay+a[j].v));
            dp[i][j][1]=min(dp[i][j][1],dp[i][j-1][1]+(a[j].x-a[j-1].x)*(delay+a[j].v));

#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;
#define N 1005
#define INF 0x7ffffff
int dp[N][N][];
int n,V,X;
int sum[N];
struct node
{
int x;
int v;
} a[N];
int cmp(node b,node c)
{
return b.x<c.x;
}
int Delay(int l,int r)
{
if(l>r)
return ;
return sum[r]-sum[l-];
}
void DP()
{
int res;
for(int i=; i<=n; i++)
{
for(int j=; j<=n; j++)
dp[i][j][]=dp[i][j][]=INF;
}
for(int i=; i<=n; i++)
{
if(a[i].x==X)
{
res=i;
break;
}
}
dp[res][res][]=dp[res][res][]=;
for(int i=res; i>=; i--)//i循环restaurant左边的
{
for(int j=res; j<=n; j++)//j循环restaurant右边的
{
int delay=Delay(,i-)+Delay(j+,n);
if(i==j)
continue;
dp[i][j][]=min(dp[i][j][],dp[i+][j][]+(a[i+].x-a[i].x)*(delay+a[i].v));
dp[i][j][]=min(dp[i][j][],dp[i+][j][]+(a[j].x-a[i].x)*(delay+a[i].v));
dp[i][j][]=min(dp[i][j][],dp[i][j-][]+(a[j].x-a[i].x)*(delay+a[j].v));
dp[i][j][]=min(dp[i][j][],dp[i][j-][]+(a[j].x-a[j-].x)*(delay+a[j].v));
}
}
}
int main()
{
while(scanf("%d%d%d",&n,&V,&X)!=EOF)
{
for(int i=; i<=n; i++)
{
scanf("%d%d",&a[i].x,&a[i].v);
}
a[n+].x=X;
a[n+].v=;
n++;
sort(a+,a+n+,cmp);
memset(sum,,sizeof(sum));
for(int i=; i<=n; i++)
sum[i]=sum[i-]+a[i].v;
DP();
printf("%d\n",min(dp[][n][],dp[][n][])*V);
}
return ;
}

38 / 65 Problem G HDU 4283 You Are the One

有n个男屌丝事先按1,2,3,,,,,,n的顺序排好,每个人都有一个不开心值unhappy[i],如果第i个人第k个上台找对象,那么该屌丝男的不开心值就会为(k-1)*unhappy[i],因为在他前面有k-1个人嘛,导演为了让所有男屌的总不开心值最小,搞了一个小黑屋,可以通过小黑屋来改变男屌的出场顺序

注意:这个小黑屋是个栈,男屌的顺序是排好了的,但是可以通过入栈出栈来改变男屌的出场顺序

解题思路:(操度娘所知~度娘你好腻害)

dp[i][j]表示区间[i,j]的最小总不开心值

把区间[i,j]单独来看,则第i个人可以是第一个出场,也可以是最后一个出场(j-i+1),也可以是在中间出场(1   ~  j-i+1)

不妨设他是第k个出场的(1<=k<=j-i+1),那么根据栈后进先出的特点,以及题目要求原先男的是排好序的,那么::

第  i+1  到 i+k-1  总共有k-1个人要比i先出栈,

第 i+k   到j 总共j-i-k+1个人在i后面出栈

举个例子吧:

有5个人事先排好顺序  1,2,3,4,5

入栈的时候,1入完2入,2入完3入,如果我要第1个人第3个出场,那么入栈出栈顺序是这样的:

1入,2入,3入,3出,2出,1出(到此第一个人就是第3个出场啦,很明显第2,3号人要在1先出,而4,5要在1后出)

这样子,动态转移方程就出来啦,根据第i个人是第k个出场的,将区间[i,j]分成3个部分

dp[i][j]=min(dp[i][j],dp[i+1,i+k-1]+dp[i+k,j]+(k-1)*a[i]+(sum[j]-sum[i+k-1])*k);

(sum[j]-sum[i+k-1])*k 表示 后面的 j-i-k+1个人是在i后面才出场的,那么每个人的不开心值都会加个 unhappy,sum[i]用来记录前面i个人的总不开心值,根据题目,每个人的unhappy是个累加的过程,多等一个人,就多累加一次

记忆化搜索

#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
using namespace std;
int dp[][];
int a[],sum[];
int n;
int solve(int i,int j)
{
int &ans=dp[i][j];
if(ans!=-) return ans;
if(i>=j) return ;
ans=<<;
for(int k=;k<=j-i+;k++){
ans=min(ans,solve(i+,i+k-)+solve(i+k,j)+(k-)*a[i]+(sum[j]-sum[i+k-])*k);
}
return ans;
}
int main()
{
int t,iCase=;
cin>>t;
while(t--){
cin>>n;
sum[]=;
for(int i=;i<=n;i++){
scanf("%d",&a[i]);
sum[i]=sum[i-]+a[i];
}
memset(dp,-,sizeof dp);
printf("Case #%d: %d\n",iCase++,solve(,n));
}
return ;
}

dp

#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
#define maxn 110
#define inf 0x7fffffff
using namespace std;
int dp[maxn][maxn];
int sum[maxn];
int a[maxn];
int n;
int main()
{
int t,iCase=;
scanf("%d",&t);
while(t--){
sum[]=;
scanf("%d",&n);
for(int i=;i<=n;i++){
scanf("%d",&a[i]);
sum[i]=sum[i-]+a[i];
}
memset(dp,,sizeof dp);
for(int i=;i<=n;i++){
for(int j=i+;j<=n;j++){
dp[i][j]=inf;
}
}
for(int p=;p<=n;p++){
for(int i=;i<=n-p+;i++){
int j=i+p-;
for(int k=;k<=p;k++){
dp[i][j]=min(dp[i][j],dp[i+][i+k-]+dp[i+k][j]+(k-)*a[i]+(sum[j]-sum[i+k-])*k);
}
}
}
printf("Case #%d: %d\n",iCase++,dp[][n]);
}
return ;
}

34 / 60 Problem H HDU 2476 String painter

给出两个串s1和s2,一次只能将一个区间刷一次,问最少几次能让s1=s2

例如zzzzzfzzzzz,长度为11,我们就将下标看做0~10

先将0~10刷一次,变成aaaaaaaaaaa

1~9刷一次,abbbbbbbbba

2~8:abcccccccba

3~7:abcdddddcba

4~6:abcdeeedcab

5:abcdefedcab

这样就6次,变成了s2串了

第二个样例也一样

0

先将0~10刷一次,变成ccccccccccb

1~9刷一次,cdddddddddcb

2~8:cdcccccccdcb

3~7:cdcdddddcdcb

4~6:cdcdcccdcdcb

5:cdcdcdcdcdcb

最后竟串尾未处理的刷一次

就变成了串2cdcdcdcdcdcd

所以一共7次

#include <stdio.h>
#include <string.h>
#include <algorithm>
using namespace std; char s1[],s2[];
int dp[][];//dp[i][j]为i~j的刷法
int ans[],i,j,k,len; int main()
{
while(~scanf("%s%s",s1,s2))
{
len = strlen(s1);
memset(dp,,sizeof(dp));
for(j = ; j<len; j++)
{
for(i = j; i>=; i--)//j为尾,i为头
{
dp[i][j] = dp[i+][j]+;//先每个单独刷
for(k = i+; k<=j; k++)//i到j中间所有的刷法
{
if(s2[i]==s2[k])
dp[i][j] = min(dp[i][j],(dp[i+][k]+dp[k+][j]));//i与k相同,寻找i刷到k的最优方案
}
}
}
for(i = ; i<len; i++)
ans[i] = dp[][i];//根据ans的定义先初始化
for(i = ; i<len; i++)
{
if(s1[i] == s2[i])
ans[i] = ans[i-];//如果对应位置相等,这个位置可以不刷
else
{
for(j = ; j<i; j++)
ans[i] = min(ans[i],ans[j]+dp[j+][i]);//寻找j来分割区间得到最优解
}
}
printf("%d\n",ans[len-]);
} return ;
}