程序设计与算法(二):5. 动态规划

目录

例 数字三角形

动态规划解题一般思路

例 最长上升子序列

例 0-1背包问题

例子源于慕课课程:程序设计与算法二

例 数字三角形

 输入格式

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;
}

空间优化

  1. maxSum用一维数组存
  2. 没有数组,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. 具有最优子结构性质。即最优解包含的子问题的解也是最优解
  2. 无后效性。若干个状态值一旦确定,此后的状态值只与这若干个状态值有关

例 最长上升子序列

输入数据

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()函数

例 0-1背包问题

最详细动态规划解析——背包问题_oscarwin-CSDN博客_动态规划背包问题

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

跳坑坑

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值