【TSOJ课程】11 1059 数塔问题

课程29_11 1059 数塔问题


题目:

题目描述:

从数塔顶层出发,每个结点可以选择向左走或向右走,要求一直走到塔底,使得走过的路径上的数值和最小。
例如对于下面这样的数塔

    1
  2   3
4   5   6

按照 1 - 2 - 4 的路线走,可取得路径上的数值和的最小值为 7

输入描述:

每组输入的第一个行表示行数,最大不超过 1000 行。
后面每行为这个数塔特定行包含的正整数。这些正整数不大于 10000。

输出描述:

对于每组测试数据,输出一行答案。

样例输入:

3
1
2 3
4 5 6

样例输出:

7


解析:

今年课程的第一道dp(动态规划)题。

什么叫动态规划呢?动态规划是求多阶段决策问题的数学方法。这个算法的作用正如其名,是用来规划决策的,它经常被用于决策最优解的题目之中。

说了好多术语,我们用大白话来说说吧。如果有这么一种情况:你需要做出多次选择,并且要达到最好的情况。那么这就是多阶段决策问题。很多很多的题目都是这种类型,比如走迷宫,每次(最多)都有上下左右四个方向可以选择;再比如这题的数塔,每次你都要选择左边或者右边某一个分支。

然而,动态规划并不是只要有“抉择”就可以用的算法。它还需要满足以下几个条件

  • 最优子结构性质。如果局部最优+局部最优+……=全局最优,那么就满足。什么意思呢?如果你一直选最好的情况,就能到达最好的情况,那就满足。举个例子,走迷宫的时候,始终走距离终点最近的那条路,走出的就一定是最短的路径。对于本题,始终选择结果较小(注意不是数值较小)的一条路,就一定是最小的一条路了。不过这一点不需要太多去关注,因为绝大部分的多阶段决策问题都是最优子结构的
  • 无后效性。如果你的每次选择都不会影响后面的选择的顺序,那么就满足。这个也比较难以理解,但是作为初学者我们仍然记住:大部分的的多阶段决策问题都是无后效性的,就可以了。

那么动态规划究竟是怎么实现的呢?

首先问一个最简单的问题,就拿题目给出的那个数塔来说吧,如果你知道:在数塔顶端的时候,选择左边那条路,无论如何怎么选,最小的结果只可能是6选择右边那条路,无论如何怎么选,最小的结果只可能是8。那么,你会选择哪条路呢?很明显,题目要求的是最小值,我们肯定选择左边那条路,可问题来了——我们怎么知道选择左边那条路的“无论如何最小值”是多少呢?答案就是去算它的“无论如何最小值”。而计算左边的“无论如何最小值”的方法也是一样的,我们知道在2的那个地方,如果往左,无论如何最小的情况是4,而往右的无论如何最小情况时5,很明显,我们选择左侧,那么2所在位置的“无论如何最小值”就是4(左边)+2(2它自己)=6。而计算到最后一行的“无论如何最小值”就已经显而易见了,因为最后一行根本不需要选择了,他们的“无论如何最小值”就是他们本身。那么对于这个数塔,他们的无论如何最小值如下所示:

    7
   6 8
  4 5 6	

这个时候,顶端的7,就是最优的情况。

相信如果你反应比较快的话,就已经知道动规的原理了。我们这个“最优情况”的数组(也就是上图所示的数组)里的每一个值,都等于:它所可以选择的所有选项中的最优值 + 它本身

比如第二行第一个,它本身是2,而它的两种选择:向左、向右,向左是4,向右是5,题目要求的最优是“最小”,所以选择较小的那个,2+4=6。再比如第一行第一个,它本身是1,向左是6,向右是8,所以选择向左,所以6+1=7。

所以对于这题,如果我们从原理入手去考虑,动态规划是“递归”(自己调用自己)的,也就是:从起点出发,每次都走最好的情况,但是刚开始并不知道哪个最好,就计算每个选项的值,这样自然就递归了。但如果我们从原理入手去考虑,动态规划是“迭代”(循环)的,也就是:从终点出发,计算出终点周围的情况,然后再向外推,直到到达起点为止。


解题:

这道题的动态规划的思路很简单,我们采用递归的方法,从数塔塔底往起点进发

首先要开两个数组,一个存题目给的数据(命名为tower),而另外一个存对应位置的最优值(命名为dp)。

我们首先初始化已知的最优值——也就是最后一行的数据,因为最后一行根本不需要选择了,已经到底了,所以他们的最优值就是他们本身(最优值的公式是 min(左边最优值,右边最优值)+自己的数据, min表示二者中较小的一个, 很明显最后一行的前半项为0)。

然后我们开始从下往上计算。每一个位置的最优值 = min( dp[下一行][左边那个], dp[下一行][右边那个] ) + tower[本行][自己在的位置]。因为每个数据都是由下一行的dp数组以及tower数组决定的,tower数组是已知的,而下一行的dp数组已经计算完了,所以可以顺利的完成计算。

给出的参考代码中第一个for循环用来读取数据,第二个for循环用来初始化dp数组的最后一行,第三个for循环(两个for循环的嵌套)用来计算每个位置的值。

这种算法需要把每个位置都算一遍,即需要计算(1+H)*H/2,其中H是塔高。而如果考虑选择方法的话,因为每次都有2种选择,所以一共有2^( H*(H-1)/2 )种,很明显前者比后者算的次数要少非常多。

优化:题目告诉我们数塔最多有1000行,动态规划是需要开两个数组的,一个存数据本身,还有一个存最优值,也就是需要开两个1000x1000的数组。但是很明显,这是一个金字塔型的数据,如果用二维数组去存储,有一半的空间都被浪费了。所以这里我们选择用一维数组去存储,然后写一个函数trans,trans可以把一个二维坐标(第i行第j个数据)转化为一维坐标,这么做可以节省一半的内存空间。

参考代码:

// TSOJ-1059 数塔问题
#include 
using namespace std;

int tower[500500],dp[500500];

int trans(int i, int j)
{
	return i*(i+1)/2+j;
}

int main()
{
	int hei;
	while(cin>>hei)
	{
		for(int i=0;i<(1+hei)*hei/2;i++)
			cin>>tower[i];
		for(int i=0;i<hei;i++)
			dp[trans(hei-1,i)] = tower[trans(hei-1,i)];
		for(int i=hei-2;i>=0;i--)
			for(int j=0;j<i+1;j++)
				dp[trans(i,j)] = tower[trans(i,j)] + ((dp[trans(i+1,j)]>dp[trans(i+1,j+1)])?dp[trans(i+1,j+1)]:dp[trans(i+1,j)]);
		cout<<dp[0]<<endl;
	}
	return 0;
}

你可能感兴趣的:(C++)