最近两周做了动态规划的23道经典好题,涉及到区间、树形、数位等三种动态规划类型,现在将这23道题的题解写在下面,方便大家借鉴以及我加深记忆。
upd at:20190814 20:46.T7二叉苹果树
1、石子合并
经典的区间DP问题,枚举合并的堆数作为阶段,设f[i][j]表示i->j这段区间内的最优方案,考虑在这段区间内枚举断点k,不难得到f[i][j]=min(f[i][k]+f[k+1][j]+sum(i,j))(最大值同理)。破环为链后直接进行DP即可。
#include<iostream> #include<cstring> #include<cstdio> using namespace std; int read() { int x=0,f=1;char ch=getchar(); while(ch<‘0‘||ch>‘9‘){if(ch==‘-‘)f=0;ch=getchar();} while(ch>=‘0‘&&ch<=‘9‘){x=(x<<3)+(x<<1)+(ch^48);ch=getchar();} if(f)return x;return -x; } int n,a[1005],f[505][505],g[505][505],prefix[505],minn=21374404,maxn; int main() { //freopen("A.in","r",stdin); //freopen("A.out","w",stdout); memset(f,20,sizeof(f)); n=read(); for(int i=1;i<=n;i++) { a[i]=read(); a[i+n]=a[i]; } for(int i=1;i<=n*2;i++) { prefix[i]=prefix[i-1]+a[i]; f[i][i]=0; } for(int i=2;i<=n;i++) { for(int j=1;j<=2*n-i+1;j++) { int end=i+j-1; for(int k=j;k<end;k++) { f[j][end]=min(f[j][end],f[j][k]+f[k+1][end]+prefix[end]-prefix[j-1]); g[j][end]=max(g[j][end],g[j][k]+g[k+1][end]+prefix[end]-prefix[j-1]); } } } for(int i=1;i<=n;i++) { minn=min(minn,f[i][i+n-1]); maxn=max(maxn,g[i][i+n-1]); } printf("%d\n%d\n",minn,maxn); //fclose(stdin); //fclose(stdout); return 0; }
石子合并
2、能量项链
同“石子合并”,将第一题的求和换成题目指定的模拟规则即可。
#include<iostream> #include<cstring> #include<cstdio> using namespace std; int read() { int x=0,f=1;char ch=getchar(); while(ch<‘0‘||ch>‘9‘){if(ch==‘-‘)f=0;ch=getchar();} while(ch>=‘0‘&&ch<=‘9‘){x=(x<<3)+(x<<1)+(ch^48);ch=getchar();} if(f)return x;return -x; } int n,a[1005],f[505][505],b[1005],maxn; int main() { //freopen("B.in","r",stdin); //freopen("B.out","w",stdout); n=read(); for(int i=1;i<=n;i++) { a[i]=read(); a[i+n]=a[i]; } for(int i=1;i<=n*2-1;i++) { b[i]=a[i+1]; } b[n*2]=a[1]; for(int i=2;i<=n;i++) { for(int j=1;j<=2*n-i+1;j++) { int end=i+j-1; for(int k=j;k<end;k++) { f[j][end]=max(f[j][end],f[j][k]+f[k+1][end]+a[j]*b[k]*b[end]); } } } for(int i=1;i<=n;i++) { maxn=max(maxn,f[i][i+n-1]); } printf("%d\n",maxn); //fclose(stdin); //fclose(stdout); return 0; }
能量项链
3、凸多边形的划分
设f[i][j]为i号节点到j号节点组成的凸多边形的最优剖分,我们可以在这段区间内找到一个非i、j的顶点k,剖分出一个以i,j,k为顶点的三角形和两个凸多边形.
然后问题就转化成了如何求这两个小凸多边形的和,从小区间到大区间转移的过程中,小区间是已经被计算过的,直接调用即可。
状态转移方程如下:f[i][j]=min(f[i][j],f[i][k]+f[k][j]+a[i]*a[j]*a[k]);
注意,此题需要用高精度或int128
#include<iostream> #include<cstdio> #include<cstring> #define int __uint128_t using namespace std; int read() { int x=0,f=1;char ch=getchar(); while(ch<‘0‘||ch>‘9‘){if(ch==‘-‘)f=0;ch=getchar();} while(ch>=‘0‘&&ch<=‘9‘){x=(x<<3)+(x<<1)+(ch^48);ch=getchar();} if(f)return x;return -x; } void myitoa(int a,char* s) { int w=0; while(a>0) { s[++w]=a%10+‘0‘; a/=10; } s[0]=w; } int n,a[505],f[110][110]; char s[110]; signed main() { //freopen("C.in","r",stdin); //freopen("C.out","w",stdout); n=read(); for(int i=1;i<=n;i++)a[i]=read(); for(int i=2;i<=n-1;i++) { for(int j=1;j<=n-i;j++) { int end=i+j; f[j][end]=1e30; for(int k=j+1;k<end;k++) { f[j][end]=min(f[j][end],f[j][k]+f[k][end]+a[j]*a[k]*a[end]); } } } myitoa(f[1][n],s); for(int i=s[0];i>=1;i--) { cout<<s[i]; } //fclose(stdin); //fclose(stdout); return 0; }
凸多边形的划分
4、括号匹配
通过题目很容易发现这道题的边界值:当i和i+1可以匹配的时候,它们合并的代价为0,否则为2。直接按照区间DP模板进行合并,两部分的代价加起来取最小就是最优值,特别地,当两个端点本身就可以匹配的时候,还要和中间的再取一次min。
#include<iostream> #include<cstring> #include<cstdio> using namespace std; int read() { int x=0,f=1;char ch=getchar(); while(ch<‘0‘||ch>‘9‘){if(ch==‘-‘)f=0;ch=getchar();} while(ch>=‘0‘&&ch<=‘9‘){x=(x<<3)+(x<<1)+(ch^48);ch=getchar();} if(f)return x;return -x; } char a[155]; int n,f[155][155]; int main() { memset(f,0x3f,sizeof(f)); cin>>(a+1); n=strlen(a+1); for(int i=1;i<=n;i++)f[i][i]=1; for(int i=1;i<=n;i++) { if(a[i]==‘(‘&&a[i+1]==‘)‘)f[i][i+1]=0; else if(a[i]==‘[‘&&a[i+1]==‘]‘)f[i][i+1]=0; else f[i][i+1]=2; } for(int i=2;i<=n;i++) { for(int j=1;j<=n-i+1;j++) { int end=i+j-1; for(int k=j;k<end;k++) { f[j][end]=min(f[j][end],f[j][k]+f[k+1][end]); } if(a[j]==‘(‘&&a[end]==‘)‘)f[j][end]=min(f[j][end],f[j+1][end-1]); else if(a[j]==‘[‘&&a[end]==‘]‘)f[j][end]=min(f[j][end],f[j+1][end-1]); } } cout<<f[1][n]<<endl; return 0; }
括号匹配
5、分离与合体
需要记录合并的地方,然后递归输出即可。
#include<iostream> #include<cstring> #include<cstdio> using namespace std; int read() { int x=0,f=1;char ch=getchar(); while(ch<‘0‘||ch>‘9‘){if(ch==‘-‘)f=0;ch=getchar();} while(ch>=‘0‘&&ch<=‘9‘){x=(x<<3)+(x<<1)+(ch^48);ch=getchar();} if(f)return x;return -x; } int n,a[505],f[505][505],b[505][505]; void dfs(int x,int y,int dep,int k) { if(x>=y)return; if(dep==k) { printf("%d ",b[x][y]); return; } dfs(x,b[x][y],dep+1,k); dfs(b[x][y]+1,y,dep+1,k); } int main() { n=read(); for(int i=1;i<=n;i++)a[i]=read(); for(int i=2;i<=n;i++) { for(int j=1;j<=n-i+1;j++) { int end=i+j-1; for(int k=j;k<end;k++) { if(f[j][k]+f[k+1][end]+(a[j]+a[end])*a[k]>f[j][end]) { f[j][end]=f[j][k]+f[k+1][end]+(a[j]+a[end])*a[k]; b[j][end]=k; } } } } printf("%d\n",f[1][n]); for(int i=1;i<n;i++) { dfs(1,n,1,i); } }
分离与合体
6、矩阵取数游戏
DP五分钟,高精两小时!!!!!!这题DP其实还蛮好想的,因为小区间是由大区间转移而来,所以每一个区间要么是由左边取一个数后得到,要么是由右边取一
数得到,不难得到如下的转移方程:
f[i][j]=max(f[i-1][j]+base[m-j+i-1]*a[i-1],f[i][j+1]+base[m-j+i-1]*a[j+1]);
最后由于空区间无法表示,最后还要再取一边max。
#include<iostream> #include<cstring> #include<cstdio> #define int long long using namespace std; int read() { int x=0,f=1;char ch=getchar(); while(ch<‘0‘||ch>‘9‘){if(ch==‘-‘)f=0;ch=getchar();} while(ch>=‘0‘&&ch<=‘9‘){x=(x<<3)+(x<<1)+(ch^48);ch=getchar();} if(f)return x;return -x; } int n,m,a[110],f[110][110][110],base[110][110],ans[110],maxn,s1[110],s2[110],s3[110],s4[110],s5[110],s6[110],s7[110],s8[110]; void Mark(int c[]) { for(int i=1;i<=30;i++) { c[i+1]+=c[i]/10000; c[i]%=10000; } for(int i=30;i>=1;i--) { if(c[i]) { c[0]=i;break; } } } void Mul(int a[],int b,int c[]) { for(int i=1;i<=a[0];i++)c[i]=a[i]*b; Mark(c); } void Add(int a[],int b[],int c[]) { Mark(a);Mark(b); if(a[0]>b[0])c[0]=a[0]; else c[0]=b[0]; for(int i=1;i<=c[0];i++)c[i]=a[i]+b[i]; Mark(c); } bool compare(int a[],int b[]) { Mark(a);Mark(b); if(a[0]<b[0])return 0;if(a[0]>b[0])return 1; for(int i=a[0];i>=1;i--) { if(a[i]<b[i])return 0; if(b[i]<a[i])return 1; } return 0; } void pre() { base[0][0]=base[0][1]=1; for(int i=1;i<=m;i++)Mul(base[i-1],2,base[i]); } void write(int ans[]) { cout<<ans[ans[0]]; for(int i=ans[0]-1;i>=1;i--) { cout<<ans[i]/1000; cout<<ans[i]/100%10; cout<<ans[i]/10%10; cout<<ans[i]%10; } } signed main() { //freopen("F.in","r",stdin); //freopen("F.out","w",stdout); n=read();m=read(); pre(); while(n--) { memset(f,0,sizeof(f)); memset(s5,0,sizeof(s5)); memset(s6,0,sizeof(s6)); for(int i=1;i<=m;i++)a[i]=read(); for(int i=1;i<=m;i++) { for(int j=m;j>=i;j--) { memset(s1,0,sizeof(s1)); memset(s2,0,sizeof(s2)); memset(s7,0,sizeof(s7)); memset(s8,0,sizeof(s8)); Mul(base[m-j+i-1],a[i-1],s1); Add(s1,f[i-1][j],s7); Mul(base[m-j+i-1],a[j+1],s2); Add(s2,f[i][j+1],s8); if(compare(s7,s8))memcpy(f[i][j],s7,sizeof(f[i][j])); else memcpy(f[i][j],s8,sizeof(f[i][j])); } } for(int i=1;i<=m;i++) { memset(s3,0,sizeof(s3)); memset(s4,0,sizeof(s4)); Mul(base[m],a[i],s3); Add(f[i][i],s3,s4); if(compare(s4,s5))memcpy(s5,s4,sizeof(s5)); } Add(ans,s5,s6); memcpy(ans,s6,sizeof(ans)); } write(ans); //fclose(stdin); //fclose(stdout); return 0; } /* 2 10 96 56 54 46 86 12 23 88 80 43 16 95 18 29 30 53 88 83 64 67 */
矩阵取数游戏
7、二叉苹果树
建树后依次枚举给左右子树保留的树枝,并不难的记忆化搜索。
#include<iostream> #include<cstring> using namespace std; int mp[1005][1005],n,q,l[1005],r[1005],a[1005],f[1005][1005]; void Maketree(int node) { for(int i=1;i<=n;i++) { if(mp[node][i]==-1)continue; l[node]=i;a[i]=mp[node][i]; mp[node][i]=mp[i][node]=-1; Maketree(i); break; } for(int i=1;i<=n;i++) { if(mp[node][i]==-1)continue; r[node]=i;a[i]=mp[node][i]; mp[node][i]=mp[i][node]=-1; Maketree(i); break; } } int dfs(int u,int w) { if(w==0)return 0; if(l[u]==0&&r[u]==0)return a[u]; if(f[u][w])return f[u][w]; for(int i=0;i<=w-1;i++) { f[u][w]=max(f[u][w],dfs(l[u],i)+dfs(r[u],w-i-1)+a[u]); } return f[u][w]; } int main() { ios::sync_with_stdio(0);cin.tie(0);cout.tie(0); memset(mp,-1,sizeof(mp)); cin>>n>>q; for(int x,y,z,i=1;i<n;i++) { cin>>x>>y>>z; mp[x][y]=mp[y][x]=z; } Maketree(1); cout<<dfs(1,q+1)<<endl; return 0; }
二叉苹果树
原文地址:https://www.cnblogs.com/szmssf/p/11354589.html