动态规划1.3--背包问题之搞特殊

1、不可颠倒的内外循环

(1)外循环为物品

对于纯完全背包问题,其for循环的先后循环是可以颠倒的!
如果问装满背包有几种方式的话?那么两个for循环的先后顺序就有很大区别了。

518.零钱兑换II
给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。

示例 1:
输入: amount = 5, coins = [1, 2, 5]
输出: 4
解释: 有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1

注意,你可以假设:
0 <= amount (总金额) <= 5000
1 <= coin (硬币面额) <= 5000
硬币种类不超过 500 种
结果符合 32 位符号整数

一看到钱币数量不限,就知道这是一个完全背包。
但本题和纯完全背包不一样,纯完全背包是能否凑成总金额,而本题是要求凑成总金额的个数!组合不强调元素之间的顺序,排列强调元素之间的顺序。
以上文的示例1为例,利用动态规划五部曲分析:
(1)确定dp数组以及下标含义
dp[j]表示总金额为 j 时,凑成的硬币组合数。
(2)确定状态转移方程
总金额为 j 时,就等于总金额为j-coins[i]时加一个coins[i]硬币,所以组合数
(3)dp数组的初始化
初始化dp={0};
总金额为0 时,组合只有一种,就是不选任何硬币,所以dp[0]=1;

(4)确定遍历的顺序
本题中我们是外层for循环遍历物品(钱币),内层for遍历背包(金钱总额),还是外层for遍历背包(金钱总额),内层for循环遍历物品(钱币)呢?
首先,dp[j]表示的是一个组合数,组合数内硬币顺序任意,当先遍历硬币,再遍历总金额时:

外层遍历物品(硬币)

for (int i = 0; i < coins.size(); i++) { // 遍历物品
    for (int j = coins[i]; j <= amount; j++) { // 遍历背包容量
        dp[j] += dp[j - coins[i]];
    }
}

对于每个硬币,都分别找出在不同的总金额背包时,该背包的组合数
如果把两个for交换顺序:

外层遍历背包(总金额)

代码如下:

for (int j = 0; j <= amount; j++) { // 遍历背包容量
    for (int i = 0; i < coins.size(); i++) { // 遍历物品
        if (j - coins[i] >= 0) dp[j] += dp[j - coins[i]];
    }
}

对于每个总金额,都按照硬币顺序,分别找出其排列数
当我们用以为滚动数组时,很难发现其中的区别,当我们用二维的数组,就很容易发现,外层循环物品,内层循环背包时,用的是本行本列的前一个值求和;而外层循环背包,内层循环物品时,用的是本列的前一个值,和最后一行前一个值求和。
(5)举例检验

(2)外循环为背包

139. 单词拆分
给定一个非空字符串 s 和一个包含非空单词的列表 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。
说明:
拆分时可以重复使用字典中的单词。
你可以假设字典中没有重复的单词。

示例 1:
输入: s = "leetcode", wordDict = ["leet", "code"]
输出: true
解释: 返回 true 因为 "leetcode" 可以被拆分成 "leet code"。

dict 中的单词没有使用次数的限制,因此这是一个完全背包问题。

该问题涉及到字典中单词的使用顺序,也就是说物品必须按一定顺序放入背包中,例如下面的 dict 就不够组成字符串 "leetcode":
["lee", "tc", "cod"]
求解顺序的完全背包问题时,对物品的迭代应该放在最里层,对背包的迭代放在外层,只有这样才能让物品按一定顺序放入背包中。(组合:元素排列有顺序

    bool wordBreak(string s, vector& wordDict) {
        int l=s.size(),n=wordDict.size();
        vectordp(l+1,false);
        dp[0]=true;
        for(int j=1;j<=l;j++){
            for(int i=0;i=0&&s.substr(j - sz, sz) == wordDict[i])
                    dp[j]=dp[j]||dp[j-sz];
            }
        }
        return dp[l];
    }

2、是否恰好装满

