NumberTriangle简称NT,是由正整数排列成三角形的形式。求从最上顶点到底边数字串成的路径上的最大路径长度,即路径上的数字和。非底边的每个数字都有2条子树路径(左子树和右子树)可供选择。
当然,可用最简单的方法将所有可能的路径全部遍历一遍,存储所有路径结果,然后比较得出最大的值,但时间复杂度为O(2n),而DP则减少一半(具体待验证)。观察NT可得,中间有些结果会被重复计算和使用,因此为了减少重复计算带来的不必要的时间消耗,最有效的方法是动态规划:后面的结果可以利用前面已经计算过的结果,即后面的结果是前面结果的累加或累乘。这里我们可用一个向量来存储前面已经计算得到的结果,当后面需要用到时,直接获取。
那么,NT的dynamic算法如何实现呢?
首先,从底到顶,每一个父节点都是选择2个子节点中的最大的一个,然后在加上当前父节点的值构成当前父节点的最大路径值,这其实是递归求解,与蛮力求解所有路径上的距离递归方式相同,但是不同之处是这里每求解得到一个父节点值就把它存储在另一个二维向量中,注意保持存储的位置一一对应,即当前位置下标对应当前父节点的值。这里有一个保存的技巧:将计算得到保存的向量初始化为负值,当然初始值只要与计算中的值相区别即可,这样我们就可以判断中间存储的值是否已经计算过,若已经计算过则不用重复递归,而是直接取值即可,若还是等于负数,则继续递归计算。
例如在本文中,设定中间存储的初始值为-1,如果不等于-1,则说明没有存储计算,则递归计算,若存在值,则无需再次递归计算,而是直接取之前已经递归计算过的值,避免了重复计算,通过空间换时间以提高时间效率。
本文在一个5层的蛮力递归次数=31,而dynamic递归次数为15次,总体的算法如下:
int DynamicProgramming::numberTriangle(vector<vector<int>>&num)
{
int result;
vector<vector<int>> roteLength;
roteLength.resize(num.size());
for (int i = 0; i < num.size();i++)
{
roteLength[i].resize(num[i].size());
}
for (int i = 0; i < roteLength.size();i++)
{
for (int j = 0; j < roteLength[i].size();j++)
{
roteLength[i][j] = -1;
}
}
result = conqueTriangle(roteLength, num, 0, 0); //这里可改进并计算第几层到第几层的最大或者最小距离
return result;
}
DP递归求解如下:
int DynamicProgramming::conqueTriangle(vector<vector<int>>&roteLength, vector<vector<int>>&num, int i, int j)
{
if (i + 1 < roteLength.size())
{
if (roteLength[i + 1][j] == -1) //特注:这里的判断是DP和蛮力法递归的本质区别
{
roteLength[i + 1][j] = conqueTriangle(roteLength, num, i + 1, j); //存储当前第i+1行j列的值,这是每一个DP的核心关键所在
}
if (roteLength[i+1][j+1] == -1) //如果不等于-1,则说明没有存储计算,则递归计算,若存在值,则无需再次递归计算,而是直接取之前已经递归计算过的值,避免了重复计算,通过空间换时间来提高时间效率
{
roteLength[i + 1][j + 1] = conqueTriangle(roteLength, num, i + 1, j + 1);
}
roteLength[i][j] = maxSubLength(roteLength[i + 1][j], roteLength[i + 1][j + 1]) + num[i][j]; //获得当前第i,j
}
else
{
roteLength[i][j] = num[i][j];
}
return roteLength[i][j];
}
int DynamicProgramming::maxSubLength(int leftRote, int rightRote)
{
return leftRote > rightRote ? leftRote : rightRote;
}
总结:DP求解的本质是存储中间值,后面的值需要用到中间值或者前面的值时则直接取出来用,无需每次递归计算,所以后面的值是前面值的累加或者累乘。因此DP问题有一个重要特性:前面的值无后效性,对后面的值无影响,仅仅是累加或者累乘得到后面的值,这样当后面多个值是前面某一个值的累加或者累乘时,虽然后面的值不同,但是都同用前面的一个值a,那么值a不应该对后面的某个值产生影响,即不应该对后面的某个值产生影响的特性,若对后面某一个产生影响,则其它后面的值就不能确定是否前面的值的特性也会带来影响,这样就不能得到准确的结果,所以DP问题中计算得到的前面的值是无后效性的(即DP问题的解是无后效性的),这样后面的值才能共用前面的值。从而减少重复计算,达到利用DP的高效求解本质目的。