动态规划入门
五、动态规划入门
1、动态介绍
1.1动态规划基本思路
动态规划是编程解题的一种重要手段,1951年美国数学家R.Bellman等人,根据一类多阶段问题的特点,把多阶段决策问题变成一系列相互联系的单阶段问题,然后逐个解决。与此同时,他提出这类问题的最优化原理,从而创建了解决最优化问题的一种新方法:动态规划。
动态规划算法通常用于求解某种最优性质的问题。
我们可以用一个表来记录所有已解的子问题的答案。不管该子问题以后是否被用到,只要被计算过,就讲其结果填入表中。这就是动态规划法的基本思路。
1.2 动态规划基本概念
1.2.1 阶段
把所给问题的求解过程恰当地分成若干个相互联系的阶段,以便于求解。过程不同,阶段数就可能不同。描述阶段的变量称为阶段变量,常用 表示。阶段的划分,一般是根据时间和空间的自然特征来划分,但要便于把问题的过程转化为多阶段决策的过程。
1.2.2 状态
状态表示每个阶段开始面临的自然状况或客观条件,它不以人们的主观意志为转移,也称为不可控因素。通常一个阶段有若干个状态,状态通可以用一个或一组数来描述,称为状态变量。
1.2.3 决策
表示当过程处于某一阶段的某个状态时,可以做出不同的决定,从而确定下一阶段的状态,这种决定称为决策。不同的决策对应着不同的数值,描述决策的变量称决策变量。
1.2.4 状态转移方程
动态规划中本阶段的状态往往是上一阶段的状态和上一阶段的决策的结果,由第 i段的状态dp[i],和决策 u[i]来确定第 i+1段的状态。状态转移表示为F(i + 1) = T(f(i), u(i)) ,称为状态转移方程。
1.2.5 策略
各个阶段决策确定后,整个问题的决策序列就构成了一个策略,对每个实际问题,可供选择的策略有一定范围,称为允许策略集合。允许策略集合中达到最优效果的策略称为最优策略
1.3 动态规划的优化原理与无后效性
1.3.1 最优化原理
一个过程的最优决策具有这样的性质:即无论其初始状态和初始决策如何,其今后诸策略对以第一个决策所形成的状态作为初始状态的过程而言,必须构成最优策略”。也就是说一个最优策略的子策略,也是最优的。
1.3.2 无后效性
如果某阶段状态给定后,则在这个阶段以后过程的发展不受这个阶段以前各个状态的影响。
2、动态规划问题
2.1 可乐机回家问题
2.1.1 问题描述
可怜的可乐机要回家,已知小可乐机在 左下角 (1,1) 位置,家在 右上角 (n,n) 坐标处。小可乐机走上一个格子 (i,j) 会花费一定的体力 a[i][j],而且小可乐机只会往家的方向走,也就是只能往上,或者往右走。小可乐机想知道他回到家需要花费的最少体力是多少, 求你帮帮小可乐机吧qwq
例如下图所示,格子中的数字代表走上该格子花费的体力:
2.1.2 解题思路
对于该图来说,最优策略已在图上标出,最少花费体力为:3 + 2 + 4 + 3 = 123 + 2 + 4 + 3 = 12。
我们把走到一个点看做一个状态,对小可乐机来说,走到一个点只有两种方式,一个是从下面走到该点,一种是从左边走到该点。那么点 (i,j) 要么是从 (i-1,j) 走到 (i,j),要么是从点 (i,j-1) 走到 (i,j)。
所以从哪个点走到 (i,j) 就是一个 决策。接下来,我们用 dp(i,j) 来代表走到点 (i,j) 一共花费的最少体力。
我们需要花费最少力气走到家,所以可以得到状态转移方程:dp(i,j) = min(dp(i-1,j), dp(i,j-1)) + a[i][j] 。根据转移方程,我们可以推出走到每个点花费的最少体力。
对于图中的边界点,要在转移前加上判断是否为边界,如:点 (1,3) 只能从点 (1,2) 走过来,点 (3,1) 只能从点 (2,1) 走过来等等。
动态规划的题目的核心是写出状态转移方程,对于一个动态规划的题目,如果我们能写出转移方程那么代码实现就变得简单多了。大部分的动态规划题目,在计算出转移方程后,可以用类似于递推的循环结构,来写出代码。
2.1.3 代码实现
#include<iostream>
#include<algorithm>
using namespace std;
int a[100][100]; // a数组代表在点(i,j)花费的体力
int dp[100][100]; // dp数组代表走到点(i,j)一共花费的最少体力
int main(){
int n;
cin>>n;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++){
cin>>a[i][j];
}
}
dp[1][1] = 0;//先把初始边界赋值好,再利用递推进行求解
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++){
if (i == 1 && j == 1){
continue;
}
else if (i == 1){ //边界点
dp[i][j] = dp[i][j-1] + a[i][j];
}
else if (j == 1){ //边界点
dp[i][j] = dp[i-1][j] + a[i][j];
}
else {
dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + a[i][j]; //转移方程
}
}
}
cout<<dp[n][n]<<endl;
return 0;
2.2 蒜头君下山问题
2.2.1 问题描述
蒜头在玩一款游戏,他在一个山顶,现在他要下山,山上有许多水果,蒜头每下一个高度就可以捡起一个水果,并且获得水果的能量。山的形状如图所示:
3
1 2
6 2 3
3 5 4 1
这是一个高度为4的山,数字代表水果的能量。每次下一个高度,蒜头需要选择是往左下走,还是往右下走。例如:对于上图的情况,蒜头能获得的最大能量为,3+1+6+5=15。现在,蒜头希望你能帮他计算出下山能获得的最大能量。
2.2.2 解题思路
状态转移方程:f[i][j]=f[i][j]+max(f[i-1][j],f[i-1][j])
2.2.3 代码实现
#include<iostream>
using namespace std;
const int N=1e3+9;
const int int=1000000000;
int f[N][N];
int main(){
int n;
cin>>n;
for(int i=1;i<=n;++n){
for(int j=1;j<=i;==j){
cin>>f[i][j];
}
}
int ma=-inf;//定义ma表示最后的答案,并初始化为负无穷
for(int i=1;i<=n;==i){//递推式
for(int j=1;j<=i;==j){
f[i][j]+=max(f[i-1][j],f[i-1][j-1]);
if(i==n){//在最后一行找最大值
ma=max(f[i][j],ma)
}
}
}
if(ma==-inf){
ma=0;
}
cout<<ma<<endl;
retrun 0;
}
2.3 三维的蒜头君回家问题
2.3.1 问题描述
蒜头君要回家(0,0,0),且值蒜头君当前位置(x,y,z)希望能尽快回到家中,请你帮他计算出回家所需要的最短路程。蒜头君生活的城市可以看做是一个 xyz三维的网格,其中有道路有障碍,钥匙和家所在的地方可以看做是道路,可以通过。蒜头君可以在城市中沿着上下左右前后6个方向移动,移动一个格子算做走一步。
2.3.2 解题思路
在做多维动态规划时,第一步是描述事物,在描述的时候先不必管我们使用空间的大小。当我们可以准确描述清楚一个物体的各个状态的时候,在考虑优化,看看有没有哪个描述是没有必要的,哪个描述是可以通过另外一个值表示。
当多维空间的时候,不能像二维空间那样找规律,这时候需要自己的推理,写出递推方程式。
递推式:f[i][j][k]=min(f[i-1][j][k],f[i][j-1][k],f[i][j][k-1])+f[i][j][k])+f[i][j][k]
2.3.3 代码实现
#include<iostream>
using namespace std;
const int N=1e2+9;
const int inf = 1000000000;
int f[N][N][N];
int main(){
int x,y,z;
cin>>x>>y>>z;
for(int i=0;i<=x;++i){//接收数据
for(int j=0;j<=y;++j){
for(int k=0;k<=z;++k){
cin>>f[i][j][k];
}
}
}
for(int i=0;i<=x;++i){
for(int j=0;j<=y;++j){
for(int k=0;k<=z;++k){
int mi=inf;//定义一个变量初始值为正无穷大,保存转移过来的三个状态的最小值
if(i!=0){
mi=min(mi,f[i-1][j][k])//找f[i-1][j][k]是否最小
}
if(j!=0){
mi=min(mi,f[i][j-1][k])//找f[i][j-1][k]是否最小
}
if(k!=0){
mi=min(mi,f[i][j][k-1])//找f[i][j][k-1]是否最小
}
if(mi!=inf){//通过吗mi是否变化判断f[i][j][k]仍为本身
f[i][j][k]+=mi
}
}
}
}
cout<<f[x][y][z]<<endl;
return 0;
}