DP的训练

codesonic

2018-07-17 20:25:38

Personal

因为我DP太弱了所以要多加训练啊。。。现在放一个专版DP题来切,做一道就放一道,按从简单到难由上到下(个人看法) ### 1.[P1052 过河](https://www.luogu.org/problemnew/show/P1052) 如果不用离散就是一道简单DP 因为独木桥长度太长了,而石头少,所以我们可以把它离散下来 然后就是简单题了 ```cpp #include<iostream> #include<cstdio> #include<cstring> #include<algorithm> using namespace std; const int maxn=400010; int l,s,t,m,ans; int f[maxn]; int a[maxn],d[maxn],rck[maxn]; bool check[maxn]; #define ll int namespace fib{char b[300000]= {},*f=b;} #define gc ((*fib::f)?(*(fib ::f++)):(fgets(fib::b,sizeof(fib::b),stdin)?(fib::f=fib::b,*(fib::f++)):-1)) inline void in(ll &x){x=0;char c;bool f=0;while((c=gc)>'9'||c<'0')if(c=='-')f=!f;x=c-48;while((c=gc)<='9'&&c>='0')x=x*10+c-48;if(f)x=-x;} namespace fob{char b[300000]= {},*f=b,*g=b+300000-2;} #define pob (fwrite(fob::b,sizeof(char),fob::f-fob::b,stdout),fob::f=fob::b,0) #define pc(x) (*(fob::f++)=(x),(fob::f==fob::g)?pob:0) struct foce{~foce(){pob;fflush(stdout);}} _foce; namespace ib{char b[100];} inline void out(ll x){if(x==0){pc(48);return;}if(x<0){pc('-');x=-x;}char *s=ib::b;while(x) *(++s)=x%10,x/=10;while(s!=ib::b) pc((*(s--))+48);} inline void outn(ll x){out(x);pc('\n');} inline void swap(ll &x,ll &y){ll t=x;x=y;y=t;} inline ll jdz(ll x){return x>0?x:-x;} inline ll max(ll x,ll y){return x>y?x:y;} inline ll min(ll x,ll y){return x<y?x:y;} int main() { in(l);in(s);in(t);in(m); for(register int i=1;i<=m;++i) in(a[i]); sort(a+1,a+m+1); for(register int i=1;i<=m;i++){ d[i]=(a[i]-a[i-1])%2520; } memset(check,0,sizeof(check)); for(register int i=1;i<=m;i++){ check[a[i]=a[i-1]+d[i]]=1; } int len=a[m]; for(register int i=1;i<=len+t;++i){ f[i]=m; } f[0]=0; for(register int i=1;i<=len+t;++i){ for(int j=s;j<=t;j++){ if(i-j>=0) f[i]=min(f[i],f[i-j]); f[i]+=check[i]; } } ans=m; for(register int i=len;i<len+t;i++) ans=min(ans,f[i]); outn(ans); return 0; } ``` ### 2.[车的放置](https://www.luogu.org/problemnew/show/P1350) 一道神仙题,题解里给出了吊打各种做法的做法。。。 其他做法窝根本想不到啊。。。 f[i][j]表示前i行放j个的方案,然后记录每一列的高度,翻转一下dp就行了 可以学习的地方: ### 对于不规则图形,可以通过反转使它规则 ```cpp #include<bits/stdc++.h> using namespace std; const int N=2e3+5; const int maxn=2010,modd=100003; int f[maxn][maxn],h[maxn]; inline void read(int &x) { x=0; int f=1; char c=getchar(); while(c<'0'||c>'9') { if(c=='-')f=-f; c=getchar(); } while(c>='0'&&c<='9') { x=x*10+c-'0'; c=getchar(); } x*=f; } int a,b,c,d,k; int main() { read(a);read(b);read(c);read(d),read(k); for(int i=1;i<=c;i++) f[i][0]=1,h[i]=d; for(int i=1;i<=a;i++) h[c+i]=d+b,f[c+i][0]=1; for(int i=c+1;i<=a+c;i++) f[i][0]=1,h[i]=b+d; f[0][0]=1; for(int i=1;i<=a+c;i++) for(int j=1;j<=k;j++) f[i][j]=(f[i-1][j]+f[i-1][j-1]*(h[i]-j+1))%modd; printf("%d",f[a+c][k]); return 0; } ``` ### 3.[乌龟棋](https://www.luogu.org/problemnew/show/P1541) 一道不错的递推题 四维DP 每一维枚举一种卡牌的数量 枚举“我从哪里来(即使用了哪一张卡牌到此地)”进行得分计算 发现有点像DAG上的最短路=_= 就这么做了 ```cpp #include<iostream> #include<cstring> #include<cstdio> using namespace std; int f[55][55][55][55]; int a[355]; int x[5]; int main() { memset(x,0,sizeof(x)); int n,m; scanf("%d%d",&n,&m); for(int i=1;i<=n;i++) scanf("%d",&a[i]); for(int i=1;i<=m;i++){ int tmp; scanf("%d",&tmp); x[tmp]++; } f[0][0][0][0]=a[1]; for(int i=0;i<=x[1];i++) for(int j=0;j<=x[2];j++) for(int k=0;k<=x[3];k++) for(int l=0;l<=x[4];l++){ int v=1+i+2*j+3*k+4*l; if(i!=0) f[i][j][k][l]=max(f[i][j][k][l],f[i-1][j][k][l]+a[v]); if(j!=0) f[i][j][k][l]=max(f[i][j][k][l],f[i][j-1][k][l]+a[v]); if(k!=0) f[i][j][k][l]=max(f[i][j][k][l],f[i][j][k-1][l]+a[v]); if(l!=0) f[i][j][k][l]=max(f[i][j][k][l],f[i][j][k][l-1]+a[v]); } printf("%d\n",f[x[1]][x[2]][x[3]][x[4]]); return 0; } ``` ### 4.[取数] 因为不能拿出来公开所以没有链接 ![](https://cdn.luogu.com.cn/upload/pic/27732.png) 设f[i][j][1]为i到j区间,B先取剩的答案,f[i][j][0]为i到j区间,A先取剩的答案。 显然当我们取到f[l][r][0]时,答案=min(f[l+1][r][1],f[l][r-1][1]) 于是 ```cpp #include<cstdio> #include<iostream> using namespace std; int a[2010]; int f[2010][2010][2];//0=A,1=B; int main() { int n; scanf("%d",&n); for(int i=1;i<=n;i++){ scanf("%d",&a[i]); f[i][i][0]=f[i][i][1]=a[i]; } for(int len=2;len<=n;len++){ for(int i=1,j=len;j<=n;i++,j++){ f[i][j][0]=min(f[i+1][j][1],f[i][j-1][1]); f[i][j][1]=max(f[i+1][j][0],f[i][j-1][0]); } } for(int i=1;i<=n;i++){ for(int j=1;j<=n;j++){ printf("%d ",f[i][j][0]); } printf("\n"); } printf("\n\n"); for(int i=1;i<=n;i++){ for(int j=1;j<=n;j++){ printf("%d ",f[i][j][1]); } printf("\n"); } return 0; } ``` ### 5.[染色](https://www.luogu.org/problemnew/show/P4170) 首先设f[i][j]为从i到j最少染色次数,显然对于任何的i,f[i][i]=1; 接下来对于其他的情况,若c[i]=c[j],显然f[i][j]可以由然f[i][j-1]或然f[i+1][j]一次染掉。 所以 f[i][j]=min(f[i+1][j],f[i][j-1]) 对于c[i]!=c[j],我们需要枚举个断点,分成两次染掉 所以对于i到j-1中所有的k f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]) ```cpp #include<cstdio> #include<cstring> using namespace std; inline int min(int a,int b){ return a<b?a:b; } char c[60]; int f[60][60]; int main() { scanf("%s",c); memset(f,0x7F,sizeof(f)); int n=strlen(c); for(int i=0;i<n;i++){ f[i][i]=1; } for(int len=1;len<=n;len++) for(int i=0,j=len;j<=n;i++,j++) if(c[i]==c[j]) f[i][j]=min(f[i+1][j],f[i][j-1]); else for(int k=i;k<j;k++) f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]); printf("%d",f[0][n-1]); return 0; } ``` ### 6.[单人纸牌](https://www.luogu.org/problemnew/show/P1837) 对于每一个状态计数,记录成为下一个状态的概率,比如到了某一状态有x种选择方法,则对这几种选择方案造成的状态的概率加上1/x即可 ```cpp #include<cstdio> #include<algorithm> #include<cstring> using namespace std; long double f[5][5][5][5][5][5][5][5][5]; bool check[10][10]; char c[10][10]; char tmp[20]; int head[20]; int main() { for(int i=1; i<=9; i++) { gets(tmp); c[i][1]=tmp[9]; c[i][2]=tmp[6]; c[i][3]=tmp[3]; c[i][4]=tmp[0]; } f[0][0][0][0][0][0][0][0][0]=1; for(head[1]=0; head[1]<=4; head[1]++) for(head[2]=0; head[2]<=4; head[2]++) for(head[3]=0; head[3]<=4; head[3]++) for(head[4]=0; head[4]<=4; head[4]++) for(head[5]=0; head[5]<=4; head[5]++) for(head[6]=0; head[6]<=4; head[6]++) for(head[7]=0; head[7]<=4; head[7]++) for(head[8]=0; head[8]<=4; head[8]++) for(head[9]=0; head[9]<=4; head[9]++) { if(long double p=f[head[1]][head[2]][head[3]][head[4]][head[5]][head[6]][head[7]][head[8]][head[9]]) { int cnt=0; memset(check,0,sizeof(check)); for(int i=1; i<9; i++) { for(int j=i+1; j<=9; j++) { if(head[i]+1<=4 && head[j]+1<=4 && c[i][head[i]+1]==c[j][head[j]+1]) { check[i][j]=1; cnt++; } } } if(cnt) { for(int i=1; i<9; i++) { for(int j=i+1; j<=9; j++) { if(check[i][j]) { head[i]++,head[j]++; f[head[1]][head[2]][head[3]][head[4]][head[5]][head[6]][head[7]][head[8]][head[9]]+=p/(long double)(cnt*1.0); head[i]--,head[j]--; } } } } } } printf("%.6Lf",f[4][4][4][4][4][4][4][4][4]); } ``` ### 7. [[ZJOI2005]午餐](https://www.luogu.org/problemnew/show/P2577) 毒瘤ZJOI题~~不是可怜题没兴趣~~ 显然吃饭慢的要先打饭 排序,前缀和优化 设f i,j 表示1窗口用了i时间,第j个人最早吃完饭的时间 然后就和01背包那样做做 ```cpp #include<cstdio> #include<algorithm> #include<cstring> #include<string> using namespace std; const int maxn=210; struct node{ int x,y; }a[maxn]; int b[maxn],n; int f[maxn][maxn*maxn]; inline int min(int a,int b){ return a>b?b:a; } inline int max(int a,int b){ return a<b?b:a; } inline bool cmp(node a,node b){ return a.y>b.y; } int main() { scanf("%d",&n); for(int i=1;i<=n;i++) scanf("%d%d",&a[i].x,&a[i].y); sort(a+1,a+1+n,cmp); memset(f,127,sizeof f); f[0][0]=0; for(int i=1;i<=n;i++) b[i]=b[i-1]+a[i].x; for(int i=1;i<=n;i++){ for(int j=0;j<=b[i];j++){ f[i][j]=min(f[i][j],max(f[i-1][j],b[i]+a[i].y-j)); if(j>=a[i].x) f[i][j]=min(f[i][j],max(f[i-1][j-a[i].x],a[i].y+j)); } } int ans=(1<<30); for(int i=1;i<=b[n];i++) ans=min(ans,f[n][i]); return printf("%d\n",ans),0; } ``` ### 8. [P1270 “访问”美术馆](https://www.luogu.org/problemnew/show/P1270) 树形DP,有个很有用的技巧就是可以边dfs边输入,学习到了 主要就是类似背包的方法分配给左子树和右子树时间 ```cpp // luogu-judger-enable-o2 #include<iostream> #include<cstdio> using namespace std; int n; int cnt=0; #define in2(x,y) in(x);in(y) #define ll int namespace fib{char b[300000]= {},*f=b;} #define gc ((*fib::f)?(*(fib ::f++)):(fgets(fib::b,sizeof(fib::b),stdin)?(fib::f=fib::b,*(fib::f++)):-1)) inline void in(ll &x){x=0;char c;bool f=0;while((c=gc)>'9'||c<'0')if(c=='-')f=!f;x=c-48;while((c=gc)<='9'&&c>='0')x=x*10+c-48;if(f)x=-x;} namespace fob{char b[300000]= {},*f=b,*g=b+300000-2;} #define pob (fwrite(fob::b,sizeof(char),fob::f-fob::b,stdout),fob::f=fob::b,0) #define pc(x) (*(fob::f++)=(x),(fob::f==fob::g)?pob:0) struct foce{~foce(){pob;fflush(stdout);}} _foce; namespace ib{char b[100];} inline void out(ll x){if(x==0){pc(48);return;}if(x<0){pc('-');x=-x;}char *s=ib::b;while(x) *(++s)=x%10,x/=10;while(s!=ib::b) pc((*(s--))+48);} inline void outn(ll x){out(x);pc('\n');} inline void swap(ll &x,ll &y){ll t=x;x=y;y=t;} inline ll jdz(ll x){return x>0?x:-x;} inline ll max(ll x,ll y){return x>y?x:y;} inline ll min(ll x,ll y){return x<y?x:y;} const int maxn=1010; int f[maxn][maxn]; void solve(){ int idx,tim,l,r; in2(tim,idx); tim<<=1; int root=++cnt; if(idx) for(int i=1;i<=n;++i) f[root][i]=min((i-tim)/5,idx); else{ l=cnt+1; solve(); r=cnt+1; solve(); for(int i=tim;i<=n;i++) for(int j=0;j<=i-tim;j++) f[root][i]=max(f[root][i],f[l][j]+f[r][i-j-tim]); } } int main(){ in(n); n--; solve(); outn(f[1][n]); return 0; } ``` ### 9.[[AHOI2009]中国象棋](https://www.luogu.org/problemnew/show/P2051) 毒瘤题,我抄题解没开llWA了无数遍过的 dp[i][j][k]表示放了前i行,有j列是有1个棋子,有k列有两个棋子 我们可以不用状压记录j和k分别是哪些(因为不影响后面),于是我们可以这样设。。。 然后一遍推过去就行啦=w= 不过这种题给我想我呀是真的想不到qwq。。。不像单人纸牌我能想。。。 还是要多加训练啊 ```cpp #include<iostream> #include<cstring> #include<cstdio> using namespace std; const int modd=9999973; int f[110][110][110]; inline int C(int num){ return num*(num-1)/2; } int main(){ int n,m; scanf("%d%d",&n,&m); f[0][0][0]=1; for(int i=0;i<n;i++){ for(int j=0;j<=m;j++){ for(int k=0;j+k<=m;k++){ if(f[i][j][k]){ f[i+1][j][k]=(f[i+1][j][k]+f[i][j][k])%modd; if(m-j-k>=1) f[i+1][j+1][k]=(f[i+1][j+1][k]+f[i][j][k]*(m-j-k))%modd; if(j>=1) f[i+1][j-1][k+1]=(f[i+1][j-1][k+1]+f[i][j][k]*j)%modd; if(m-j-k>=2) f[i+1][j+2][k]=(f[i+1][j+2][k]+f[i][j][k]*C(m-j-k))%modd; if(m-j-k>=1&&j>=1) f[i+1][j][k+1]=(f[i+1][j][k+1]+f[i][j][k]*(m-j-k)*j)%modd; if(j>=2) f[i+1][j-2][k+2]=(f[i+1][j-2][k+2]+f[i][j][k]*C(j))%modd; } } } } long long ans=0; for(int i=0;i<=m;i++) for(int j=0;i+j<=m;j++) ans=(ans+f[n][i][j])%modd; printf("%lld\n",ans); return 0; } ``` ### 10.[[Code+#1]找爸爸](https://www.luogu.org/problemnew/show/P4059) 神仙题,状态的设计十分巧妙。实在不是考场上随随便便能想出来的Orz (udpdate 一年后,现在觉得很好想了) 设f[i][j][k]为第一个字符串匹配了i,第二个匹配了j个,k=0,表示此时两个子字符串末尾都没有空格,k=1,第一个有空格,k=2,第二个有空格,因为如果两个都是空格答案肯定不是最优,所以不需要考虑 然后直接做就行了 ```cpp // luogu-judger-enable-o2 #include<iostream> #include<cstring> #include<string> #include<cstdio> using namespace std; const int maxn=3010; const int INF=(1<<30); char s1[maxn],s2[maxn]; int n,m,x[maxn],y[maxn],fa[260]; int dis[maxn][maxn],a,b; int f[maxn][maxn][3]; inline int maxx(int a,int b){ return a<b?b:a; } inline int maxx3(int a,int b,int c){ return maxx(a,maxx(b,c)); } int main() { fa['A']=0,fa['T']=1,fa['G']=2,fa['C']=3; scanf("%s\n%s",s1+1,s2+1); n=strlen(s1+1),m=strlen(s2+1); for(int i=1;i<=n;i++){ x[i]=fa[s1[i]]; } for(int i=1;i<=m;i++){ y[i]=fa[s2[i]]; } for(int i=0;i<=3;i++) for(int j=0;j<=3;j++) scanf("%d",&dis[i][j]); scanf("%d%d",&a,&b); for(int i=1;i<=maxx(n,m);i++){ f[i][0][0]=f[0][i][0]=f[i][0][2]=f[0][i][1]=-INF; f[i][0][1]=f[0][i][2]=-a-b*(i-1); } f[0][0][1]=f[0][0][2]=-INF; for(int i=1;i<=n;i++){ for(int j=1;j<=m;j++){ f[i][j][0]=maxx3(f[i-1][j-1][0],f[i-1][j-1][1],f[i-1][j-1][2])+dis[x[i]][y[j]]; f[i][j][1]=maxx3(f[i][j-1][1]-b,f[i][j-1][2]-a,f[i][j-1][0]-a); f[i][j][2]=maxx3(f[i-1][j][2]-b,f[i-1][j][1]-a,f[i-1][j][0]-a); } } printf("%d\n",maxx3(f[n][m][0],f[n][m][1],f[n][m][2])); return 0; } ``` ### 11.[卫宫家今天的饭](https://www.luogu.com.cn/problem/P5664) ## 减去无用状态!!! 优化DP时,可以考虑什么状态设计最有用,当前剩余什么无用状态,然后无用状态怎么丢掉(两个状态改为一个状态) 很好的启示,我当时CSP时大脑空白,练度不够。 然后要个容斥,把合法方案改为总方案-不合法方案 这个容斥也要记下来。 ```cpp #include<bits/stdc++.h> using namespace std; #define int long long const int maxn=114,maxm=2010; const int mod=998244353; int n,m,a[maxn][maxm],s[maxn]; int f[maxn][maxn<<1],g[maxn][maxn]; //f:不合法,g:总方案 //fijk表示第i行 col列的个数选了j,其他列选了k //不关心jk具体数值,仅需要jk的差 // 故优化为j=j-k //fij表示第i行col列的个数比其他列多了j个 signed main(){ scanf("%lld%lld",&n,&m); for(int i=1;i<=n;i++) for(int j=1;j<=m;j++){ scanf("%lld",&a[i][j]); (s[i]+=a[i][j])%=mod; } int ans=0; for(int col=1;col<=m;col++){ memset(f,0,sizeof f); f[0][n]=1; for(int i=1;i<=n;i++) for(int j=n-i;j<=n+i;j++) f[i][j]=(f[i-1][j] + (f[i-1][j-1]*a[i][col]*1ll)%mod + (1ll*f[i-1][j+1]*((s[i]-a[i][col])%mod))%mod)%mod; for(int i=1;i<=n;i++) ans=(ans+f[n][n+i])%mod; } g[0][0]=1; for(int i=1;i<=n;i++) for(int j=0;j<=n;j++) g[i][j]=(g[i-1][j]+(j>0?g[i-1][j-1]*s[i]*1ll%mod:0))%mod; for(int i=1;i<=n;i++) ans=(ans-g[n][i]+mod)%mod; printf("%lld\n",(mod-ans)%mod); } ``` 注意一下下标为负数的ub和取模技艺即可 ### 12.[地精部落](https://www.luogu.org/problemnew/show/P2467) 对于一个状态f[i][j],可以从i-1得出,对于j与j-1不相邻,因为不相邻所以没有什么影响(意会,大概就是不相邻的话不会有影响)。。。所以f[i][j]=f[i][j-1],如果相邻,那么除去第一个,后面的状态变成f[i-1][j-1](仍利用第一种情况的性质,山峰和山谷交换后的方案数相同,此时j-1变为山谷,因为相同可以假装他是山峰,少了一个数所以i-1),发现此时区间的含义变了,应改为i-1+1-(j-1)=i-j-1。大概就是f[j+1][i]和f[j][i-1]概念差不多。。。 最后把两种加起来就可以了 ~~DP真的是解释不清楚qwq~~ ```cpp #include<iostream> #include<cstdio> using namespace std; int f[4010][2];//滚动数组优化 int main() { int n,k; f[2][0]=1; scanf("%d%d",&n,&k); for(int i=3;i<=n;i++) for(int j=2;j<=i;j++) f[j][i&1]=(f[j-1][i&1]+f[i-j+1][(i-1)&1])%k; int ans=0; for(int i=2;i<=n;i++){ ans=(ans+f[i][n&1])%k; } printf("%d",(ans<<1)%k); return 0; } ```