dp问题总结

动态规划

dp的状态的选取原则为原问题和子问题中会变化的量

凑零钱问题,

最少需要几枚硬币凑成指定amount,硬币数量无限。
这种问题的状态只有amount,可选择的硬币并不是状态,因为数量是无限的,原问题和子问题中的状态不互相影响。
地址
在这里插入图片描述

int coinChange(vector<int>& coins, int amount) {
    // 数组大小为 amount + 1,初始值也为 amount + 1
    vector<int> dp(amount + 1, amount + 1);
    // base case
    dp[0] = 0;
    // 外层 for 循环在遍历所有状态的所有取值
    for (int i = 0; i < dp.size(); i++) {
        // 内层 for 循环在求所有选择的最小值
        for (int coin : coins) {
            // 子问题无解,跳过
            if (i - coin < 0) continue;
            dp[i] = min(dp[i], 1 + dp[i - coin]);
        }
    }
    return (dp[amount] == amount + 1) ? -1 : dp[amount];
}

因为每一种硬币的数量都是无限的,可以看到硬币并不属于一种状态,每个amount状态都会遍历所有的coins,而不是amount=8的时候选择了2元硬币,子问题amount=6的时候就不能选择2元硬币了

这算是完全背包问题??
链接

目标和问题链接

在这里插入图片描述
典型的01背包问题,可选择的物品是变化的(当前选了,那就不能找之前也选过这个物品的子问题了),因此也是一个状态,两个状态就用dp[][],上面凑零钱是一个状态,因此用一维dp[]就行

/* 计算 nums 中有几个子集的和为 sum */
int subsets(int[] nums, int sum) {
    int n = nums.length;
    int[][] dp = new int[n + 1][sum + 1];
    // base case
    for (int i = 0; i <= n; i++) {
        dp[i][0] = 1;
    }
    
    for (int i = 1; i <= n; i++) {
        for (int j = 0; j <= sum; j++) {
            if (j >= nums[i-1]) {
                // 两种选择的结果之和
                dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i-1]];
            } else {
                // 背包的空间不足,只能选择不装物品 i
                dp[i][j] = dp[i-1][j];
            }
        }
    }
    return dp[n][sum];
}

最优子结构

(这里有个比较容易混的概念,最优子结构,子问题之间互不影响,与我刚才说的原问题和子问题不一样,原问题和子问题状态互不互相影响和子问题之间影响不是一个概念)。

状态说是相互影响不恰当,应该说是变化的。没有互相影响

状态互相影响是指这么一种情况,01背包问题,供选择的物品只有一件,那当前问题(原问题)中选择了电脑,子问题中就不能找选过电脑的备忘录。但这并不表示两个问题之间相互影响。因为原问题和子问题是独立的,并不是子问题获取最有解后会影响原问题获取最优解
在这里插入图片描述

零钱兑换找组合数这种,也是动态规划为题,状态只有amount,但这里for循环内外层就需要考虑一下了,如果for(int coin : coins)放在内层,这样会出现重复的情况,算出来的组合数大于实际的数量,因为在内层的话算出来的是排列数,比如有1,2,5三种硬币,amount=3,此时dp[3] = dp[2] + dp[1],这个时候dp[2]是包含两种组合情况的,{1, 1}和{2},dp[1]是一种组合{1},当选择硬币1时,也就是dp[2]时,那dp[3]的组合情况是{1, 1, 1}和{2, 1},当选择硬币2时,也就是dp[1]时,dp[3]的组合是{1, 2},和之前的{2,1}是重复的,所以for(int coin : coins)放在内层(也就是01背包问题的模板,横向是容量,纵向是选择这种想法,其实不一样,因为这里只有一个状态amount,硬币的选择并不是状态,而01背包问题物品也是状态,因此这里只是两层for循环写法类似,实际问题不同)算出来的是排列数,不是组合数。
在这里插入图片描述

一个比较好的总结

链接🔗
在这里插入图片描述

这里面将两种凑零钱问题都归结为完全背包问题,
由此可以明白,完全背包问题和01背包的区别在于每种物品有无数件,那也和之前分析的状态吻合了,每种物品有无数件,那原问题取这件物品,他的子问题也可以取这件,就不要拿那个01背包的表来思考了,直接就看一个amount一维数组(下图和本题无关,仅供参考),当成一个一维dp来想,遍历amount,dp也只是一维dp[],注意,这种问题原本就是一维,而01背包原来是二维问题,因为有两个状态,但是可以降维成一维,和原本是一纬的背包问题(只有一个状态)不同

而背包问题的又有两种情况,根据问题来判断是需要组合数还是排列数,大部分问题都是组合数,因此物品的for循环遍历放在外层,如果是放在内层,求出来的是排列数,可能比实际数量大
**特例:**爬楼梯问题,是求排列数,因为如果三层,先1后2和先2后1是不一样的leetcode链接
在这里插入图片描述

在这里插入图片描述
上图链接

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值