代码随想录——动态规划之背包问题(超详细)

01背包问题

问题描述
给定一组物品,每种物品有一个重量 weight[i] 和一个价值 value[i]。有一个容量为 capacity的背包,要求选择一些物品放入背包中,使得背包中物品的总重量不超过 capacity,且总价值最大。每种物品只能选择一次(要么选,要么不选)。

二维 dp 数组解法

1. dp 数组的含义

dp[i][j] 表示:i 个物品在容量为 j 的背包中能够获得的最大价值

2. dp 数组的初始化
  • dp[i][0] = 0(背包容量为 0 时,无论选多少物品,价值都为 0)。
  • dp[0][j] = 0(没有物品时,价值为 0)。
3. 递推公式

对于每个物品 i,两种情况:

  1. 不选 i 号物品dp[i][j] = dp[i-1][j]

  2. i 号物品(前提是 j >= weight[i]

    d p [ i ] [ j ] = m a x ⁡ ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − w e i g h t [ i ] ] + v a l u e [ i ] ) dp[i][j]=max⁡(dp[i−1][j],dp[i−1][j−weight[i]]+value[i]) dp[i][j]=max(dp[i1][j],dp[i1][jweight[i]]+value[i])

4. 遍历顺序

先遍历物品、先遍历背包容量都可以

for (int i = 1; i <= n; i++) {  // 遍历物品
    for (int j = 0; j <= W; j++) {  // 遍历背包容量(正序遍历)
        if (j >= weight[i])
            dp[i][j] = max(dp[i-1][j], dp[i-1][j - weight[i]] + value[i]);
        else
            dp[i][j] = dp[i-1][j]; // 不能选物品 i,则价值不变
    }
}

一维 dp 数组优化

1. dp 数组的含义

dp[j] 表示:容量为 j 的背包能获得的最大价值(在遍历到某个物品 i 时的状态)。

2. dp 数组的初始化
  • dp[0] = 0(背包容量为 0 时,价值为 0)。
  • 其余 dp[j] 也初始化为 0(表示初始状态下没有选择任何物品)。
3. 递推公式

d p [ j ] = m a x ⁡ ( d p [ j ] , d p [ j − w e i g h t [ i ] ] + v a l u e [ i ] ) dp[j]=max⁡(dp[j],dp[j−weight[i]]+value[i]) dp[j]=max(dp[j],dp[jweight[i]]+value[i])

为什么可以省略 dp[i-1][j]

  • 因为 dp[j] 依赖于 dp[j - weight[i]],若从小到大遍历 j,会覆盖 dp[j - weight[i]] 的旧值。
  • 所以 j 必须倒序遍历,保证 dp[j - weight[i]] 在本轮计算前仍然存储的是上一轮 i-1 时的值。
4. 遍历顺序(注意倒序遍历容量)
for (int i = 0; i < n; i++) {  // 遍历物品
    for (int j = W; j >= weight[i]; j--) {  // 遍历背包容量(倒序)
        dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
    }
}

三、总结

二维 dp 数组一维 dp 数组(空间优化)
dp 含义dp[i][j]:前 i 个物品、背包容量 j 时的最大价值dp[j]:容量 j 时的最大价值
初始化dp[i][0] = 0dp[0][j] = 0dp[0] = 0,其余 dp[j] = 0
递推公式dp[i][j] = max(dp[i-1][j], dp[i-1][j - weight[i]] + value[i])dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
遍历顺序物品在外层,容量 j 正序遍历物品在外层,容量 j 倒序遍历
空间复杂度O(nW)O(W)

416.分割等和子集

416. 分割等和子集

思路:01背包问题

给定一个数组 nums,判断是否能将其拆分为两个和相等的子集。等价于在 nums 中找到一个子集,其总和为 sum / 2

方法一:基于最大价值的 DP

  1. 定义 dp[j]:表示容量 j 的背包能获得的最大价值

  2. 目标:如果 dp[target] == target,说明可以凑成 sum/2,即可以拆分成功。

  3. 递推公式
    d p [ j ] = max ⁡ ( d p [ j ] , d p [ j − n u m s [ i ] ] + n u m s [ i ] ) dp[j] = \max(dp[j], dp[j - nums[i]] + nums[i]) dp[j]=max(dp[j],dp[jnums[i]]+nums[i])

    • 不选 nums[i]dp[j] 维持不变。
    • nums[i]dp[j] 变成 dp[j - nums[i]] + nums[i]
  4. 遍历顺序

    • 外层 遍历物品 i
    • 内层 倒序 遍历 j,防止覆盖 dp[j - nums[i]]
代码一(一维dp)
#include <stdbool.h>
#include <string.h>

bool canPartition(int* nums, int numsSize) {
    int sum = 0;
    for (int i = 0; i < numsSize; i++) sum += nums[i];

    if (sum % 2 != 0) return false;
    int target = sum / 2;

    int dp[target + 1];
    memset(dp, 0, sizeof(dp));  // 初始化为 0

    for (int i = 0; i < numsSize; i++) {  // 遍历物品
        for (int j = target; j >= nums[i]; j--) {  // **倒序遍历**
            dp[j] = fmax(dp[j], dp[j - nums[i]] + nums[i]);
        }
    }

    return dp[target] == target;
}
代码二(二维dp)
#include <stdbool.h>
#include <string.h>
#include <stdio.h>

bool canPartition(int* nums, int numsSize) {
    int sum = 0;
    for (int i = 0; i < numsSize; i++) sum += nums[i];

    if (sum % 2 != 0) return false;  // 如果总和是奇数,无法拆分
    int target = sum / 2;

    int dp[numsSize + 1][target + 1];
    memset(dp, 0, sizeof(dp));  // 初始化所有值为 0

    for (int i = 1; i <= numsSize; i++) {
        for (int j = 0; j <= target; j++) {
            if (j >= nums[i - 1]) {
                dp[i][j] = fmax(dp[i-1][j], dp[i-1][j - nums[i - 1]] + nums[i - 1]);
            } else {
                dp[i][j] = dp[i-1][j];
            }
        }
    }

    return dp[numsSize][target] == target;
}

方法二:基于可行性的布尔 DP

  1. 定义 dp[j]:表示是否存在一个子集,其和恰好为 j

  2. 目标:如果 dp[target] == true,说明可以拆分成功。

  3. 递推公式:
    d p [ j ] = d p [ j ] ∨ d p [ j − n u m s [ i ] ] dp[j]=dp[j]∨dp[j−nums[i]] dp[j]=dp[j]dp[jnums[i]]

    • 不选 nums[i]dp[j] 维持不变。
    • nums[i](前提 j >= nums[i]):dp[j] = dp[j] || dp[j - nums[i]]
  4. 遍历顺序:

    • 外层 遍历物品 i
    • 内层 倒序 遍历 j,防止覆盖 dp[j - nums[i]]
