动态规划初步(1)

动态规划是一种用途很广的问题求解方法,它本身并不是一个特定的算法,而种手段。下面通过一个题目阐述动态规划的基本思路和特点。

一,数字三角形问题:

      7

                 3         9

            8        2        8

      4        7        6        7

2         5      4         3        5

    在上面的数字三角形中寻找一条从顶部到底部的路径,使得路径上所经过的数字和最大。

路径上的每一步都只能往左下或右下走。只需要求出这个最大和即可,不必给出具体路径。

输入格式:

4   //三角形行数。下面是三角形

7

3    8

8    1    0

2    7    4    4

题目分析:

 熟悉的回潮法,可能会立刻发现这是一个动态的决策问题:每次有两种选择一左下或右下。如果用回溯法求出所有可能的路线,就可以从中选出最优路线。但和往常一样,回溯法的效率太低。 一个n层数字三角形的完整路线有2-1条,当n很大时回溯法的速度将让人无法忍受。
   为了得到高效的算法,需要用抽象的方法思考问题:把当前的位置(i,j)看成一个状态(还记得吗? ),然后定义状态(i, j)的指标函数d(i, j)为从格子(i, j)出发时能得到的最大和(包括格子(i,j)本身的值)。在这个状态定义下,原问题的解是d(1, 1). 
      下面看看不同状态之间是如何转移的。从格子(i, j)出发有两种决策。如果往左走,则走到(i+1, j)后需要求“从(i+1, j)出发后能得到的最大和”这一问题,即d(i+1,j)。类似地,往右走之后需要求解d(i+1, j+1).由于可以在这两个决策中自由选择,所以应选择d(i+1,)和d(i+1j+1)中较大的一个。换句话说,得到了所谓的状态转移方程:

      d(i,j)= a(i,j)+ max{d(i+1,j),d(i+1,j+1)}


      如果往左走,那么最好情况等于(i, j)格子里的值a(i, j)与“从(i+1, j)出发的最大总和”之和,此时需注意这里的“最大”二字。如果连“从(i+1,j)出发走到底部”这部分的和都不是最大的,加上a(i, j)之后肯定也不是最大的。这个性质称为最优子结构( optimalsubstructure),也可以描述成“全局最优解包含局部最优解”。不管怎样,状态和状态转移方程一起完整地描述了具体的算法。


具体代码如下:

#include<stdio.h>
#define Max 6
int maxSum[Max][Max]; //存放对应三角形元素到底部的值
int n;
int D[Max][Max];
 
int max(int x,int y )
{
	return x>=y?x:y;
}
 
int main()
{
	int i,j;
	scanf("%d",&n);
	for(i=1;i<=n;i++)
		for(j=1;j<=i;j++)
			scanf("%d",&D[i][j]);
	for(i=1;i<=n;i++)
		maxSum[n][i] = D[n][i];//最后一行到底部的值等于三角形底部行对应的值
	for(i=n-1;i>=1;i--)
		for(j=1;j<=i;j++)
			maxSum[i][j]=max(maxSum[i+1][j],maxSum[i+1][j+1])+D[i][j];//取较大值
	printf("%d",maxSum[1][1]);
}

动态规划的核心是状态和状态转移方程。


 

二,记忆化搜索与递推


方法1:递归计算。程序如下(需注意边界处理) :

int solve(int 1,int j) 

{
return a[1][11 + (1 == n ? 0 : max(solve(i+1,j),solve(i+1,j+1));
}


这样做是正确的,但时间效率太低,其原因在于重复计算。子问题被重复计算了多次。

方法2:递推计算。程序如下(需再次注意边界处理) :

int i, j;
for(j = 1; j <= n; j++) 
d[n][j] = a[n][j];
for(i = n-1; i >= 1; i--)
for(j=1;j<=i;j++)
d[i][j] = a[i][j] + max(d[i+1][j],d[i+1] [j+1]);


程序的时间复杂度显然是O(n?), 但为什么可以这样计算呢?原因在于: i是逆序枚举的,因此在计算d[i][j]前,它所需要的d[i+1][j]和d[i+1][j+1]-定已经计算出来了。
可以用递推法计算状态转移方程。递推的关键是边界和计算顺序。在多数情况下,递推法的时间复杂度是:状态总数x每个状态的决策个数x决策时间。如果不同状态的决策个数不同,需具体问题具体分析。

方法3:记忆化搜索。程序分成两部分。首先用“"memset(d,-1,sizeof()d;"把d全部初始化为-1,然后编写递归函数:

int solve(int i, int j) 
{
if(d[i][i] >= 0) return d[i][j];
returnd[i][j]=a[i][j]+ (i==n ? 0 : max (solve (i+1,), solve(i+1,j+1)));

}

上述程序依然是递归的,但同时也把计算结果保存在数组d中。题目中说各个数都是非负的,因此如果已经计算过某个d[i][],则它应是非负的。这样,只需把所有d初始化为-1,即可通过判断是否d[i][i]≥0得知它是否已经被计算过。
最后,千万不要忘记在计算之后把它保存在可句中。根据c语言“赋值语句本身有返回值”的规定,可以把保存d[i][]的工作合并到函数的返回语句中。
上述程序的方法称为记忆化(memoization),它虽然不像递推法那样显式地指明了计算顺序,但证每个结点只访问”一次。

可以用记忆搜索的方法计算状态转移方程。当采用记忆化搜索时,不必事先声明确定各状态的计算顺序,但需要记录每个状态“是否已经计算过”。

猜你喜欢

转载自blog.csdn.net/qq_43627087/article/details/88749921