浅谈动态规划

浅谈动态规划

一、算法综述

在我的理解中,动态规划核心要点是如何拆分问题,即对问题状态的定义和状态方程的定义。动态规划应该在考虑全局的情况下,根据状态转移方程求解当前状态的最优解,从而最终得出结果。它有“最优子结构”和“后效性”的特点。它适用于每个阶段的最优状态可以从之前某个阶段的某个或某些状态直接得到(最优子结构)而不管之前这个状态是如何得到的(后效性)。与贪心所不同的是,贪心是一定是由上一个状态所得到。

什么是状态?或者可以说是当前情况下所符合条件的最优解。

什么是状态转移方程?可以说是当前状态和之前最优状态之间的关系。

在求解动态规划问题中,比如求解当前状态时,必须要求之前的最优状态。而每一次求当前状态都要重复求之前的状态,造成很大的浪费,所以往往用“缓存”来解决这个问题,通俗的说是用空间来换取时间。

而对状态转移方程的求法,因为当前最优状态是由之前最优状态所求得,而我们并不需要考虑之前最优状态是如何具体求得(后效性),所以很容易想到用递推来描述状态转移方程。在一篇文章中,博主认为动态规划是由递归演变而来,我觉得这种说法在这方面也是正确的。

思考动态规划还有很重要的一点是边界,我们把这种子问题在一定时候就不再需要提出子子问题的情况叫做边界,没有边界就会出现死循环。

这就是动态规划,具有“最优子结构”、“子问题重叠”、“边界”和“无后效性”的特点。


二、算法的优缺点分析(包含但不限于以下两个方面的讨论)

1、算法适合解决那些问题

1)最优子结构

2)子问题重叠

3)问题存在边界

4)子问题相互独立

2、算法的适应性讨论

在求解问题时,某些结果需要重复计算或不必要进行计算,这时用动态规划能有效的避免这个问题,大大减少其时间复杂度。

给出动态规划时间复杂度的决定因素:

时间复杂度=状态总数*每个状态转移的状态数*每次状态转移的时间


对于动态规划的优点,时间复杂度相对于其他算法,优势很明显,但是其缺点,消耗空间大,当所给出范围很大时,堆栈中很可能并不能满足所需要的空间大小,往往对其的解决办法是降低数组维度,或者去除一些不必要的状态数等。


三、算法举例

1、例子一: 01-背包问题

1)题目描述

有编号分别为a,b,c,d,e的五件物品,它们的重量分别2,2,6,5,4,它们的价值分别是6,3,5,4,6,现在给你个承重为10的背包,如何让背包里装入的物品具有最大的价值总和?

2)解题思路

根据问题,很容易得出当前最优解是取上一次最优解和之前某次最优解加上当前状态的最大值。a,b,c,d,e的承重为10的最大价值是a,b,c,d承重为10的最大价值和b,c,d,e承重为8的最大价值+a的价值的最大值。由此引出状态转移方程

f[i][j] = max(f[i-1][j],f[i-1][j-x]+valueX)

根据状态方程填表

a8表示a,b,c,d,e重量为8时的最大价值

C5表示c,d,e重量为5时的最大价值

以此类推

3)完整源程序

#include <iostream>
#include <cstring>
using namespace std;

int main(){
	int maxValue[5][10];
	int w[5]={2,2,6,5,4};
	int v[5]={6,3,5,4,6};
	int Max = 0;
	memset(maxValue,0,sizeof(maxValue));
	for(int i = 3;i<10;i++){//确定边界 
		maxValue[4][i] = 6;
	}
	
	for(int i = 3;i>=0;i--){//根据状态转移方程填表
		for(int j = 0;j<10;j++){
			maxValue[i][j] = max(maxValue[i+1][j],maxValue[i+1][j-w[i]]+v[i]);
			if(maxValue[i][j] > Max){
				Max = maxValue[i][j];
			}
		} 
	}	
	for(int i = 0;i<5;i++){
		for(int j = 0;j<10;j++){
			cout.width(3);
			cout<<maxValue[i][j];
		}
		cout<<endl;
	}
	cout<<"MaxValue:"<<Max<<endl;
	return 0;
} 

4)测试用例


2、例子二:hdu1024

1)题目描述

Problem Description

Now I think you have got an AC in Ignatius.L's "Max Sum" problem. To be a brave ACMer, we always challenge ourselves to more difficult problems. Now you are faced with a more difficult problem. Given a consecutive number sequence S1, S2, S3, S4 ... Sx, ... Sn (1 ≤ x ≤ n ≤ 1,000,000, -32768 ≤ Sx ≤ 32767). We define a function sum(i, j) = Si + ... + Sj (1 ≤ i ≤ j ≤ n). Now given an integer m (m > 0), your task is to find m pairs of i and j which make sum(i1, j1) + sum(i2, j2) + sum(i3, j3) + ... + sum(im, jm) maximal (ix ≤ iy ≤ jx or ix ≤ jy ≤ jx is not allowed). But I`m lazy, I don't want to write a special-judge module, so you don't have to output m pairs of i and j, just output the maximal summation of sum(ix, jx)(1 ≤ x ≤ m) instead. ^_^



Input

Each test case will begin with two integers m and n, followed by n integers S1, S2, S3 ... Sn. Process to the end of file.


Output

Output the maximal summation described above in one line.



Sample Input

1 3 1 2 3

2 6 -1 4 -2 3 -2 3



Sample Output

6

8


2)解题思路

设状态为 sum[i,j],表示前 j 项分为 i 段的最大和,且第 i 段必须包含 data[j],则状态转移方程如下:

sum[i,j] = max{sum[i,j − 1] + data[j],max{sum[i − 1,t] + data[j]}},