代码一(一维dp)
#include <stdbool.h>
#include <string.h>

bool canPartition(int* nums, int numsSize) {
    int sum = 0;
    for (int i = 0; i < numsSize; i++) sum += nums[i];

    if (sum % 2 != 0) return false;
    int target = sum / 2;

    bool dp[target + 1];
    memset(dp, false, sizeof(dp));  
    dp[0] = true;  // 和为 0 时一定可行

    for (int i = 0; i < numsSize; i++) {  
        for (int j = target; j >= nums[i]; j--) {  // **倒序遍历**
            dp[j] = dp[j] || dp[j - nums[i]];
        }
    }

    return dp[target];
}

代码二(二维dp)
#include <stdbool.h>
#include <string.h>
#include <stdio.h>

bool canPartition(int* nums, int numsSize) {
    int sum = 0;
    for (int i = 0; i < numsSize; i++) sum += nums[i];

    if (sum % 2 != 0) return false;  // 总和是奇数,不能拆分
    int target = sum / 2;

    bool dp[numsSize + 1][target + 1];
    memset(dp, false, sizeof(dp));  // 初始化所有值为 false

    for (int i = 0; i <= numsSize; i++) dp[i][0] = true;  // 和为 0 时一定可行

    for (int i = 1; i <= numsSize; i++) {
        for (int j = 1; j <= target; j++) {
            if (j >= nums[i - 1]) {
                dp[i][j] = dp[i-1][j] || dp[i-1][j - nums[i - 1]];
            } else {
                dp[i][j] = dp[i-1][j];
            }
        }
    }

    return dp[numsSize][target];
}

1049.最后一块石头的重量Ⅱ

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

思路:01背包问题

1. DP 数组含义

  • dp[j] 表示容量为 j 的背包能装的最大重量。
  • 在这个问题中,石头的重量既是物品的重量,也是物品的价值。
  • 我们需要找到一个子集,使得它们的总重量尽可能接近 target = sum / 2

2. DP 数组初始化

  • dp[0] = 0:表示背包容量为 0 时,能装的最大重量为 0。
  • 其他 dp[j] 初始化为 0,表示初始状态下,背包容量为 j 时,能装的最大重量为 0。

3. DP 状态转移方程

  • 对于每个石头 stones[i],我们有两种选择:

    • 不选当前石头:dp[j] 保持不变。
    • 选当前石头:dp[j - stones[i]] + stones[i]
  • 状态转移方程为:

    dp[j] = fmax(dp[j], dp[j - stones[i]] + stones[i]);

4. DP 遍历顺序

  • 外层循环:遍历每个石头(物品),从 0stonesSize - 1
  • 内层循环:倒序遍历背包容量,从 targetstones[i]
    • 倒序遍历是为了确保每个石头只被使用一次(0-1 背包的特性)。
    • 如果正序遍历,会导致同一个石头被重复使用(完全背包的特性)。

5. 返回值

  • 最终的最小差值为 sum - 2 * dp[target]
    • dp[target] 是容量为 target 的背包能装的最大重量。
    • 另一堆石头的重量为 sum - dp[target]
    • 两堆石头的重量差为 (sum - dp[target]) - dp[target] = sum - 2 * dp[target]
代码
int lastStoneWeightII(int* stones, int stonesSize) {
    // 1. 计算所有石头的总重量
    int sum = 0;
    for (int i = 0; i < stonesSize; i++) {
        sum += stones[i];
    }

    // 2. 确定目标值 target
    // 将石头分成两堆,使得两堆的重量差最小
    // 目标是将其中一堆的重量尽可能接近 sum / 2
    int target = sum / 2;

    // 3. DP 数组定义
    // dp[j] 表示容量为 j 的背包能装的最大重量
    // 这里将石头的重量同时视为物品的重量和价值
    int dp[target + 1];

    // 4. DP 数组初始化
    // 初始化为 0,表示背包容量为 j 时,能装的最大重量为 0
    memset(dp, 0, sizeof(dp));

    // 5. 动态规划填充 DP 数组
    // 外层循环遍历每个石头(物品)
    for (int i = 0; i < stonesSize; i++) {
        // 内层循环倒序遍历背包容量
        // 从 target 到 stones[i],确保每个石头只被使用一次
        for (int j = target; j >= stones[i]; j--) {
            // 状态转移方程
            // dp[j] 表示不选当前石头时的最大重量
            // dp[j - stones[i]] + stones[i] 表示选当前石头时的最大重量
            // 取两者的最大值
            dp[j] = fmax(dp[j], dp[j - stones[i]] + stones[i]);
        }
    }

    // 6. 返回值
    // 最终的最小差值为总重量减去两堆石头的重量
    // 其中一堆的重量为 dp[target],另一堆的重量为 sum - dp[target]
    // 差值为 (sum - dp[target]) - dp[target] = sum - 2 * dp[target]
    return sum - 2 * dp[target];
}

494.目标和

494. 目标和

思路:01背包问题

1.DP 数组定义

  • dp[i][j] 表示前 i 个元素中,和为 j 的组合数。

2.DP 数组初始化

  • dp[0][0] = 1:表示前 0 个元素中,和为 0 的组合数为 1。
  • 其他 dp[i][j] 初始化为 0。

3.状态转移

  • 对于每个元素 nums[i],更新 dp[i][j]
    • 如果不选 nums[i],则 dp[i][j] = dp[i-1][j]
    • 如果选 nums[i],则 dp[i][j] += dp[i-1][j - nums[i]]

4.遍历顺序

  • 外层循环遍历元素(i 从 1 到 numsSize)。
  • 内层循环遍历目标和(j 从 0 到 newTarget)。
  • 对于每个元素 nums[i-1],更新 dp[i][j]
    • 如果不选 nums[i-1],则 dp[i][j] = dp[i-1][j]
    • 如果选 nums[i-1],则 dp[i][j] += dp[i-1][j - nums[i-1]]

5. 返回值

  • 返回 dp[numsSize][newTarget],表示前 numsSize 个元素中,和为 newTarget 的组合数。
