动态规划专题之数塔问题

动态规划系列专题讲义

专题一:数塔问题
/*
	Name: 动态规划专题之数塔问题 
	Author: 巧若拙 
	Description:7625_三角形最佳路径问题
描述:如下所示的由正整数数字构成的三角形: 
7 
3 8 
8 1 0 
2 7 4 4 
4 5 2 6 5 

从三角形的顶部到底部有很多条不同的路径。对于每条路径,把路径上面的数加起来可以得到一个和,和最大的路径称为最佳路径。你的任务就是求出最佳路径上的数字之和。 
注意:路径上的每一步只能从一个数走到下一层上和它最近的下边(正下方)的数或者右边(右下方)的数
输入
第一行为三角形高度100>=h>=1,同时也是最底层边的数字的数目。
从第二行开始,每行为三角形相应行的数字,中间用空格分隔。
输出
最佳路径的长度数值。
样例输入
5
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
样例输出
30
*/
#include<iostream>  
#include<cstring>
  
using namespace std;  
  
const int MAX = 100;   
int map[MAX][MAX];  
int B1[MAX][MAX]; //备忘录,记录从位置(x,y)到达底行所获得的最大值
int B2[MAX][MAX]; //备忘录,记录从顶点到位置(x,y)所获得的最大值 
int bestP; //记录最优解 
  
void DFS(int n, int x, int y, int curP);//n表示行数,x,y表示当前位置坐标,curP表示目前已走路径上的权值和 
int DP_1(int n, int x, int y);//n表示行数,计算从位置(x,y)到达底行所获得的最大值 
int DP_2(int n); //动态规划;顺推法 
int DP_3(int n); //动态规划(逆推法)
  
int main()   
{  
    int n;  
    cin >> n;  
    for (int i=0; i<n; i++)  
    {  
        for (int j=0; j<=i; j++)  
        {  
            cin >> map[i][j];  
        }  
    }  
    
    //回溯算法
    DFS(n, 0, 0, map[0][0]);  
    cout << bestP << endl;  
    
	//记忆化搜索(备忘录算法)
	memset(B1, -1, sizeof(B1)); //先初始化B1的值全为-1  
    cout << DP_1(n, 0, 0) << endl;  
    
    //动态规划(顺推法)
    cout << DP_2(n) << endl;  
    
    //动态规划(逆推法)
    cout << DP_3(n) << endl;  
      
    return 0;  
}  

算法1:回溯算法,需要用到全局变量map[MAX][MAX],另有bestP初始化为0。
void DFS(int n, int x, int y, int curP)//n表示行数,x,y表示当前位置坐标,curP表示目前已走路径上的权值和 
{  
    if (x == n-1) //语句1
    {
		if (curP > bestP)
			bestP =  //语句2
	}
	else
	{
		DFS(n, x+1, y, curP+map[x+1][y]); //向正下方走 
		DFS( ); //语句3 
	}
}  

问题1:能否把语句1改为if (x == n)?为什么? 
问题2:将语句2和语句3补充完整。

参考答案:
问题1:不能修改,因为递归出口是x == n-1,因为数组的下标是从0开始的,(n-1)表示第n行(底行)。
问题2:语句2:bestP = curP;
       语句3:DFS(n, x+1, y+1, curP+map[x+1][y+1]);

算法2:记忆化搜索(备忘录算法),需要用到全局变量map[MAX][],另有B1[MAX][]初始化为-1。
int DP_1(int n, int x, int y)//n表示行数,计算从位置(x,y)到达底行所获得的最大值 
{  
    if (B1[x][y] != -1)  
		return  //语句1
 	
	if (x == n-1)
		B1[x][y] =  //语句2
	else
		B1[x][y] = map[x][y] + max(DP_1(n,x+1,y), DP_1(n,x+1,y+1));
	
    return B1[x][y]; 
}  

问题1:将语句1和语句2补充完整。
问题2:与算法1(回溯算法)相比,算法2(备忘录算法)有哪些优越之处?

参考答案:
问题1:语句1:return B1[x][y];
       语句2:B1[x][y] = map[x][y];
问题2:回溯算法进行了重复搜索,而备忘录算法利用二维数组B1[x][y]记录了已搜索路径,无需重复搜索,大大提高了效率。