1449. 数位成本和为目标值的最大数字
给你一个整数数组 cost 和一个整数 target 。请你返回满足如下规则可以得到的 最大 整数:
给当前结果添加一个数位(i + 1)的成本为 cost[i] (cost 数组下标从 0 开始)。
总成本必须恰好等于 target 。
添加的数位中没有数字 0 。
由于答案可能会很大,请你以字符串形式返回。
如果按照上述要求无法得到任何整数,请你返回 "0" 。

示例 1:
输入:cost = [4,3,2,5,6,7,2,5,5], target = 9
输出:"7772"
解释:添加数位 '7' 的成本为 2 ,添加数位 '2' 的成本为 3 。所以 "7772" 的代价为 23+ 31 = 9 。 "977" 也是满足要求的数字,但 "7772" 是较大的数字。
数字 成本
1 -> 4
2 -> 3
3 -> 2
4 -> 5
5 -> 6
6 -> 7
7 -> 2
8 -> 5
9 -> 5

这道题是一个很明显的完全背包问题,与纯完全背包的主要区别在于: 总成本必须恰好等于target 。这种题目也是有模板的!

恰好装满 VS 可以不装满
题目有两种可能,一种是要求背包恰好装满,一种是可以不装满(只要不超过容量就行)。而本题是要求恰好装满的。而这两种情况仅仅影响我们dp数组初始化

  • 恰好装满。只需要初始化dp[0] 为 0, 其他初始化为负数即可。
  • 可以不装满。 只需要全部初始化为 0,即可。

原因很简单,我多次强调过dp数组本质上是记录了一个个子问题。 dp[0]是一个子问题,dp[1]是一个子问题。。。
有了上面的知识就不难理解了。 初始化的时候,我们还没有进行任何选择,那么也就是说dp[0] = 0,因为我们可以通过什么都不选达到最大值0。而dp[1],dp[2]...则在当前什么都不选的情况下无法达成,也就是无解,因为为了区分,我们可以用负数来表示,当然你可以用任何可以区分的东西表示,比如 None。

3、“隐秘”的背包

有些问题乍一看和背包问题没关系,但是只要们善于思考,就可以转化为背包问题!

494. 目标和
给定一个非负整数数组,a1, a2, ..., an, 和一个目标数,S。现在你有两个符号 + 和 -。对于数组中的任意一个整数,你都可以从 + 或 -中选择一个符号添加在前面。
返回可以使最终数组和为目标数 S 的所有添加符号的方法数。

示例:
输入:nums: [1, 1, 1, 1, 1], S: 3
输出:5
解释:
-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
一共有5种方法让最终目标和为3

提示:
数组非空,且长度不会超过 20 。
初始的数组的和不会超过 1000 。
保证返回的最终结果能被 32 位整数存下。

每个元素只有一个,目标数S,方法数。这个我们可以联想到01背包、恰好装满、组合数。那么,我们使用动态规划五部曲来分析一下:
对于整数数组的每个数,我们有两种添加方法:+nums[i],-nums[i]。这样是没办法进行遍历的.
所以假设加法总和为x,减法的绝对值总和为y,sum表示nums数组之和。则有:;
所以
(1)确定dp数组以及下标含义
dp[j]表示加法和为 j 时,添加符号的方法数。
(2)确定状态转移方程
加法和为 j 时,就等于加法为j-coins[i]时加一个coins[i]硬币,所以组合数

(3)dp数组的初始化
x=(S+sum)/2
当(S+sum)%2==1时,表示通过加减运算得不到目标值S,返回0;
初始化dp[]={0}, size=(S+sum)/2+1;
dp[0]表示加法和为0,即全是减法,有一种方法:;
(4)确定遍历的顺序

  • 0-1背包
    背包循环(加法总和[(S+sum)/2,nums[i]])为降序循环。
  • 组合数
    外层为物品循环(nums),内层为背包循环。
    (5)举例检验
        for(int i=0;i=nums[i];j--)
            {
                dp[j]+=dp[j-nums[i]];
            }
        }
        return dp[(S+sum)/2];

你可能感兴趣的:(动态规划1.3--背包问题之搞特殊)