动态规划专题之数塔问题

动态规划系列专题讲义

 

专题二:数塔问题

/*

       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 pre[MAX]; //pre[j]相当于B2[i+1][j]

int cur[MAX]; //cur[j]相当于B2[i][j]

int F[MAX];//记录当前行的最优解

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); //动态规划(降维优化1)

int DP_4(int n); //动态规划(降维优化2)

int DP_5(int n); //动态规划(利用原数据)

void PrintPath(int n); //输出最佳路径,需要用到全局变量B2[MAX][]

void PrintPath_2(int n); //输出最佳路径,需要用到全局变量map[MAX][]

 

int main()  

   int n; 

   cin >> n; 

   for (int i=0; i<n; i++) 

   { 

       for (int j=0; j<=i; j++) 

       { 

           cin >> map[i][j]; 

       } 

   } 

     

   //回溯算法,需要用到全局变量map[MAX][MAX],另有bestP初始化为0

   DFS(n, 0, 0, map[0][0]); 

   cout << bestP << endl; 

   

       //记忆化搜索(备忘录算法),需要用到全局变量map[MAX][],另有B1[MAX][]初始化为-1

       memset(B1,-1, sizeof(B1)); //先初始化B1的值全为-1 

   cout << DP_1(n, 0, 0) << endl; 

   

    //动态规划(逆推法),需要用到全局变量B2[MAX][]

   cout << DP_2(n) << endl; 

   

   //动态规划(逆推法),需要用到全局变量pre[]和cur[]

   cout << DP_3(n) << endl; 

   

   //动态规划(逆推法),需要用到全局变量F[]

   cout << DP_4(n) << endl; 

   

   PrintPath(n); //输出最佳路径,需要用到全局变量B2[MAX][]

   

   //动态规划(逆推法),需要用到全局变量map[MAX][]

   cout << DP_5(n) << endl; 

   

   PrintPath_2(n); //输出最佳路径,需要用到全局变量map[MAX][]

     

   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) 

       for(int j=0; j<n; j++)  //语句1

              B2[n-1][j]= map[n-1][j];

   for (int i=n-2; i>=0; i--)   //语句2

   { 

       for (int j=i; j>=0; j--)   //语句3

       { 

           B2[i][j] = map[i][j] + max(B2[i+1][j], B2[i+1][j+1]);

       } 

   } 

   return B2[0][0]; 

}

 

问题1:语句1所在循环体的作用是什么?它相当于算法2(备忘录算法)中的哪个语句?

问题2:语句2能否改为:for (int i=0; i<=n-2; i++)?为什么?

问题3:语句3能否改为:for (int j=0; j<=i; j++)?为什么?

 

参考答案:

问题1:语句1所在循环体的作用是为第n行赋值,它是初始条件,相当于算法2中的语句2(递归出口)。

问题2:不能。因为算法3是从下向上走,最终到达顶点的逆推法,必须逆序扫描行坐标。

问题3:可以。因为算法3的状态方程为:B2[i][j] = map[i][j] +max(B2[i+1][j], B2[i+1][j+1]);是利用下一行元素来计算上一行元素的值,与本行元素无关,故列坐标j递增或递减均可。

 

算法4:动态规划:使用2个一维数组存储记录,需要用到全局变量map[MAX][], 另有pre[]和cur[]均初始化为0。

int DP_3(int n)  

   for (int j=0; j<n; j++)  

              pre[j]= map[n-1][j];

   for (int i=n-2; i>=0; i--)   

   { 

       for (int j=i; j>=0; j--)  

       { 

           cur[j] =  //语句1  

       }

              for(int j=i; j>=0; j--)  

       { 

           pre[j] =  //语句2

       } 

   } 

   return pre[0]; 

}

问题1:将语句1和语句2补充完整。

问题2:和算法3作比较,DP_3(int n)中的pre[j]和cur[j]分别相当于DP_2(int n)中的哪两个变量?

 

参考答案:

问题1:语句1:cur[j] = map[i][j] + max(pre[j], pre[j+1]);

      语句2:pre[j] = cur[j];

问题2:pre[j]相当于DP_2(int n)中的B2[i+1][j],cur[j]相当于B2[i][j]。

 

算法5:动态规划:使用1个一维数组存储记录,需要用到全局变量map[MAX][], 另有F[]初始化为0。

int DP_4(int n)  

   for (int j=0; j<n; j++)  

              F[j]= map[n-1][j];

   for (int i=n-2; i>=0; i--)

   { 

       for (int j=0; j<=i; j++)   //语句1

       { 

           F[j] = map[i][j] + max(F[j], F[j+1]); 

       }

   } 

   return   //语句2

}

 

问题1:语句1能否改为:for (int j=i; j>=0; j--) ?为什么?

问题2:将语句2补充完整。

 

参考答案:

问题1:不能。我们与用二维数组记录结果的算法做比较:

在二维数组算法中B2[i][j] = map[i][j] + max(B2[i+1][j], B2[i+1][j+1]),即第i行第j列的元素,由第i+1行的元素决定,且列坐标j小的元素由j大的元素决定。由于我们记录了B2[i+1][]的值,故在求B2[i][j]时,列坐标j递增或递减均可。

若我们用一维数组F[j]代替B2[i][j],则只记录了列坐标j,未记录行坐标i,在同一行中,必须先求出列坐标j较小的元素,再求j较大的元素。这样先改变的是下标j较小的元素,且其不会影响j大的元素。故在内层循环中,应该让循环变量j的值从小到大递增。

问题2:语句2:return F[0]; 

 

算法6:动态规划(逆推法),需要用到全局变量map[MAX][]。

int DP_3(int n)

   for (int i=n-2; i>=0; i--)

   { 

       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:与算法3(利用B2[MAX][])相比,算法6(利用map [MAX][])有哪些优劣之处?

 

参考答案:

问题1:算法3是经典的动态规划算法,它利用全局变量B2[MAX][]来记录子问题的解,以空间换时间,提高了效率;算法6的思路和算法3是一样的,它巧妙地利用了原二维数组的空间,直接把元素和作为新的元素值,不需要分配额外的空间,但是因为修改了原始数据,不利于对算法做进一步的降维优化。

 

拓展练习:

原题只要求算出最佳路径上的数字之和,并未要求输出最佳路径。现在要求:

1.      在算法3 int DP_2(int n)的基础上,编写函数void PrintPath(int n);//输出最佳路径。

2.      在算法6 int DP_5(int n)的基础上,编写函数void PrintPath_2(int n);//输出最佳路径。

 

参考答案:

void PrintPath(int n) //输出最佳路径

       intj = 0;

   for (int i=0; i<n-1; i++) //输出前n-1行

   { 

              cout<< map[i][j] << "->"; //输出上一行元素

       if (B2[i+1][j] < B2[i+1][j+1]) //向右下方走,否则向正下方走

       { 

           j++;

       } 

   } 

   //输出底层元素  

   cout << map[n-1][j] << endl; 

 

void PrintPath_2(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。

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值