#动态规划dp入门

什么是动态规划呢?
动态规划(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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值