动态规划模板总结

动态规划

动态规划,英文:Dynamic Programming,简称DP,如果某一问题有很多重叠子问题,使用动态规划是最有效的。

所以动态规划中每一个状态一定是由上一个状态推导出来的,这一点就区分于贪心,贪心没有状态推导,而是从局部直接选最优的。

对于动态规划问题,四步解决

  • 确定dp数组(dp table)以及下标的含义

  • 确定递推公式

  • dp数组如何初始化

  • 确定遍历顺序

    最后可以举例推导dp数组,然后可以打印dp数组对比一下

最简单和经典的案例:斐波那契数列(兔子数列)

image-20210823203856183

分析过程

1.确定dp数组以及下标的含义

dp[i]的定义为:第i个数的斐波那契数值是dp[i]

2.确定递推公式

状态转移方程 dp[i] = dp[i - 1] + dp[i - 2]

3.dp数组如何初始化

dp[0] = 0;
dp[1] = 1;

4.确定遍历顺序

从递归公式dp[i] = dp[i - 1] + dp[i - 2];中可以看出,dp[i]是依赖 dp[i - 1] 和 dp[i - 2],那么遍历的顺序一定是从前到后遍历的

一维dp:

70. 爬楼梯

//和斐波那契数列一样

class Solution {
public:
    int climbStairs(int n) {
        if(n<=2) return n;
        vector<int> v(n+1);
        v[0]=0;
        v[1]=1;
        v[2]=2;
        for (int i =3 ; i < n+1; ++i) {
            v[i]=v[i-1]+v[i-2];
        }
        return v[n];
    }
};

746. 使用最小花费爬楼梯(最小消耗版本爬楼梯)
/*
1.dp[i]:是到达每一层的花费
2.确定递推公式:dp[i]=min(dp[i-1],dp[i-2])+cost[i]
3.dp的初始化:dp[0]=cost[0],dp[1]=cost[1]
4.确定遍历顺序:dp顺序从左到右
*注意结果从后两位中挑选
*/
class Solution {
public:
    int minCostClimbingStairs(vector<int>& cost) {
       
        int dp0=cost[0];
        int dp1=cost[1];
        int tem=0;
        int sum=dp0+dp1;
        for (int i = 2; i <cost.size(); ++i) {
            tem=min(dp0,dp1)+cost[i];
            dp0=dp1;
            dp1=tem;
        }
        return min(dp0,dp1);

    }
};


343. 整数拆分
/*
1.dp[i]:分拆数字i,可以得到的最大乘积为dp[i]。
2.确定递推公式:dp[i]最大乘积是怎么得到的呢?

其实可以从1遍历j,然后有两种渠道得到dp[i].
一个是j * (i - j) 直接相乘
一个是j * dp[i - j],相当于是拆分(i - j)
所以 dp[i] = max({dp[i], (i - j) * j, dp[i - j] * j});

3.dp的初始化:为方便计算,dp[0] dp[1] 就直接给0。初始化dp[2] = 1。

4.确定遍历顺序
dp[i] 是依靠 dp[i - j]的状态(因为i-j<i),所以遍历i一定是从前向后遍历,先有dp[i - j]再有dp[i]。
*/
class Solution {
public:
    int integerBreak(int n) {
        vector<int> dp(59,0);
        dp[2]=1;
        for (int i = 3; i <=n; ++i) {
            for (int j = 1; j <i ; ++j) {
                dp[i]=max(dp[i],max(j*(i-j),j*dp[i-j]));
            }
        }
        return dp[n];
    }
};


96. 不同的二叉搜索树
/*
1.dp[i] : 1到i为节点组成的二叉搜索树的个数为dp[i]。

2.确定递推公式:dp[i] += dp[j-1] * dp[i-j]; ,j-1 为j为头结点左子树节点数量,i-j 为以j为头结点右子树节点数量

3.dp初始化:初始化dp[0]就可以了。
从定义上来讲,空节点也是一颗二叉树,也是一颗二叉搜索树,这是可以说得通的。
从递归公式上来讲,dp[以j为头结点左子树节点数量] * dp[以j为头结点右子树节点数量] 中以j为头结点左子树节点数量为0,也需要dp[以j为头结点左子树节点数量] = 1, 否则乘法的结果就都变成0了。

4.确定遍历顺序:
顺序遍历。因为j <=i,节点数为i的状态是依靠 i之前节点数的状态。

参考:https://leetcode-cn.com/problems/unique-binary-search-trees/solution/96-bu-tong-de-er-cha-sou-suo-shu-dong-ta-vn6x/
*/

