动态规划

 

什么是动态规划?

 

用一句话解释动态规划就是 “记住你之前做过的事”,如果更准确些,其实是 “记住你之前得到的答案”。是通过空间换取时间的算法

举个大家工作中经常遇到的例子。

在软件开发中,大家经常会遇到一些系统配置的问题,配置不对,系统就会报错,这个时候一般都会去 Google 或者是查阅相关的文档,花了一定的时间将配置修改好。

过了一段时间,去到另一个系统,遇到类似的问题,这个时候已经记不清之前修改过的配置文件长什么样,这个时候有两种方案,一种方案还是去 Google 或者查阅文档,另一种方案是借鉴之前修改过的配置,第一种做法其实是万金油,因为你遇到的任何问题其实都可以去 Google,去查阅相关文件找答案,但是这会花费一定的时间,相比之下,第二种方案肯定会更加地节约时间,但是这个方案是有条件的,条件如下:

  • 之前的问题和当前的问题有着关联性,换句话说,之前问题得到的答案可以帮助解决当前问题
  • 需要记录之前问题的答案

当然在这个例子中,可以看到的是,上面这两个条件均满足,大可去到之前配置过的文件中,将配置拷贝过来,然后做些细微的调整即可解决当前问题,节约了大量的时间

从这些描述中发现,对于一个动态规划问题,我们只需要从两个方面考虑,那就是 找出问题之间的联系,以及 记录答案,这里的难点其实是找出问题之间的联系,记录答案只是顺带的事情,利用一些简单的数据结构就可以做到

解动态规划题目其实就是拆解问题,定义状态的过程,严格说来,动态规划并不是一个具体的算法,而是凌驾于算法之上的一种 思想

动态规划问题的四个步骤

 

  • 问题拆解,找到问题之间的具体联系
  • 状态定义
  • 递推方程推导
  • 实现

这里还是拿 Quora 上面的例子来讲解,“1+1+1+1+1+1+1+1” 得出答案是 8,那么如何快速计算 “1+ 1+1+1+1+1+1+1+1”,我们首先可以对这个大的问题进行拆解,这里我说的大问题是 9 个 1 相加,这个问题可以拆解成 1 + “8 个 1 相加的答案”,8 个 1 相加继续拆,可以拆解成 1 + “7 个 1 相加的答案”,… 1 + “0 个 1 相加的答案”,到这里,第一个步骤 已经完成。

状态定义 其实是需要思考在解决一个问题的时候我们做了什么事情,然后得出了什么样的答案,对于这个问题,当前问题的答案就是当前的状态,基于上面的问题拆解,你可以发现两个相邻的问题的联系其实是 后一个问题的答案 = 前一个问题的答案 + 1,这里,状态的每次变化就是 +1

定义好了状态,递推方程就变得非常简单,就是 dp[i] = dp[i - 1] + 1,这里的 dp[i] 记录的是当前问题的答案,也就是当前的状态,dp[i - 1] 记录的是之前相邻的问题的答案,也就是之前的状态,它们之间通过 +1 来实现状态的变更

最后一步就是实现了,有了状态表示和递推方程,实现这一步上需要重点考虑的其实是初始化,就是用什么样的数据结构,根据问题的要求需要做那些初始值的设定。

 

 

数字三角形

 

在上面的数字三角形中寻找一条从顶部到底边的路径,使得 路径上所经过的数字之和最大。路径上的每一步都只能往左下或 右下走。只需要求出这个最大和即可,不必给出具体路径。 三角形的行数大于1小于等于100,数字为 0 - 99

解题思路:

#include <iostream>
#include <cstdio>
using namespace std;
#define MAX 100
int D[MAX][MAX];
int n;
int MaxSum(int i,int j){//求D[i][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]; 
}
int main(){
	int i,j;
	cin >>n;
	for(int i=1;i<=n;i++){
		for(int j=1;j<=i;j++){
			cin >>D[i][j];
		}
	}
	cout <<MaxSum(1,1);
} 

#include <iostream>
#include <cstdio>
using namespace std;
#define MAX 100
int D[MAX][MAX];
int maxSum[MAX][MAX];//用来保存已经计算过的maxSum[i][j] 
int n;
int MaxSum(int i,int j){
	//如果已经计算过,就不用在继续 
	if( maxSum[i][j] != -1 ) return maxSum[i][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]; 
}
int main(){
	int i,j;
	cin >>n;
	for(int i=1;i<=n;i++){
		for(int j=1;j<=i;j++){
			cin >>D[i][j];
			maxSum[i][j]=-1;
		}
	}
	cout <<MaxSum(1,1);
}

 

 

 

