动态规划初步
一、递归计算
#include <iostream> using namespace std; /* 问题: 由非负组成的三角形,第一行只有一个数,除了最下行之外 * 每个数的左下方和右下方各有一个数,从第一行的数开始,每次可以 * 往左下或右下走一格,直到走到最下行,沿途的数相加,怎么走和最大? */ const int N = 4; const int a[N][N] = {{1}, {3, 2}, {4, 10, 1}, {4, 3, 2, 20}}; /* 把当前位置看成一个状态(i, j),定义状态的指标函数d(i, j) * 原问题的解是d(0, 0), * 状态转移方程: d(i, j) = a(i, j) + max{d(i+1, j), d(i+1, j+1)} */ int d(int i, int j) { if(i == N) //走到底 { return 0; } if(j > i) //边界处理 { return 0; } cout << "(" << i + 1 << ", " << j + 1 << ")" << endl; int x = d(i+1, j); int y = d(i+1, j+1); return a[i][j] + (x > y ? x : y); // 如果return语句这么写的话会导致一次重复计算 //return a[i][j] + (d(i+1, j) > d(i+1, j+1) ? d(i+1, j) : d(i+1, j+1)); } int main() { /* for(int i = 0; i < N; i++) { for(int j = 0; j < N; j++) cout << a[i][j] << " "; cout << endl; } */ cout << d(0, 0) << endl; return 0; }
运行结果:
(1, 1) (2, 1) (3, 1) (4, 1) (4, 2) (3, 2) (4, 2) (4, 3) (2, 2) (3, 2) (4, 2) (4, 3) (3, 3) (4, 3) (4, 4)
24
Process returned 0 (0x0) execution time : 0.599 s Press any key to continue.
结论:用直接递归的方法计算状态转移方程效率十分低下
原因:相同的子问题被重复计算了多次,(3, 2), (4, 2), (4, 3)
二、递推计算
#include <iostream> using namespace std; /* 问题: 由非负组成的三角形,第一行只有一个数,除了最下行之外 * 每个数的左下方和右下方各有一个数,从第一行的数开始,每次可以 * 往左下或右下走一格,直到走到最下行,沿途的数相加,怎么走和最大? */ const int N = 4; const int a[N][N] = {{1}, {3, 2}, {4, 10, 1}, {4, 3, 2, 20}}; /* 把当前位置看成一个状态(i, j),定义状态的指标函数d(i, j) * 原问题的解是d(0, 0), * 状态转移方程: d(i, j) = a(i, j) + max{d(i+1, j), d(i+1, j+1)} */ int main() { /* for(int i = 0; i < N; i++) { for(int j = 0; j < N; j++) cout << a[i][j] << " "; cout << endl; } */ int d[N][N] = {0}; for(int j = 0; j <= N; j++) d[N-1][j] = a[N-1][j]; //先处理最下行 for(int i = N-2; i >= 0; i--) //从下往上递推 { for(int j = 0; j <= i; j++) //从左往右枚举了列 { // 每个点的权重由下方和右下方的点推出 d[i][j] = a[i][j] + (d[i+1][j] > d[i+1][j+1] ? d[i+1][j] : d[i+1][j+1]); } } cout << d[0][0] << endl; return 0; }
运行结果:
24
Process returned 0 (0x0) execution time : 0.285 s Press any key to continue.
结论:程序的时间负责度为 O(n^2),递推的关键是边界和计算顺序,
原因:i 是逆序枚举的,因此在计算d[i][j]前,她所需要的d[i+1][j]和d[i+1][j+1]一定已经计算出来了
三、记忆化搜索
#include <iostream> #include <cstring> using namespace std; /* 问题: 由非负组成的三角形,第一行只有一个数,除了最下行之外 * 每个数的左下方和右下方各有一个数,从第一行的数开始,每次可以 * 往左下或右下走一格,直到走到最下行,沿途的数相加,怎么走和最大? */ const int N = 4; const int a[N][N] = {{1}, {3, 2}, {4, 10, 1}, {4, 3, 2, 20}}; /* 把当前位置看成一个状态(i, j),定义状态的指标函数d(i, j) * 原问题的解是d(0, 0), * 状态转移方程: d(i, j) = a(i, j) + max{d(i+1, j), d(i+1, j+1)} */ int ans[N][N]; //递归中的计算结果保存在这里 int d(int i, int j) { if(i == N) //走到底 return 0; if(j > i) //边界处理 return 0; if(ans[i][j] >= 0) //已经计算过 return ans[i][j]; cout << "(" << i + 1 << ", " << j + 1 << ")" << endl; int x = d(i+1, j); int y = d(i+1, j+1); return ans[i][j] = a[i][j] + (x > y ? x : y); } int main() { //初始化为-1 memset(ans, -1, sizeof(ans)); cout << d(0,0); return 0; }
运行结果:
(1, 1)
(2, 1)
(3, 1)
(4, 1)
(4, 2)
(3, 2)
(4, 3)
(2, 2)
(3, 3)
(4, 4)
24
Process returned 0 (0x0) execution time : 0.144 s
Press any key to continue.
结论:由于不相同的节点一共只有O(n^2)个,无论以怎样的顺序访问,时间复杂度均为O(n^2)
当采用记忆化搜索时,不必事先确定各状态的计算顺序,但需要记录每个状态“是否已经计算过”。
原因:程序递归同时把结果保存在数组中,题目中各个数都是非负的,因此如果计算过某个d[i][j],则她应该是非负的,
这样只需把所有d初始化为-1,既可通过判断是否d[i][j]>=0 得知她是否已经被计算过。