代码一:二维dp
int findTargetSumWays(int* nums, int numsSize, int target) {
    // 1. 计算数组的总和
    int sum = 0;
    for (int i = 0; i < numsSize; i++) {
        sum += nums[i];
    }

    // 2. 边界条件
    // 如果 target 的绝对值大于 sum,直接返回 0
    if (abs(target) > sum) return 0;
    // 如果 (sum + target) 是奇数,直接返回 0
    if ((sum + target) & 1) return 0;

    // 3. 计算目标值
    int newTarget = (sum + target) / 2;

    // 4. DP 数组定义
    // dp[i][j] 表示前 i 个元素中,和为 j 的组合数
    int dp[numsSize + 1][newTarget + 1];

    // 5. DP 数组初始化
    for (int i = 0; i <= numsSize; i++) {
        for (int j = 0; j <= newTarget; j++) {
            dp[i][j] = 0;
        }
    }
    dp[0][0] = 1; // 前 0 个元素中,和为 0 的组合数为 1

    // 6. 动态规划填充 DP 数组
    for (int i = 1; i <= numsSize; i++) {
        for (int j = 0; j <= newTarget; j++) {
            dp[i][j] = dp[i - 1][j]; // 不选 nums[i-1]
            if (j >= nums[i - 1]) {
                dp[i][j] += dp[i - 1][j - nums[i - 1]]; // 选 nums[i-1]
            }
        }
    }

    // 7. 返回值
    return dp[numsSize][newTarget];
}
代码二:一维dp

DP 数组的含义dp[j] 表示 选取若干个数,使其和恰好为 j 的方案数

DP 数组初始化dp[0] = 1(表示选取空集的方案数为 1),其余 dp[j] = 0(初始时和不可能是 j)。

DP 状态转移方程dp[j] = dp[j] + dp[j - nums[i]],即 不选或选当前元素 两种方案的总和。

DP 遍历顺序先遍历 nums[i],再倒序遍历 j,确保每个元素只能选一次,符合 0-1 背包特性。

返回值dp[newTarget],即 选取若干个数,使其和为 newTarget 的方案数

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int findTargetSumWays(int* nums, int numsSize, int target) {
    // 1. 计算数组总和
    int sum = 0;
    for (int i = 0; i < numsSize; i++) {
        sum += nums[i];
    }

    // 2. 边界检查
    if (abs(target) > sum) return 0;   // target 不能超过 sum
    if ((sum + target) & 1) return 0;  // (sum + target) 必须为偶数

    // 3. 计算背包容量
    int newTarget = (sum + target) / 2;
    
    // 4. 定义 DP 数组,并初始化
    int dp[newTarget + 1];
    memset(dp, 0, sizeof(dp));
    dp[0] = 1;  // 选取空集时,和为 0 的方案数为 1

    // 5. 0-1 背包填充 DP 数组(倒序遍历)
    for (int i = 0; i < numsSize; i++) {
        for (int j = newTarget; j >= nums[i]; j--) {
            dp[j] += dp[j - nums[i]];
        }
    }

    // 6. 返回结果
    return dp[newTarget];
}

474.一和零

474. 一和零

思路:01背包问题

1. DP 数组含义

  • dp[i][j] 表示最多使用 i0j1 时,可以选择的字符串的最大数量。
  • 这是一个二维动态规划问题,i 表示 0 的数量限制,j 表示 1 的数量限制。

2. DP 数组初始化

  • dp[0][0] = 0:表示不使用任何 01 时,可以选择的最大字符串数量为 0。
  • 其他 dp[i][j] 初始化为 0,表示初始状态下,最多使用 i0j1 时,可以选择的最大字符串数量为 0。

3. DP 数组递推

  • 对于每个字符串 strs[k],其 0 的数量为 num0[k]1 的数量为 num1[k]

  • 更新 dp[i][j] 的状态转移方程为:

    d p [ i ] [ j ] = m a x ( d p [ i ] [ j ] , d p [ i − n u m 0 [ k ] ] [ j − n u m 1 [ k ] ] + 1 ) dp[i][j] = max(dp[i][j], dp[i - num0[k]][j - num1[k]] + 1) dp[i][j]=max(dp[i][j],dp[inum0[k]][jnum1[k]]+1)

    • 如果不选当前字符串,则 dp[i][j] 保持不变。
    • 如果选当前字符串,则需要减去当前字符串的 01 的数量,并加 1(表示选择当前字符串)。

4. DP 数组遍历

  • 外层循环:遍历每个字符串(物品),从 0strsSize - 1
  • 内层循环:倒序遍历 0 的数量(从 mnum0[k])。
  • 最内层循环:倒序遍历 1 的数量(从 nnum1[k])。
  • 倒序遍历确保每个字符串只被使用一次(0-1 背包的特性)。

5. 返回值

  • 返回 dp[m][n],表示最多使用 m0n1 时,可以选择的字符串的最大数量。
代码
int findMaxForm(char** strs, int strsSize, int m, int n) {
    // 1. 统计每个字符串中 0 和 1 的数量
    int num0[strsSize], num1[strsSize];
    memset(num0, 0, sizeof(num0)); // 初始化 num0 数组为 0
    memset(num1, 0, sizeof(num1)); // 初始化 num1 数组为 0

    // 遍历每个字符串,统计其中 0 和 1 的数量
    for (int i = 0; i < strsSize; i++) {
        for (int j = 0; strs[i][j] != '\0'; j++) {
            if (strs[i][j] == '0') num0[i]++; // 统计 0 的数量
            else num1[i]++;                   // 统计 1 的数量
        }
    }

    // 2. DP 数组定义
    // dp[i][j] 表示最多使用 i 个 0 和 j 个 1 时,可以选择的字符串的最大数量
    int dp[m + 1][n + 1];
    memset(dp, 0, sizeof(dp)); // 初始化 dp 数组为 0

    // 3. 动态规划填充 DP 数组
    // 外层循环遍历每个字符串(物品)
    for (int k = 0; k < strsSize; k++) {
        // 内层循环倒序遍历 0 的数量(从 m 到 num0[k])
        for (int i = m; i >= num0[k]; i--) {
            // 最内层循环倒序遍历 1 的数量(从 n 到 num1[k])
            for (int j = n; j >= num1[k]; j--) {
                // 状态转移方程
                // dp[i][j] 表示不选当前字符串时的最大数量
                // dp[i - num0[k]][j - num1[k]] + 1 表示选当前字符串时的最大数量
                // 取两者的最大值
                dp[i][j] = fmax(dp[i][j], dp[i - num0[k]][j - num1[k]] + 1);
            }
        }
    }

    // 4. 返回值
    // dp[m][n] 表示最多使用 m 个 0 和 n 个 1 时,可以选择的字符串的最大数量
    return dp[m][n];
}

完全背包问题(与01进行比较)

