区间上的dp状态设计最基本的形式:
\(F[i]\)表示以i结尾的最优值或方案数。
\(F[i][k]\)表示以i结尾附加信息为k的最优值或方案数。
当然可以有多维附加信息。
转移的话往往是枚举上一个断点。
\(F[i]=max \{ F[j]+ w(j+1,i) | j是一个满足转移条件的断点\}\)。
另一个很常见的是:$ f[i][j]$前i个位置分成j段/选出j个的最优值。
这是最简单的一类序列上的dp
bzoj1003
有m个码头和e条航线,每天航线有成本。有连续n天需要从1号码头到m号码头运输货物。每个码头会在某些天数区间内不许经过。每更换一次运输路线,要付出k的成本。
求这n天的最小总成本。
m<=20, n<=100
SOLUTION:
其实就是分成很多段,每一段选同一个运输路线,然后得到一个最优的划分方案,使得成本最小。
f[i]表示前i天的运输最小成本。
\(f[i]=min\{ f[j]+k+w(j+1,i)*(i-j) | j<i \}\)
其中w(x,y)表示最短的在第x天到第y天都能用的路线长度;
处理方法:
首先枚举所有的x,y,然后利用最短路算法(这里dijkstra)算出x~y这些天都可以满足的1到m的最短路径;(计算方法:首先数组$use[i][j] $记录第i个点在第j天是否可以使用,在dijkstra时传入x和y,提前预处理,用数组used[i]记录在第x天到第y天是否可以使用码头i,在更新时如果遇到不能使用的点,直接continue掉即可)
复杂度O(N^2 * m * log(m)) 然后数据范围很小嘛所以可以过√
CODE:
#include<cstdio> #include<cstring> #include<algorithm> #include<iostream> #include<queue> #define pr pair<int,int> #define mk make_pair using namespace std; inline int read(){ int ans=0; char last=' ',ch=getchar(); while(ch>'9'||ch<'0') last=ch,ch=getchar(); while(ch>='0'&&ch<='9') ans=(ans<<1)+(ans<<3)+ch-'0',ch=getchar(); if(last=='-') ans=-ans; return ans; } const int inf=2147483647; struct node{ int to,dis,nxt; }e[500]; int ecnt,head[25]; void add(int from,int to,int dis){ ++ecnt; e[ecnt].to=to; e[ecnt].dis=dis; e[ecnt].nxt=head[from]; head[from]=ecnt; } int n,m,k,E; int d; long long wei[110][110]; bool use[25][110]; void hs(int p,int f,int t){//记录第p个码头在第f天到第t天不能用 for(int i=f;i<=t;i++) use[p][i]=1; } bool vis[25],used[25]; int dis[25]; priority_queue<pr,vector<pr>,greater<pr> > q; long long dijkstra(int x,int y){ while(!q.empty()) q.pop(); memset(used,0,sizeof(used)); memset(vis,0,sizeof(vis)); q.push(mk(0,1)); for(int i=1;i<=m;i++) dis[i]=inf; for(int i=x;i<=y;i++) for(int j=1;j<=m;j++) if(use[j][i]) used[j]=1;//预处理x~y天不能用的码头,标记为1; dis[1]=0; while(!q.empty()){ int u=q.top().second; q.pop(); if(vis[u]) continue; vis[u]=1; for(int i=head[u],v,w;i;i=e[i].nxt){ v=e[i].to;w=e[i].dis; if(used[v]) continue;//遇到不能用的码头,continue; if(dis[v]>dis[u]+w){ dis[v]=dis[u]+w; q.push(mk(dis[v],v)); } } } return dis[m]; } long long f[110]; void dp(){//计算前n天从1=>m码头的最短花费 //显然最短花费是 for(int i=1;i<=n;i++){ f[i]=(long long)wei[1][i]*i; for(int j=1;j<i;j++){ f[i]=min(f[i],f[j]+k+wei[j+1][i]*(i-j)); } } } int main(){ n=read();m=read(); k=read();E=read(); for(int i=1,u,v,w;i<=E;i++){ u=read(); v=read(); w=read(); add(u,v,w); add(v,u,w); } d=read(); for(int i=1,p,a,b;i<=d;i++){ p=read(); a=read(); b=read(); hs(p,a,b); } for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) wei[i][j]=dijkstra(i,j); dp(); cout<<f[n]<<endl; return 0; }
bzoj1296 粉刷匠
有n条木板要被粉刷,每条木板分为m个格子,每个格子需要被刷成蓝色或红色。
每次粉刷可以在一条木板上给连续的一段格子刷上相同的颜色。每个格子最多被刷一次。
问若只能刷k次,最多正确粉刷多少格子。
n,m<=50, k<=2500
如果只有一条木板,那么设\(g[i][j]\)表示前i个格子刷j次的最多正确格子
然后枚举一个k,表示前k个格子刷了j-1次,第k+1到第i个格子刷一次的情况,这样依次枚举求出\(g[i][j]\)的最大值
\[ g[i][j]=max\{ g[k][j-1]+w(k+1,i) | k<i \}\]
w(x,y)为第x到第y个格子的最多同色格子数,哪个颜色出现的多刷哪个,直接记一个前缀和即可。???对怎么处理w(x,y)表示疑惑???
大概是这个意思叭:
for(int i=1;i<=m;i++){ if(color[i]=='0') { Sumr[i]=Sumr[i-1]+1; Sumb[i]=Sumb[i-1]; } else { Sumb[i]=Sumb[i-1]+1; Sumr[i]=Sumr[i-1]; } } w[i][j]=max(Sumb[j]-Sumb[i-1],Sumr[j]-Sumr[i-1]);
有多条木板,设\(f[i][j]\)表示前i个木板刷j次的最大答案。
\(f[i][j]=Max\{ f[i-1][k]+g_i[m][j-k] | k<=j \}\)
也就是需要先处理出每一块木板的g数组;
然后叭,其实这是一个有TLE的代码,但\(o_2\)优化,你值得拥有!
基本上都是上面↑讲到的式子叭,但是有几个要注意的点:
- 处理g数组时,枚举k要从0开始枚举;
- \(g[i][j][k]\)在k!=0时,初值为1;k=0时初值为0;
#include<bits/stdc++.h> using namespace std; inline int read(){ int ans=0; char last=' ',ch=getchar(); while(ch>'9'||ch<'0') last=ch,ch=getchar(); while(ch>='0'&&ch<='9') ans=(ans<<1)+(ans<<3)+ch-'0',ch=getchar(); if(last=='-') ans=-ans; return ans; } int n,m,t; int g[55][55][2505]; char color[60]; int Sumr[55],Sumb[55]; int f[55][2505]; void hs(int n){ for(int i=1;i<=m;i++) for(int j=1;j<=t;j++){ g[n][i][j]=1; for(int k=0;k<i;k++) g[n][i][j]=max(g[n][i][j],g[n][k][j-1]+max(Sumr[i]-Sumr[k],Sumb[i]-Sumb[k])); } } void clear(){ for(int i=0;i<=m;i++) Sumr[i]=Sumb[i]=0; } int main(){ n=read();m=read();t=read(); for(int i=1;i<=n;i++) { scanf("%s",color+1); clear(); for(int j=1;j<=m;j++){ if(color[j]=='1') { Sumr[j]=Sumr[j-1]; Sumb[j]=Sumb[j-1]+1; } else { Sumr[j]=Sumr[j-1]+1; Sumb[j]=Sumb[j-1]; } } hs(i); } for(int i=1;i<=n;i++) for(int j=0;j<=t;j++) for(int k=0;k<=j;k++) f[i][j]=max(f[i][j],f[i-1][k]+g[i][m][j-k]); cout<<f[n][t]<<endl; return 0; }
括号序列模型及解法
给定一个长度为n的仅包含左右括号和问号的字符串,将问号变成左括号或右括号使得该括号序列合法,求方案总数。
例如(())与()()都是合法的括号序列。
n<=3000。
然后反正没找到例题叭,所以没有办法验证对错了,只能先写写看;
然鹅并没有写对啊,太难过了
只能先复制一下zhhx的solution了(溜
令\(dp[i][j]\)表示当前到第i个字符,现在还有j个左括号。
那么分三种情况考虑。
- 若第i+1个字符是左括号,则能转移到\(dp[i+1][j+1]\)。
- 若第i+1个字符是右括号,则能转移到\(dp[i+1][j-1]\)。
- 若第i+1个字符是问号,则能转移到\(dp[i+1][j-1]\)与\(dp[i+1][j+1]\)。
最终\(dp[n][0]\)就是方案总数啦。
时间复杂度为\(O(n^2)\)。
首先感谢小蒟蒻皮皮鱼的友情代码(真的自己写枯了,果然还是太弱了
思维上确实不如神仙皮皮鱼
神仙皮皮鱼把这道题出成了.jpg
#include<bits/stdc++.h> using namespace std; inline int read() { int ans=0; char last=' ',ch=getchar(); while(ch>'9'||ch<'0') last=ch,ch=getchar(); while(ch>='0'&&ch<='9') ans=(ans<<1)+(ans<<3)+ch-'0',ch=getchar(); if(last=='-') ans=-ans; return ans; } const int mxn=10010; const int mod=1000000007; int n; char c[mxn]; int f[3][10010]; int main(){ n=read(); scanf("%s",c+1); f[0][0]=1; for(int i=1;i<=n;i++) { for(int j=i;j>=0;j--) { f[i&1][j]=0; if(c[i]=='Y') { if(j>0) f[i&1][j]=f[(i-1)&1][j-1]%mod; } if(c[i]=='H') { f[i&1][j]=f[(i-1)&1][j+1]%mod; } if(c[i]=='C') { f[i&1][j]=f[(i-1)&1][j+1]%mod; if(j>0) f[i&1][j]=(f[i&1][j]+f[(i-1)&1][j-1])%mod; } } } printf("%d",f[n&1][0]%mod); return 0; }
BZOJ3709
在一款电脑游戏中,你需要打败n只怪物(从1到n编号)。为了打败第i只怪物,你需要消耗d[i]点生命值,但怪物死后会掉落血药,使你恢复a[i]点生命值。任何时候你的生命值都不能降到0(或0以下)。
请问是否存在一种打怪顺序,使得你可以打完这n只怪物而不死掉。
N<=10^5
这个题是不是蜜汁眼熟?
没错!它就是lyd之前讲贪心讲到的那个题:
#include<iostream> #include<cstdio> #include<algorithm> #include<cmath> #include<cstring> #define ll long long using namespace std; inline int read(){ int ans=0; char last=' ',ch=getchar(); while(ch>'9'||ch<'0') last=ch,ch=getchar(); while(ch>='0'&&ch<='9') ans=(ans<<1)+(ans<<3)+ch-'0',ch=getchar(); if(last=='-') ans=-ans; return ans; } int n,x,y,upc,downc; ll z; struct node{ int a,d,id; }up[100005],down[100005]; bool cmp1(node i,node j){ return i.d<j.d; } bool cmp2(node i,node j){ return i.a>j.a; } int main(){ n=read();scanf("%lld",&z); for(int i=1;i<=n;i++){ x=read();y=read(); if(x>y) down[++downc].a=y,down[downc].d=x,down[downc].id=i; else up[++upc].a=y,up[upc].d=x,up[upc].id=i; } sort(up+1,up+upc+1,cmp1); sort(down+1,down+downc+1,cmp2); for(int i=1;i<=upc;i++){ z-=up[i].d; if(z<=0){ printf("NIE"); return 0; } z+=up[i].a; } for(int i=1;i<=downc;i++){ z-=down[i].d; if(z<=0) { printf("NIE"); return 0; } z+=down[i].a; } printf("TAK\n"); for(int i=1;i<=upc;i++)printf("%d ",up[i].id); for(int i=1;i<=downc;i++) printf("%d ",down[i].id); return 0; }
bzoj4922
给出一些括号序列,要求选择一些括号序列拼接成一个合法的括号序列,使得总长最大。
1<=n<=300,表示括号序列的个数
括号序列的长度len不超过300.
这是一道调了3天的题(我太难了)
首先对于已经配对的括号,我们不必再去考虑,因此可以先将其删掉:
举个例子:
))()(),显然对于子串"()()",我们可以不考虑它。
那么最后消成的,一定是这样的序列:))…)((…((,左端为右括号,右端为左括号
因此我们可以先将这一部分处理掉:
//用x来记录没有消掉的右括号的数量,y记录没有消掉的左括号的数量 /*结构体struct node{ int l,r,len; //l表示这个序列的左端右括号数 //r表示这个序列的右端左括号数 //len表示这段序列的长度 }a[mx]*/ for(int i=1,len,x,y;i<=n;i++){ scanf("%s",s+1); x=y=0; len=strlen(s+1); sum+=len;//记录整个字符串共有多长 a[i].len=len; for(int i=1;i<=len;i++) { if(s[i]=='(') y++; else y?y--:x++; } a[i].l=x; a[i].r=y; }
然后回到了一个很熟悉的题目:
按照这道题的贪心思路,我们把左括号看成+1,右括号看成-1,首先肯定是要考虑左边右括号小于右边左括号的,因此将左括号数量>右括号数量的排在前面,右括号数量>左括号数量的排在后面;
然后对于每一部分,我们又应该怎么排列呢?
对于左括号数量>右括号数量的,我们按照右括号数量,右括号越少,排的位置越靠前。
对于左括号数量<右括号数量的,我们按照左括号数量,左括号越多,排的位置越靠前。
bool cmp(node x,node y){ if((x.l<x.r)&&(y.l<y.r)) return x.l<y.l;//如果比较的两个字符串都是左括号数>右括号数,比较右括号数量,右括号少的在前; if((x.l<x.r)&&(y.l<y.r)) return x.l<x.r;//如果比较的两个字符串中有一个满足左括号数>右括号数,另一个不满足(两个都满足的情况会在上面return掉),那么我们看一下x是否满足右括号数<左括号数,如果满足,显然x在前y在后,反之。 return x.r>y.r;//最后剩下的是左括号数<右括号数的,也就按照左括号数从大到小排序; } int main(){ …… sort(a+1,a+n+1,cmp); …… return 0; }
按照这样排序之后,我们进行dp:
设\(f[i][j]\)表示前i个字符串,左右括号之和为j(左+1右-1)时的长度最长是多少w?
然后考虑转移:
对于\(f[i][j]\)如果不选第i个子串:\(f[i][j]<=f[i-1][j]\)
如果选择第i个子串:\(f[i][j]<=f[i-1][j-a[i].r+a[i].l]+a[i].len\)
因此,\(f[i][j]\)最终结果是在两者中取max:\(f[i][j]=max(f[i-1][j],f[i-1][j-a[i].r+a[i].l]+a[i].len);\)
然后注意考虑\(j-a[i].r+a[i].l>=0\)
\(CODE:\)
#include<cstdio> #include<algorithm> #include<cstring> #include<iostream> #define ll long long using namespace std; inline int read(){ int ans=0; char last=' ',ch=getchar(); while(ch>'9'||ch<'0') last=ch,ch=getchar(); while(ch>='0'&&ch<='9') ans=(ans<<1)+(ans<<3)+ch-'0',ch=getchar(); if(last=='-') ans=-ans; return ans; } int n,B,A,ans; char s[310]; struct node{ int l/*left )*/,r/*right (*/,len; }a[310]; int f[310][90001]; bool cmp(node x,node y){ if((x.l<x.r)&&(y.l<y.r)) return x.l<y.l; if((x.l<x.r)||(y.l<y.r)) return x.l<x.r; return x.r>y.r; } int main(){ n=read(); //x:right brackets //y:left brackets int sum=0; for(int i=1,len,x,y;i<=n;i++){ scanf("%s",s+1); x=y=0; len=strlen(s+1); sum+=len; a[i].len=len; for(int i=1;i<=len;i++) { if(s[i]=='(') y++; else y?y--:x++; } a[i].l=x; a[i].r=y; } sort(a+1,a+n+1,cmp); memset(f,200,sizeof(f)); f[0][0]=0; for(int i=1;i<=n;i++){ for(int j=0;j<=sum;j++){ f[i][j]=f[i-1][j]; if(i==n&&a[i].r!=0&&j==0) continue; if(j+a[i].l-a[i].r>=0) f[i][j]=max(f[i][j],f[i-1][j+a[i].l-a[i].r]+a[i].len); } } printf("%d",f[n][0]); return 0; }