笔者初识dp还是度娘引见的(辣鸡的学校没有algorithm~)
当下是个资源丰富的时代,基本上各种书上面都有介绍动态规划的概念(然而从来没看懂过...)
记得当初写过一道整数划分的题目,TLE没过,同级的一哥们发了一份代码,一个循环~~~
当时我的一脸感激,然而内心是崩溃的: What FUCK This !?
个人觉得认识dp之前你应该先知道: 递推 , 搜索 , 记忆化搜索 , 分治;
与之对应的题目:
1. 超级楼梯(fibonacci) ; 危险的组合(f[i]=f[i-4]+2*f[i-1]) ; RPG难题 ;
2. 可重全排列 ; 迷宫最短路 ;
3. 角谷猜想(加强版的)
4. 第k小的数; 逆序对 ; a^b%m(快速幂的那种)
...(如果都可以独立过一遍了)N天后:卧槽,太水了!
Ok continue....
其实动态规划就是一种排好顺序的记忆化搜索! 对建模影响比较大的是对数的理解;
它融搜索分治递推的思想于一体,然后看起来比较操蛋
看一个递推的,全错位排列:
题目是说,n封信和n个信封,编号1-n,问:所有信装错的方案数f(n);
(ps:机灵一点的百度了一个公式,秒出答案了,老实的人还在看题解.....)
对于第i份信,如果和它前面的某封(设为x)恰好装反,那么f(i) = f(i-2) * (i-1);
如果没有恰好:我们把i信看成装在x里是对的,那么f(i)=f(i-1)*(i-1);
巧的是这两种情况刚好互斥~
#include<cstdio> long long a[100]; int main(){ int n; scanf("%d",&n); a[1]=0;a[2]=1;a[3]=2; for(int i=4;i<=n;i++) a[i]=(i-1)*(a[i-1]+a[i-2]); printf("%lld\n",a[n]); return 0; }
这TM和动规有什么关系吗?呵呵...你说呢?
别问了,问多了烧脑子~
直接看几个模型就Over吧:
1. 位置记忆
最长不下降子序列:(这个太明显的记忆化~)
input:
10
12 13 15 12 14 19 21 21 20 17
output:
6
#include<cstdio> #include<algorithm> int n,a[101],dp[101],ans=0; int main(){ scanf("%d",&n); for(int i=0;i<n;i++) scanf("%d",a+i); dp[0]=1; for(int i=1;i<n;i++){ for(int j=i-1;j>=0;j--) if(a[i]>=a[j]) dp[i]=std::max(dp[i],dp[j]+1); ans=(ans>dp[i]?ans:dp[i]); } printf("%d\n",ans); return 0; }
0-1背包(可取部分的那种直接贪单位质量最大:v[i]/c[i]降排即可对吧?)
输入n表示n个物品,vol表示体积,w[i]是重量,c[i]是体积,求在vol体积下最重可达多重?
input:
7 15
4 6
3 9
2 8
7 7
12 18
6 11
5 7
output:
34
按取第几件物品来划分阶段: 能取就取最优(dp[i-1][j]表示没取,dp[i-1][j-c[i]]+w[i]就是取i物品),不能取就不取
#include<cstdio> #include<algorithm> int dp[101][1010],c[101],w[101],n,vol; int main(){ scanf("%d%d",&n,&vol); for(int i=0;i<n;i++) scanf("%d%d",c+i,w+i); for(int i=0;i<n;i++) for(int j=0;j<=vol;j++){ if(j>=c[i]&&i>0) dp[i][j]=std::max(dp[i-1][j],dp[i-1][j-c[i]]+w[i]); else if(i==0)dp[i][j]=w[i]; else dp[i][j]=dp[i-1][j]; } printf("%d\n",dp[n-1][vol]); return 0; } /* dp[i][j]表示对第i件物品,在j容量的情况下最重重量 */
3. 累加状态
过河卒:(马拦过河卒:象棋那种马)对方一匹马,你的卒子过河后只能往右或者往下(这让我想起了T步...),问从(0,0)到(n,m)的路线多少种
input:
6 6 3 2
output:
17
#include<cstdio> #include<cstring> int dp[101][101],n,m,mx,my; void _set(int x,int y){ if(x<0||x>n||y<0||y>m)return ; dp[x][y]=0; } int main(){ scanf("%d%d%d%d",&n,&m,&mx,&my); memset(dp,-1,sizeof(dp)); _set(mx-2,my-1);_set(mx-2,my+1);_set(mx-1,my-2); _set(mx-1,my+2);_set(mx,my);_set(mx+1,my-2); _set(mx+1,my+2);_set(mx+2,my-1);_set(mx+2,my+1); for(int i=0,c=1;i<=m;i++){if(!dp[0][i])c=0;dp[0][i]=c;} for(int i=0,c=1;i<=n;i++){if(!dp[i][0])c=0;dp[i][0]=c;} for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) if(dp[i][j])dp[i][j]=dp[i-1][j]+dp[i][j-1]; printf("%d\n",dp[n][m]); return 0; } /*这个阶段一太明显了dp[i][j]=dp[i-1][j]+dp[i][j-1]*/
老生常谈,不多说直接贴代码
#include<cstdio> #include<algorithm> int dp[101][101],n,k;//dp[i][j]表示把j分成i份 int main(){ scanf("%d%d",&n,&k); for(int i=0;i<=n;i++)dp[1][i]=1; for(int i=2;i<=k;i++) for(int j=0;j<=n;j++) if(j>=i)dp[i][j]=dp[i-1][j]+dp[i][j-i]; else dp[i][j]=dp[i-1][j]; printf("%d\n",dp[k][n]); return 0; }
5. 插入比较
石子归并(老题目了,经典区间规划,就像Floyd一样)不做无用复述...就是说n堆石头,只能和相邻的合并(首尾不可直接合并),合并的代价是a的重量加b的重量,求最优合并使得代价最小
#include<cstdio> #include<cstring> #include<algorithm> int dp[101][101],n,w[101]; int main(){ scanf("%d",&n); memset(dp,127,sizeof(dp)); for(int i=1;i<=n;i++){ scanf("%d",w+i); w[i]+=w[i-1]; dp[i][i]=0; } for(int i=n-1;i>0;i--)//起点, for(int j=i+1;j<=n;j++)//终点 for(int k=i;k<j;k++)//插入 dp[i][j]=std::min(dp[i][j],dp[i][k]+dp[k+1][j]-w[i-1]+w[j]); printf("%d\n",dp[1][n]); return 0; }
6. 累加相关类别
还有一些常见的(蓝桥杯的牌型种数,K好数之类的)牌型种数:13张牌,不考虑花色和顺序,值看点数,求从52张中抽13张的方案数:
#include<cstdio> int dp[15][15]; int main(){ dp[1][0]=dp[1][1]=dp[1][2]=dp[1][3]=dp[1][4]=1; //f(i,j)表示有i种排,发你j张的可能方案 for(int i=2;i<=13;i++) for(int j=0;j<=13;j++) for(int k=0;k<=4&&k<=j;k++)//看成排序,对于第j张取0-4 dp[i][j]+=dp[i-1][j-k]; printf("%d",dp[13][13]); return 0; }
总之:
搜索是要找关键字界限;
动规是要找决策阶段,然后把要用的先算出来就ok(然而初始化是最恶心的,调试倒是可以取小数据手算~)
找界限,找阶段,找转移,排计算顺序,
恩,略懂~