DP总结
DP使用规则(摘抄自http://www.cnblogs.com/huangxincheng/archive/2012/02/13/2349664.html):
① 最优化原理(最优子结构性质):
如果一个问题的最优策略它的子问题的策略也是最优的,则称该问题具有“最优子结构性质”。
② 无后效性:
当一个问题被划分为多个决策阶段,那么前一个阶段的策略不会受到后一个阶段所做出策略的影响。
③ 子问题的重叠性:
这个性质揭露了动态规划的本质,解决冗余问题,重复的子问题我们可以记录下来供后阶段决策时
直接使用,从而降低算法复杂度。
求解步骤:
① 描述最优解模型。
② 递归的定义最优解,也就是构造动态规划方程。
③ 自底向上的计算最优解。
④ 最后根据计算的最优值得出问题的最佳策略。
DP归类:
一、01背包:时间复杂度O(N*V),N表示N个物品,V表示背包重量
二、完全背包:O(N*V)个状态需要求解,但求解每个状态f[v]的时间是O(V/c),总的复杂度是超过O(VN)的。
有的题目要求“恰好装满背包”时的最优解,有的题目则并没有要求必须把背包装满。一种区别
这两种问法的实现方法是在初始化的时候有所不同。
如果是第一种问法,要求恰好装满背包,那么在初始化时除了f[0]为0其它f[1..V]均设为-∞,
这样就可以保证最终得到的f[N]是一种恰好装满背包的最优解。
如果并没有要求必须把背包装满,而是只希望价格尽量大,初始化时应该将f[0..V]全部设
为0。
三、最长公共子序列
int dp[1005][1005];//可以使用动态数组优化内存 char a[1005],b[1005],re[1005][1005]; void dfs(int i,int j) { if(i<1||j<1) return; if(re[i][j]=='c') { dfs(i-1,j-1); cout<<a[i]; } else dfs(i-(re[i][j]=='x'),j-(re[i][j]=='y')); } int main() { while(~scanf("%s %s",a+1,b+1)) { int la=strlen(a+1),lb=strlen(b+1),i,j; memset(dp,0,sizeof(dp)); //printf("%s\n",a+1); for(i=1;i<=la;++i) for(j=1;j<=lb;++j) { if(a[i]==b[j]) { dp[i][j]=dp[i-1][j-1]+1; re[i][j]='c'; } else { if(dp[i][j-1]>dp[i-1][j]) { dp[i][j]=dp[i][j-1]; re[i][j]='y'; } else{ dp[i][j]=dp[i-1][j]; re[i][j]='x'; } } } printf("%d\n",dp[la][lb]); dfs(la,lb); printf("\n"); } return 0; }
四、经典的数塔问题:
从上往下、从下往上都可以:从上往下代码更简洁
上——下:
int dp[105][105]; int main() { int n,i,j; while(~scanf("%d",&n)) { memset(dp,0,sizeof(dp)); for(i=1;i<=n;++i) for(j=1;j<=i;++j) { scanf("%d",&dp[i][j]); dp[i][j]+=dp[i-1][j-1]>dp[i-1][j]?dp[i-1][j-1]:dp[i-1][j];//important } printf("%d\n",*max_element(dp[n],dp[n]+n)); } return 0; }
下——上:
int main() { int t,n,a[105][105],i,j; while(~scanf("%d",&n)) { for(i=0;i<n;++i) for(j=0;j<=i;++j) scanf("%d",&a[i][j]); for(i=n-1;i>0;--i) for(j=0;j<i;++j) { a[i-1][j]+=a[i][j]>a[i][j+1]?a[i][j]:a[i][j+1]; } printf("%d\n",a[0][0]); } return 0; }
五、最大和问题:
1、一维:
for(i=0;i<n;++i) { scanf("%d",&a); if(sum<0) sum=a; //如果前面的数已经小于0了,重新记录sum else sum+=a; Max=sum>Max?sum:Max; }
2、二维:
for(i=1;i<=r;++i) //r为行,c为列 for(j=0;j<c;++j) { scanf("%d",&map[i][j]); map[i][j]=map[i][j]+map[i-1][j];//竖着降维 } for(i=1,m=map[1][0];i<=r;++i) //m赋值给第一个元素 for(j=i;j<=r;++j) for(k=max=0;k<c;++k) { temp=map[j][k]-map[i-1][k];//k时,i和j之间的矩阵 max=(max>=0?max:0)+temp;//一维的做法,max记录的是当前两行之间的最大矩阵和 m=max>m?max:m;//m记录的是从前面遍历到这个位置,最大的矩阵和为多少 } printf("%d\n",m); memset(map,0,sizeof(map));//放后面更省时间
3、三维:
可以推广到N维:
#define FOR(i,s,t) for(int i=(s);i<=(t);++i) void expand(int i,int &b0,int &b1,int &b2) { b0=i&1; i>>=1; b1=i&1; i>>=1; b2=i&1; } int sign(int b0,int b1,int b2) { return (b0+b1+b2)%2==1?1:-1; } const int maxn=30; const long long INF=1LL<<60; long long S[maxn][maxn][maxn]; long long sum(int x1,int x2,int y1,int y2,int z1,int z2) { int dx=x2-x1+1,dy=y2-y1+1,dz=z2-z1+1; long long s=0; for(int i=0;i<8;++i) { int b0,b1,b2; expand(i,b0,b1,b2); s-=S[x2-b0*dx][y2-b1*by][z2-b2*dz]*sign(b0,b1,b2); } return s; } int main() { int t; scanf("%d",&t); while(t--) { int a,b,c,b0,b1,b2; scanf("%d%d%d",&a,&b,&c); memset(S,0,sizeof(S)); FOR(x,1,a) FOR(y,1,b) FOR(z,1,c) scanf("%lld",&S[x][y][z]); FOR(x,1,a) FOR(y,1,b) FOR(z,1,c) FOR(i,1,7) { expand(i,b0,b1,b2); S[x][y][z]+=S[x-b0][y-b1][z-b2]*sign(b0,b1,b2); } long long ans =-INF; FOR(x1,1,a) FOR(x2,x1,a) FOR(y1,1,b) FOR(y2,y1,b) { long long M=0; FOR(z,1,c) { long long s=sum(x1,x2,y1,y2,1,z); ans=max(ans,s-M); M=min(M,s); } } printf("%lld\n",ans); if(t) printf("\n"); } return 0; }
六、整数拆分:
/* 第一行:将n划分成若干正整数之和的划分数。 状态转移方程:dp[i][j]:和为i、最大数不超过j的拆分数 dp[i][j]可以分为两种情况:1、拆分项至少有一个j 2、拆分项一个j也没有 dp[i][j]=dp[i-j][[j]+dp[i][j-1] 第二行:将n划分成k个正整数之和的划分数。 dp[n-k][k]:相当于把k个1从n中拿出来,然后和n-k的拆分项相加的个数 第三行:将n划分成若干最大不超过k的正整数之和的划分数。 dp[n][k] 第四行:将n划分成若干奇正整数之和的划分数。 dp1[i][j]是当前的划分数为i,最大值为j时的中的划分数,则状态转移方程为 dp1[i][j]=dp1[i][i]if(j>i&&j%2==1) =dp1[i][i-1]if(j>i&&j%2==0)(最大数不可能为偶数) =dp1[i-j][j]+dp1[i][j-2]没用到j时划分不变,即dp1[i][j-2],用到则是dp1[i-j][j]; 第五行:将n划分成若干完全不同正整数之和的划分数。 dp2[i][j]可以分两种情况:1、dp1[i][j-1]为不选择j时的方案 2、dp1[i-j][j-1]为选择j时的方案 0-1背包:dp2[i][j]=dp2[i][j-1]+dp2[i-j][j-1] 第六行:将n划分成不超过k个正整数之和的划分数。 dp[n][k]: ferrers共轭图像 比如 24=5+5+5+4+3+2,6个数,最大数为5 24=6+6+5+4+3 5个数,最大数为6 */ #include <stdio.h> #define N 52 int dp[N][N] = {0} , dp1[N][N] = {0} , dp2[N][N] = {0} ; void Divid(){ dp[0][0] = 1 ; for( int i = 0 ; i < N ; i++ ) for( int j = 1 ; j < N ; j++ ){ if( i < j ) //当i<j时 只能j分到i而已,所以情况有1种 dp[i][j] = dp[i][i] ; else dp[i][j] = dp[i-j][j] + dp[i][j-1] ; } } void Divid1(){ for( int i = 1 ; i < N ; i++ ) //各种初始化 dp1[i][1] = 1 ; //最大值为1时只能有本身组成,Answer为1 for( int i = 1 ; i < N ; i+=2 ) dp1[0][i] = 1 ; dp1[0][0] = 1 ; for( int i = 1 ; i < N ; i++ ) for( int j = 3 ; j < N ; j+=2 ){ //j是永远的奇数 if( j > i ){ if( i&1 ) dp1[i][j] = dp1[i][i] ; else dp1[i][j] = dp1[i][i-1] ; } else dp1[i][j] = dp1[i-j][j] + dp1[i][j-2] ; } } void Divid2(){ for( int i = 1 ; i < N ; i++ ){ dp2[0][i] = 1 ; dp2[1][i] = 1 ; } for( int i = 2 ;i < N ; i++ ) for( int j = 1 ;j < N ; j++ ){ if( j > i ) dp2[i][j] = dp2[i][i] ; else dp2[i][j] = dp2[i-j][j-1] +dp2[i][j-1] ; //01背包 } } int main(){ int n , k ; Divid() ; Divid1() ; Divid2() ; while( ~scanf( "%d%d" , &n , &k ) ) { printf( "%d\n" , dp[n][n] ) ; //拆分成若干个正整数 printf( "%d\n" , dp[n-k][k] ) ; //拆分成k个正整数 printf( "%d\n" , dp[n][k]) ; //拆分成最大数为k printf( "%d\n" , n&1 ? dp1[n][n] : dp1[n][n-1] ) ; //拆分成若干个奇数 printf( "%d\n\n" , dp2[n][n]) ; //拆分成若干个不同的正整数 } return 0; }
Cut the rope:求解把长度为L的绳子分成至少2段的不同长度的绳子的方案数:
首先考虑,若分解成k段,则n的值至少为1+2+3+4+...+k=(k+1)*k/2
所以本题k的最大值为315
假定dp[k][n]表示为可以分成k段和为n的方案数,
情况分为两种:
1、只有一个1的,则等于dp[k-1][n-k],相当与从n里拿走k个1,可以分成k-1段的方案数
2、没有1的,则等于dp[k][n-k],相当于从n里拿走k个1,可以分成k段的方案数
所以 dp[k][n]=dp[k-1][n-k]+dp[k][n-k]
#include<iostream> #include<cstdio> #include<algorithm> #include<cmath> #include<cstring> #include<string> using namespace std; #define LL long long #define N 50005 #define mod 1000000 int dp[2][N],sum[N]; //dp滚动数组 int main() { int n,t,i,j,k; memset(sum,0,sizeof(sum)); /*这里dp[0]表示3开始后才可以分,如果想象成每段加1,则会有重复的 想象成每段+2,则dp[0][i]=dp[0][i-2]+1不会有重复的*/ for(i=3;i<=N;++i) //当绳子长度至少为3时,才有解噢 sum[i]=dp[0][i]=dp[0][i-2]+1; for(k=3;k<316;++k) { int *p1=dp[k&1],*p2=dp[(k+1)&1]; //用数组指针指向dp数组,这样很方便噢 for(i=k*(k+1)/2-1;i>0;i--) p1[i]=0; //由于之前可能用过,所以必须要重新清0,不然会WA(通常滚动数组都要重新清0) for(i=k*(k+1)/2;i<=N;++i) { p1[i]=p1[i-k]+p2[i-k]; //状态转移方程 p1[i]=p1[i]>=mod?p1[i]-mod:p1[i]; //对mod求余 sum[i]+=p1[i]; sum[i]=sum[i]>=mod?sum[i]-mod:sum[i]; //对mod求余 } } scanf("%d",&t); while(t--) { scanf("%d",&n); printf("%d\n",sum[n]); } return 0; }
七、深入浅出切汉诺塔之类的状态题:
题目1:用2*1或1*2的骨牌把m*n的棋盘完全覆盖,求覆盖方案数:
1、当高度(h)和宽度(w)为奇数时:
area=h*w;
骨牌面积:2
h*w / 2!=0 -> 不能用骨牌覆盖
2、记f[i][s1]为第i-1行全满且第i行状态为s1时的种数,f[i-1][s2]为第i-2行全满且第i-1行状态为s2时的种数,则骨牌的覆盖方案数会等于f[h][w<<1-1](第h行全满状态):
结论:第i行的放置方案数,取决于第i-1行的放置方案数
对于每一个位置,3种放置方案:
1. 竖直放置
2. 水平放置
3. 不放置
d为当前列号 ,初始化d, s1, s2都为0;对应以上三种放置方法,s1, s2的调整为:
1. d = d + 1, s1 << 1 | 1, s2 << 1;
2. d = d + 2, s1 << 2 | 3, s2 << 2 | 3;
3. d = d + 1, s1 << 1, s2 << 1 | 1;
先就第一种情况解释一下,另外的两种情况可以类推
S1<<1|1即为把s1的二进制表示后面加上一个1,对就于本题来说就是(d+1)列上放
置,s2<<1即为把s2的二进制表示后面加上一个0,对于本题来说就是(d+1)列上不放置。
但为什么s1、s2会如此变化呢?s1对应于本行的状态,s2对应于上一行的状态,能竖直放置意味着上一行的(d+1)列是空着的,因此此时上一行的状态为s2<<1,同时竖置放置了之后,则本行(d+1)行放置了东西,状态于是变为s1<<1|1;
3、初始时的f值,可以假设第0行全满,第一行只有两种放法:
1. 水平放置 d = d + 2, s << 2 | 3;
2. 不放置 d = d + 1, s << 1;
# include <stdio.h> # include <string.h> #include<iostream> using namespace std; long long a[2][3000]; int t,n,m; void dfs(int d,int mm) //d列号 { if(d==m) { a[0][mm]++;//使用了滚动数组,当然是a[0]咯 return; } if(d+1<=m) dfs(d+1,mm<<1); if(d+2<=m) dfs(d+2,mm<<2|3); } void DFS(int d,int mm,int nn)//mm对应于上一行状态,nn对应于下一行状态 { if(d==m) { a[t][nn]+=a[(t+1)%2][mm]; return; } if(d+1<=m) //判断防越界 { DFS(d+1,mm<<1,nn<<1|1); //第一种放置 DFS(d+1,mm<<1|1,nn<<1); //第三种放置 } if(d+2<=m) DFS(d+2,mm<<2|3,nn<<2|3); //第二种放置 } int main() { int i; while(scanf("%d%d",&n,&m)&&n&&m) { if((n*m)%2) { printf("0\n"); continue; } if(n>m) //当输入的 w > h 时,对调,因为横向的运算是指数级的, 而列向的是线性的. n^=m,m^=n,n^=m; //相当于swap(n,m) memset(a,0,sizeof(a)); dfs(0,0); //初始化第一行 for(i=2;i<=n;i++) { t=(i+1)%2; DFS(0,0,0); memset(a[(t+1)%2],0,sizeof(a[0])); } cout<<a[(n+1)%2][(1<<m)-1]<<endl; } return 0; }