给定一组物品,每种物品有一个重量 weight[i]和一个价值 value[i]。有一个容量为 capacity的背包,要求选择一些物品放入背包中,使得背包中物品的总重量不超过 capacity,且总价值最大。每种物品可以选择多次(无限次)。

1. dp数组含义

  • 完全背包dp[j]表示容量为j的背包,能装下的最大价值。
  • 01背包dp[j]表示容量为j的背包,能装下的最大价值。

两者的dp数组含义相同,都是表示在给定容量下的最大价值。

2. dp数组初始化

  • 完全背包dp[0] = 0,表示背包容量为0时,最大价值为0。其他位置可以初始化为0,表示未选择任何物品时的价值。
  • 01背包dp[0] = 0,表示背包容量为0时,最大价值为0。其他位置可以初始化为0,表示未选择任何物品时的价值。

两者的初始化方式相同。

3. dp数组递推公式

  • 完全背包dp[j] = max(dp[j], dp[j - weight[i]] + value[i]),其中weight[i]value[i]分别表示第i种物品的重量和价值。
  • 01背包dp[j] = max(dp[j], dp[j - weight[i]] + value[i]),其中weight[i]value[i]分别表示第i种物品的重量和价值。

两者的递推公式相同,但完全背包问题允许重复选择物品,因此在遍历时有所不同。

4. dp数组遍历顺序

  • 完全背包
    • 外层循环:遍历物品,从第1个物品到第n个物品。
    • 内层循环:遍历背包容量,从weight[i]capacity从小到大遍历。
    • 从小到大遍历的原因是允许重复选择物品,因此在更新dp[j]时,dp[j - weight[i]]可能已经包含了当前物品的多次选择。
    • 也可以先遍历背包,后遍历物品
  • 01背包
    • 外层循环:遍历物品,从第1个物品到第n个物品。
    • 内层循环:遍历背包容量,从capacityweight[i]从大到小遍历。
    • 从大到小遍历的原因是每个物品只能选择一次,因此在更新dp[j]时,dp[j - weight[i]]不能包含当前物品的选择。

518.零钱兑换Ⅱ

518. 零钱兑换 II

思路:完全背包

1.问题转化

  • 将硬币视为物品,每种硬币可以无限次使用(完全背包问题)。
  • 我们需要找到所有组合方式,使得硬币的总和等于 amount

2. DP 数组定义

  • dp[j] 表示凑成金额 j 的组合数。

3. DP 数组初始化

  • dp[0] = 1:表示凑成金额 0 的组合数为 1(不选任何硬币)。
  • 其他 dp[j] 初始化为 0。

4. 动态规划填充 DP 数组

  • 外层循环遍历硬币(物品)。
  • 内层循环正序遍历金额(从 coins[i]amount)。
  • 对于每个硬币 coins[i],更新 dp[j]
    • dp[j] += dp[j - coins[i]]

5. 返回值

  • 返回 dp[amount],表示凑成金额 amount 的组合数。
代码

正常来说按照上面的思路写代码就可以通过了,但是测试用例中存在返回值为0,即无法凑齐的数据,但是在计算的中间值中存在大于int甚至long long的值。有下面两种方法:

1.在计算dp数组前先判断是否能凑齐

int change(int amount, int* coins, int coinsSize) {
    // 1. DP 数组定义
    // dp[j] 表示凑成金额 j 的组合数
    int dp[amount + 1];
    memset(dp, 0, sizeof(dp)); // 初始化为 0

    // valid[j] 表示是否可以凑成金额 j
    bool valid[amount + 1];
    memset(valid, false, sizeof(valid)); // 初始化为 false
    valid[0] = true; // 金额 0 可以被凑成

    // 2. 检查是否可以凑成目标金额
    for (int i = 0; i < coinsSize; i++) {
        for (int j = coins[i]; j <= amount; j++) {
            valid[j] = valid[j] | valid[j - coins[i]]; // 更新 valid 数组
        }
    }

    // 如果无法凑成目标金额,直接返回 0
    if (!valid[amount]) return 0;

    // 3. DP 数组初始化
    dp[0] = 1; // 凑成金额 0 的组合数为 1

    // 4. 动态规划填充 DP 数组
    for (int i = 0; i < coinsSize; i++) {
        for (int j = coins[i]; j <= amount; j++) {
            dp[j] += dp[j - coins[i]]; // 状态转移方程
        }
    }

    // 5. 返回值
    return dp[amount];
}

2.用unsigned long long。。。

int change(int amount, int* coins, int coinsSize) {
    // 1. DP 数组定义
    // dp[j] 表示凑成金额 j 的组合数
    unsigned long long dp[amount + 1];
    memset(dp, 0, sizeof(dp)); // 初始化为 0

    // 2. DP 数组初始化
    dp[0] = 1; // 凑成金额 0 的组合数为 1

    // 3. 动态规划填充 DP 数组
    for (int i = 0; i < coinsSize; i++) {
        for (int j = coins[i]; j <= amount; j++) {
            dp[j] += dp[j - coins[i]]; // 状态转移方程
        }
    }

    // 4. 返回值
    return dp[amount];
}

377.组合总和Ⅳ

377. 组合总和 Ⅳ

思路:完全背包

和上一题(518. 零钱兑换 II)思路类似,不同点在于遍历顺序不同,具体如下:

1. DP 数组定义

  • dp[i] 表示凑成目标值 i 的组合数。

2. DP 数组初始化

  • dp[0] = 1:表示凑成目标值 0 的组合数为 1(不选任何数字)。

3. 动态规划填充 DP 数组

  • 外层循环遍历目标值 i(从 1target)。
  • 内层循环遍历数组 nums
    • 如果 nums[j] <= i,则 dp[i] += dp[i - nums[j]]

4. 返回值

  • 返回 dp[target],表示凑成目标值 target 的组合数。
代码
int combinationSum4(int* nums, int numsSize, int target) {
    // 1. DP 数组定义
    // dp[i] 表示凑成目标值 i 的组合数
    unsigned long long dp[target + 1];
    memset(dp, 0, sizeof(dp)); // 初始化为 0

    // 2. DP 数组初始化
    dp[0] = 1; // 凑成目标值 0 的组合数为 1

    // 3. 动态规划填充 DP 数组
    for (int i = 1; i <= target; i++) {
        for (int j = 0; j < numsSize; j++) {
            if (nums[j] <= i) {
                dp[i] += dp[i - nums[j]]; // 状态转移方程
            }
        }
    }

    // 4. 返回值
    return dp[target];
}

零钱兑换

322. 零钱兑换

问题描述

