附上题目链接:http://bailian.openjudge.cn/practice/1088/
看到这道题目,思路肯定是有的,用样例输入来说,一共25个结点的矩阵。只要求出从每个点开始往下滑的最长滑行长度。然后在这25个结果里取最大值。就是题目的答案。然后如何求出一个结点的最大滑行长度呢。不管是递归的思路还是递推的思路,都要先写出状态转移方程。那么这道题的状态转移方程还是比较简单的。一个结点的最大滑行长度为:他上下左右四个结点里,(比他高度低的那些结点的最大滑行长度+1.)的最大值。如果一个结点的四周没有比他高度低的结点,那么他的最大滑行长度为1。这就是边界值。知道这些之后,递归的代码就非常好写了。但是,简单的递归可能会产生非常多的重复计算,例如,你在求解数字为25的结点的结果的时候,会递归得去求解一遍数字为18的结点的结果,而你求解数字为19的结点的时候也会去递归得求解数字为18的结点的结果。因为简单的递归时间复杂度会很高,一定会超时的。这就是动态规划要解决的问题。现在问题在于许多结点被重复计算了,所以导致时间浪费。那么我们不妨设置一个同样规模的result二维数组来保存每一个结点的结果。二维数组里每一个值都初始化为-1(最大滑行长度一定是一个正数,所以-1是不可能的),用-1来表示该结点的结果(下文中都把每个结点的最大滑行长度简称为结果)还未求出。然后在递归函数的最前面进行一次if判断,如果结果已经求出,那么直接返回结果,来阻止重复计算,达到良好的时间复杂度。接下来上代码
#include
#include
#include
#include
using namespace std;
void getdata(vector > &height, const int R, const int C);
int MemoryRecursion(vector > &height);//记忆型递归解法
int Recursion(vector > &height, vector >&result, int i, int j);//递归主体
int AllForOneDP(vector > &height);//人人为我型递推
int OneForAllDp(vector > &height);//我为人人型递推
int main()
{
int R, C;//R行C列
scanf("%d%d", &R, &C);
vector >height(R);//初始化空的R行
getdata(height, R, C);
cout << MemoryRecursion(height) << endl;
//cout << AllForOneDP(height) << endl;
//cout << OneForAllDp(height) << endl;
system("pause");
return 0;
}
int OneForAllDp(vector > &height)//我为人人型递推
{
struct tablesort
{
int i;
int j;
};
int R = height.size();
int C = height[0].size();
vector ascend(R*C);//递增的表排序结点
for (int i = 0; i < R*C; ++i)//初始化表
{
ascend[i].i = i / C;
ascend[i].j = i % C;
}
sort(ascend.begin(), ascend.end(), [&height](const tablesort &a, const tablesort &b)->bool
{
return (height[a.i][a.j] < height[b.i][b.j]);
}
);//用c++11的lambnd表达式进行快排表排序
/*开始我为人人型递推*/
int maxlength = 0;
vector > result(height);
for (auto i = result.begin(); i != result.end(); ++i)
{
for (auto j = i->begin(); j != i->end(); ++j)
{
*j = 1;//所有结点的最大长度都至少为1
}
}
/*我为人人,下标从0开始,因为每次循环的时候,是说明当前结点的最大长度已经求出,更新他附近的结点*/
for (size_t x = 0; x < ascend.size(); ++x)//按高度递增的顺序更新每个结点的最大滑行长度
{//所以求解当前结点时,他四周比他高度低的结点的最大滑行长度已经求出
int high = height[ascend[x].i][ascend[x].j];//先计算出当前结点的高度
if (ascend[x].i > 0 && height[ascend[x].i - 1][ascend[x].j] > high)//如果上面的结点高度更高
{
result[ascend[x].i - 1][ascend[x].j] = max(result[ascend[x].i - 1][ascend[x].j],
result[ascend[x].i][ascend[x].j] + 1);
}
if (ascend[x].j > 0 && height[ascend[x].i][ascend[x].j - 1] > high)//如果左边的结点高度更高
{
result[ascend[x].i][ascend[x].j - 1] = max(result[ascend[x].i][ascend[x].j - 1],
result[ascend[x].i][ascend[x].j] + 1);
}
if (ascend[x].i < R - 1 && height[ascend[x].i + 1][ascend[x].j] > high)//如果下面结点的高度更高
{
result[ascend[x].i + 1][ascend[x].j] = max(result[ascend[x].i + 1][ascend[x].j],
result[ascend[x].i][ascend[x].j] + 1);
}
if (ascend[x].j < C - 1 && height[ascend[x].i][ascend[x].j + 1] > high)//如果右边的结点高度更高
{
result[ascend[x].i][ascend[x].j + 1] = max(result[ascend[x].i][ascend[x].j + 1],
result[ascend[x].i][ascend[x].j] + 1);
}
maxlength = max(maxlength, result[ascend[x].i][ascend[x].j]);//取整个滑雪道的最大值
}
return maxlength;
}
int AllForOneDP(vector > &height)//人人为我型递推
{
struct tablesort
{
int i;
int j;
};
int R = height.size();
int C = height[0].size();
vector ascend(R*C);//递增的表排序结点
for (int i = 0; i < R*C; ++i)//初始化表
{
ascend[i].i = i / C;
ascend[i].j = i % C;
}
sort(ascend.begin(), ascend.end(), [&height](const tablesort &a, const tablesort &b)->bool
{
return (height[a.i][a.j] < height[b.i][b.j]);
}
);//用c++11的lambnd表达式进行快排表排序
/*开始人人为我递推*/
int maxlength = 1;//要初始化为1,不能初始化为0,因为下面的x循环是从1开始的
/*如果初始化为0,那么当只有一个结点时,maxlength不会被更新*/
vector > result(height);
for (auto i = result.begin(); i != result.end(); ++i)
{
for (auto j = i->begin(); j != i->end(); ++j)
{
*j = 1;//所有结点的最大长度都至少为1
}
}
for (size_t x = 1; x < ascend.size(); ++x)//按高度递增的顺序求解每个结点的最大滑行长度
{//所以求解当前结点时,他四周比他高度低的结点的最大滑行长度已经求出
int high = height[ascend[x].i][ascend[x].j];//先计算出当前结点的高度
if (ascend[x].i > 0 && height[ascend[x].i - 1][ascend[x].j] < high)//如果上面的结点高度更低
{
result[ascend[x].i][ascend[x].j] = max(result[ascend[x].i][ascend[x].j],
result[ascend[x].i - 1][ascend[x].j] + 1);
}
if (ascend[x].j > 0 && height[ascend[x].i][ascend[x].j - 1] < high)//如果左边的结点高度更低
{
result[ascend[x].i][ascend[x].j] = max(result[ascend[x].i][ascend[x].j],
result[ascend[x].i][ascend[x].j - 1] + 1);
}
if (ascend[x].i < R - 1 && height[ascend[x].i + 1][ascend[x].j] < high)//如果下面结点的高度更低
{
result[ascend[x].i][ascend[x].j] = max(result[ascend[x].i][ascend[x].j],
result[ascend[x].i + 1][ascend[x].j] + 1);
}
if (ascend[x].j < C - 1 && height[ascend[x].i][ascend[x].j + 1] < high)//如果右边的结点高度更低
{
result[ascend[x].i][ascend[x].j] = max(result[ascend[x].i][ascend[x].j],
result[ascend[x].i][ascend[x].j + 1] + 1);
}
maxlength = max(maxlength, result[ascend[x].i][ascend[x].j]);//取整个滑雪道的最大值
}
return maxlength;
}
int MemoryRecursion(vector > &height)//记忆型递归解法
{
int maxlength = 0;
int R = height.size();
int C = height[0].size();
vector > result(height);
for (auto i = result.begin(); i != result.end(); ++i)
{
for (auto j = i->begin(); j != i->end(); ++j)
{
*j = -1;//矩阵所有点出发的长度都初始化为-1,表示还未求出
}
}
for (int i = 0; i < R; ++i)
{
for (int j = 0; j < C; ++j)
{
if (Recursion(height, result, i, j) > maxlength) { maxlength = result[i][j]; }
}
}
return maxlength;
}
int Recursion(vector > &height, vector >&result, int i, int j)//从i,j出发的最大长度
{
if (result[i][j] == -1)//如果还没有求出
{
if(i>0 && height[i-1][j] < height[i][j])//如果上面的结点高度更低
{//上面结点的最长路径的长度+1
result[i][j] = max(result[i][j], Recursion(height, result, i - 1, j) + 1);
}
if (j > 0 && height[i][j - 1] < height[i][j])//如果左边结点的高度更低
{//看看能否更新为左边结点的最长路径+1
result[i][j] = max(result[i][j], Recursion(height, result, i, j - 1) + 1);
}
if (i < static_cast(height.size() - 1) && height[i + 1][j] < height[i][j])//如果下面结点的高度更低
{//看看能否更新为下面结点的最长路径+1
result[i][j] = max(result[i][j], Recursion(height, result, i + 1, j) + 1);
}
if (j < static_cast(height[0].size() - 1) && height[i][j + 1] < height[i][j])
{//看看能否更新为右边结点的最长路径+1
result[i][j] = max(result[i][j], Recursion(height, result, i, j + 1) + 1);
}
if (result[i][j] == -1) { result[i][j] = 1; }//如果四周没有比他低的点
}
return result[i][j];
}
void getdata(vector > &height, const int R, const int C)
{
for (int i = 0; i < R; ++i)
{
height[i].reserve(C);
for (int j = 0; j < C; ++j)
{
int x; scanf("%d", &x);
height[i].push_back(x);
}
}
}
可以看到,递归的代码还是非常简单的。MemoryRecursion函数就是动态规划自顶向下的记忆型递归解法。
但是用递归解题有频繁的函数调用导致额外的时间开销,递归层数过多导致爆栈的问题。因此,我们还要介绍自底向上的循环的解法,之所以写这篇博客,就是因为我觉得要用这道题的自底向上的循环做法,很有意思。之前提到,这题的状态方程的边界条件为:如果一个结点的四周没有比他高度低的结点,那么他的最大滑行长度为1。自底向上的思路要求我们,先知道高度低的结点的结果,然后再去求解,高度比他高的结点的结果。因此,我们求解的顺序是按照高度从低到高的顺序来的。然而题目中的二维数组并不是高度有序的。因此,在进行自底向上的递推之前,我们需要先对结点进行从低从高的排序。但是这是一个二维数组啊,而且,如果我们直接对二维数组进行排序,那么结点之间的关系就被打乱了,即,原来在甲结点左边的结点,排序后可能跑到甲结点右边去了。这样我们就无法进行状态递推。因此我个人觉得,这题要用递推型的做法,要用表排序。即创建一个序列,这个序列来表示二维数组的结点高度的顺序。例如,对样例来说,该序列的最后一个元素是一对坐标对,为{2,2},因为最大的元素25,所在二维矩阵的下标为[2][2].碍于篇幅,关于表排序的详细实现方法在此不再赘述,有兴趣的读者可以百度浙江大学mooc的数据结构,选择陈越姥姥的数据结构慕课进行学习哈哈哈哈。(替姥姥打一波小广告)。实现了表排序以后,这题就变成了中规中矩的动态规划题目了。注释极为详细。具体细节可以翻阅注释。需要提到的是,因为递推型的算法有额外的排序操作,因此实际的时间复杂度可能反而比递归的算法较慢?这看起来似乎不能理解。但是在OJ上跑出来的结果确实如此。我能想到的唯一的理由就是,递归型的解法,会自己确定解结点的顺序。而递推的解法则需要人为确定,这道题目人为确定顺序有额外的nlgn的复杂度。因为加大了时间开销。