动态规划理论和基础
Posted 两片空白
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了动态规划理论和基础相关的知识,希望对你有一定的参考价值。
目录
一.什么是动态规划
动态规划,英文:dynamic programming,简称dp。动态规划是分治思想的延申,通俗一点就是大事化小,小事化无,将大问题化解成小问题的分治过程,通常会用一个一维或者二维数组保存小问题已经处理好的解,并提供给后面处理更大规模的问题时直接使用这些结果。
如果某一问题有很多重叠的子问题,并且每一个状态可以由上一个或者多个状态推到出来,可以使用动态规划。
从上面的定义可以看出这与递归的思想很是类似,其实动态规划可以说成是递归的一种优化,省去了函数栈帧的开辟。
特点:
1.把原问题分解成了n个相似问题的子问题。
2.所有子问题都只需要解决一次。
3.存储子问题的解。
本质:找到问题状态的定义和状态转移方程的定义。状态与状态之间的关系。
一般从四个角度来思考:
1.状态的定义。这个就决定了你后面回怎么去推导出转移方程。
2.状态的转移方程,怎么实现前面的状态推导出后面的状态。
3.状态的初始化,这个决定了你后面会得到什么结果。
4.返回结果
千万别觉得只要只要转移方程出来了结果就出来了,其它几个角度同样很重要。
可以解决的一些问题:
1.动态规划基础问题。(斐波那契,台阶问题,不同路径等问题)
2.背包问题。(01背包问题,完全背包问题)。
3.打家劫舍
4.股票问题
5.子序列问题。
注意:一般结果都是通过循环得到,循环开始一般从初始化结果下一个开始。
二.基础问题分析
1.爬楼梯问题
力扣 70题爬楼梯:https://leetcode-cn.com/problems/climbing-stairs/submissions/
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
从四个角度分析:
状态定义:爬上i级台阶的方法F(i)。
转移方程:F(i)=F(i-1)+F(i-2)。因为爬上i级台阶由两种方法,从i-1级台阶爬上来,从i-2级台阶爬上来。所以转移方程是,爬上i-1级台阶的方法加上爬上i-2级台阶的方法。
初始化:F(1)=1,F(0)=1。爬上第一级台阶的方法只有一种。在零级台阶,其实并不需要爬可以这样想爬上第二级台阶有两种方法,其实可以定义初始化定义F(2)=2,也能得出结果,至于F(0)=1,只是一个辅助结果。
返回值:F(n)。爬上第n层台阶的方法。
class Solution {
public:
int climbStairs(int n) {
if(n==1){
return 1;
}
//保存结果
vector<int> dp(n+1,1);
//初始化
dp[1]=1;
dp[0]=1;
//转移方程
for(int i=2;i<=n;i++){
dp[i]=dp[i-1]+dp[i-2];
}
return dp[n];
}
};
优化:
class Solution {
public:
int climbStairs(int n) {
//优化,使用两个变量保存上两次的结果
if(n==1){
return 1;
}
//初始化
int first=1;
int second=1;
//结果
int result=0;
//转移方程
for(int i=2;i<=n;i++){
result=first+second;
first=second;
second=result;
}
return result;
}
};
力扣746 使用最小花费爬楼梯,https://leetcode-cn.com/problems/min-cost-climbing-stairs/
描述
数组的每个下标作为一个阶梯,第 i 个阶梯对应着一个非负数的体力花费值 cost[i](下标从 0 开始)。
每当你爬上一个阶梯你都要花费对应的体力值,一旦支付了相应的体力值,你就可以选择向上爬一个阶梯或者爬两个阶梯。
请你找出达到楼层顶部的最低花费。在开始时,你可以选择从下标为 0 或 1 的元素作为初始阶梯。
状态定义:爬上i级台阶的最小花费。
转移方程:F(i)=min(F(i-2)+cost(i-2),F(i-1)+cost(i-1))。因为爬上i级台阶由两种方法,从i-1级台阶爬上来,从i-2级台阶爬上来。最小花费就是爬上第i-1阶台阶的最小花费加上从i-1阶台阶爬上第i阶台阶和爬上第i-2阶台阶的最小花费加上从第i-2阶台阶爬上第i阶台阶最小值
初始化:F(0)=0,F(1)=1。如题意,从可以从第0阶台阶开始,也可以从第1阶台阶开始。所以第0阶台阶和第1阶台阶的花费为0。
返回值:F(n)。爬上第n层台阶的最小花费。
class Solution {
public:
int minCostClimbingStairs(vector<int>& cost) {
//台阶数
int n=cost.size();
//保存结果
vector<int> dp(n+1,0);
//初始化
dp[0]=0;
dp[1]=0;
//转移方程
for(int i=2;i<=n;i++){
dp[i]=min((dp[i-2]+cost[i-2]),(dp[i-1]+cost[i-1]));
}
return dp[n];
//优化
int n=cost.size();
int first=0;
int second=0;
int result=0;
for(int i=2;i<=n;i++){
result=min(first+cost[i-2],second+cost[i-1]);
first=second;
second=result;
}
return result;
}
};
牛客变态跳台阶问题,跳台阶扩展
描述
一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶(n为正整数)总共有多少种跳法。
状态定义:爬上i级台阶的方法F(i)。
转移方程:F(i)=2*F(i-1)+F(i-2)。
由题意有:F(i)=F(i-1)+F(i-2)+......+F(1)+F(0);
又:F(i-1)=F(i-2)+......+F(1)+F(0);
所以:F(i)=F(i-1)+F(i-1)=2*F(i-1)。
初始化:F(1)=1,F(0)=0。爬上第一级台阶的方法只有一种。与普通爬楼梯一样,F(0)只是一个辅助结果,其实可以初始化F(2)=2。
返回值:F(n)。爬上第n层台阶的方法。
class Solution {
public:
int jumpFloorII(int number) {
vector<int> dp(number+1,0);
//初始化
dp[0]=0;
dp[1]=1;
//转移方程
for(int i=2;i<=number;i++){
dp[i]=2*dp[i-1];
}
return dp[number];
}
};
牛客 又见台阶: NC553 又见台阶
描述:
台阶一共有{n}n层,有一些台阶上有积水。
牛牛一开始在第0层,它每次可以跳奇数层台阶,他想跳到第n层,但是它不希望在跳跃的过程中踩到积水。
已知有{m}m个台阶上有积水。
请问牛牛在不踩到积水的情况下跳到第n层有多少种不同的方案。如果不可能到达第{n}n层,则答案为0。为了防止答案过大,答案对1e9+7取模。
状态定义:跳上第i级台阶的方法。
转移方程:由题意,跳台阶只能跳奇数层台阶,所以这里要分一下台阶数的奇偶。
如果台阶数i为奇数:因为只能跳奇数层台阶即有
F(i)=F(i-1)+F(i-3)+F(i-5)+......+F(0),即跳上i级台阶为前面所有跳上偶数台阶和。
如果台阶数i为偶数:有
F(i)=F(i-1)+F(i-3)+F(i-5)+......+F(0),即跳上i级台阶数为前面所有跳上奇数台阶和。
定义两变量记录奇数台阶和SumOdd,偶数台阶和Sumeven
i不为有水台阶:
F(i)= i为奇数:Sumeven(前面所有偶数层和)
i为偶数:SumOdd(前面所有奇数层和)
最后要更新和:
奇数层和:SumOdd+=F(i)
偶数层和:Sumeven+=F(i)
i为有水台阶:
F(i)= 0;
不需要更新奇数层和偶数层和。
初始化:F(0)=0,SumOdd=0,Sumeven=1。跳上第0层台阶方法数为0,因为0为偶数层,Sumeven初始化为1。也可以这样想,第一层为奇数层,跳上去一定有一种方法,它为前面偶数层和,所以初始化Sumeven=1。
返回值:F(n)。
class Solution {
public:
int solve(int n, int m, vector<int>& a) {
// write code here
const int M=1e9+7;
//初始化
int SumOdd=0;//跳到i前所有奇数层和
int Sumeven=1;//跳到i前所有偶数层和
//保存结果
vector<int> dp(n+1,0);
//记录遇到水坑个数
int c=0;
for(int i=1;i<=n;i++){
//当已经跨过所有水坑或者不是水坑,就计算跳上个数
if(c>m||i!=a[c]){
//两种情况
//偶数层
if(i%2==0){
//为奇数和
dp[i]=SumOdd;
//更新偶数和
Sumeven+=SumOdd;
}
//奇数层
else{
//为偶数和
dp[i]=Sumeven;
//更新奇数和
SumOdd+=Sumeven;
}
}
else{
//是水坑
c++;
}
}
return dp[n]%M;
}
};
2.不同路径问题
力扣62 https://leetcode-cn.com/problems/unique-paths/
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
由于到达第Finish的路径数与当前位置的层数和列数有关,所以要建立一个二维数组来保存结果。
状态定义:到达Finish位置(i层,j列)的不同路径数F[ i ][ j ]。
转移方程:F[ i ][ j ]=F[ i-1 ][ j ] + F[ i ][ j-1 ]。因为只能向下或者向右走。到达i层j列,只能从上一层的同一列或者同一层的前一列到达。
初始化:F[ i ][ 0 ]=1,F[ 0 ][ j ]=1。第一行和第一列只右一宗路径到达,一直向右或者向左走。
返回值:F[ m ][ n ]。
class Solution {
public:
int uniquePaths(int m, int n) {
//保存结果数组加初始化
vector<vector<int>> dp(m,vector<int>(n,1));
//转移方程
for(int i=1;i<m;i++){
for(int j=1;j<n;j++){
dp[i][j]=dp[i-1][j]+dp[i][j-1];
}
}
return dp[m-1][n-1];
}
};
力扣63 不同路径 II:https://leetcode-cn.com/problems/unique-paths-ii/
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。
现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
网格中的障碍物和空位置分别用
1
和0
来表示。
由于到达第Finish的路径数与当前位置的层数和列数有关,所以要建立一个二维数组来保存结果。
状态定义:到达Finish位置(i层,j列)的不同路径数F[ i ][ j ]。
转移方程:
不是障碍:
F[ i ][ j ]=F[ i-1 ][ j ] + F[ i ][ j-1 ]。因为只能向下或者向右走。到达i层j列,只能从上一层的同一列或者同一层的前一列到达。
是障碍:
F[ i ][ j ]=0。因为到达不了。
初始化:第一行如果没有障碍,初始化为1,有障碍,障碍物之前初始化为1,后面初始化为0(因为到达不了了)。第一列没有障碍。初始化为1,障碍物之前初始化为1,后面初始化为0(因为到达不了了)。
返回值:F[ m ][ n ]。
class Solution {
public:
int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
//记录行列
int row=obstacleGrid.size();
int col=obstacleGrid[0].size();
vector<vector<int>> dp(row,vector<int>(col,0));
//初始化
for(int i=0;i<row;i++){
if(obstacleGrid[i][0]!=1){
dp[i][0]=1;
}
else{
break;
}
}
for(int i=0;i<col;i++){
if(obstacleGrid[0][i]!=1){
dp[0][i]=1;
}
else{
break;
}
}
//转移方程
for(int i=1;i<row;i++){
for(int j=1;j<col;j++){
if(obstacleGrid[i][j]!=1){
dp[i][j]=dp[i-1][j]+dp[i][j-1];
}
else{
dp[i][j]=0;
}
}
}
return dp[row-1][col-1];
}
};
3.整数拆分(剪绳子)问题
力扣:343 https://leetcode-cn.com/problems/integer-break/
给定一个正整数 n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。 返回你可以获得的最大乘积。
状态定义:数值为i的数差分成若干份中乘积最大的值。
转移方程:max(i*(i-j),j*dp[i-j])
当i>=2时,这个数肯定可以拆分为至少两个数。当一个数拆分为j时有两种情况
1.只拆分成两份,一个为j,一个为i-j,乘积为i*(i-j)。
2.拆分成多份,一个为j,一个就是将i-j拆分,并且要是i-j拆封后乘积的最大值,即值为i的乘积为j*dp[i-j]。
为什么会有这两种情况?
因为i>=2时,数i至少可以拆分成两份,第一种情况是拆分成两份的情况,第二种情况是拆分成多份的情况。如果直接使用dp[j]*dp[i-j]这是一下就拆分成了至少四份,忽略了拆分成两份,三份的情况。
有人肯定会跟我有一样的想法,为什么j不用拆分呢?
我的理解是,j是在不断循环遍历增加的,循环遍历的过程中dp[i-j]已经将j数值情况拆分了。
初始化:dp[0]=0,dp[1]=0。值为0和1都不能拆分
返回值:dp[n]。
class Solution {
public:
int integerBreak(int n) {
//初始化。dp[0]=0,dp[1]=0
vector<int> dp(n+1,0);
//一般都是从初始化后一个值开始
for(int i=2;i<=n;i++){
int Max=0;
//计算到一半,后面都是重复的计算
for(int j=1;j<=i-j;j++){
//转移方程
Max=max(Max,max(j*(i-j),j*dp[i-j]));
}
dp[i]=Max;
}
return dp[n];
}
};
大家可以试一下剪绳子:https://leetcode-cn.com/problems/jian-sheng-zi-lcof/
4.不同的二叉搜索树
力扣:96 https://leetcode-cn.com/problems/unique-binary-search-trees/
给你一个整数
n
,求恰由n
个节点组成且节点值从1
到n
互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。
状态定义:一共i个结点可以构成互不相同的二叉搜索树个数。
转移方程:dp[i]=Sum(dp[j-1]*dp[i-j]),j>=1&&j<=i
i个节点,构建二叉搜索树可以按照不同的根节点来构造。就是以1作为根节点来构造二叉搜索树,以2为根节点来构造二叉搜索树,直到以i为根节点构造二叉搜索树。
由于二叉搜索树的左右子树也是二叉搜索树,i个结点构建二叉搜索树的个数,等于左子树结点构造的二叉搜索树的个数乘以右子树节点数构建二叉搜索树的个数。
由图当n个结点,以i作为根节点,左子树结点个数为i-1个,右子树结点个数为n-i个,由于这些结点个数肯定小于n,之前已经求出,符合动态规划。
初始化:dp[0]=1。可以理解为空结点也是二叉搜索树,所以为1。或者是,当左子树或者右子树没有结点时,构造的二叉树为相反一边构造的二叉搜索树个数。
返回值:dp[n]
class Solution {
public:
int numTrees(int n) {
vector<int> dp(n+1,0);
//初始化,
dp[0]=1;
for(int i=1;i<=n;i++){
int sum=0;
for(int j=1;j<=i;j++){
//从结点1到i结点作为根节点的二叉树之和
sum+=(dp[j-1]*dp[i-j]);
}
dp[i]=sum;
}
return dp[n];
}
};
以上是关于动态规划理论和基础的主要内容,如果未能解决你的问题,请参考以下文章