其中i ≤ j ≤ n,i − 1 ≤ t < j 分为两种情况:

• 情况一,data[j] 包含在第 i 段之中,sum[i,j − 1] + data[j]。

• 情况二,data[j] 独立划分成为一段,max{sum[i − 1,t] + data[j]}。

例如如上表格,首先很容易得出j<m不存在,即数的个数小于分割的段数是不存在的。为什么sum[i][j]一定要包含data[j]呢?仔细分析可以知道只有包含data[j]才能保证他和上一段是连续的,类似于在当前状态下再接一个数。

表格中(1,3)位置为7(下标从0开始),根据状态转移方程,求法就是

max( (1,2)位置的数+data[3],上一行中j<3的最大的数+data[3]),即max(2+3,4+3) = 7;其余类似可求得。

当然,这里数据量很大,二维数组可能并不能满足要求,考虑用一维数组进行替代。

3)完整源程序

#include <iostream>
#include <cstring>
using namespace std;

int tempMax[1000001];
int sum[1000001];
int data[1000001];

int main(){
	int m,n,max_num;
	int temp;
	while(cin>>m){
		cin>>n;
		for(int i = 0;i<n;i++){
			cin>>data[i];
		}
		tempMax[0] = INT_MIN;
		max_num=INT_MIN;
		sum[0] = data[0];
		for(int i=1;i<n;i++){//第一次设置边界 
			sum[i] = max(sum[i-1]+data[i],data[i]);	
				tempMax[i] = max(tempMax[i-1],sum[i-1]);
		}
			
		for(int j = 1;j<m;j++){
			temp = sum[j-1];
			memset(sum,0,n*sizeof(int));
			sum[j] = temp +data[j];
			tempMax[j] = sum[j];
			for(int k = j+1;k<n;k++){
				sum[k] = max(sum[k-1]+data[k],tempMax[k]+data[k]);
				tempMax[k] = max(tempMax[k-1],sum[k-1]);
			}
		}
		for(int i = m-1;i<n;i++){
			if(max_num<sum[i]){
				max_num = sum[i];
			}
			
		}
		
		cout<<max_num<<endl;
			
		}
		
	return 0;
} 

4)测试实例


2、例子三:紫书里的单向TSP

1)题目描述

给你一个n行m列的整数矩形,从第一列任何一个位置出发每次往右,右上或右下走一格,最终到达最后一列。要求经过的整数之和最小,整个矩形是环形的,即第一行的上一行是最后一行,最后一行的下一行是第一行,输出路径上每列的行号,多解时输出字典序最小的。 (TSP)

2)解题思路

这是一个多阶段决策问题,每一列就是一个阶段,在问题中很容易得出当前状态是由前一列的右上,右,右下得到的,要得到每个状态的最小值(即当前最优状态),只需比较前一列右上,右,右下的最小值(最优状态)就好了。于是,就得到了下面的状态转移方程:

dp[i][j]=dp[i][j]+min(dp[i-1][j-1],dp[i][j-1],dp[i+1][j-1])

3)完整源程序

#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>
#include <cstring>
using namespace std;
const int maxx=10000;
int G[maxx][maxx];
int dp[maxx][maxx];
int n,m,ar[3];
int compare_min(int a,int b,int c)   //比较a,b,c中最小的一个
{
     ar[0]=a,ar[1]=b,ar[2]=c;
     sort(ar,ar+3);
     return ar[0];
}
int main()
{
     cin>>n>>m;
     for(int i=1;i<=n;i++)
          for(int j=1;j<=m;j++)
               cin>>G[i][j];
     for(int j=0,i=1;i<=n;i++)
          dp[i][j]=0;
     for(int j=1;j<=m;j++)//根据状态转移方程递推求解 
          for(int i=1;i<=n;i++)
          {
               if(i==1)//在第一行时 
                    dp[i][j]=G[i][j]+compare_min(dp[n][j-1],dp[i][j-1],dp[i+1][j-1]);
               else if(i==n)//最后一行时 
                    dp[i][j]=G[i][j]+compare_min(dp[i-1][j-1],dp[i][j-1],dp[1][j-1]);
               else 
                    dp[i][j]=G[i][j]+compare_min(dp[i-1][j-1],dp[i][j-1],dp[i+1][j-1]);
          }
     int ans=INT_MAX;
     for(int j=m,i=1;i<=n;i++)//在dp最后一行中找到最小的值 
          if(ans>dp[i][j])
               ans=dp[i][j];
     cout<<ans<<endl;
     int road[m],cns=0;
     for(int j=m;j>=1;j--)
     {
          for(int i=1;i<=n;i++)//找到符合ans的j坐标 
          {
               if(dp[i][j]==ans)
               {
                    road[cns]=i;
                    break;
               }
          }
          ans=ans-G[road[cns]][j];//ans减去当前数 
          cns++;
     }
     for(int i=m-1;i>=0;i--)
          cout<<road[i]<<" ";;
     cout<<endl;
     return 0;
}

4)测试用例


四、参考文献

1、刘汝佳. 算法竞赛入门经典(第2版). 清华大学出版社, 2014.

2.Thomas H.Cormen, Charles E.Leiserson, Ronald L.Rivest,等. 算法导论(原书第3版)[J]. 计算机教育, 2013(12):51-51.

3.知乎专题《什么是动态规划?动态规划的意义是什么?》

4.廖慧芬, 邵小兵. 动态规划算法的原理及应用[J]. 中国科技信息, 2005(21A):42-42.

5.李端, 钱富才, 李力,等. 动态规划问题研究[J]. 系统工程理论与实践, 2007, 27(8):56-64.

编辑于 2018-01-06