算法3:动态规划(顺推法),需要用到全局变量map[MAX][],另有B2[MAX][]初始化为0。
int DP_2(int n)//动态规划;顺推法
{  
	B2[0][0] = map[0][0];
	for (int i=1; i<n; i++)//语句1 
		B2[i][0] = map[i][0] + B2[i-1][0];
		
    for (int i=1; i<n; i++) 
    {  
        for (int j=1; j<=i; j++) //语句2 
        {  
            B2[i][j] = map[i][j] + max(B2[i-1][j], B2[i-1][j-1]);
        }  
    }
	
	int s = B2[n-1][0];
	for (int j=1; j<n; j++) //语句3  
	{
		if (s < B2[n-1][j])
			s = B2[n-1][j];
	}  
	
    return s;  
}  

问题1:语句1所在循环体的作用是什么?为什么要单独列出来?能否合成在语句2中?
问题2:语句2能否改为:for (int j=i; j>0; j--) ?为什么?
问题3:语句3能否改为:for (int j=n-1; j>0; j--) ?为什么?

参考答案: 
问题1:语句1所在循环体的作用是为第一列赋值。单独列出来的原因是第一列的元素已经处于最左侧,其左上方无元素,故其值直接由其正上方元素决定;而语句2中的元素值由其左上方和正上方元素的较大值来决定。如果把语句1中的循环体并入语句2中,则需要判断其下标是否越界,降低了效率。
问题2:可以。因为算法3是从上往下推的顺推法,其状态方程为:B2[i][j] = map[i][j] + max(B2[i-1][j], B2[i-1][j-1]);是利用上一行元素来计算下一行元素的值,与本行元素无关,故列坐标j递增或递减均可。
问题3:可以。因为语句2所在循环体的作用是在底行寻找最大值,顺序或逆序扫描数组B2[n-1][j]均可,故列坐标j递增或递减均可。

算法4:动态规划(逆推法),需要用到全局变量map[MAX][]。
int DP_3(int n) //动态规划(逆推法) 
{  
    for (int i=n-2; i>=0; i--)  //语句1  
    {  
        for (int j=i; j>=0; j--)   
        {  
            map[i][j] += max(map[i+1][j], map[i+1][j+1]);  
        }  
    }  
    return map[0][0];  
}

问题1:语句1能否改为:for (int i=n; i>0; i--) ?为什么?
问题2:与算法3(顺推法)相比,算法4(逆推法)有哪些优越之处?

参考答案: 
问题1:不能。因为算法4是从右下角斜向上走,最终到达顶点的逆推法,它直接利用数字三角形的特征,从倒数第2行的最右列元素开始,直接把其正下方和右下方元素中的较大值累加到自身,这样在处理左上角元素的时候,刚好得到最优解。
	语句1是外层循环,从下往上遍历各行,故循环变量i需要递减。
问题2:主要优点有二。一是顺推法得到所有的解后,需要遍历底行找出最大值;而逆推法直接得出最大值,效率更高。
二是顺推法需要先求出第一列的值,然后根据状态方程求出其他元素的值;而逆推法可以直接利用递推式计算出所有元素值,代码相对更简洁。

拓展练习:原题只要求算出最佳路径上的数字之和,并未要求输出最佳路径。现在要求在算法4 int DP_3(int n)的基础上,编写函数void PrintPath(int n);//输出最佳路径。

参考答案:
void PrintPath(int n) //输出最佳路径 
{  
    int i = 0, j = 0;  
      
    for (int k=1; k<n; k++) //输出前n-1行   
    {  
        if (map[i+1][j] > map[i+1][j+1]) //向正下方走  
        {  
            cout << map[i][j] - map[i+1][j] << "->";  
            i++;  
        }   
        else //向右下方走  
        {  
            cout << map[i][j] - map[i+1][j+1] << "->";  
            i++;  j++;  
        }   
    }  
    //输出底层元素   
    cout << map[i][j] << endl;  
}

课后练习:
练习1:2728_摘花生
描述:Hello Kitty 想摘点花生送给她喜欢的米老鼠。她来到一片有网格状道路的矩形花生地(如下图),从西北角进去,东南角出来。
 
地里每个道路的交叉点上都有种着一株花生苗,上面有若干颗花生,经过一株花生苗就能摘走该它上面所有的花生。Hello Kitty只能向东或向南走,不能向西或向北走。问Hello Kitty 最多能够摘到多少颗花生。

输入
第一行是一个整数T,代表一共有多少组数据。1<=T <= 100
接下来是T组数据。
每组数据的第一行是两个整数,分别代表花生苗的行数R和列数 C ( 1<= R,C <=100),每组数据的接下来R行数据,从北向南依次描述每行花生苗的情况。每行数据有 C 个整数,按从西向东的顺序描述了该行每株花生苗上的花生数目 M ( 0<= M <= 1000)。
输出
对每组输入数据,输出一行,内容为Hello Kitty能摘到得最多的花生颗数。
样例输入
2
2 2
1 1
3 4
2 3
2 3 4
1 6 5
样例输出
8
16

