目录
例子源于慕课课程:程序设计与算法二
例 数字三角形
输入格式
5
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
分析思路
用二维数组存放输入的数字三角形
D(r,j):r行j列的数字(r,j从1开始算)
MaxSum(r,j):从D(r,j)开始到底边的各条路径中,最佳路径和
问题:即MaxSum(1,1)
典型的递归问题
从D(r,j)出发,只能走D(r+1,j)/D(r+1,j+1)
递归代码:
#define MAX 101
int D[MAX][MAX];
int n;
int MaxSum(int i, int j);
int main() {
int i, j;
for (i = 1; i <= n; i++) {
for (j = 1; j <= i; j++) {
cin >> D[i][j];
}
}
cout << MaxSum(1, 1) << endl;
}
int MaxSum(int i, int j) {
if (i == n)
return D[i][j];
int x = MaxSum(i + 1, j);
int y= MaxSum(i + 1, j+1);
return max(x, y) + D[i][j];
}
貌似没问题,然而复杂度为O(2^n),n<=100,严重超时
因为在深度遍历每条路径时,存在大量重复计算,为了避免重复计算,我们只计算MaxSum(r,j)一次,计算后将其保存起来,用到的时候就不用再计算,一共有n行,总共n(n+1)/2个数,时间复杂度为O(n^2)
记忆递归型动归程序(因为把路径"记忆"下来):
#define MAX 101
int D[MAX][MAX];
int maxsum[MAX][MAX];//记忆每个MaxSum
int n;
int MaxSum(int i, int j);
int main() {
int i, j;
cin >> n;
for (i = 1; i <= n; i++) {
for (j = 1; j <= i; j++) {
cin >> D[i][j];
maxsum[i][j] = -1;//初始化
}
}
cout << MaxSum(1, 1) << endl;
}
int MaxSum(int i, int j) {
if (maxsum[i][j] != -1)
return maxsum[i][j];//如果已经计算过,直接返回不用计算
if (i == n)
maxsum[i][j]= D[i][j];
else {
int x = MaxSum(i + 1, j);
int y = MaxSum(i + 1, j + 1);
maxsum[i][j]= max(x, y) + D[i][j];
}
return maxsum[i][j];
}
递归转递推:
很容易知道最后一行的最短路径即为D[n][j],可以倒着推出第n-1行的maxSum,即为 maxSum[i][j] = max(maxSum[i + 1][j], maxSum[i + 1][j + 1]) + D[i][j];
全部值如图
递推代码
#define MAX 101
int D[MAX][MAX];
int n;
int maxSum[MAX][MAX];
int main() {
int i, j;
//输入
cin >> n;
for (i = 1; i <= n; i++)
for (j = 1; j <= i; j++)
cin >> D[i][j];
//最后一行的maxSum
for (j = 1; j <= n; j++)
maxSum[n][j] = D[n][j];
//递推
for (i = n-1; i > 0; i--)
for (j = 1; j <= i; j++)
maxSum[i][j] = max(maxSum[i + 1][j], maxSum[i + 1][j + 1]) + D[i][j];
cout << maxSum[1][1] << endl;
return 0;
}
空间优化
- maxSum用一维数组存
- 没有数组,maxSum用D的最后一行存,设一个maxSum指针指向最大值
没有数组的优化:
#define MAX 101
int D[MAX][MAX];
int n;
int *maxSum;
int main() {
int i, j;
//输入
cin >> n;
for (i = 1; i <= n; i++)
for (j = 1; j <= i; j++)
cin >> D[i][j];
maxSum = D[n];
//优化
for (i = n-1; i > 0; i--)
for (j = 1; j <= i; j++)
maxSum[j] = max(maxSum[j], maxSum[j + 1]) + D[i][j];
cout << maxSum[1] << endl;
return 0;
}
动态规划解题一般思路
一般的动态规划问题,我们容易想到递归求解,那递归为什么要转化成动规呢?
- 递归涉及到函数调用,时间上可能比递推慢
- 递归可能数据过多,导致栈溢出(极少数)
递归到动规一般转换方法
- 递归函数有n个参数,就定义一个n维的数组
- 数组的下标是递归函数参数的取值范围,数组元素的值是递归函数的返回值
- 这样就可以从递归边界开始,逐步填充数组,相当于递归的逆过程,比如数字三角形递归从上到下,动规从下到上求解
动态规划解题一般思路
1.将原问题分解为子问题
- 子问题和原问题形式相同/相似,只不过规模变小了,子问题都解决,原问题即解决
- 子问题解一旦求出就保存,即只计算一次
2.确定状态
- 和子问题相关的变量的一组取值成为一个状态,一个状态对应一个/多个子问题,一个状态有一个值,即为子问题的解,比如数字三角形,行和列确定一个状态,值为maxSum
- 所有状态的值构成“状态空间”,其大小于时间复杂度直接相关,例如数字三角形状态空间大小为n(n+1)/2,求解一个状态的所需时间是常数阶,整个问题时间复杂度是状态空间大小*求解一个状态的所需时间,故时间复杂度为O(n^2)
3.确定一些初始状态的边界值
4.确定状态转移方程
找出如何从一个或多个值已知的状态,求出另一个状态的值,即“人人为我”递推型,此递推公式可称为状态转移方程
数字三角形的状态转移方程:
能用动规解决的问题的特点
- 具有最优子结构性质。即最优解包含的子问题的解也是最优解
- 无后效性。若干个状态值一旦确定,此后的状态值只与这若干个状态值有关
例 最长上升子序列
输入数据
1<=N<=100
0<=t<=10000
输出要求
最长子序列长
输入样例
7
1 7 3 5 9 4 8
输出样例
4
解题思路
1.子问题
- 设前n和元素的最长上升子序列长是F(n),则 F(n)=x,如果序列最后一个元素比an+1小,F(n+1)=x+1,如果大的话就不确定,这样不具有“无后效性”,是错误的
- 找子问题:求以ak为终点的最大上升子序列的长度。虽然子问题和原问题形式不完全一样,但只要子问题解决了,原问题就解决了,如果an+1>ak,长度就加1,反之不加1
2.确定状态
- 变量:数的位置k,因此状态就是k
- 状态k对应的“值”就是长度,一共n个状态
3.找出状态转移方程
- 初始状态:mazLen(1)=1;
- mazLen(k)=max{maxLen(i): 1<=i<k 且 ai < ak 且 k+1 }+1;若找不到这样的i,则mazLen(k)=1
人人为我递归型代码
#include<iostream>
#include<stdio.h>
#include<stdlib.h>
#include<algorithm>
#include<cmath>
using namespace std;
#define MAX 10010
int n;
int a[MAX];
int maxLen[MAX];
int main() {
cin >> n;
int i, j;
for (i = 1; i <= n; i++) {
cin >> a[i];
maxLen[i] = 1;
}
for (i = 2; i <= n; i++) {
//每次求以第i个数(a[i])为终点的子序列长度
for (j = 1; j < i; j++)
if (a[i] > a[j])
maxLen[i] = max(maxLen[i], maxLen[j] + 1);
}
cout << *max_element(maxLen + 1, maxLen + n + 1)<<endl;//stl函数
}//时间复杂度O(n^2)
max_element()函数参考文章:
stl max函数_std :: max_element()函数