给定一个硬币数组 coins 和一个目标金额 amount,计算凑成目标金额所需的最少硬币数。如果无法凑成目标金额,返回 -1

思路:完全背包

1.问题转化

  • 将硬币视为物品,每种硬币可以无限次使用(完全背包问题)。
  • 我们需要找到最少的硬币数,使得它们的总和等于 amount

2. DP 数组定义

  • dp[j] 表示凑成金额 j 所需的最少硬币数。
  • valid[j] 表示是否可以凑成金额 j

3. 检查是否可以凑成目标金额

  • 使用 valid 数组记录是否可以凑成金额 j
  • 对于每个硬币 coins[i],更新 valid[j]
    • valid[j] = valid[j] | valid[j - coins[i]]

4. DP 数组初始化

  • dp[0] = 0:表示凑成金额 0 所需的硬币数为 0。
  • 其他 dp[j] 初始化为 amount + 1,表示初始状态下无法凑成金额 j

5. 动态规划填充 DP 数组

  • 外层循环遍历硬币(物品)。
  • 内层循环正序遍历金额(从 coins[i]amount)。
  • 对于每个硬币 coins[i],更新 dp[j]
    • dp[j] = min(dp[j], dp[j - coins[i]] + 1)

6. 返回值

  • 返回 dp[amount],表示凑成金额 amount 所需的最少硬币数。
代码
int coinChange(int* coins, int coinsSize, int amount) {
    // 1. DP 数组定义
    // dp[j] 表示凑成金额 j 所需的最少硬币数
    int dp[amount + 1];
    for (int i = 0; i <= amount; i++) {
        dp[i] = amount + 1; // 初始化为一个较大的值
    }

    // valid[j] 表示是否可以凑成金额 j
    bool valid[amount + 1];
    memset(valid, false, sizeof(valid)); // 初始化为 false
    valid[0] = true; // 金额 0 可以被凑成

    // 2. 检查是否可以凑成目标金额
    for (int i = 0; i < coinsSize; i++) {
        for (int j = coins[i]; j <= amount; j++) {
            valid[j] = valid[j] | valid[j - coins[i]]; // 更新 valid 数组
        }
    }

    // 如果无法凑成目标金额,直接返回 -1
    if (!valid[amount]) return -1;

    // 3. DP 数组初始化
    dp[0] = 0; // 凑成金额 0 所需的硬币数为 0

    // 4. 动态规划填充 DP 数组
    for (int i = 0; i < coinsSize; i++) {
        for (int j = coins[i]; j <= amount; j++) {
            dp[j] = fmin(dp[j], dp[j - coins[i]] + 1); // 状态转移方程
        }
    }

    // 5. 返回值
    return dp[amount];
}

279.完全平方数

279. 完全平方数

思路:完全背包、动态规划

这是一个典型的 完全背包问题,因为每个完全平方数可以无限次使用。我们需要找到最少数量的完全平方数,使得它们的和等于 nn

1.动态规划定义

  • dp[j]:表示组成数字 j所需的最少完全平方数的数量。
  • 初始化
    • dp[j] 初始化为 j,表示最坏情况下,数字 j 可以由 j 个 1 组成(因为 1 是完全平方数)。

2.状态转移方程

对于每个完全平方数 i 2 i^2 i2,更新 dp[j]

  • 如果 j > = i 2 j>=i^2 j>=i2,则 dp[j] = min(dp[j], dp[j - i*i] + 1)
  • 解释:如果选择当前完全平方数 i 2 i^2 i2,则组成数字 j 所需的最少完全平方数为 dp[j - i*i] + 1

3.遍历顺序

  • 外层循环:遍历所有可能的完全平方数 i 2 i^2 i2,其中 i 从 1 到 n \sqrt{n} n
  • 内层循环:正序遍历数字 j(从 i 2 i^2 i2n)。
    • 正序遍历是因为每个完全平方数可以无限次使用(完全背包问题)。

4.返回值

  • 返回 dp[n],表示组成数字 n 所需的最少完全平方数的数量。
代码
int numSquares(int n) {
    // 1. 初始化 DP 数组
    int dp[n + 1];
    for (int i = 0; i <= n; i++) {
        dp[i] = i; // 初始化为最坏情况,即全部由 1 组成
    }

    // 2. 动态规划填充 DP 数组
    for (int i = 1; i * i <= n; i++) {
        for (int j = i * i; j <= n; j++) {
            dp[j] = fmin(dp[j], dp[j - i * i] + 1); // 状态转移方程
        }
    }

    // 3. 返回结果
    return dp[n];
}

139.单词拆分

139. 单词拆分

题目描述

给定一个字符串 s 和一个单词字典 wordDict,判断 s 是否可以由 wordDict 中的单词拼接而成。字典中的单词可以重复使用。

示例

  • 输入:s = "leetcode", wordDict = ["leet", "code"]
  • 输出:true
  • 解释:"leetcode" 可以由 "leet""code" 拼接而成。
思路:动态规划,完全背包问题

字典中的单词可以重复使用,所以这是个完全背包问题

1. 动态规划定义

  • dp[j]:表示字符串 s 的前 j 个字符是否可以由 wordDict 中的单词拼接而成。
  • 初始化
    • dp[0] = true:表示空字符串可以被拼接。
    • 其他 dp[j] 初始化为 false

2. 状态转移方程

对于每个位置 j,遍历字典中的每个单词 wordDict[i]

  • 如果 wordDict[i]s[j-wordLen:j] 的子串,则更新 dp[j]
    • dp[j] = dp[j] || dp[j - wordLen]

3. 遍历顺序

  • 外层循环:遍历字符串 s 的每个位置 j(从 1n)。
  • 内层循环:遍历字典中的每个单词 wordDict[i]

4. 返回值

  • 返回 dp[n],表示整个字符串 s 是否可以被拼接。
代码
#include <stdio.h>
#include <stdbool.h>
#include <string.h>

// 判断字符串 s 是否可以由 wordDict 中的单词拼接而成
bool wordBreak(char* s, char** wordDict, int wordDictSize) {
    int n = strlen(s);
    bool dp[n + 1]; // dp 数组大小为 n + 1
    memset(dp, false, sizeof(dp));
    dp[0] = true; // 空字符串可以被拼接

    for (int j = 1; j <= n; j++) { // 遍历字符串 s 的每个位置
        for (int i = 0; i < wordDictSize; i++) { // 遍历 wordDict 中的每个单词
            int wordLen = strlen(wordDict[i]);
            if (j < wordLen) continue; // 如果单词长度大于当前子串长度,跳过

            // 检查 wordDict[i] 是否是 s[j-wordLen:j] 的子串
            bool flag = true;
            for (int k = 0; k < wordLen; k++) {
                if (s[j - wordLen + k] != wordDict[i][k]) {
                    flag = false;
                    break;
                }
            }

            // 如果 wordDict[i] 是 s[j-wordLen:j] 的子串,更新 dp[j]
            if (flag) {
                dp[j] = dp[j] || dp[j - wordLen];
            }
        }
    }

    return dp[n]; // 返回整个字符串 s 是否可以被拼接
}