class Solution {
public:
    int numTrees(int n) {
        vector<int> dp(n+1,0);
        dp[0]=1;
        for (int i = 1; i <n+1; ++i) {
            for (int j = 1; j <=i ; ++j) {
                dp[i]+=dp[j-1]*dp[i-j];
            }
        }
        return dp[n];
    }
};

二维dp:

62. 不同路径
/*
1.dp[i][j] :表示从(0 ,0)出发,到(i, j) 有dp[i][j]条不同的路径。
2.递推公式:想要求dp[i][j],只能有两个方向来推导出来,即dp[i - 1][j] 和 dp[i][j - 1]。
dp[i][j] = dp[i - 1][j] + dp[i][j - 1],
3.dp数组的初始化:dp[i][0]一定都是1,因为从(0, 0)的位置到(i, 0)的路径只有一条,那么dp[0][j]也同理。
4.确定遍历顺序:
这里要看一下递归公式dp[i][j] = dp[i - 1][j] + dp[i][j - 1],dp[i][j]都是从其上方和左方推导而来,那么从左到右一层一层遍历就可以了。
*/
class Solution {
public:
    int uniquePaths(int m, int n) {//二维的更好理解
        vector<vector<int>> dp(m, vector<int>(n, 0));
        for (int i = 0; i < m; i++) dp[i][0] = 1;
        for (int j = 0; j < n; j++) dp[0][j] = 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];
    }

    int uniquePaths2(int m, int n) {//一维数组用来dp,优化空间复杂度
        vector<int> dp(n);
        for (int i = 0; i < n; i++) dp[i] = 1;//第一行初始化

        for (int j = 1; j < m; j++) {//从第二行开始
            for (int i = 1; i < n; i++) {//上一行的dp数组拿下来直接往右迭代加
                dp[i] += dp[i - 1];//更新
            }
        }
        return dp[n - 1];
    }
};


63. 不同路径 II
/*
1.dp[i][j] :表示从(0 ,0)出发,到(i, j) 有dp[i][j]条不同的路径。

2.递推公式:dp[i][j] = dp[i - 1][j] + dp[i][j - 1]。
但需要注意一点,因为有了障碍,(i, j)如果就是障碍的话应该给0,表示它无法为后面或者下面的路径做贡献。

3.dp数组初始化
vector<vector<int>> dp(m, vector<int>(n, 0));//初始化为0
for (int i = 0; i < m && obstacleGrid[i][0] == 0; i++) dp[i][0] = 1;
for (int j = 0; j < n && obstacleGrid[0][j] == 0; j++) dp[0][j] = 1;

4.确定遍历顺序:从左到右,从上到下

*/
class Solution {
public:
    int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
         int m=obstacleGrid.size();
         int n=obstacleGrid[0].size();

         vector<vector<int>>dp(m,vector<int>(n,0));
        for (int i = 0; i < m&& obstacleGrid[i][0]==0; ++i)  dp[i][0]=1;
        for (int i = 0; i < n&& obstacleGrid[0][i]==0; ++i)  dp[0][i]=1;
        
        for (int i = 1; i <m; ++i) {
            for (int j = 1; j < n; ++j) {
                if(obstacleGrid[i][j]==1) dp[i][j]=0;
                else dp[i][j]=dp[i-1][j]+dp[i][j-1];
            }
        }
        return dp[m-1][n-1];
    }
};


背包问题模板:

void test_2_wei_bag_problem1() {
    vector<int> weight = {1, 3, 4};
    vector<int> value = {15, 20, 30};
    int bagWeight = 4;

    // 二维数组
    vector<vector<int>> dp(weight.size() + 1, vector<int>(bagWeight + 1, 0));

    // 初始化
    for (int j = weight[0]; j <= bagWeight; j++) {
        dp[0][j] = value[0];
    }

    // weight数组的大小 就是物品个数
    for(int i = 1; i < weight.size(); i++) { // 遍历物品
        for(int j = 0; j <= bagWeight; j++) { // 遍历背包容量
            if (j < weight[i]) dp[i][j] = dp[i - 1][j];
            else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);

        }
    }

    cout << dp[weight.size() - 1][bagWeight] << endl;
}

int main() {
    test_2_wei_bag_problem1();
}

********************************************************************************************

