【LeetCode专题】动态规划-1

本专题为动态规划算法的应用


0-1背包问题

0-1背包问题模板详情可见我的个人总结【动态规划专题】


1. 状态定义
0-1背包问题的二维状态定义**

  • f[i][j] : 只选前i个物品, 总体积 <= j 的 Max

2. 状态分析

(1) 当前背包容量不够时(j < v[i]),选不了第 i 个物品,因此前i个物品的最优解和前 i - 1一样

  • f[i][j] = f[i - 1][j]

(2) 当前背包容量够第 i 个时(j ≥ v[i]),可选第 i 个物品,因此需要继续决策 选 or 不选 第i个物品

  • 选:f[i][j] = f[i - 1][j - v[i] + w[i]
  • 不选:f[i][j] = f[i - 1][j]
  • 最后取Max

3. 状态转移方程:
f[i][j] = max(f[i-1][j], f[i-1][j-v[i]] + w[i]
4. 优化 :0-1背包问题的一维状态

  • 在分析二维状态时,第i轮的状态只和第i - 1轮有关,因此可以去掉数组第一维
  • [注] : 每轮遍历 j 的过程中, i 是不变的(都是第 i 轮的状态),
    但是当前第i轮的状态需要第 i - 1轮的状态, 所以需要逆序枚举

一些题目需要转为0-1背包问题去求解

LeetCode 494. 目标和

题目 示例:

输入:nums = [1,1,1,1,1], target = 3
输出:5
解释:一共有 5 种方法让最终目标和为 3 。
-1 + 1 + 1 + 1 + 1 = 3
+1 - 1 + 1 + 1 + 1 = 3
+1 + 1 - 1 + 1 + 1 = 3
+1 + 1 + 1 - 1 + 1 = 3
+1 + 1 + 1 + 1 - 1 = 3

思路
题意要添加正/负号,使得数组和为target,所以设所有正数的和为s,数组所有数的和为sum_n,则所有负数的和为sum_n-s
现在需要求出s - (sum_s -s) = target的所有方案数
只需求出 s = (sum_n + t)/2的所有方案数
s = (sum_n + t)/2为新的target
-----> 问题转换为从nums中选择一些数字, 使这些数字的和 s = target 所有的方案数(0-1背包问题)
其中num[i]表示物品的重量,target表示背包的容量

代码

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) { 
        // +的和 : s 
        // -的和 : sum_n - s
        // s - (sum_n-s) = t
        // 2s - sum_n = t
        // s = (sum_n + t) / 2
        // <==> 问题转换为从nums中选择一些数字, 使这些数字的和 = (sum_n + t)/2, 所有的方案数
        target += accumulate(nums.begin(), nums.end(), 0);
        if (target < 0 || target % 2) return 0;
        target /= 2;
        int n = nums.size();
        int f[n+1][target+1];
        // f[i][j] 代表考虑前 i 个数,当前计算结果为 j 的方案数
        // f[0][0] 代表不考虑任何数,凑出计算结果为0的方案数为1种
        memset(f, 0, sizeof(f));
        f[0][0] = 1;
        for(int i = 0; i < n; i++){
            for(int c = 0; c <= target; c++) {
                if(c < nums[i]) f[i+1][c] = f[i][c]; // 不够选
                else f[i+1][c] = f[i][c] + f[i][c-nums[i]]; // 不选 + 选
            }
        }
        return f[n][target];
    }
};

空间优化方案一:一维DP数组+逆序枚举

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) { 
        target += accumulate(nums.begin(), nums.end(), 0);
        if (target < 0 || target % 2) return 0;
        target /= 2;
        int n = nums.size();
        int f[target+1];
        memset(f, 0, sizeof(f));
        f[0] = 1;
        for(int i = 0; i < n; i++){
            for(int c = target; c >= 0; c--) {
                if(c >= nums[i]) f[c] = f[c] + f[c-nums[i]]; // 不选 + 选
            }
        }
        return f[target];
    }
};

空间优化方案二:利用求mod,滚动数组

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        target += accumulate(nums.begin(), nums.end(), 0);
        if (target < 0 || target % 2) return 0;
        target /= 2;
        int n = nums.size();
        int f[2][target+1];
        // 空间优化方法二:只用2个滚动数组,进行状态转换
        memset(f, 0, sizeof(f));
        f[0][0] = 1;
        for(int i = 0; i < n; i++){
            for(int c = 0; c <= target; c++) {
                if(c < nums[i]) f[(i+1)%2][c] = f[i%2][c]; // 不够选
                else f[(i+1)%2][c] = f[i%2][c] + f[i%2][c-nums[i]]; // 不选 + 选
            }
        }
        return f[n%2][target];
    }
};

LeetCode 416. 分割等和子集

题目 示例:

输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 。

思路
nums中选一些数,使得这些数的总和sum = target(target = sum_n / 2)
问题转换为0-1背包问题
dp[i][j]表示从前i个数中选,是否存在【所选数的总和s=j的方案】

  • 如果存在dp[i][j] = 1
  • 如果不存在dp[i][j] = 0

代码

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int sum = accumulate(nums.begin(), nums.end(), 0);
        if(sum % 2 == 1) return false;
        int target = sum / 2, n = nums.size();
        bool dp[2][target + 1];
        memset(dp, 0, sizeof(dp));
        dp[0][0] = 1;
        for(int i = 0; i < n; i++){
            for(int j = 0; j <= target; j++){
                if(nums[i] > j) dp[(i+1)%2][j] = dp[i%2][j] > 0;
                else dp[(i+1)%2][j] = dp[i%2][j] + dp[i%2][j-nums[i]] > 0;
            }
        }
        // cout << dp[n][target];
        return dp[n%2][target];
    }
};