回溯代码(若没想到dp,可以用回溯,拿到一部分分数也不错)

bool wordBreak(char* s, char** wordDict, int wordDictSize) {
    // 如果 s 为空字符串,返回 true
    if (s[0] == '\0') {
        return true;
    }

    bool res = false;
    for (int i = 0; i < wordDictSize; i++) {
        // 如果 wordDict[i] 比 s 长,跳过
        if (strlen(wordDict[i]) > strlen(s)) {
            continue;
        }

        // 检查 wordDict[i] 是否是 s 的前缀
        bool flag = true;
        for (int j = 0; wordDict[i][j] != '\0'; j++) {
            if (s[j] != wordDict[i][j]) {
                flag = false;
                break;
            }
        }

        // 如果是前缀,递归检查剩余部分
        if (flag) {
            res = wordBreak(s + strlen(wordDict[i]), wordDict, wordDictSize);
            if (res) {
                return true; // 如果找到匹配,直接返回 true
            }
        }
    }

    return res; // 如果没有找到匹配,返回 false
}

背包问题详细总结

1. 0/1 背包

问题描述

  • 特点:每个物品最多只能选一次。
  • 目标:在不超过背包容量 V V V 的条件下,使得背包内物品的总价值最大。

状态定义

  • 二维状态
    • d p [ i ] [ j ] dp[i][j] dp[i][j] 表示前 i i i 个物品,在总容量为 j j j 时的最大价值。
  • 一维状态(空间优化):
    • d p [ j ] dp[j] dp[j] 表示容量为 j j j 时的最大价值。