//一维更优且简介,复杂度也降低
void test_1_wei_bag_problem() {
    vector<int> weight = {1, 3, 4};
    vector<int> value = {15, 20, 30};
    int bagWeight = 4;

    // 初始化
    vector<int> dp(bagWeight + 1, 0);
    for(int i = 0; i < weight.size(); i++) { // 遍历物品
        for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量,要逆序来,防止前面数组被覆盖
            dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
        }
    }
    cout << dp[bagWeight] << endl;
}

int main() {
    test_1_wei_bag_problem();
}

例题

416. 分割等和子集
class Solution {
public:
    bool canPartition(vector<int>& nums) {
        vector<int> dp(10001,0);//题目限制 最大的一半

        int sum=0;
        for (int i = 0; i < nums.size(); ++i) {
            sum+=nums[i];
        }
        if(sum&1) return false;//奇数直接return false

        for (int i = 0; i < nums.size(); ++i) {//遍历物品(weight)
            for (int j = sum/2; j >=nums[i] ; --j) {//遍历背包大小(bagsize)遍历方向从后向前
                dp[j]=max(dp[j],dp[j-nums[i]]+nums[i]);//weight[i] 和value[i]都是num[i]
            }
        }
        if(dp[sum/2]==sum/2) return true;
        return false;
    }
}


1049. 最后一块石头的重量 II
/*
本题其实就是尽量让石头分成重量相同的两堆,相撞之后剩下的石头最小,这样就化解成01背包问题了。
本题物品的weight[i]为store[i],value[i]也为store[i]。
*/

class Solution {
public:
    int lastStoneWeightII(vector<int>& stones) {
        vector<int> dp(15001, 0);
        int sum = 0;
        for (int i = 0; i < stones.size(); i++) sum += stones[i];
        int target = sum / 2;
        for (int i = 0; i < stones.size(); i++) { // 遍历物品
            for (int j = target; j >= stones[i]; j--) { // 遍历背包
                dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);
            }
        }
        return sum - dp[target] - dp[target];
    }
};
int count;
    public int reversePairs(int[] nums) {
        this.count = 0;
        merge(nums, 0, nums.length - 1);
        return count;
    }

    public void merge(int[] nums, int left, int right) {
        int mid = left + ((right - left) >> 1);
        if (left < right) {
            merge(nums, left, mid);
            merge(nums, mid + 1, right);
            mergeSort(nums, left, mid, right);
        }
    }

    public void mergeSort(int[] nums, int left, int mid, int right) {
        int[] temparr = new int[right - left + 1];
        int index = 0;
        int temp1 = left, temp2 = mid + 1;

        while (temp1 <= mid && temp2 <= right) {
            if (nums[temp1] <= nums[temp2]) {
                temparr[index++] = nums[temp1++];
            } else {
                //用来统计逆序对的个数
                count += (mid - temp1 + 1);
                temparr[index++] = nums[temp2++];
            }
        }
        //把左边剩余的数移入数组
        while (temp1 <= mid) {
            temparr[index++] = nums[temp1++];
        }
        //把右边剩余的数移入数组
        while (temp2 <= right) {
            temparr[index++] = nums[temp2++];
        }
        //把新数组中的数覆盖nums数组
        for (int k = 0; k < temparr.length; k++) {
            nums[k + left] = temparr[k];
        }
    }

分类解题模板
背包问题大体的解题模板是两层循环,分别遍历物品nums和背包容量target,然后写转移方程,
根据背包的分类我们确定物品和容量遍历的先后顺序,根据问题的分类我们确定状态转移方程的写法

首先是背包分类的模板:
1、0/1背包:外循环nums,内循环target,target倒序且target>=nums[i];
2、完全背包:外循环nums,内循环target,target正序且target>=nums[i];
3、组合背包:外循环target,内循环nums,target正序且target>=nums[i];
4、分组背包:这个比较特殊,需要三重循环:外循环背包bags,内部两层循环根据题目的要求转化为1,2,3三种背包类型的模板

然后是问题分类的模板:
1、最值问题: dp[i] = max/min(dp[i], dp[i-nums]+1)或dp[i] = max/min(dp[i], dp[i-num]+nums);
2、存在问题(bool):dp[i]=dp[i]||dp[i-num];
3、组合问题:dp[i]+=dp[i-num];

动态规划应该如何debug

找问题的最好方式就是把dp数组打印出来,看看究竟是不是按照自己思路推导的!

打家劫舍

未完待续…

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

moletop

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

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

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

打赏作者

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

抵扣说明:

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

余额充值