动态规划三:三角形最小路径和
题目:给定一个三角形,找出自顶向下的最小路径和。每一步只能移动到下一行中相邻的结点上。
例如,给定三角形:
[
[2],
[3,4],
[6,5,7],
[4,1,8,3]
]
自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 = 11)。
说明:如果你可以只使用 O(n) 的额外空间(n 为三角形的总行数)来解决这个问题,那么你的算法会很加分。
方法一:递归穷举
public static int minimumTotal(List<List<Integer>> triangle) {
int sum = 0;
//不确定共有几种枚举结果,所以使用变长数据结构;Vector,set都可以,
//最终要求集合中的最小值,并不要求集合有序
ArrayList<Integer> arrlist = new ArrayList<>();
GetMinsum(triangle,0,0,sum,arrlist);
return Collections.min(arrlist);
}
public static void GetMinsum(List<List<Integer>> triangle,int row,int col,int sum,ArrayList<Integer> arrlist){
if(row == triangle.size() || col == triangle.get(row).size()){
//每次遍历最终的和保存到数组中
arrlist.add(sum);
return ;
}
for(int i = col; i< Math.min(col+2,triangle.get(row).size());i++) {
sum += triangle.get(row).get(i);
GetMinsum(triangle, row + 1, i, sum, arrlist); //试探
sum -= triangle.get(row).get(i); //回溯
}
}
该种方法,穷举了所有的可能,然后在所有结果中找到最小值返回,在处理大规模的问题时,运行时间超过限制;穷举法示意图如下:
方法二:记忆化搜索(自顶向下)
//记忆化搜索,自顶向下
public static int minimumTotal3(List<List<Integer>> triangle) {
//memo[i]表示第i个位置上的最小和
int row = triangle.size();
int col = triangle.get(triangle.size()-1).size();
int [] memo = new int[col];
for(int i = 0; i < memo.length;i++){
memo[i] = Integer.MAX_VALUE;
}
memo[0] = triangle.get(0).get(0);
GetMinSum(triangle,row-1,col-1,memo);
Arrays.sort(memo);
return memo[0];
}
public static int GetMinSum(List<List<Integer>> triangle,int row,int col,int [] memo){
//递归出口
if(row <=0 && col<=0)
return triangle.get(0).get(0);
if(col > row)
return Integer.MAX_VALUE;
if(col < 0)
return memo[0];
//若memo[col]已经存在,则直接返回
if(memo[col] != Integer.MAX_VALUE)
return memo[col];
//若memo[col]不存在,则计算,然后返回
//i = col,表示从右往左算
for(int i =col;i>=0;i--){
//计算memo[i]时,先计算 (row-1,i-1)的最小和
int v1 = GetMinSum(triangle,row-1,i-1,memo);
//再计算 (row-1,i)的最小和
int v2 = GetMinSum(triangle,row-1,i,memo);
//memo[i] = (row,i)+min(v1,v2);
memo[i] = triangle.get(row).get(i)+Math.min(v1,v2);
}
return memo[col];
}
方法二使用记忆化搜索,而且只用了一个一维数组就完成了,计算过程稍显复杂,下面详细解释该方法的执行过程:首先通过观察可以发现,该题的输入有一个特点,是一个下三角矩阵(方阵的下三角和主对角线),因此第i行就有i个值;所以,根据输入的行数确定memo数组的大小;以下的例子开始解释该方法的执行过程:
//若memo[col]不存在,则计算,然后返回
for(int i =col;i>=0;i--){
//计算memo[i]时,先计算 (row-1,i-1)的最小和
int v1 = GetMinSum(triangle,row-1,i-1,memo);
//再计算 (row-1,i)的最小和
int v2 = GetMinSum(triangle,row-1,i,memo);
//memo[i] = (row,i)+min(v1,v2);
memo[i] = triangle.get(row).get(i)+Math.min(v1,v2);
}
先计算最后一行最后一个位置上的最小和,那么需要计算倒数第一行倒数第二个位置的最小和和倒数第一行倒数第一个位置的最小和;如此向上递归,一直到第2行,此时计算value[1][1]和value[1][0]的值;
递归回退,计算第三行数值的最小和:
继续回退,计算最后一行的最小和:
此时,所有的路径和已经计算完成,输出memo数组中的最小值,即为所求答案;此种方法要求从右往左计算(i = col,表示从右往左算)memo数组中的值,不能从左往右计算;因为每一个值的计算与其前面的值相关,若左往右计算(),则左边的值更新后,才计算右边的值,引起计算错误;
方法三:动态规划(自底向上)
通过方法一和方法二,发现了问题的重叠子问题或者说是最优子架构,那么动态规划方法可以如下,编写: public static int minimumTotalDP1(List<List<Integer>> triangle){
int [] [] matrix = new int[triangle.size()][triangle.get(triangle.size()-1).size()];
for(int i = 0; i < matrix.length;i++){
for(int j = 0; j < matrix[i].length;j++)
matrix[i][j] = Integer.MAX_VALUE;
}
for(int i = 0; i < triangle.size();i++){
for(int j = 0; j < triangle.get(i).size();j++)
matrix[i][j] = triangle.get(i).get(j);
}
int temp;
for(int i = 1; i < triangle.size();i++) {
for (int j = 0; j < triangle.get(i).size(); j++){
if( j-1 <0)
matrix[i][j] += matrix[i-1][j];
else if(j > triangle.get(i-1).size())
matrix[i][j] += matrix[i-1][j-1];
else{
if(matrix[i-1][j] == Integer.MAX_VALUE)
temp = matrix[i-1][j-1];
else
temp = matrix[i-1][j-1] < matrix[i-1][j]?matrix[i-1][j-1]:matrix[i-1][j];
matrix[i][j] += temp;
}
}
}
int sum = Integer.MAX_VALUE;
int i = triangle.size()-1;
for (int j = 0; j < triangle.get(i).size(); j++){
if(sum > matrix[i][j])
sum = matrix[i][j];
}
return sum;
}
方法三在运行过程中,定义了二维数组,其实,最后的结果只与二维数组中的最后一行有关,因此可以学习方法二,改为一维数组,具体如下:
方法四:动态规划改进
public static int minimumTotalDP2(List<List<Integer>> triangle){
//memo[i]表示计算到第i行各个位置上的最小和
int [] memo = new int[triangle.get(triangle.size()-1).size()];
for(int i = 0; i < memo.length;i++)
memo[i] = Integer.MAX_VALUE;
memo[0] = triangle.get(0).get(0);
int row = triangle.size(),col;
for(int i = 1; i < row;i++) {
col = triangle.get(i).size();
for (int j =col-1; j > 0; j--){
int temp = Math.min(memo[j-1],memo[j]);
memo[j]= temp + triangle.get(i).get(j);
}
memo[0]+=triangle.get(i).get(0);
}
Arrays.sort(memo);
return memo[0];
}
通过以上四种方法,可以详细的理解这道题目,其中,自顶向下的记忆化搜索方法二,考虑的细节最多,运行速度最快,占用空间最小,同时也难以理解,值得详细研究一下。