一,去除冗余:
例题1:乌龟棋:
1.定义动规状态:dp【a】【b】【c】【d】,表示1,2,3,4的卡片分别用了a,b,c,d张。
2.动规关系:来自于四种状态的决策:①dp【a】【b-1】【c】【d】+val(s);
#include<bits/stdc++.h>
using namespace std;
#define Maxn 40
int dp[Maxn+5][Maxn+5][Maxn+5][Maxn+5],val[400],cnt[4];
int main(){
int n,m;
scanf("%d%d",&n,&m);
for(int i=0;i<n;i++)scanf("%d",val+i);
for(int i=0,a;i<m;i++){
scanf("%d",&a);
cnt[a-1]++;
}
dp[0][0][0][0]=val[0];
for(int a=0;a<=cnt[0];a++){
for(int b=0;b<=cnt[1];b++){
for(int c=0;c<=cnt[2];c++){
for(int d=0;d<=cnt[3];d++){
int s=a+2*b+3*c+4*d,ans=0;
dp[a][b][c][d]=val[s];
if(a)ans=max(ans,dp[a-1][b][c][d]);
if(b)ans=max(ans,dp[a][b-1][c][d]);
if(c)ans=max(ans,dp[a][b][c-1][d]);
if(d)ans=max(ans,dp[a][b][c][d-1]);
dp[a][b][c][d]+=ans;
}
}
}
}
printf("%d",dp[cnt[0]][cnt[1]][cnt[2]][cnt[3]]);
return 0;
}
分析:可以变成三维的数组,只记录 b c d 三维的卡牌的。定义dp[b][c][d]在没更新前表示dp[a-1][b][c][d],更新后表示dp[a][b][c][d]。所以....
效果:减小空间占用。
二,状态重定义:
方法1;重新定义:因为第一块墙壁的不同颜色方法是“等价”的。所以略去第一块墙壁的状态定义。
int f[15][15];
int main(){
int n,k,ans=0;
scanf("%d %d",&n,&k);//n块墙壁,k种颜色
f[1][0]=1;//只有涂0号颜色时,符合我们的假设
for(int i=2;i<=n;i++){
for(int j=0;j<k;j++){
for(int c=0;c<k;c++){
if(c==j)continue;
f[i][j]+=f[i-1][c];
}
}
}
for(int i=1;i<k;i++)ans+=f[n][i];
ans*=k;
printf("%d",ans);
return 0;
}
方法2:(这不就是高中环排的经典问题吗!)直接定义f[n]为符合题意 n块格子的方法总数:
根据容斥原理分类:如果n-1块格子与第一块不同,前n-1块也是合法的f[n-1],又因为第n块此时有k-2中选择,所以是f[n-1]*(k-2).
如果相同,前n-2块一定是合法的f[n-2],第n-1块只有一种选择,第n块有k-1种选择,所以是f[n-2]*(k-1)*1
注意为什么从4开始,因为当<=3时,上述的方法根本没那么多格子。
#include<bits/stdc++.h>
using namespace std;
int f[15];
int main(){
int n,k;
scanf("%d %d",&n,&k);//n块墙壁,k种颜色
f[1]=k;
f[2]=k*(k-1);
f[3]=k*(k-1)*(k-2);
for(int i=4;i<=n;i++)f[i]=f[i-1]*(k-2)+f[i-2]*(k-1);
printf("%d",f[n]);
return 0;
}
例题2:扔鸡蛋
1,定义动规状态:f[n][k]代表前n个鸡蛋,测k层楼最多最少的次数。(第一个最多,指最坏的情况。第二个最少指策略最好情况。)
2.确定状态转移方程:dp[i][j]=min{max(dp[i][n-k],dp[i-1][k-1])}+1;k∈【1,n】
什么意思呢?枚举第i枚鸡蛋从k层楼扔下的所有情况,对于每一个单独的情况,有可能碎了有可能没碎。因为是最坏的情况,所以要取max值。对于所有不同的状态,找到一个楼层使得扔的次数最少,是为最好的策略。
#include<bits/stdc++.h>
using namespace std;
long long int f[32][100000];
int main(){
int n,m;
scanf("%d %d",&n,&m);
for(int i=1;i<=m;i++)f[1][i]=i;
for(int i=2;i<=n;i++){
for(int j=1;j<=m;j++){
f[i][j]=j;//先初始化一个最大值
for(int k=1;k<=j;k++){
f[i][j]=min(f[i][j],max(f[i][j-k],f[i-1][k-1])+1);
}
}
}
printf("%lld",f[n][m]);
return 0;
}
优化方案 状态重定义:在dp[i][j]=k中,发现j和k正相关,所以将j和k二者互换。
因为在这道题中,次数k相比起楼层j,其实是一个很小的量。所以可以节约时间。
动规状态:dp[i][k]=j,i个鸡蛋扔k次,最多能扔几层楼j。当dp[i][k-1]<m<dp[i][k]时,答案即为k。
动规方程:dp[i][k]=dp[i-1][k-1](如果鸡蛋碎了,测下面的楼层)+dp[i][k-1](鸡蛋没碎)+1
三,优化转移过程:
例题1:切割回文:
之前用区间dp做,dp[i][j]代表从i到j最少切多少刀。现在只关注最后一刀所在的位置,dp(i)=min{dp【k】+1}(k需要满足s【k=1,i】是回文串。
例题2:最长上升子序列:
原本做法:
现在做法:
(注意:len数组的单调性可以被证明)
也就是说,len【i】数组用来记录,序列长度为i的时候序列末尾的最小值。在求dp【i】的值的时候, 我们只需要在len数组中,找到刚好小于val【i】的位置,这个位置的编号(前面子序列的长度)+1,也即dp【i】。