引论:这是第二次在算法课上的上机实践了,虽然逐渐有了思考算法的感觉,但这一次上机实践还是并不轻松。关键在于在实验课前对第三章的知识掌握得还不足够,经常会因为明明知道理论上如何解题而苦于无法将其实现。不过,通过本次上机实践,我的收获是非常丰富的,下面用实践课题目中的第一题《数字三角形》开始分享我的经验。
一、实践题目
给定一个由 n行数字组成的数字三角形如下图所示。试设计一个算法,计算出从三角形的顶至底的一条路径(每一步可沿左斜线向下或右斜线向下),使该路径经过的数字总和最大。
输入格式:
输入有n+1行:
第 1 行是数字三角形的行数 n,1<=n<=100。
接下来 n行是数字三角形各行中的数字。所有数字在0-99 之间。
输出格式:
输出最大路径的值。
输入样例:
在这里给出一组输入。例如:
5
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
输出样例:
在这里给出相应的输出。例如:30
二、问题描述
初看这一道题的时候,差点陷入到了迷惑之中,不过简单分析一下题目后发现是那样的熟悉——题目要求我们从数字三角形的最顶端数字开始,选取一条能够使经过的数字总和最大的路径。而这不就和老师在第三章开头课说的例子一样吗?从A地到D地选取一条经过B、C两地的最短路径。A到D的最短路径取决于B到D,而B到D取决于C到D。我们将这种思想反推至这道题目,要求出数字三角形的最短路径,从顶部数字开始到底部数字,这要先取决于顶部数字到第二行的最大和,再取决于第二行数字到第三行的最大和,以此类推至数字三角形的最后一行。好的,有了这种动态规划的思想我们离解决问题还有一半距离了!
那么我们开始分析,因为每一行的最大和都取决于上一行的数字最大和,那么,我们自底而上分析,重填输入进来的二维数组。重写时,数字三角形的最后一行是不变的,因为最后一行的数字们没有下面一行的数字影响,而从倒数第二行开始,这些数字重写后变为,它自身与和它相邻的最后一行两个数字之和的最大值。举个例子,即a[i][j]的大小是max{(a[i+1][j] + a[i][j]), (a[i+1][j+1])}。那么我们自下而上重写数组,最后得到新的最顶端的数字,即是最大值路径。
三、算法描述
首先列出我解题的代码
1 #include <iostream> 2 #include <algorithm> 3 using namespace std; 4 5 int main() 6 { 7 int num; 8 cin>>num; 9 const int n = num; 10 int array[n][n]; 11 12 for (int i = 0; i < n; i++) 13 { 14 for (int j = 0; j <= i; j++) //按数字三角型的形式,填充二维数组(矩阵)的下三角 15 { 16 cin>>array[i][j]; 17 } 18 } 19 20 //用动态规划的思想,改写array二维数组 21 for (int i = n - 2; i >= 0; i--) //因为数字三角形的最后一行的动态规划最大值是自己,所以从倒数第二行开始求自下而上时的动态规划最大值 22 { 23 for (int j = 0; j < i + 1; j++) 24 { 25 array[i][j] = max((array[i + 1][j] + array[i][j]), (array[i + 1][j + 1] + array[i][j])); //当前元素的动态规划最大值是判断它本身和它左下边或右下边的数字相加的最大值 26 } 27 } 28 cout<<array[0][0]; //因为是自下而上求最大值,所以二维数组第一个元素即是数字三角形的最大值 29 return 0; 30 }
因为改写后的数组,数字三角形的最后一行是不变的,所以我们的第一层循环从倒数第二行开始,因此定义i = n - 2,并让i不断变小;在第二层循环中,我们分析的是当前行的每个数字,每一行的数字数目为该行行号,而i为每一行的下标,所以定义j = i + 1。而我们填入数组的是该行数字和与它相邻的下一行数字组成之和的最大值。最终循环结束后,我们直接输出数组的第一个数字,即得到最大路径和。
四、算法时间及空间复杂度分析
对于时间复杂度:本算法为main函数中的重叠的两个for循环,而第一层循环是从n-2开始至0,第二层循环为从0开始至n-1,因此不难得出,本算法的时间复杂度为O(n^2)。
对于空间复杂度:因为本题是直接改写输入进来二维数组,所以没有额外申请数组和内存空间,所以空间复杂度为O(1)。
五、心得体会
通过本次上机实践,我更好地巩固了第三章的知识,动态规划思想理解起来不难,但需要多次的动手实践来让自己真正掌握它。通过自己动手编程解决这一道题让我加深了上节课所学知识的印象。在想出上述解题算法之前,我在草稿本上列出了很多种填表的方式,但难以实现出来,即使解决了这一道题,我目前对填表法的掌握还并不熟练。不过,通过做这一道实验题,我真正明白了动态规划思想的厉害之处。因为实验题目集中的第二题是书上的原题,所以相对于第二题,我在第一题花费的时间较多。因此,在解决动态规划思想的题目中,我们不能纠结于如何写出完完整整的递推式,而要多结合题目实际。
同时,解决这道题目也不仅仅是我一个人想出来的,这道题目的核心思想是我的搭档为我讲解的,因此光靠自己一个人我是无法解决这道题目的。这又再次体现出结对编程的重要性,两个人的头脑风暴得出来的结果是实验课中最好的成果。
通过本次实验,我希望自己能够再接再厉,不断钻研算法知识,争取解决更多的问题!