一、基本概念
动态规划过程是:每次决策依赖于当前状态,又随即引起状态的转移。一个决策序列就是在变化的状态中产生出来的,所以,这种多阶段最优化决策解决问题的过程就称为动态规划。
二、基本思想与策略
基本思想与分治法类似,也是将待求解的问题分解为若干个子问题(阶段),按顺序求解子阶段,前一子问题的解,为后一子问题的求解提供了有用的信息。在求解任一子问题时,列出各种可能的局部解,通过决策保留那些有可能达到最优的局部解,丢弃其他局部解。依次解决各子问题,最后一个子问题就是初始问题的解。
由于动态规划解决的问题多数有重叠子问题这个特点,为减少重复计算,对每一个子问题只解一次,将其不同阶段的不同状态保存在一个二维数组中。
与分治法最大的差别是:适合于用动态规划法求解的问题,经分解后得到的子问题往往不是互相独立的(即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解)。
三、适用的情况
能采用动态规划求解的问题的一般要具有3个性质:
(1) 最优化原理:如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优化原理。
(2) 无后效性:即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关。
(3)有重叠子问题:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到。(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势)
四、求解的基本步骤
动态规划所处理的问题是一个多阶段决策问题,一般由初始状态开始,通过对中间阶段决策的选择,达到结束状态。这些决策形成了一个决策序列,同时确定了完成整个过程的一条活动路线(通常是求最优的活动路线)。如图所示。动态规划的设计都有着一定的模式,一般要经历以下几个步骤。
初始状态→│决策1│→│决策2│→…→│决策n│→结束状态
图1 动态规划决策过程示意图
(1)划分阶段:按照问题的时间或空间特征,把问题分为若干个阶段。在划分阶段时,注意划分后的阶段一定要是有序的或者是可排序的,否则问题就无法求解。
(2)确定状态和状态变量:将问题发展到各个阶段时所处于的各种客观情况用不同的状态表示出来。当然,状态的选择要满足无后效性。
(3)确定决策并写出状态转移方程:因为决策和状态转移有着天然的联系,状态转移就是根据上一阶段的状态和决策来导出本阶段的状态。所以如果确定了决策,状态转移方程也就可写出。但事实上常常是反过来做,根据相邻两个阶段的状态之间的关系来确定决策方法和状态转移方程。
(4)寻找边界条件:给出的状态转移方程是一个递推式,需要一个递推的终止条件或边界条件。
一般,只要解决问题的阶段、状态和状态转移决策确定了,就可以写出状态转移方程(包括边界条件)。
实际应用中可以按以下几个简化的步骤进行设计:
(1)分析最优解的性质,并刻画其结构特征。
(2)递归的定义最优解。
(3)以自底向上或自顶向下的记忆化方式(备忘录法)计算出最优值
(4)根据计算最优值时得到的信息,构造问题的最优解
五、算法实现的说明
动态规划的主要难点在于理论上的设计,也就是上面4个步骤的确定,一旦设计完成,实现部分就会非常简单。
使用动态规划求解问题,最重要的就是确定动态规划三要素:
(1)问题的阶段 (2)每个阶段的状态
(3)从前一个阶段转化到后一个阶段之间的递推关系。
递推关系必须是从次小的问题开始到较大的问题之间的转化,从这个角度来说,动态规划往往可以用递归程序来实现,不过因为递推可以充分利用前面保存的子问题的解来减少重复计算,所以对于大规模问题来说,有递归不可比拟的优势,这也是动态规划算法的核心之处。
确定了动态规划的这三要素,整个求解过程就可以用一个最优决策表来描述,最优决策表是一个二维表,其中行表示决策的阶段,列表示问题状态,表格需要填写的数据一般对应此问题的在某个阶段某个状态下的最优值(如最短路径,最长公共子序列,最大价值等),填表的过程就是根据递推关系,从1行1列开始,以行或者列优先的顺序,依次填写表格,最后根据整个表格的数据通过简单的取舍或者运算求得问题的最优解。
f(n,m)=max{f(n-1,m), f(n-1,m-w[n])+P(n,m)}
上面为转载 转自 传送下面是简单的动态规划模板
1.最长上升子序列LIS
a.普通做法
//时间复杂度 O(n^2) #include<iostream> #include<cstdio> #include<cstdlib> #include<cstring> #include<algorithm> #include<cmath> #define N 10000 //此做法 n不能太大 using namespace std; int main(){ int n; int a[N],f[N],mx,ans=0; scanf("%d",&n); for(int i=1;i<=n;i++) scanf("%d",&a[i]); for(int i=1;i<=n;i++) f[i]=1; for(int i=1;i<=n;i++) { mx=0; for(int j=1;j<=i-1;j++) if(f[j]>mx&&a[j]<a[i])//改成 <= 就是不下降子序列 { mx=f[j];f[i]=f[j]+1; } } for(int i=1;i<=n;i++) ans=max(ans,f[i]); printf("%d",ans); return 0; }b.LIC的优化
在每次从1到i-1循环时太浪费时间 多开一个g数组 存下当前的最优的最长上升序列
//时间复杂度降低到了nlogn的级别 用到lower_bound函数(lower_bound类似二分查找) //更用改进版就不用普通做法 low_bound(begin,end,val) 返回插入val使序列还是有序序列的第一个元素地址 减去begin地址后就是他的数组序号-1(数组从1开始) #include<iostream> #include<cstdio> #include<cstdlib> #include<cstring> #include<algorithm> #include<cmath> #define inf 0x7fffffff #define N 5000000 using namespace std; int n,a[N],f[N],g[N],ans;//g数组存一个最长的上升有序序列 int main(){ scanf("%d",&n); for(int i=1;i<=n;i++) scanf("%d",&a[i]); for(int i=1;i<=n;i++) {g[i]=inf;f[i]=1;} for(int i=1;i<=n;i++){//倒着从n到1循环就是最长下降子序列 int k=lower_bound(g+1,g+1+n,a[i])-(g+1);//一定要减(g+1) 此时k=插入位置的前一个位置 因为数组从1开始的 g[k+1]=a[i]; f[i]=k+1; /* 可以写成这样 int k=lower_bound(g+1,g+1+n,a[i])-(g); g[k]=a[i]; f[i]=k; */ } for(int i=1;i<=n;i++) ans=max(ans,f[i]); printf("%d",ans); return 0; }
简单的dp练习题
1.合唱队形
//https://vijos.org/p/1098 //求一个最长上升一个最长下降 然后f1[i]+f2[i]最大的就是最优的中间位置 答案为f1[i]+f2[i]-1(减去自己) #include<iostream> #include<cstdio> #include<cstdlib> #include<cstring> #include<algorithm> #include<cmath> #define inf 0x7fffffff #define N 5000000 using namespace std; int n,a[N],f1[N],g1[N],ans,f2[N],g2[N],mx=0; int main(){ scanf("%d",&n); for(int i=1;i<=n;i++) scanf("%d",&a[i]); for(int i=1;i<=n;i++) {g1[i]=inf;g2[i]=inf;f1[i]=1;f2[i]=1;} for(int i=1;i<=n;i++){ int k=lower_bound(g1+1,g1+1+n,a[i])-(g1+1); g1[k+1]=a[i]; f1[i]=k+1; } for(int i=n;i>=1;i--){ int k=lower_bound(g2+1,g2+1+n,a[i])-(g2+1); g2[k+1]=a[i]; f2[i]=k+1; } for(int i=1;i<=n;i++){ if(f1[i]+f2[i]-1>mx) mx=f1[i]+f2[i]-1; } printf("%d",n-mx); return 0; }
2.codevs1620轮船问题
//http://codevs.cn/problem/1620/ //先对一岸进行排序 然后求另一岸的最长上升子序列 #include<iostream> #include<cstdio> #include<cstdlib> #include<cstring> #include<algorithm> #include<cmath> #define inf 0x7fffffff #define N 5005 using namespace std; int x,y,n,f[N],g[N],a[N],mx; struct node{ int x,y; }bank[N]; bool cmp(node xx,node yy){ if(xx.y!=yy.y) return xx.y<yy.y; else return xx.x>yy.x; //题目说任何两个城市都没有相同的友好城市,但是样例有。。所以xx.x>yy.x这样 如果有多个点对应一个城市 只会取中间一个(不会取多个) } int main(){ scanf("%d%d",&x,&y); scanf("%d",&n); for(int i=1;i<=n;i++) scanf("%d%d",&bank[i].x,&bank[i].y); sort(bank+1,bank+1+n,cmp); for(int i=1;i<=n;i++){f[i]=1;g[i]=inf;} for(int i=1;i<=n;i++) a[i]=bank[i].x; for(int i=1;i<=n;i++){ int k=lower_bound(g+1,g+1+n,a[i])-(g+1); g[k+1]=a[i]; f[i]=k+1; } for(int i=1;i<=n;i++) mx=max(mx,f[i]); printf("%d ",mx); return 0; }
3.马拦过河卒vijos1121
#include<iostream> #include<cstdio> #include<cstdlib> #include<cstring> #include<algorithm> #include<cmath> using namespace std; int main() { int i,j,x,y,n,m,f[100][100]; long long ans[100][100]; scanf("%d%d%d%d",&n,&m,&x,&y); memset(f,1,sizeof(f)); memset(ans,0,sizeof(ans)); ans[0][0]=1; f[x][y]=0;f[x+1][y+2]=0;f[x-1][y+2]=0; f[x+1][y-2]=0;f[x-1][y-2]=0;f[x+2][y+1]=0; f[x-2][y+1]=0;f[x+2][y-1]=0;f[x-2][y-1]=0; for (i=1; i<=n; i++) if (f[i][0]) ans[i][0]=1; else break; for (i=1; i<=m; i++) if (f[0][i]) ans[0][i]=1; else break; for (i=1; i<=n; i++) for (j=1; j<=m; j++) if (f[i][j]) ans[i][j]=ans[i-1][j]+ans[i][j-1]; printf("%lld ",ans[n][m]); return 0; }
4.树塔
/* 1 2 3 4 5 6 2 4 2 1 给出一个树塔 从顶点往下走 求经过路径上的最大权值和 Input 4 1 2 3 4 5 6 2 4 2 1 Output 13 */ #include<iostream> #include<cstdio> #include<cstdlib> #include<cstring> #include<algorithm> #include<cmath> #define inf 0x7fffffff #define N 4000 using namespace std; int f[N][N],a[N][N],mx; int main(){ int n; scanf("%d",&n); for(int i=1;i<=n;i++) for(int j=1;j<=i;j++) scanf("%d",&a[i][j]); for(int i=1;i<=n;i++) for(int j=1;j<=i;j++) f[i][j]=max(f[i-1][j],f[i-1][j-1])+a[i][j]; for(int i=1;i<=n;i++) mx=max(mx,f[n][i]); printf("%d",mx); return 0; }