状态转移方程

  • 二维版
    d p [ i ] [ j ] = { d p [ i − 1 ] [ j ] (不选第  i  个物品) d p [ i − 1 ] [ j − w i ] + v i (选第  i  个物品, 需满足  j ≥ w i ) dp[i][j] = \begin{cases} dp[i-1][j] & \text{(不选第 $i$ 个物品)} \\ dp[i-1][j-w_i] + v_i & \text{(选第 $i$ 个物品, 需满足 $j \ge w_i$)} \end{cases} dp[i][j]={dp[i1][j]dp[i1][jwi]+vi(不选第 i 个物品)(选第 i 个物品需满足 jwi)
    归纳为:
    d p [ i ] [ j ] = max ⁡ ( d p [ i − 1 ] [ j ] ,   d p [ i − 1 ] [ j − w i ] + v i ) ( j ≥ w i ) dp[i][j] = \max\Big(dp[i-1][j],\ dp[i-1][j-w_i] + v_i\Big) \quad (j \ge w_i) dp[i][j]=max(dp[i1][j], dp[i1][jwi]+vi)(jwi)

  • 一维版(倒序更新):
    for  i = 1  to  n : for  j = V  downto  w i : d p [ j ] = max ⁡ ( d p [ j ] ,   d p [ j − w i ] + v i ) \text{for } i = 1 \text{ to } n:\\ \quad \text{for } j = V \text{ downto } w_i:\\ \quad\quad dp[j] = \max\big(dp[j],\ dp[j-w_i] + v_i\big) for i=1 to n:for j=V downto wi:dp[j]=max(dp[j], dp[jwi]+vi)

迭代顺序

  • 倒序遍历容量 j j j,以确保每个物品只被使用一次,避免重复计入当前物品。

注意事项

  • 更新顺序:使用一维数组时,必须从大到小遍历容量 j j j,否则会重复使用同一物品。
  • 初始状态:通常 d p [ 0 ] = 0 dp[0] = 0 dp[0]=0,其他状态应根据问题情况初始化(如不可达状态可以初始化为负无穷)。
  • 边界检查:确保在访问 d p [ j − w i ] dp[j-w_i] dp[jwi] 前,容量 j ≥ w i j \ge w_i jwi

2. 完全背包

问题描述

  • 特点:每个物品可以无限次选取。
  • 目标:在背包容量 V V V 内,选取物品使得总价值最大,并允许每个物品多次选用。

状态定义

  • 与 0/1 背包类似,使用 d p [ i ] [ j ] dp[i][j] dp[i][j] 或优化为 d p [ j ] dp[j] dp[j]

状态转移方程

  • 二维版
    d p [ i ] [ j ] = max ⁡ ( d p [ i − 1 ] [ j ] ,   d p [ i ] [ j − w i ] + v i ) ( j ≥ w i ) dp[i][j] = \max\Big(dp[i-1][j],\ dp[i][j-w_i] + v_i\Big) \quad (j \ge w_i) dp[i][j]=max(dp[i1][j], dp[i][jwi]+vi)(jwi)

    注意:这里使用了 d p [ i ] [ j − w i ] dp[i][j-w_i] dp[i][jwi] 表明同一物品可以被重复使用。

  • 一维版(正序更新):
    for  i = 1  to  n : for  j = w i  to  V : d p [ j ] = max ⁡ ( d p [ j ] ,   d p [ j − w i ] + v i ) \text{for } i = 1 \text{ to } n:\\ \quad \text{for } j = w_i \text{ to } V:\\ \quad\quad dp[j] = \max\big(dp[j],\ dp[j-w_i] + v_i\big) for i=1 to n:for j=wi to V:dp[j]=max(dp[j], dp[jwi]+vi)

迭代顺序

  • 正序遍历容量 j j j,确保在更新状态时可以多次利用当前物品的状态。

注意事项

  • 遍历顺序:必须正序遍历容量 j j j,否则无法保证同一物品能够被重复选用。
  • 初始化:同样需要正确初始化 d p [ 0 ] = 0 dp[0]=0 dp[0]=0,其他状态根据问题定义设置。
  • 状态依赖:在更新时注意依赖的是当前物品的状态(即 d p [ j − w i ] dp[j-w_i] dp[jwi] 在同一轮中可能已经更新)。

3. 多重背包(有限个数背包)

问题描述

  • 特点:每个物品有一个有限的选取次数 c n t i cnt_i cnti(既不无限也不只有一次)。
  • 目标:在背包容量 V V V 内,选取物品使得总价值最大,同时满足每个物品最多选取 c n t i cnt_i cnti 次。

状态定义

  • 同样可以用 d p [ i ] [ j ] dp[i][j] dp[i][j] 或优化为一维数组 d p [ j ] dp[j] dp[j] 表示最大价值。

状态转移方程

  • 直接枚举法
    d p [ i ] [ j ] = max ⁡ 0 ≤ k ≤ c n t i ,    j ≥ k × w i { d p [ i − 1 ] [ j − k × w i ] + k × v i } dp[i][j] = \max_{0 \le k \le cnt_i,\; j \ge k \times w_i} \Big\{ dp[i-1][j-k \times w_i] + k \times v_i \Big\} dp[i][j]=0kcnti,jk×wimax{dp[i1][jk×wi]+k×vi}
    或在一维数组中进行类似枚举(需倒序遍历保证不重复使用)。

  • 二进制拆分法

    • 将每个物品的数量 c n t i cnt_i cnti 拆分为若干个数量为 1 , 2 , 4 , … , r 1, 2, 4, \ldots, r 1,2,4,,r 的物品,使得这些数量之和等于 c n t i cnt_i cnti
    • 拆分后每个“子物品”视为 0/1 背包问题处理(使用倒序遍历)。
  • 单调队列优化(高级技巧):

    • 在某些特定场景下,可以利用单调队列优化内层循环,减少枚举次数。

迭代顺序

  • 直接枚举法:外层枚举物品,内层枚举每个物品可选的数量 k k k,同时对容量进行倒序遍历。
  • 二进制拆分后:将问题转化为多个 0/1 背包问题,使用倒序遍历更新。

实现代码

#include <stdio.h>
#include <stdlib.h>

// 取两个数的最小值
int min_int(int a, int b) {
    return a < b ? a : b;
}

/*
 * 函数名: multiKnapsackDirect
 * 功能  : 多重背包问题的直接枚举实现(朴素方法)。
 * 参数  :
 *   - n       : 物品总数
 *   - V       : 背包总容量
 *   - weight  : 物品重量数组,下标 0 ~ n-1
 *   - value   : 物品价值数组,下标 0 ~ n-1
 *   - cnt     : 每个物品可选次数数组,下标 0 ~ n-1
 *   - dp      : dp 数组,大小为 V+1,存储每个容量下的最优解(需预先初始化)
 *
 * 说明:
 *   对于每个物品 i,遍历背包容量 j(从大到小),并枚举选取该物品的数量 k(1~cnt[i])。
 *   其中倒序遍历确保同一物品不会被重复选用。
 */
void multiKnapsackDirect(int n, int V, int weight[], int value[], int cnt[], int dp[]) {
    // 遍历每个物品
    for (int i = 0; i < n; i++) {
        // 倒序遍历背包容量,防止重复使用同一物品
        for (int j = V; j >= 0; j--) {
            // 枚举选取该物品的数量 k(从1到cnt[i])
            for (int k = 1; k <= cnt[i]; k++) {
                int totalWeight = k * weight[i];  // 当前选取 k 个物品的总重量
                // 如果当前容量 j 足够容纳 k 个该物品,则尝试更新 dp[j]
                if (j >= totalWeight) {
                    int candidate = dp[j - totalWeight] + k * value[i];
                    if (candidate > dp[j]) {
                        dp[j] = candidate;
                    }
                } else {
                    // 如果当前重量超过 j,后续更大 k 的值也一定不满足
                    break;
                }
            }
        }
    }
}

/*
 * 函数名: multiKnapsackBinarySplitting
 * 功能  : 多重背包问题的二进制拆分优化实现。
 * 参数  :
 *   - n       : 物品总数
 *   - V       : 背包总容量
 *   - weight  : 物品重量数组,下标 0 ~ n-1
 *   - value   : 物品价值数组,下标 0 ~ n-1
 *   - cnt     : 每个物品可选次数数组,下标 0 ~ n-1
 *   - dp      : dp 数组,大小为 V+1,存储每个容量下的最优解(需预先初始化)
 *
 * 说明:
 *   将每个物品的数量 cnt[i] 拆分成若干个数量为 1, 2, 4, ... 的组,
 *   转化为若干个 0/1 背包问题进行处理,从而降低内层循环的次数。
 */
void multiKnapsackBinarySplitting(int n, int V, int weight[], int value[], int cnt[], int dp[]) {
    // 遍历每个物品
    for (int i = 0; i < n; i++) {
        int quantity = cnt[i]; // 当前物品剩余可选数量
        int k = 1;             // 拆分因子,依次为 1, 2, 4, ...
        // 对当前物品进行二进制拆分
        while (quantity > 0) {
            // 取当前组的数量,不超过剩余数量
            int num = min_int(k, quantity);
            quantity -= num;
            int totalWeight = num * weight[i]; // 当前组总重量
            int totalValue = num * value[i];   // 当前组总价值
            // 0/1 背包更新:倒序遍历容量,保证当前拆分组只用一次
            for (int j = V; j >= totalWeight; j--) {
                int candidate = dp[j - totalWeight] + totalValue;
                if (candidate > dp[j]) {
                    dp[j] = candidate;
                }
            }
            k *= 2; // 翻倍
        }
    }
}

/*
 * 函数名: multiKnapsackMonotonicQueue
 * 功能  : 多重背包问题的单调队列优化实现(适用于大容量、多重物品)。
 * 参数  :
 *   - n       : 物品总数
 *   - V       : 背包总容量
 *   - weight  : 物品重量数组,下标 0 ~ n-1
 *   - value   : 物品价值数组,下标 0 ~ n-1
 *   - cnt     : 每个物品可选次数数组,下标 0 ~ n-1
 *   - dp      : dp 数组,大小为 V+1,存储每个容量下的最优解(需预先初始化)
 *
 * 说明:
 *   对于当前物品 i,其重量为 w,价值为 v,数量为 cnt,
 *   对所有满足 j ≡ r (mod w) 的背包容量 j(记 j = r + t*w,t 为非负整数)进行如下转移:
 *
 *       dp[r + t*w] = max_{s in [max(0, t-cnt), t]} { dp[r + s*w] - s*v } + t*v
 *
 *   为高效求解右侧窗口内的最大值,采用单调队列优化。
 */
void multiKnapsackMonotonicQueue(int n, int V, int weight[], int value[], int cnt[], int dp[]) {
    // 遍历每个物品
    for (int i = 0; i < n; i++) {
        int w = weight[i];   // 当前物品重量
        int v = value[i];    // 当前物品价值
        int num = cnt[i];    // 当前物品数量限制
        
        // 对于每个余数 r (0 <= r < w),处理 j = r, r+w, r+2w, ... 不超过 V 的所有容量
        for (int r = 0; r < w; r++) {
            // 定义双端队列,用于维护区间最大值(队列中存储 t 的值,t 表示背包中“层数”,即 j = r + t*w)
            // 为简单起见,这里分配的数组大小取 (V/w + 10),确保足够存储
            int *deque = (int *)malloc(sizeof(int) * (V / w + 10));
            int head = 0, tail = -1; // 初始化队列为空

            // 计算 t 的最大值:满足 r + t*w <= V
            int maxT = (V - r) / w;
            // 遍历每个 t,对应背包容量 j = r + t*w
            for (int t = 0; t <= maxT; t++) {
                int j = r + t * w;  // 当前容量位置

                // 计算函数 f(t) = dp[j] - t*v
                int cur_f = dp[j] - t * v;

                // 移除队列头部的元素,如果其对应的 t 小于当前 t - num,
                // 意味着该元素不在滑动窗口 [t - num, t] 内(窗口大小为 num+1)
                while (head <= tail && deque[head] < t - num) {
                    head++;
                }

                // 维护队列单调性:
                // 队列中存储的 t 值对应的 f(t) 值应按递减顺序排列
                // 移除队列尾部所有 f 值不大于当前 cur_f 的元素
                while (head <= tail) {
                    int last_t = deque[tail];
                    int last_f = dp[r + last_t * w] - last_t * v;
                    if (last_f <= cur_f) {
                        tail--;
                    } else {
                        break;
                    }
                }

                // 将当前 t 加入队列尾部
                deque[++tail] = t;

                // 队列头部存储的 t 值即为窗口内 f(t) 的最大值对应的下标
                int best_t = deque[head];
                // 根据公式更新 dp[j]:dp[j] = f(best_t) + t*v
                dp[j] = dp[r + best_t * w] - best_t * v + t * v;
            }
            free(deque);
        }
    }
}

注意事项

  • 效率问题:直接枚举 k k k 可能会使时间复杂度过高,建议使用二进制拆分来优化。
  • 拆分细节:二进制拆分时要注意剩余部分的处理(当 c n t i cnt_i cnti 不是 2 的幂次时)。
  • 边界检查:同样确保在更新状态前满足 j ≥ k × w i j \ge k \times w_i jk×wi 的条件。

三者比较

特性0/1 背包完全背包多重背包
选取限制每个物品最多选一次每个物品可无限次选取每个物品最多选取 c n t i cnt_i cnti 次(有限,但可能大于1)
状态转移(二维) d p [ i ] [ j ] = max ⁡ ( d p [ i − 1 ] [ j ] ,    d p [ i − 1 ] [ j − w i ] + v i ) dp[i][j]=\max\Big(dp[i-1][j],\; dp[i-1][j-w_i]+v_i\Big) dp[i][j]=max(dp[i1][j],dp[i1][jwi]+vi) d p [ i ] [ j ] = max ⁡ ( d p [ i − 1 ] [ j ] ,    d p [ i ] [ j − w i ] + v i ) dp[i][j]=\max\Big(dp[i-1][j],\; dp[i][j-w_i]+v_i\Big) dp[i][j]=max(dp[i1][j],dp[i][jwi]+vi) d p [ i ] [ j ] = max ⁡ 0 ≤ k ≤ c n t i ,    j ≥ k w i { d p [ i − 1 ] [ j − k w i ] + k v i } dp[i][j]=\max_{0\le k\le cnt_i,\; j\ge k w_i}\Big\{dp[i-1][j-k w_i]+k v_i\Big\} dp[i][j]=0kcnti,jkwimax{dp[i1][jkwi]+kvi}
状态转移(一维)倒序更新:
for  j = V  downto  w i :    d p [ j ] = max ⁡ ( d p [ j ] ,    d p [ j − w i ] + v i ) \text{for } j=V \text{ downto } w_i:\; dp[j]=\max\big(dp[j],\; dp[j-w_i]+v_i\big) for j=V downto wi:dp[j]=max(dp[j],dp[jwi]+vi)
正序更新:
for  j = w i  to  V :    d p [ j ] = max ⁡ ( d p [ j ] ,    d p [ j − w i ] + v i ) \text{for } j=w_i \text{ to } V:\; dp[j]=\max\big(dp[j],\; dp[j-w_i]+v_i\big) for j=wi to V:dp[j]=max(dp[j],dp[jwi]+vi)
直接枚举:
for  j = V  downto  0 : for  k = 0  to  c n t i  (if  j ≥ k w i ) :    d p [ j ] = max ⁡ ( d p [ j ] ,    d p [ j − k w i ] + k v i ) \text{for } j=V \text{ downto } 0:\\ \quad \text{for } k=0 \text{ to } cnt_i \text{ (if } j\ge k w_i):\; dp[j]=\max\big(dp[j],\; dp[j-k w_i]+k v_i\big) for j=V downto 0:for k=0 to cnti (if jkwi):dp[j]=max(dp[j],dp[jkwi]+kvi)
或使用二进制拆分后转为 0/1 背包
迭代顺序容量 j j j 倒序遍历容量 j j j 正序遍历视实现方式:直接枚举时容量倒序;二进制拆分后为倒序(0/1 背包方式)
时间复杂度 O ( n V ) O(nV) O(nV) O ( n V ) O(nV) O(nV)直接枚举: O ( n ⋅ c n t i ⋅ V ) O(n \cdot cnt_i \cdot V) O(ncntiV)
二进制拆分: O ( n log ⁡ ( c n t i ) ⋅ V ) O(n\log(cnt_i)\cdot V) O(nlog(cnti)V)
常用技巧一维数组优化、边界条件处理一维数组优化、正序更新确保无限选二进制拆分、单调队列优化(高级)

总结与注意事项

  • 更新顺序至关重要

    • 0/1 背包:必须倒序遍历容量,防止在同一轮中重复选取物品。
    • 完全背包:需要正序遍历容量,保证同一物品可以多次选取。
    • 多重背包:直接枚举时同样需要倒序遍历;若使用二进制拆分,则问题转化为多个 0/1 背包问题,同样采用倒序遍历。
  • 状态初始化

    • 通常将 d p [ 0 ] dp[0] dp[0] 初始化为 0,其它状态根据具体问题设定(有时不可达的状态需设为负无穷)。
  • 边界条件处理

    • 在更新 d p [ j ] dp[j] dp[j] 前,应检查 j ≥ w i j \ge w_i jwi j ≥ k × w i j \ge k \times w_i jk×wi,以防数组越界或错误更新。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值