【道德经·第二十八章】知其雄,守其雌,为天下溪。为天下溪,常德不离,复归于婴儿。知其白,守其黑,为天下式,为天下式,常德不忒,复归于无极。
本文参考资料来自此处
背包问题的解题模板
二维版本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
   | int knapsack(int W, int N, vector<int>& wt, vector<int>& val) {          vector<vector<int> > dp(N+1, vector<int>(W+1, 0));     for(int i = 1; i <= N; i++) {         for(int w = 1; w <= W; w++) {             if(w - wt[i-1] < 0) {                                  dp[i][w] = dp[i-1][w];             } else {                                  dp[i][w] = max(dp[i-1][w-wt[i-1]] + val[i-1]),                                dp[i-1][w]);             }         }     }
      return dp[N][W]; }
  | 
 
状态压缩:
1 2 3 4 5 6 7 8 9 10 11
   | int knapsack(int W, int N, vector<int>& wt, vector<int>& val) {          vector<int> dp(W+1, 0);     for(int i = 1; i <= N; i++) {         for(int j = W; j >= wt[i-1]; j--) {             dp[j] = max(dp[j], dp[j-wt[i-1]]+val[i-1]);         }     }
      return dp[W]; }
  | 
 
背包问题分类
常见的背包类型主要有以下几种:
-  0/1背包问题:每个元素最多选取一次
 
- 完全背包问题:每个元素可以重复选择
 
- 组合背包问题:背包中的物品要考虑顺序
 
- 分组背包问题:不止一个背包,需要遍历每个背包
 
而每个背包问题要求的也是不同的,按照所求问题分类,又可以分为以下几种:
- 最值问题:要求最大值/最小值
 
- 存在问题:是否存在…………,满足…………
 
- 组合问题:求所有满足……的排列组合
 
首先是背包分类的模板:
-  0/1背包:外循环nums,内循环target,target倒序且target>=nums[i];
 
- 完全背包:外循环nums,内循环target,target正序且target>=nums[i];
 
- 组合背包:外循环target,内循环nums,target正序且target>=nums[i];
 
- 分组背包:这个比较特殊,需要三重循环:外循环背包bags,内部两层循环根据题目的要求转化为1,2,3三种背包类型的模板
 
然后是问题分类的模板:
- 最值问题: dp[i] = max/min(dp[i], dp[i-nums]+1)或dp[i] = max/min(dp[i], dp[i-num]+nums);
 
- 存在问题(bool):dp[i]=dp[i]||dp[i-num];
 
- 组合问题:dp[i]+=dp[i-num];
 
从一堆石头中,每次拿两块重量分别为x,y的石头,若x=y,则两块石头均粉碎;若x<y,两块石头变为一块重量为y-x的石头求最后剩下石头的最小重量(若没有剩下返回0)
0/1背包最值问题:外循环stones,内循环target=sum/2倒序,应用转移方程1
1 2 3 4 5 6 7 8 9
   | int lastStoneWeightII(vector<int> &stones) {     int sum = accumulate(stones.begin(), stones.end(), 0);     int target = sum / 2;     vector<int> dp(target + 1);     for (int stone : stones)         for (int i = target; i >= stone; i--)             dp[i] = max(dp[i], dp[i - stone] + stone);     return (sum - dp[target]) - dp[target]; }
  | 
 
给定amount,求用任意数量不同面值的零钱换到amount所用的最少数量
完全背包最值问题:外循环coins,内循环amount正序,应用状态方程1
1 2 3 4 5 6 7 8 9 10 11 12 13
   | int coinChange(vector<int> &coins, int amount) {          vector<long long> dp(amount + 1, INT_MAX);           dp[0] = 0;       for (int coin : coins) {         for (int i = 0; i <= amount; i++) {             if (coin <= i)                 dp[i] = min(dp[i], dp[i - coin] + 1);         }     }     return dp[amount]==INT_MAX ? -1 : dp[amount]; }
  | 
 
分割等和子集:判断是否能将一个数组分割为两个子集,其和相等
0-1背包存在性问题:是否存在一个子集,其和为target=sum/2,外循环nums,内循环target倒序,应用状态方程2
1 2 3 4 5 6 7 8 9 10 11 12
   | bool canPartition(vector<int> &nums) {     int sum = accumulate(nums.begin(), nums.end(), 0);     if (sum % 2 == 1)           return false;     int target = sum / 2;      vector<int> dp(target + 1, 0);      dp[0] = true;        for (int num : nums)         for (int i = target; i >= num; i--)             dp[i] = dp[i] || dp[i - num];     return dp[target]; }
  | 
 