练习2:7614_最低通行费
描述:一个商人穿过一个 N*N 的正方形的网格,去参加一个非常重要的商务活动。他要从网格的左上角进,右下角出。
每穿越中间1个小方格,都要花费1个单位时间。商人必须在(2N-1)个单位时间穿越出去。而在经过中间的每个小方格时,都需要缴纳一定的费用。
这个商人期望在规定时间内用最少费用穿越出去。请问至少需要多少费用?
注意:不能对角穿越各个小方格(即,只能向上下左右四个方向移动且不能离开网格)。
输入
第一行是一个整数,表示正方形的宽度N (1 <= N < 100);
后面 N 行,每行 N 个不大于 100 的整数,为网格上每个小方格的费用。
输出
至少需要的费用。
样例输入
5
1 4 6 8 10 
2 5 7 15 17 
6 8 9 18 20 
10 11 12 19 21 
20 23 25 29 33 
样例输出
109
提示
样例中,最小值为109=1+2+5+7+9+12+19+21+33。

练习3:2718_移动路线
描述:桌子上有一个m行n列的方格矩阵,将每个方格用坐标表示,行坐标从下到上依次递增,列坐标从左至右依次递增,左下角方格的坐标为(1,1),则右上角方格的坐标为(m,n)。
小明是个调皮的孩子,一天他捉来一只蚂蚁,不小心把蚂蚁的右脚弄伤了,于是蚂蚁只能向上或向右移动。小明把这只蚂蚁放在左下角的方格中,蚂蚁从左下角的方格中移动到右上角的方格中,每步移动一个方格。蚂蚁始终在方格矩阵内移动,请计算出不同的移动路线的数目。
对于1行1列的方格矩阵,蚂蚁原地移动,移动路线数为1;对于1行2列(或2行1列)的方格矩阵,蚂蚁只需一次向右(或向上)移动,移动路线数也为1……对于一个2行3列的方格矩阵,如下图所示:
-------------------
|(2,1)|(2,2)|(2,3)|
-------------------
|(1,1)|(1,2)|(1,3)|
-------------------
蚂蚁共有3种移动路线:
路线1:(1,1) → (1,2) → (1,3) → (2,3)
路线2:(1,1) → (1,2) → (2,2) → (2,3)
路线3:(1,1) → (2,1) → (2,2) → (2,3)
输入
输入只有一行,包括两个整数m和n(0<m+n<=20),代表方格矩阵的行数和列数,m、n之间用空格隔开
输出
输出只有一行,为不同的移动路线的数目。
样例输入
2 3
样例输出
3

提示:移动路线类似数塔问题,但不是求最佳路径,而是求方案总数,请尝试分别用二维数组和一维数组来记录结果。

练习4:5294_挖地雷
题目描述:在一个地图上有N个地窖(N<=20),每个地窖中埋有一定数量的地雷。同时,给出地窖之间的连接路径。当地窖及其连接的数据给出之后,某人可以从第一个地窖开始挖地雷,然后可以沿着指出的连接往下挖(仅能选择一条路径),当无连接时挖地雷工作结束。
设计一个挖地雷的方案,使某人能挖到最多的地雷。

输入描述 Input Description
第1行只有一个数字,表示地窖的个数N。
第2行有N个数,分别表示每个地窖中的地雷个数。
第3行至第N+1行表示地窖之间的连接情况:
第3行有n-1个数(0或1),表示第一个地窖至第2个、第3个、…、第n个地窖有否路径连接。
如第3行为1 1 0 0 0 … 0,则表示第1个地窖至第2个地窖有路径,至第3个地窖有路径,至第4个地窖、第5个、…、第n个地窖没有路径。
第4行有n-2个数,表示第二个地窖至第3个、第4个、…、第n个地窖有否路径连接。
… …
第n+1行有1个数,表示第n-1个地窖至第n个地窖有否路径连接。(为0表示没有路径,为1表示有路径)。

输出描述 Output Description
第一行表示挖得最多地雷时的挖地雷的顺序,各地窖序号间以一个空格分隔,不得有多余的空格。
第二行只有一个数,表示能挖到的最多地雷数。

样例输入 Sample Input
5
10 8 4 7 6
1 1 1 0
0 0 0
1 1
1
样例输出 Sample Output
1 3 4 5
27

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值