什么是动态规划呢?
动态规划(dynamic programming)是运筹学的一个分支,是求解决策过程(decision process)最优化的数学方法。20世纪50年代初美国数学家R.E.Bellman等人在研究多阶段决策过程(multistep decision process)的优化问题时,提出了著名的最优化原理(principle of optimality),把多阶段过程转化为一系列单阶段问题,利用各阶段之间的关系,逐个求解,创立了解决这类过程优化问题的新方法——动态规划。我们简称dp
动态规划问题的一般分类:
动态规划一般可分为线性动规,区域动规,树形动规,背包动规四类。
举例:
线性动规:拦截导弹,合唱队形,挖地雷,建学校,剑客决斗等;
区域动规:石子合并, 加分二叉树,统计单词个数,炮兵布阵等;
树形动规:贪吃的九头龙,二分查找树,聚会的欢乐,数字三角形等;
背包问题:01背包问题,完全背包问题,分组背包问题,二维背包,装箱问题,挤牛奶(同济ACM第1132题)等;
应用实例:
最短路径问题 ,项目管理,网络流优化等;
概念意义:
动态规划程序设计是对解最优化问题的一种途径、一种方法,而不是一种特殊算法。不像搜索或数值计算那样,具有一个标准的数学表达式和明确清晰的解题方法。动态规划程序设计往往是针对一种最优化问题,由于各种问题的性质不同,确定最优解的条件也互不相同,因而动态规划的设计方法对不同的问题,有各具特色的解题方法,而不存在一种万能的动态规划算法,可以解决各类最优化问题。因此读者在学习时,除了要对基本概念和方法正确理解外,必须具体问题具体分析处理,以丰富的想象力去建立模型,用创造性的技巧去求解。我们也可以通过对若干有代表性的问题的动态规划算法进行分析、讨论,逐渐学会并掌握这一设计方法。
入门题举例(POJ1163):
数字三角形问题:在数字三角形中寻找一条从顶部到底部的路径,使得路径上所经过的数字之和最大。路径上的每一步只能往左下或右下走。只需要求出这个最大和即可,不必给出具体路径。(三角形的行数大于1小于等于100,数字为0-99)
解题思路:
用二维数组存放数字三角形。
D(r,j):第r行第j个数字(r,j从1开始算)
MaxSum(r,j):从D(r,j)到底边的各条路径中,最佳路径的数字之和。
问题:求MaxSum(1,1)
dp往往与递归密不可分。
D(r,j)出发,下一步只能走D(r+1,j)或者D(r+1,j+1)。故对于N行的三角形:
if(r==N)
MaxSum(r,j)=D(r,j)
else
MaxSum=Max{MaxSum(r+1,j),Maxsum(r+1,j+1)}+D(r,j)
代码:
#include <iostream>
#include <algorithm>
using namespace std;
#define MAX 101
int D[MAX][MAX];int n;
int maxSum[MAX][MAX];
int MaxSum(int i,int j){
if(maxSum[i][j]!=-1)
return maxSum[i][j];
if(i==n)
maxSum[i][j]=D[i][j];
else{
int x=MaxSum(i+1,j);
int y=MaxSum(i+1,j+1);
maxSum[i][j]=max(x,y)+D[i][j];
}
return maxSum[i][j];
}
int main()
{
int i,j;
cin>>n;
for(i=1;i<n;i++)
for(j=1;j<=i;j++){
cin>>D[i][j];
maxSum[i][j]=-1;
}
cout<<MaxSum(1,1)<<endl;
}
既然是动态规划,所以要有一种动态的思想来考虑问题。dp对递归思维的掌握要求比较高。
这里有一点需要强调的是,避免重复运算,所以一开始做标记-1,如果每个位置对应的长度有了,则覆盖标记。复杂度就成为O(n^2),不然会很大很大爆表。
入门题举例:最长上升子序列
#include<iostream>
#include<cstring>
#include<map>
#include<algorithm>
using namespace std;
const int MAXN=1010;
int a[MAXN];
int maxLen[MAXN];
int main()
{
int N; cin>>N;
for(int i=1;i<=N;i++)
{
cin>>a[i];maxLen[i]=1;
}
for(int i=2;i<=N;++i){
//每次求以第i个数为终点的最长上升子序列的长度
for(int j=1;j<i;++j){
//查看第j个数为终点的最长上升子序列
if(a[i]>a[j])
maxLen[i]=max(maxLen[i],maxLen[j]+1);
}
}
cout<<*max_element(maxLen+1,maxLen+N+1);//这个是STL库中的求数组中最大元素。
return 0;
}
DP的通常解题思路(以寻找最长上升子序列为例):
(1)找子问题:
即“求以ak(k=1,2,3…N)为终点的最长上升子序列的长度”。一个上升子序列中最右边的那个数,称为该子序列的"终点"。虽然这个子问题和原问题形式上并不完全一样,但是只要这N个子问题解决了,那么这N个子问题的解中,最大的那个就是整个问题的解。
(2)确定状态:
子问题只和一个变量——数字的位置有关。因此序列中数的位置k就是"状态",而状态k对应的"值",就是以ak作为“终点”的最长上升子序列的长度。状态一共有N个。
(3)找出转移状态的方程:
maxLen(k)表示以ak作为"终点"的最长上升子序列的长度,那么:
初始状态:maxLen(1)=1
末端状态:maxLen(k)=max{maxLen(i):1<=i<k且ai<ak且k!=1}+1如果找不到这样的i,则maxLen(k)=1
maxLen(k)的值,就是在ak左边,"终点"的数值小于ak,且长度最大的那个上升子序列的长度再加1.因为ak左边任何"终点"小于ak的子序列,加上ak后就能形成一个更大的上升子序列,时间复杂度O(n^2)
关于"无后效性":
无后效性是指如果在某个阶段上过程的状态已知,则从此阶段以后过程的发展变化仅与此阶段的状态有关,而与过程在此阶段以前的阶段所经历过的状态无关。利用动态规划方法求解多阶段决策过程问题,过程的状态必须具备无后效性。
所谓无后效性原则,指的是这样一种性质:某阶段的状态一旦确定,则此后过程的演变不再受此前各状态及决策的影响。也就是说,“未来与过去无关”,当前的状态是此前历史的一个完整总结,此前的历史只能通过当前的状态去影响过程未来的演变。具体地说,如果一个问题被划分各个阶段之后,阶段k中的状态只能通过阶段k+1中的状态通过状态转移方程得来,与其他状态没有关系,特别是与未发生的状态没有关系,这就是无后效性。
对于不能划分阶段的问题,不能用动态规划来解;对于能划分阶段,但不符合最优化原理,也不能用动态规划来解;既能划分阶段,又符合最优化原理,但不具备无后效性原则的,还是不能用动态规划来解;误用动态规划程序设计方法求解会导致错误的结果。
(例题)求最长公共子序列的问题:
#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
using namespace std;
char sz1[1000];
char sz2[1000];
int maxLen[1000][1000];
int main()
{
while(cin>>sz1>>sz2)
{
int length1=strlen(sz1);
int length2=strlen(sz2);
int nTmp;
int i,j;
for(i=0;i<=length1;i++) maxLen[i][0]=0;
for(j=0;j<=length2;j++) maxLen[0][j]=0;
for(i=1;i<=length1;i++){
for(j=1;j<=length2;j++){
if(sz1[i-1]==sz2[j-1])
maxLen[i][j]=maxLen[i-1][j-1]+1;
else
maxLen[i][j]=max(maxLen[i-1][j],maxLen[i][j-1]);
}
}
cout<<maxLen[length1][length2]<<endl;
}
return 0;
}
个人觉得还是找状态最重要,把每一个节点的状态表示出来,在这里显然是maxLen[i][j]代表每个点的状态(即当前的最长子序列)。对于这个题我们采用二维数组dp求解方法,可以构建一个矩阵模型来看
最佳加法表达式:
题意:由一个1…9组成的字串,问如果将m个+号插入进去,所形成的的表达式中,那个最小的和的表达式是多少
#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
using namespace std;
const int INF=99999;
int a[1005],num[1005][1005];
int V(int m,int n)
{
if(m==0) return num[1][n];//无加号
else if(n<m+1) return INF;//加号过多
else{
int t=INF;
for(int i=m;i<=n-1;i++)//这里是n-1,排除了最后一个数字后面放加号的情况
t=min(t,V(m-1,i)+num[i+1][n]);
return t;
}
}
int main()
{
int n,m;
while(cin>>n>>m)
{
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=1;i<=n;i++)
{
num[i][i]=a[i];//只有一个数字时
for(int j=i+1;j<=n;j++)
{
num[i][j]=num[i][j-1]*10+a[j];
}
}
cout<<V(m,n)<<endl;
}
return 0;
}