#include <iostream>
#include <cstdio>
using namespace std;
#define MAX 100
int D[MAX][MAX];
int MaxSum[MAX][MAX];
int n;
int main(){
	cin >>n;
	for(int i=1;i<=n;i++){
		for(int j=1;j<=i;j++){
			cin >>D[i][j];
		}
	}
	for(int i=1;i<=n;i++){
		MaxSum[n][i]=D[n][i];
	}
	for(int i=n-1;i>=1;i--){
		for(int 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];
} 

#include <iostream>
#include <cstdio>
using namespace std;
#define MAX 100
int D[MAX][MAX];
int n;int * maxSum;
int main(){
	cin >>n;
	for(int i=1;i<=n;i++){
		for(int j=1;j<=i;j++){
			cin >>D[i][j];
		}
	}
	maxSum = D[n]; //maxSum指向第n行 
	for(int i=n-1;i>=1;i--){
		for(int j=1;j<=i;j++){
			maxSum[j]=max(maxSum[j],maxSum[j+1])+D[i][j];
		}
	}
	cout <<maxSum[1];
}

爬楼梯

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

注意:给定 n 是一个正整数。

题目解析

爬楼梯,可以爬一步也可以爬两步,问有多少种不同的方式到达终点,我们按照上面提到的四个步骤进行分析:

  • 问题拆解

    我们到达第 n 个楼梯可以从第 n – 1 个楼梯和第 n – 2 个楼梯到达,因此第 n 个问题可以拆解成第 n – 1 个问题和第 n – 2 个问题,第 n – 1 个问题和第 n – 2 个问题又可以继续往下拆,直到第 0 个问题,也就是第 0 个楼梯 (起点)

  • 状态定义

    “问题拆解” 中已经提到了,第 n 个楼梯会和第 n – 1 和第 n – 2 个楼梯有关联,那么具体的联系是什么呢?你可以这样思考,第 n – 1 个问题里面的答案其实是从起点到达第 n – 1 个楼梯的路径总数,n – 2 同理,从第 n – 1 个楼梯可以到达第 n 个楼梯,从第 n – 2 也可以,并且路径没有重复,因此我们可以把第 i 个状态定义为 “从起点到达第 i 个楼梯的路径总数”,状态之间的联系其实是相加的关系。

  • 递推方程

    “状态定义” 中我们已经定义好了状态,也知道第 i 个状态可以由第 i – 1 个状态和第 i – 2 个状态通过相加得到,因此递推方程就出来了 dp[i] = dp[i - 1] + dp[i - 2]

  • 实现

    你其实可以从递推方程看到,我们需要有一个初始值来方便我们计算,起始位置不需要移动 dp[0] = 0,第 1 层楼梯只能从起始位置到达,因此 dp[1] = 1,第 2 层楼梯可以从起始位置和第 1 层楼梯到达,因此 dp[2] = 2,有了这些初始值,后面就可以通过这几个初始值进行递推得到。

#include <iostream>
#include <cstdio>
using namespace std;
int N[100];
int  climbStaris(int n){
	N[0]=0;N[1]=1;N[2]=2;
	for(int i=3;i<=n;i++){
		N[i]=N[i-1]+N[i-2];
	}
	return N[n];
	
}
int main(){
	int n;
	cin >>n;
	cout <<climbStaris(n);
	return 0;
}

 

最大子序和

 

给定一个整数数组 num,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

 

  • 问题拆解:“以 i 结尾的所有子数组中,和最大的是多少?”,然后我们去试着拆解,这里其实只有两种情况:

       i 这个位置的元素自成一个子数组;

       i 位置的元素的值 + 以 i – 1 结尾的所有子数组中的子数组和最大的值

    你可以看到,我们把第 i 个问题拆成了第 i – 1 个问题,之间的联系也变得清晰

  • 定义状态: “以 i 结尾的所有子数组的最大值
  • 递推方程:sumMax[i]=max(sumMax[i-1]+array[i],array[i])
#include <iostream>
#include <cstdio>
#include <algorithm> 
using namespace std;
int num[100];
int sum[100];
int main(){
	int n;
	cin >>n;
	for(int i=1;i<=n;i++){
		cin >>num[i]; 
	}
	sum[1]=num[1];
	int maxNum=sum[1];
	for(int i=2;i<=n;i++){
		sum[i]=max(sum[i-1]+num[i],num[i]);
		maxNum=max(maxNum,sum[i]);
	}
	cout <<maxNum;
	return 0;
} 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值