目标和:给数组里的每个数字添加正负号得到target
数组和sum,目标和s, 正数和x,负数和y,则x+y=sum,x-y=s,那么x=(s+sum)/2=target
0-1背包不考虑元素顺序的组合问题:选nums里的数得到target的种数,外循环nums,内循环target倒序,应用状态方程3
1 2 3 4 5 6 7 8 9 10 11 12
   | int findTargetSumWays(vector<int> &nums, int s) {     int sum = accumulate(nums.begin(), nums.end(), 0);     if ((sum + s) % 2 != 0 || sum < s)         return 0;     int target = (sum + s) / 2;     vector<int> dp(target + 1);     dp[0] = 1;     for (int num : nums)         for (int i = target; i >= num; i--)             dp[i] += dp[i - num];     return dp[target]; }
  | 
 
完全平方数:对于一个正整数n,找出若干个完全平方数使其和为n,返回完全平方数最少数量
完全背包的最值问题:完全平方数最小为1,最大为sqrt(n),故题目转换为在nums=[1,2…..sqrt(n)]中选任意数平方和为target=n。外循环nums,内循环target正序,应用转移方程1
1 2 3 4 5 6 7 8 9 10 11
   | int numSquares(int n) {     vector<int> dp(n + 1, INT_MAX);      dp[0] = 0;     for (int num = 1; num <= sqrt(n); num++) {         for (int i = 0; i <= n; i++) {             if (i >= num * num)                 dp[i] = min(dp[i], dp[i - num * num] + 1);         }     }     return dp[n]; }
  | 
 
组合总和IV:在nums中任选一些数,和为target
考虑顺序的组合问题:外循环target,内循环nums,应用状态方程3
1 2 3 4 5 6 7 8 9 10 11
   | int combinationSum4(vector<int> &nums, int target) {     vector<int> dp(target + 1);     dp[0] = 1;     for (int i = 1; i <= target; i++) {         for (int num : nums) {             if (num<=i && dp[i - num]<INT_MAX-dp[i])                  dp[i] += dp[i - num];         }     }     return dp[target]; }
  | 
 
爬楼梯问题
楼梯的阶数一共为target,一次可以走的步数为nums[i]。一共有多少种走法?
考虑顺序的组合问题:外循环target,内循环nums,应用状态方程3
1 2 3 4 5 6 7 8 9 10 11 12
   | public int combinationSum4(int[] nums, int target) {     int[] dp = new int[target + 1];     dp[0] = 1;     for(int i = 1; i <= target; i++){         for(int j = 0; j < nums.length; j++){             if(i>=nums[j] && dp[i - nums[j]]<INT_MAX-dp[i]){                 dp[i] += dp[i - nums[j]];             }         }     }     return dp[target]; }
  | 
 
零钱兑换2:任选硬币凑成指定金额,求组合总数
完全背包不考虑顺序的组合问题:外循环coins,内循环target正序,应用转移方程3
1 2 3 4 5 6 7 8 9
   | int change(int amount, vector<int> &coins) {     vector<int> dp(amount + 1);     dp[0] = 1;     for (int coin : coins)         for (int i = 1; i <= amount; i++)             if (i >= coin)                 dp[i] += dp[i - coin];     return dp[amount]; }
  | 
 
投掷骰子的方法数:d个骰子,每个有f个面(点数为1,2,…f),求骰子点数和为target的方法
分组0/1背包的组合问题:dp[i][j]表示投掷i个骰子点数和为j的方法数;三层循环:最外层为背包d,然后先遍历target后遍历点数f
应用二维拓展的转移方程3:dp[i][j]+=dp[i-1][j-f];
1 2 3 4 5 6 7 8 9
   | int numRollsToTarget(int d, int f, int target) {     vector<vector<int>> dp(d + 1, vector<int>(target + 1, 0));     dp[0][0] = 1;     for (int i = 1; i <= d; i++)         for (int j = 1; j <= target; j++)             for (int k = 1; k <= f && j >= k; k++)                 dp[i][j] += dp[i - 1][j - k];     return dp[d][target]; }
  | 
 
位我上者,灿烂星空;道德律令,在我心中。