空间优化:方法同上题,此处略

LeetCode 1049. 最后一块石头的重量 II

题目 示例:
在这里插入图片描述

输入:stones = [2,7,4,1,8,1]
输出:1
解释:
组合 2 和 4,得到 2,所以数组转化为 [2,7,1,8,1],
组合 7 和 8,得到 1,所以数组转化为 [2,1,1,1],
组合 2 和 1,得到 1,所以数组转化为 [1,1,1],
组合 1 和 1,得到 0,所以数组转化为 [1],这就是最优值。

思路
stones中选两组数,记第一组数的总和为sum1,第二组总和为sum2
要使得最后剩下的一块石头【重量最小】,则需要使得 | sum1 - sum2 |最小

因此需要从stones中选一些数,使得这些数总和sum在不超过sum_n / 2时,得到的sum最大
问题转换为0-1背包问题
dp[i][j]表示从前i个数中选,所选数的总和s<=j的最大总和s
背包容量为sum_n / 2, 物品价值为sum

代码

class Solution {
public:
    int lastStoneWeightII(vector<int>& stones) {
        int n = stones.size();
        int sum = accumulate(stones.begin(), stones.end(), 0);
        int target = sum / 2;
        int dp[n+1][target+1]; // 从前n个数中选, 体积不超过sum的最大value(此题value=sum)
        memset(dp, 0, sizeof(dp));
        for(int i = 0; i < n; i++){
            for(int j = 0; j <= target; j++){
                if(stones[i] > j) dp[i+1][j] = dp[i][j]; // 体积超过 不选
                else{
                    dp[i+1][j] = max(dp[i][j], dp[i][j-stones[i]] + stones[i]);
                }
            }
        }
        // cout << dp[n][target];
        return sum - 2 * dp[n][target];
    }
};

完全背包问题

完全背包问题模板详情可见我的个人总结【动态规划专题】

图源灵神:
在这里插入图片描述

状态转移方程
f[i][j] = max(f[i - 1][j], f[i][j - v] + w)
与01背包状态方程的区别在于:第i个物品可以重复选

LeetCode 322. 零钱兑换

题目 示例:

输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1
【注意】ans为最少的硬币个数

代码:

class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        int n = coins.size();
        int dp[n+1][amount+1];
        memset(dp, 0x3f, sizeof(dp));
        dp[0][0] = 0;
        for(int i = 0; i < n; i++){
            for(int j = 0; j <= amount; j++){
                if(coins[i] > j){ // 选不了
                    dp[i+1][j] = dp[i][j];
                }else{
                    // 物品价值看作1
                    // dp[i+1][j]表示从前i个物品中选, 总金额不超过amount的最少硬币数量
                    dp[i+1][j] = min(dp[i][j], dp[i+1][j-coins[i]] +1);
                }
            }
        }
        if(dp[n][amount] < 0x3f3f) return dp[n][amount];
        return -1;
    }
};

空间优化1

class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        int n = coins.size();
        int dp[2][amount+1];
        memset(dp, 0x3f, sizeof(dp));
        dp[0][0] = 0;
        for(int i = 0; i < n; i++){
            for(int j = 0; j <= amount; j++){
                if(coins[i] > j){ // 选不了
                    dp[(i+1)%2][j] = dp[i%2][j];
                }else{
                    // 物品价值看作1
                    // dp[i+1][j]表示从前i个物品中选, 总金额不超过amount的最少硬币数量
                    dp[(i+1)%2][j] = min(dp[i%2][j], dp[(i+1)%2][j-coins[i]] +1);
                }
            }
        }
        if(dp[n%2][amount] < 0x3f3f) return dp[n%2][amount];
        return -1;
    }
};

空间优化2

class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        int n = coins.size();
        int dp[amount+1];
        memset(dp, 0x3f, sizeof(dp));
        dp[0] = 0;
        for(int i = 0; i < n; i++){
            for(int j = coins[i]; j <= amount; j++){
                // 物品价值看作1
                // dp[i+1][j]表示从前i个物品中选, 总金额不超过amount的最少硬币数量
                dp[j] = min(dp[j], dp[j-coins[i]] +1);
                
            }
        }
        if(dp[amount] < 0x3f3f) return dp[amount];
        return -1;
    }
};

LeetCode 518. 零钱兑换 II

题目 示例:

输入:amount = 5, coins = [1, 2, 5]
输出:4
解释:有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1
【注意】ans为选择的方案数cnt

代码

class Solution {
public:
    int change(int amount, vector<int>& coins) {
        int n = coins.size();
        int dp[n+1][amount+1]; // 从前i个物品中选, 金额=amount 的 数量
        memset(dp, 0, sizeof(dp));
        dp[0][0] = 1; // 从前0个选amount = 0数量定义为1
        for(int i = 0; i < n; i++){
            for(int j = 0; j <= amount; j++){
                if(coins[i] > j) dp[i+1][j] = dp[i][j]; // 不够选
                else dp[i+1][j] = dp[i][j] + dp[i+1][j - coins[i]];  // 不选第i个物品 + 选第i个物品
            }
        }
        return dp[n][amount];
    }
};

空间优化

class Solution {
public:
    int change(int amount, vector<int>& coins) {
        int n = coins.size();
        int dp[amount+1]; // 从前i个物品中选, 金额=amount 的 数量
        memset(dp, 0, sizeof(dp));
        dp[0] = 1; // 从前0个选amount = 0数量定义为1
        for(int i = 0; i < n; i++){
            for(int j = coins[i]; j <= amount; j++){
                dp[j] = dp[j] + dp[j - coins[i]];  // 不选第i个物品 + 选第i个物品
            }
        }
        return dp[amount];
    }
};

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值