题意:N个学生合影,站成左端对齐的五排,每排分别有N[1],N[2],N[3],N[4],N[5]个人,第一排站在最后,第k排站在最前边。学生的身高互不相同,现在要求,每一排从左到右身高依次降低,每一列从后往前身高依次降低。现在问总共能安排多少种合影方式。
思路:把人按身高的高低一个一个往队伍里加,先加身高高的,在加进去身高低的 。如果用a[i]表示每一排现在的人数,则加进新人时,则第一排可以随便加,则第二排有限制条件,只有当第一排的人多于第二排,并且第二排人数没有加满时,才可以往里面加人。第三,四,五排的限制条件也是这样。则因为每一排人数都是符合线性状态的,所以五排看成是五个维度,而每一维人数的变化,就会影响状态的改变,人数的多少又影响了问题的规模。所以创建一个五维数组来表示当前的状态,而又因为状态的规模由人数决定,所以上一个状态应当是五个维度任意一个维度比当前这以维度人数少的状态推过来的,即 f[ i ][ j ][ k ][ p ][ q ]=f[ i-1][ j ][ k ][ p ][ q ]+f[ i ][ j-1 ][ k ][ p ][ q ]+...(另外三个维度状态) 。 当有多个维度的状态可以分解成各个维度上状态推演而来。
由这道题想起了求两个串的最长公共子序列问题。这道题也相当于是两个维度的状态,第一个串的长度可以看成是第一个维度,第二个串的长度可以看成是第二个维度。则 f[ i ][ j ]表示第一个串的长度为 i 时,第二个串的长度为 j 时的最长公共子序列。则当前维度可以由第一个维度的长度减小推出来 f[ i-1 ][ j ]和第二个维度的长度减小推出来 f[ i ][ j-1 ] 和两个维度都减小 推出来 f[ i-1 ][ j-1 ]+1;
#include<bits/stdc++.h> using namespace std; typedef long long ll; ll f[33][33][33][33][33]; ll a[6]; int n; int main() { while(scanf("%d",&n)==1&&n) { for(int i=1;i<=n;i++) cin>>a[i]; /*for(int i=1;i<=a[1];i++) for(int j=1;j<=a[2];j++) for(int k=1;k<=a[3];k++) for(int p=1;p<=a[4];p++) for(int q=1;q<=a[5];q++) f[i][j][k][p][q]=0;*/ memset(f,0,sizeof f); f[0][0][0][0][0]=1; for(int i=0;i<=a[1];i++) for(int j=0;j<=a[2];j++) for(int k=0;k<=a[3];k++) for(int p=0;p<=a[4];p++) for(int q=0;q<=a[5];q++) { if(i<a[1]) f[i+1][j][k][p][q]+=f[i][j][k][p][q]; if(j<a[2]&&i>j) f[i][j+1][k][p][q]+=f[i][j][k][p][q]; if(k<a[3]&&j>k) f[i][j][k+1][p][q]+=f[i][j][k][p][q]; if(p<a[4]&&k>p) f[i][j][k][p+1][q]+=f[i][j][k][p][q]; if(q<a[5]&&p>q) f[i][j][k][p][q+1]+=f[i][j][k][p][q]; } printf("%lld ",f[a[1]][a[2]][a[3]][a[4]][a[5]]); } return 0; }
题意:给你一个N*M的带权矩阵,起点(1,1),终点(N,M),现在让你找两条路线使得路上经过的点的权值和最大,且每次只能往下或者往右走。
思路:线性动态规划 ,步长是线性增长的,可以用它作为阶段,但是这不足以表示一个状态,应当加上两条线路的末位置,但是又因为x1+y1=x2+y2=步长+2,所以可以用x1,x2代替y1,y2,所以实际上是三个状态参量i,x1,x2,。下面上代码
#include<bits/stdc++.h> using namespace std; long long f[3000][55][55]; long long A[55][55]; int n,m; void move_to(int i,int x_1,int y_1,int x_2,int y_2,int x01,int x02) { if(x_1==x_2&&y_1==y_2) f[i+1][x_1][x_2]=max(f[i+1][x_1][x_2],f[i][x01][x02]+A[x_1][y_1]); else f[i+1][x_1][x_2]=max(f[i+1][x_1][x_2],f[i][x01][x02]+A[x_1][y_1]+A[x_2][y_2]); } int main() { scanf("%d%d",&n,&m); for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) cin>>A[i][j]; f[0][0][1]=A[1][1]; int y1,y2; for(int i=0;i<m+n-2;i++) for(int x1=1;x1<=min(n,i+1);x1++) for(int x2=1;x2<=min(n,i+1);x2++) { y1=i+2-x1; y2=i+2-x2; move_to(i,x1,y1+1,x2,y2+1,x1,x2); move_to(i,x1,y1+1,x2+1,y2,x1,x2); move_to(i,x1+1,y1,x2,y2+1,x1,x2); move_to(i,x1+1,y1,x2+1,y2,x1,x2); } cout<<f[n+m-2][n][n]; return 0; }
题意:在N*M的带权矩阵中,找一个包含K个格子的凸连通块,这个连通块的权值和最大。并且给出连通块的具体方案,即输出连通块当中的每个块的横纵坐标
思路:从第一行到最后一行,一行一行的处理,并且已经选择的格子数随之都会随之线性增大,用来当作阶段,即把那种随着问题规模而增大或减小的量做为阶段,但是一个阶段还不足以表示一个状态,这里又选择每一行格子的 左端点,右端点,当前行左右端点的单调性作为维度,这样6个维度构成了一个状态。这样又单独考虑每一个维度和它的上一个状态维度的关系。当当前状态的某个维度可以推出上一个状态对应维度的不同的几个量时,这就要分类讨论。
同时这道题阶段和状态的关系可以看成:一个阶段包含多个状态,每个状态的最优值都是由上一个阶段的某几个与它有关的子状态进行比较得出最优值(同时上一个阶段的这几个子状态本身也是最优子状态)。这样只有把一个阶段里的所有最优子状态全部枚举出来,才能进入下一个阶段。
同时:求某一段序列的和,用前缀和之差。
#include<bits/stdc++.h> using namespace std; typedef long long ll; ll F[20][250][20][20][2]; int n,m; ll w[20][20]; ll sum[20][20]; int i,j,l,r,x,y; int K; struct node{ int l,r,x,y; }pre[20][250][20][20][2]; void read() { scanf("%d%d%d",&n,&m,&K); for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) { cin>>w[i][j]; sum[i][j]+=sum[i][j-1]; } } void update(ll dat,int L,int R,int x,int y) { ll ans=F[i][j][l][r][x][y]; node& p=pre[i][j][l][r][x][y]; if(ans>=dat) return ; else { ans=dat; p=(node){L,R,x,y}; } } void print(int ai,int k,int L,int R,int p,int q) { if(k==0) return ; node cnt; cnt.l=pre[ai][k][L][R][p][q].l,cnt.r=pre[ai][k][L][R][p][q].r; cnt.x=pre[ai][k][L][R][p][q].x,cnt.y=pre[ai][k][L][R][p][q].y; print(i-1,k-sum[ai][R]-sum[ai][L-1],cnt.l,cnt.r,cnt.x,cnt.y); for(int j=l;j<=r;j++) cout<<ai<<" "<<j<<" "; } void solve() { for(i=1;i<=n;i++) for(j=1;j<=K;j++) for(l=1;l<=M;l++) for(r=l;l<=M;r++) { ll t=sum[i][r]-sum[i][l-1]; if(t>j) break; x=1,y=0; if(j==t) {F[i][j][l][r][1][0]=t+F[i-1][0][0][1][0]; node& p;p=(node){0,0,1,0}; } else { for(int p=l;p<=r;p++) for(int q=p;q<=r;q++) update(F[i-1][j-t][p][q][1][0]+t,p,q,1,0); } x=1,y=1; for(int p=l;p<=r;p++) for(int q=r;q<=m;q++) { update(F[i-1][j-t][p][q][1][0]+t,p,q,1,0); update(F[i-1][j-t][p][q][1][1]+t,p,q,1,1); } x=0,y=1; for(int p=l;p<=r;p++) for(int q=p;q<=r;q++) { update(F[i-1][j-t][p][q][0][0]+t,p,q,0,0); update(F[i-1][j-t][p][q][0][1]+t,p,q,0,1); update(F[i-1][j-t][p][q][1][0]+t,p,q,1,0); update(F[i-1][j-t][p][q][1][1]+t,p,q,1,1); } x=0,y=0; for(int p=1;p<=l;p++) for(int q=l;q<=r;q++) { update(F[i-1][j-t][p][q][0][0]+t,p,q,0,0); update(F[i-1][j-t][p][q][1][0]+t,p,q,1,0); } } ll ans=-1; node cnt; int ai,l0,r0,p0,q0; for(i=1;i<=n;i++) for(l=1;l<=m;l++) for(r=1;r<=m;r++) for(ll p=0;p<=1;p++) for(ll q=0;q<=1;q++) { if(F[i][K][l][r][p][q]>ans) ans=F[i][K][l][r][p][q]; ai=i,l0=l,r0=r,p0=p,q0=q; } cout<<ans<<" "; print(ai,K,l0,r0,p0,q0); } int main() { read(); solve(); return 0; }
字符串和线性dp结合 P1026 https://www.luogu.org/problemnew/show/P1026
题意:给出一个长度不超过200的字符串,规定以每行20个字符的形式输入,现在要把这个字符串分成k份。问你怎么分才可以使得每份中包含的在字典中出现的单词数总和达到最大。同时给你一个字典,这个字典里列举了单词。
思路:一个序列随着你的划分,你的划分的份数和序列里字符的下标是线性增加的,所以选用份数和最后一份的最后一个字符的下标作为dp的阶段。即 dp[ i ][ j ]表示以第i个字符结尾,划分了 j 份的单词总数。动态转移方程 dp[ i ][ j ]=dp[ k ][ j-1 ]+sum[ k+1 ][ i ] .sum[ i ][ j ]为第i个字符到第j 个字符包含的单词数,提前预处理。
编程难点:1.首先是题目中说,当一个单词和其他单词不能重叠。
就是一个单词的出现后其第一个字符不能在被别的单词当成自己的一部分所使用。所以我们预处里sum数组的时候
从后往前处理:sum[i][j]=sum[i+1][j];
2.然后再去判断新增加一个字符后,是否使得单词数目增加了,这里要注意:
for(int i=1;i<=n;i++)
if(str.find(a[i])==0) return true; 为什么判断返回的是否是起始下标0,要注意。
3.注意substr的用法,其从某个string类型中提取出指定下标之间的字符。
4.同时dp的i,j,k的范围也要注意。j表示份数,所以 j 不可能小于字符的数目 i 。字符的数目都不够的话,就不可能够 j 份
同时 k 表示 j-1 份的最后一个字符下标。所以k至少是从j-1开始的。
#include<bits/stdc++.h> using namespace std; int n; int p,k; string s; string a[7]; int dp[210][50]; int sum[210][210]; bool check(int x,int y) { string str=s.substr(x,y-x+1); for(int i=1;i<=n;i++) if(str.find(a[i])==0) return true; return false; } int main() { scanf("%d%d",&p,&k); s+='0'; for(int i=1;i<=p;i++) { string x; cin>>x; s+=x; } cin>>n; for(int i=1;i<=n;i++) cin>>a[i]; int len=(int)s.length()-1; for(int j=len;j>=1;j--) for(int i=j;i>=1;i--) { sum[i][j]=sum[i+1][j]; if(check(i,j)) sum[i][j]++; } //dp[0][0]=0; //for(int i=1;i<=k;i++) dp[i][i]=dp[i-1][i-1]+sum[i][i]; //for(int i=1;i<=len;i++) dp[i][1]=sum[1][i]; for(int i=1;i<=len;i++) for(int j=1;j<=k;j++) { if(j<=i) for(int p=j-1;p<i;p++) dp[i][j]=max(dp[i][j],dp[p][j-1]+sum[p+1][i]); } cout<<dp[len][k]; return 0; }