动态规划解决数字三角形
美图:
问题描述:
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
给定一个数字三角形,如上,在这个三角形中寻找一条从顶部到底边的路径,使得路径上所经过的数字之和最大。路径上的每一步都只能往左下或右下走。求出这个最大和。
一. 我们先考虑使用递归来解决。
用二维数组D来存放数字三角形,问题转换如下:
D(i,j):表示第 i 行第 j 列的数字。(i,j从0开始)
Sum(i,j):表示从 D(i,j) 到底边的最优解,即:从 D(i,j) 到底边的各条路径中,数字之和最大的一个。
问题:求 Sum(0,0)。即:第0行第0列到底边的路径的最大和。
- 当只有一行时,它的最大路径和就是它本身。
即:Sum(i,j) =D(i,j) - 其次,它的最大路径和就是它本身加上它下面左右两个元素的最大路径和的较大者。因为是使用二维数组来存数字,就是它本身加上他正下方和他右下方两个元素的最大路径和的较大者。
即:Sum(i,j) =max{Sum(i+1,j),Sum(i+1,j+1)}+D(i,j)
写出递归方程:
根据递归方程写出递归函数。
#include<iostream>
using namespace std;
const int N = 100;
int D[N][N];
int max(int a, int b)
{
int m = a > b ? a : b;
return m;
}
int fun_d(int i,int j,int n)
{
if (i == n-1)
return D[i][j];
int Sum_L = fun_d(i + 1, j, n);
int Sum_R = fun_d(i + 1, j + 1, n);
return max(Sum_L, Sum_R)+D[i][j];
}
int main()
{
int n, i, j;
cin >> n;//n行数
for (i = 0; i < n; i++)
{
for (j = 0; j <= i; j++)
{
cin >> D[i][j];
}
}
int MaxSum = fun_d(0,0,n);
cout << MaxSum << endl;
return 0;
}
然而并没有,一般情况下,递归的时间复杂度都比较高,我们来看一下这个递归的时间复杂度。
嗯?花里胡哨的什么东西?
这个表中红色的数字就是递归过程中每个元素要进行的计算次数,what?没错,就是存在大量的重复计算,这就导致了算法的时间复杂度非常高。将每行红色的数字加起来,就是这个算法的时间复杂度,即:。what?指数级别的时间复杂度,那肯定爆炸。所以,我又默默打开了电脑,打算用头发换思路。
改进:
如果每算出一个元素的最优解,就把它保存起来,下次需要的时候直接调用即可,就可避免重复多次的计算。我们创建一个二维数组 Sum[M][N] 来保存最优解。那这样的时间复杂度是多少呢?一共需要计算多少个数据,时间复杂度就是多少,假设有 n 行数字,那么数字总数就是 n(n+1)/2,即:时间复杂度是 。
二. 通讯录式的递归方式。
也称为记忆型的递归程序。
我们牺牲空间和头发换时间,用一个二维数字 Sum[M][N] 来存放每一个元素到底边的最大和。初始时,给他每个元素都初始为 -1 ,表示该元素还没有计算出来过最优解。
在递归的时候,如果 Sum[i][j] != -1
,就表示该元素已经计算出来过最优解,我们可以直接调用它。然后当它递归到最后一行元素时,它的最优解就是当前的元素值。否则就在它正下方和右下方的元素的最优解中取一个较大者再加上该值发放该位置。
C++实现:
#include<iostream>
using namespace std;
const int N = 100;
int D[N][N];
int Sum[N][N];//存放每一个数字到底边的最大和
int max(int a, int b)
{
int m = a > b ? a : b;
return m;
}
int fun_f(int i, int j, int n)
{
if (Sum[i][j] != -1)
return Sum[i][j];
if (i == n - 1)
Sum[i][j] = D[i][j];
else
{
int Sum_L = fun_f(i + 1, j, n);//下方
int Sum_R = fun_f(i + 1, j + 1, n);//右下方
Sum[i][j] = max(Sum_L, Sum_R) + D[i][j];
}
return Sum[i][j];
}
int main()
{
int n, i, j;
cin >> n;//n行数
for (i = 0; i < n; i++)
{
for (j = 0; j <= i; j++)
{
cin >> D[i][j];
Sum[i][j] = -1;//表示该元素还没有计算出来最优解
}
}
int MaxSum = fun_f(0, 0, n);
cout << MaxSum << endl;
return 0;
}
Java实现:
package abcd;
import java.util.Scanner;
public class Example2_1{
public static void main (String args[]){
Scanner in = new Scanner(System.in);
int[][] D = new int[100][100];
int[][] Sum =new int[100][100];
int n,i,j;
n= in.nextInt();
for (i = 0; i < n; i++)
{
for (j = 0; j <= i; j++)
{
D[i][j]=in.nextInt();
Sum[i][j] = -1;//表示该元素还没有计算出来最优解
}
}
int MaxSum = fun_f(D,Sum,0, 0, n);
System.out.println(MaxSum);
}
public static int fun_f(int[][] D,int[][] Sum,int i,int j,int n)
{
if (Sum[i][j] != -1)
{
return Sum[i][j];
}
if (i == n - 1)
{
Sum[i][j] = D[i][j];
}
else
{
int Sum_L = fun_f(D,Sum,i + 1, j, n);//下方
int Sum_R = fun_f(D,Sum,i + 1, j + 1, n);//右下方
Sum[i][j] = max(Sum_L, Sum_R) + D[i][j];
}
return Sum[i][j];
}
public static int max(int a, int b)
{
int m = a > b ? a : b;
return m;
}
}
这个图就是最终每个元素到底部的最大路径和。
它的最后一行就是本来的元素,嗯?是不是可以不用递归,用我们的 由已知推未知大法 是不是可以解决呢?
三. 由已知推未知大法
算法思想:
由上面可以看出,存最优解的二维数组的最后一行就是原来数组的最后一行的值,而且一个元素都是可以由该元素的正下方元素和右下方元素推出来,刚好可以根据最后一层元素,层层递推,直到推迟第0行第0列的元素。
首先,给Sum数组的最后一行初始化为原本数组的最后一行,即:Sum[n - 1][i] = D[n - 1][i];
。
然后,使用双层循环来递推每一个元素。即:Sum[i][j] = max(Sum[i + 1][j], Sum[i + 1][j + 1]) + D[i][j];
#include<iostream>
using namespace std;
const int N = 100;
int D[N][N];
int Sum[N][N];//存放每一个数字到底边的最大和
int max(int a, int b)
{
int m = a > b ? a : b;
return m;
}
int fun_f_2(int n)
{
int i, j;
for (i = 0; i < n; i++)
{
Sum[n - 1][i] = D[n - 1][i];
}
for (i = n - 2; i >= 0; i--)
{
for (j = 0; j <= i; j++)
{
Sum[i][j] = max(Sum[i + 1][j], Sum[i + 1][j + 1]) + D[i][j];
}
}
return Sum[0][0];
}
int main()
{
int n, i, j;
cin >> n;//n行数
for (i = 0; i < n; i++)
{
for (j = 0; j <= i; j++)
{
cin >> D[i][j];
}
}
int MaxSum = fun_f_2(n);
cout << MaxSum << endl;
return 0;
}
呃呃呃,这样的话,算法的时间复杂度同样是,我们写它还有什么意义,莫慌、莫慌,且往下看。
这个算法中,我们使用了二维数组来记录已求出的结果,然后进行层层递推,得到最终的结果。但是,我们可以看到,这个二维数组的每一行在完成它的递推任务之后,它就没用了,so,我们是不是可以用一个一维数组来代替这个二维数组呢?
四. 压缩空间
这个图中,比如说要计算 D[3][0] 元素的最优解,就是 4 和 5 的较大者加上 D[3][0] ,算完之后,这个 4 就没用了,我们可以直接将 D[3][0] 元素的最优解 7 直接存放在 4 的位置上,循环往复。只需要一个一维数组就够了,最终,这个一位数组的首元素,就是最终的结果。
#include<iostream>
using namespace std;
const int N = 100;
int D[N][N];
int max(int a, int b)
{
int m = a > b ? a : b;
return m;
}
int Sum_1[N];
int fun3(int n)
{
int i, j;
for (i = 0; i < n; i++)
{
Sum_1[i] = D[n - 1][i];
}
for (i = n - 2; i >= 0; i--)
{
for (j = 0; j <= i; j++)
{
Sum_1[j] = max(Sum_1[j] , Sum_1[j + 1])+D[i][j];
}
}
return Sum_1[0];
}
int main()
{
int n, i, j;
cin >> n;//n行数
for (i = 0; i < n; i++)
{
for (j = 0; j <= i; j++)
{
cin >> D[i][j];
}
}
int MaxSum = fun3(n);
cout << MaxSum << endl;
return 0;
}
嗯?一维数组?那个存原始数据的不就是的二维数组吗,二维数组不就是多个一维数组构成的嘛,有个大胆的想法,嘿嘿嘿,是不是这个一维数组也可以不要,用存原始数据的二维数组的最后一行来代替这个一维数组。
五. 极限压缩
C++ 实现:
#include<iostream>
using namespace std;
const int N = 100;
int D[N][N];
int max(int a, int b)
{
int m = a > b ? a : b;
return m;
}
int fun3(int n)
{
int i, j;
for (i = n - 2; i >= 0; i--)
{
for (j = 0; j <= i; j++)
{
D[n-1][j] = max(D[n-1][j] , D[n-1][j + 1])+D[i][j];
}
}
return D[n-1][0];
}
int main()
{
int n, i, j;
cin >> n;//n行数
for (i = 0; i < n; i++)
{
for (j = 0; j <= i; j++)
{
cin >> D[i][j];
}
}
int MaxSum = fun3(n);
cout << MaxSum << endl;
return 0;
}
Java实现:
package abcd;
import java.util.Scanner;
public class Example2_1{
public static void main (String args[]){
Scanner in = new Scanner(System.in);
int[][] D = new int[100][100];
int n,i,j;
n= in.nextInt();
for (i = 0; i < n; i++)
{
for (j = 0; j <= i; j++)
{
D[i][j]=in.nextInt();
}
}
int MaxSum = fun3(D,n);
System.out.println(MaxSum);
}
public static int fun3(int[][] D,int n)
{
int i, j;
for (i = n - 2; i >= 0; i--)
{
for (j = 0; j <= i; j++)
{
D[n-1][j] = max(D[n-1][j] , D[n-1][j + 1])+D[i][j];
}
}
return D[n-1][0];
}
public static int max(int a, int b)
{
int m = a > b ? a : b;
return m;
}
}
编程之美,真的让人着迷。这就是许多编程大师以“聪明绝顶”为代价也要追求的东西。