01背包问题的空间优化与边界处题目解析

01背包问题的空间优化与边界处题目解析

01背包问题是经典的动态规划问题,旨在选择若干物品装入背包,使得总价值最大且不超过背包容量。每个物品只能选或不选(0或1),不可分割。

选和不选是01背包问题最大的特征

例题

01背包

题目链接:

01背包

要点:

老师代码:

#include <iostream>
#include <string.h>

using namespace std;
const int N = 1010;
int n, V, v[N], w[N];
int dp[N][N];

int main()
{
    // 读⼊数据
    cin >> n >> V;
    for(int i = 1; i <= n; i++)
    cin >> v[i] >> w[i];
    
    // 解决第⼀问
    for(int i = 1; i <= n; i++)
        for(int j = 0; j <= V; j++) // 修改遍历顺序
        {
            dp[i][j] = dp[i - 1][j];
            if(j >= v[i])
            dp[i][j] = max(dp[i][j], dp[i - 1][j - v[i]] + w[i]);
        }
    
    cout << dp[n][V] << endl;
    
    // 解决第⼆问
    memset(dp, 0, sizeof dp);
    for(int j = 1; j <= V; j++) dp[0][j] = -1;
    
    for(int i = 1; i <= n; i++)
        for(int j = 0; j <= V; j++) // 修改遍历顺序
        {
            dp[i][j] = dp[i - 1][j];
            if(j >= v[i] && dp[i - 1][j - v[i]] != -1)
            dp[i][j] = max(dp[i][j], dp[i - 1][j - v[i]] + w[i]);
        }
    cout << (dp[n][V] == -1 ? 0 : dp[n][V]) << endl;
    return 0;
}

空间优化:

#include <iostream>
#include <string.h>
using namespace std;

const int N = 1010;
int n, V, v[N], w[N];
int dp[N];

int main()
{
    // 读⼊数据
    cin >> n >> V;
    for(int i = 1; i <= n; i++)
    cin >> v[i] >> w[i];
    
    // 解决第⼀问
    for(int i = 1; i <= n; i++)
        for(int j = V; j >= v[i]; j--) // 修改遍历顺序
            dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
    cout << dp[V] << endl;
    
    // 解决第⼆问
    memset(dp, 0, sizeof dp);
    for(int j = 1; j <= V; j++) dp[j] = -1;
    for(int i = 1; i <= n; i++)
        for(int j = V; j >= v[i]; j--)
            if(dp[j - v[i]] != -1)
                dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
    cout << (dp[V] == -1 ? 0 : dp[V]) << endl;
    
    return 0;
}

老师思路:

第一问:

  1. 状态表⽰:dp[i][j] 表⽰:从前 i 个物品中挑选,总体积「不超过」 j ,所有的选法中,能挑选出来的最⼤价值

  2. 状态转移⽅程:线性 dp 状态转移⽅程分析⽅式,⼀般都是根据「最后⼀步」的状况,来分情况讨论:

    • i. 不选第 i 个物品:相当于就是去前 i - 1 个物品中挑选,并且总体积不超过 j 。此时 dp[i][j] = dp[i - 1][j]
    • ii. 选择第 i 个物品:那么我就只能去前 i - 1 个物品中,挑选总体积不超过 j - v[i] 的物品。此时 dp[i][j] = dp[i - 1][j - v[i]] + w[i] 。但是这种状态不⼀定存在,因此需要特判⼀下。
    • 综上,状态转移⽅程为: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - v[i]] + w[i])
  3. 初始化:我们多加⼀⾏,⽅便我们的初始化,此时仅需将第⼀⾏初始化为 0 即可。因为什么也不选,也能满⾜体积不⼩于 j 的情况,此时的价值为 0 。

  4. 填表顺序:根据「状态转移⽅程」,我们仅需「从上往下」填表即可。

  5. 返回值:根据「状态表⽰」,返回 dp[n][V]

第二问

第⼆问仅需微调⼀下 dp 过程的五步即可。 因为有可能凑不⻬ j 体积的物品,因此我们把不合法的状态设置为 -1 。

  1. 状态表⽰:dp[i][j] 表⽰:从前 i 个物品中挑选,总体积「正好」等于 j ,所有的选法中,能挑选出来的最⼤价值。

  2. 状态转移⽅程:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - v[i]] + w[i]) 。但是在使⽤ dp[i - 1][j - v[i]] 的时候,不仅要判断 j >= v[i] ,⼜要判断 dp[i- 1][j - v[i]] 表⽰的情况是否存在,也就是 dp[i - 1][j - v[i]] != -1

  3. 初始化:我们多加⼀⾏,⽅便我们的初始化:i. 第⼀个格⼦为 0 ,因为正好能凑⻬体积为 0 的背包; ii. 但是第⼀⾏后⾯的格⼦都是 -1 ,因为没有物品,⽆法满⾜体积⼤于 0 的情况

  4. 填表顺序:根据「状态转移⽅程」,我们仅需「从上往下」填表即可。

  5. 返回值:由于最后可能凑不成体积为 V 的情况,因此返回之前需要「特判」⼀下。

我的代码:

#include <iostream>
#include <vector>
using namespace std;

int main()
{
    int n, v;
    cin >> n >> v;
    vector<int> sz(n + 1);
    vector<int> val(n + 1);
    for(int i = 1; i <= n; i++) cin >> sz[i] >> val[i];

    //状态表示
    vector<vector<int>> dp1(n + 1, vector<int>(v + 1));
    auto dp2 = dp1;
    //初始化
    for(int j = 1; j <= v; j++) dp2[0][j] = -1;//使空位置的物品价值等于j是不存在的,而不是物品价值为0,所以填入-1

    //填表
    for(int i = 1; i <= n; i++)
    {
        for(int j = 0; j <= v; j++)
        {
            dp1[i][j] = dp1[i - 1][j];
            if(sz[i] <= j) dp1[i][j] = max(dp1[i][j], dp1[i - 1][j - sz[i]] + val[i]);
            
            dp2[i][j] = dp2[i - 1][j];
            if(sz[i] <= j && dp2[i - 1][j - sz[i]] != -1) dp2[i][j] = max(dp2[i][j], dp2[i - 1][j - sz[i]] + val[i]);
        }
    }

    cout << dp1[n][v] << endl;
    cout << (dp2[n][v] == -1 ? 0 : dp2[n][v]) << endl;

    return 0;
}

我的思路:

老师教的

对于第一问:

  • 状态表示:dp[i][j]表示 i 位置之前的物品能够装入背包体积不大于 j 的最大价值

  • 状态转移方程:

    • 如果不选 i 位置的物品 就要从 i - 1位置的物品中选择能够装入背包体积不大于 j 的最大价值

      dp[i][j] = dp[i - 1][j];

    • 如果选 i 位置 的物品 就要判断能不能选,如果不能就选择前面的方案,如果能选,则我们选上当前位置的物品val[i]之后还要从 i - 1 位置的物品中选择体积不超过j - sz[i] 的最大价值,最后吧这一种方案与前面一种方案取最大值即可

      if(sz[i] <= j) dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - sz[i]] + val[i]);

对于第二问:

  • 状态表示:dp[i][j]表示位置之前的物品能够装入背包体积刚好等于 j 的最大价值

  • 状态转移方程:

    • 如果不选 i 位置的物品, 我们就要从 i - 1 位置中选出装入背包体积等于 j 的最大价值

      dp[i][j] = dp[i - 1][j]这和之前的状态转移方程是一样的,因为状态表示变了

    • 如果选 i 位置的物品,我们仍然要判断能不能选此时还有一种情况,就是存不存在可以选 i 位置物品的情况,因为如果要使体积刚好等于 j 这种情况可能是不存在的,所以要多加一个判断条件

      if(sz[i] <= j && dp2[i - 1][j - sz[i]] != -1) dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - sz[i]] + val[i])

注意:dp2[i - 1][j - sz[i]] != -1用-1判断是因为我们需要与0的意义区分开,0表示dp[i][j]表示位置之前的物品能够装入背包体积刚好等于 j 的最大价值为0,而不是没有这种情况,只是价值为0而已

我的笔记:

  • 还是要注意数组下标的映射关系,这个比较容易出错,不管是在输入数据、初始化、填表的时候

  • 背包问题的思想

  • 对于空间优化的建议:

    • 不要深究空间优化之后状态表示以及状态转移方程的实际含义
    • 空间优化重点是处理后面的填表中用不到的数据
    • 注意填表方式可能不同,原因就是不能覆盖还需要的数据
分割等和子集

题目链接:

分割等和子集

要点:

  • 问题核心:判断数组是否能被分割成两个和相等的子集。关键在于找到子集和为总和的一半。
  • 数学条件:若总和为奇数,直接返回false;若最大元素超过总和一半,也直接返回false
  • 动态规划定义dp[i][j]表示前i个元素是否能选出和为j的子集。

老师代码:

class Solution
{
public:
    bool canPartition(vector<int>& nums)
    {
        int n = nums.size(), sum = 0;
        for(auto x : nums) sum += x;
        if(sum % 2) return false; // 如果不能平分,直接返回 false
        int aim = sum / 2; // 定义⼀下⽬标值

        vector<vector<bool>> dp(n + 1, vector<bool>(aim + 1)); // 建表
        
        for(int i = 0; i <= n; i++) dp[i][0] = true; // 初始化
        
        for(int i = 1; i <= n; i++) // 填表
            for(int j = 1; j <= aim; j++)
            {
                dp[i][j] = dp[i - 1][j];
                if(j >= nums[i - 1])
                dp[i][j] = dp[i][j] || dp[i - 1][j - nums[i - 1]];
            }
        
        // 返回结果
        return dp[n][aim];
    }
}

空间优化:

class Solution
{
public:
    bool canPartition(vector<int>& nums)
    {
        int n = nums.size(), sum = 0;
        for(auto x : nums) sum += x;
        if(sum % 2) return false; // 如果不能平分,直接返回 false
        int aim = sum / 2; // 定义⼀下⽬标值
        
        vector<bool> dp(aim + 1); // 建表
        
        dp[0] = true; // 初始化
        
        for(int i = 1; i <= n; i++) // 填表
            for(int j = aim; j >= nums[i - 1]; j--) // ⼀定要注意修改遍历顺序
                dp[j] = dp[j] || dp[j - nums[i - 1]];
        
        // 返回结果
        return dp[aim];
    }
};

老师思路:

先将问题转化成我们「熟悉」的题型。

如果数组能够被分成两个相同元素之和相同的⼦集,那么原数组必须有下⾯⼏个性质:

  • i. 所有元素之和应该是⼀个偶数;
  • ii. 数组中最⼤的元素应该⼩于所有元素总和的⼀半;
  • iii. 挑选⼀些数,这些数的总和应该等于数组总和的⼀半。

根据前两个性质,我们可以提前判断数组能够被划分。根据最后⼀个性质,我们发现问题就转化成了「01 背包」的模型:

  • i. 数组中的元素只能选择⼀次;
  • ii. 每个元素⾯临被选择或者不被选择的处境;
  • iii. 选出来的元素总和要等于所有元素总和的⼀半。

其中,数组内的元素就是物品,总和就是背包。那么我们就可以⽤背包模型的分析⽅式,来处理这道题。

请⼤家注意,「不要背」状态转移⽅程,因为题型变化之后,状态转移⽅程就会跟着变化。我们要记住的是分析问题的模式。⽤这种分析问题的模式来解决问题

  1. 状态表⽰:dp[i][j] 表⽰在前 i 个元素中选择,所有的选法中,能否凑成总和为 j 这个数

  2. 状态转移⽅程:⽼规矩,根据「最后⼀个位置」的元素,结合题⽬的要求,分情况讨论:

    • i. 不选择 nums[i] :那么我们是否能够凑成总和为 j ,就要看在前 i - 1 个元素中选,能否凑成总和为 j 。根据状态表⽰,此时 dp[i][j] = dp[i - 1][j]
    • ii. 选择 nums[i] :这种情况下是有前提条件的,此时的 nums[i] 应该是⼩于等于 j 。因为如果这个元素都⽐要凑成的总和⼤,选择它就没有意义呀。那么我们是否能够凑成总和为 j ,就要看在前 i - 1 个元素中选,能否凑成总和为 j - nums[i] 。根据状态表⽰,此时 dp[i][j] = dp[i - 1][j - nums[i]]

    综上所述,两种情况下只要有⼀种能够凑成总和为 j ,那么这个状态就是 true 。因此,状态转移⽅程为:

    dp[i][j] = dp[i - 1][j] if(nums[i - 1] <= j) dp[i][j] = dp[i][j] || dp[i - 1][j -nums[i]]

  3. 初始化:由于需要⽤到上⼀⾏的数据,因此我们可以先把第⼀⾏初始化。第⼀⾏表⽰不选择任何元素,要凑成⽬标和 j 。只有当⽬标和为 0 的时候才能做到,因此第⼀⾏仅需初始化第⼀个元素 dp[0][0] = true

  4. 填表顺序:根据「状态转移⽅程」,我们需要「从上往下」填写每⼀⾏,每⼀⾏的顺序是「⽆所谓的」。

  5. 返回值:根据「状态表⽰」,返回 dp[n][aim] 的值。 其中 n 表⽰数组的⼤⼩, aim 表⽰要凑的⽬标和。

  6. 空间优化:所有的「背包问题」,都可以进⾏空间上的优化。对于 01背包类型的,我们的优化策略是: i. 删掉第⼀维;ii. 修改第⼆层循环的遍历顺序即可。

我的代码:

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int m = nums.size();
        int sum = 0;
        for(int i = 0; i < m; i++)
            sum += nums[i];
        if(sum % 2) return false;//如果数组和为一个奇数,则他一定不能分成两个相等的整数
        int k = sum / 2;

        vector<vector<bool>> dp(m + 1, vector<bool>(k + 1));

        //初始化
        for(int i = 0; i <= m; i++) dp[i][0] = true;
        //填表
        for(int i = 1; i <= m; i++)
        {
            for(int j = 0; j <= k; j++)
            {
                dp[i][j] = dp[i - 1][j];
                if(j - nums[i - 1] >= 0) dp[i][j] = dp[i][j] || dp[i - 1][j - nums[i - 1]];
            }
        }
        
        return dp[m][k];
    }
};

空间优化:

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int m = nums.size();
        int sum = 0;
        for(int i = 0; i < m; i++)
            sum += nums[i];
        if(sum % 2) return false;
        int k = sum / 2;

        vector<bool> dp(k + 1);

        //初始化
        dp[0] = true;

        //填表
        for(int i = 1; i <= m; i++)
        {
            for(int j = k; j >= 0; j--)
            {
                if(j - nums[i - 1] >= 0) dp[j] = dp[j] || dp[j - nums[i - 1]];
            }
        }
        
        return dp[k];
    }
};

我的思路:

  • 将问题转化为“是否存在子集和为sum/2”,转化为01背包问题。
  • 初始化dp[0][0] = true(空集和为0),其他dp[0][j] = falsej > 0时无法用空集凑出)。

我的笔记:

  • 如何将这一个问题转化为01背包问题
  • 数组下标映射关系
  • 为什么要多开一行一列的dp空间?因为当我们不选也就是子集为空,以及和为0的情况是有研究价值的,也就是会出现这种情况的
目标和

题目链接:

目标和

要点:

  • 问题转化:设正数和为a,负数和为b,则a - b = targeta + b = sum,推导得a = (sum + target) / 2
  • 合法性判断:若(sum + target)为奇数或target绝对值超过sum,直接返回0

老师代码:

class Solution
{
public:
    int findTargetSumWays(vector<int>& nums, int target)
    {
        int sum = 0;
        for(auto x : nums) sum += x;
        int aim = (sum + target) / 2;
        
        // 处理⼀下边界条件
        if(aim < 0 || (sum + target) % 2) return 0;
        int n = nums.size();
        
        vector<vector<int>> dp(n + 1, vector<int>(aim + 1)); // 建表
        dp[0][0] = 1; // 初始化
        
        for(int i = 1; i <= n; i++) // 填表
            for(int j = 0; j <= aim; j++)
            {
                dp[i][j] = dp[i - 1][j];
                if(j >= nums[i - 1]) dp[i][j] += dp[i - 1][j - nums[i - 1]];
            }
        
        // 返回结果
        return dp[n][aim];
    }
}

老师思路:

本题可以直接⽤「暴搜」的⽅法解决。但是稍微⽤数学知识分析⼀下,就能转化成我们常⻅的「背包模型」的问题。

设我们最终选取的结果中,前⾯加 + 号的数字之和为 a ,前⾯加 - 号的数字之和为 b ,整个数组的总和为 sum ,于是我们有:

a + b = sum

a - b = target

上⾯两个式⼦消去 b 之后,可以得到 a = (sum + target) / 2 也就是说,我们仅需在 nums 数组中选择⼀些数,将它们凑成和为 (sum + target) / 2 即可。问题就变成了 416. 分割等和⼦集 这道题。 我们可以⽤相同的分析模式,来处理这道题

  1. 状态表⽰:dp[i][j] 表⽰:在前 i 个数中选,总和正好等于 j ,⼀共有多少种选法。

  2. 状态转移⽅程:⽼规矩,根据「最后⼀个位置」的元素,结合题⽬的要求,我们有「选择」最后⼀个元素或者「不选择」最后⼀个元素两种策略:

    • i. 不选 nums[i] :那么我们凑成总和 j 的总⽅案,就要看在前 i - 1 个元素中选,凑成总和为 j 的⽅案数。根据状态表⽰,此时 dp[i][j] = dp[i - 1][j] ;

    • ii. 选择 nums[i] :这种情况下是有前提条件的,此时的 nums[i] 应该是⼩于等于 j 。因为如果这个元素都⽐要凑成的总和⼤,选择它就没有意义呀。那么我们能够凑成总和为 j 的⽅案数,就要看在前 i - 1 个元素中选,能否凑成总和为 j - nums[i] 。根据状态表⽰,此时 dp[i][j] = dp[i - 1][j - nums[i]]

    • 综上所述,两种情况如果存在的话,应该要累加在⼀起。因此,状态转移⽅程为:dp[i][j] = dp[i - 1][j] if(nums[i - 1] <= j) dp[i][j] = dp[i][j] += dp[i - 1][j - nums[i- 1]]

  3. 初始化:由于需要⽤到「上⼀⾏」的数据,因此我们可以先把第⼀⾏初始化。第⼀⾏表⽰不选择任何元素,要凑成⽬标和 j 。只有当⽬标和为 0 的时候才能做到,因此第⼀⾏仅需初始化第⼀个元素 dp[0][0] = 1

  4. 填表顺序:根据「状态转移⽅程」,我们需要「从上往下」填写每⼀⾏,每⼀⾏的顺序是「⽆所谓的」。

  5. 返回值:根据「状态表⽰」,返回 dp[n][aim] 的值。 其中 n 表⽰数组的⼤⼩, aim 表⽰要凑的⽬标和

  6. 空间优化:所有的「背包问题」,都可以进⾏空间上的优化。对于 01背包类型的,我们的优化策略是:

    • i. 删掉第⼀维;
    • ii. 修改第⼆层循环的遍历顺序即可

我的代码:

class Solution 
{
public:
    int findTargetSumWays(vector<int>& nums, int target) 
    {
        int m = nums.size();
        int sum = 0;
        for(auto& n : nums) sum += n;
        if((sum + target) % 2) return 0;//如果是奇数,则一定不存在一种情况使
        int k = (sum + target) / 2;//如果k的值小于0,这种情况下是找不到的,因为题目给了nums中的数是大于0的
        if(k < 0) return 0;
        vector<vector<int>> dp(m + 1, vector<int>(k + 1));

        //初始化
        dp[0][0] = 1;

        //填表
        for(int i = 1; i <= m; i++)
        {
            for(int j = 0; j <= k; j++)//这里要从0开始,因为按照题目要求,nums[i]可能为0
            { 
                dp[i][j] = dp[i - 1][j];
                if(j - nums[i - 1] >= 0) dp[i][j] = dp[i][j] + dp[i - 1][j - nums[i - 1]];
            }
        }

        return dp[m][k];
    }
};

空间优化:

class Solution 
{
public:
    int findTargetSumWays(vector<int>& nums, int target) 
    {
        int m = nums.size();
        int sum = 0;
        for(auto& n : nums) sum += n;
        if((sum + target) % 2) return 0;//如果是奇数,则一定不存在一种情况使
        int k = (sum + target) / 2;
        if(k < 0) return 0;
        vector<int> dp(k + 1);

        //初始化
        dp[0] = 1;

        //填表
        for(int i = 1; i <= m; i++)
        {
            for(int j = k; j - nums[i - 1] >= 0; j--)
            {
                dp[j] += dp[j - nums[i - 1]];
            }
        }

        return dp[k];
    }
};

我的思路:

  • 使用动态规划统计组成和为a的方案数,状态转移方程为:
    dp[j] += dp[j - nums[i]](若j >= nums[i])。
  • 初始化dp[0] = 1,表示空集和为0的方案数为1。

我的笔记:

  • 注意边界条件,包括sum + target的值不能为奇数(因为k涉及除法可能存在除不尽的问题),以及k的值不能小于0(nums[i]都是大于0的数)

  • 要看清题目要求

  • 原始数组与dp表的下标映射关系

最后一块石头的重量II

题目链接:

最后一块石头的重量II

要点:

  • 问题转化:将石头粉碎过程转化为对数组元素赋予正负号,使得两部分的差值最小。最终结果等价于求数组分割成两个子集的最小差值。
  • 数学推导:设总和为sum,理想分割为两子集和接近sum/2,差值最小为sum - 2*max_subset_sum
  • 状态转移优化:用01背包求不超过sum/2的最大子集和,直接计算最终差值。

老师代码:

class Solution
{
public:
    int lastStoneWeightII(vector<int>& stones)
    {
        // 1. 准备⼯作
        int sum = 0;
        for(auto x : stones) sum += x;
        int n = stones.size(), m = sum / 2;
        
        // 2. dp
        vector<vector<int>> dp(n + 1, vector<int>(m + 1));
        for(int i = 1; i <= n; i++)
            for(int j = 0; j <= m; j++)
            {
                dp[i][j] = dp[i - 1][j];
                if(j >= stones[i - 1])
                dp[i][j] = max(dp[i][j], dp[i - 1][j - stones[i - 1]] +
                stones[i - 1]);
            }
        
        // 3. 返回结果
        return sum - 2 * dp[n][m];
    }
}

老师思路:

先将问题「转化」成我们熟悉的题型。▪ 任意两块⽯头在⼀起粉碎,重量相同的部分会被丢掉,重量有差异的部分会被留下来。那就相当于在原始的数据的前⾯,加上「加号」或者「减号」,是最终的结果最⼩即可。也就是说把原始的⽯头分成两部分,两部分的和越接近越好。▪ ⼜因为当所有元素的和固定时,分成的两部分越接近数组「总和的⼀半」,两者的差越⼩。因此问题就变成了:在数组中选择⼀些数,让这些数的和尽量接近 sum / 2 ,如果把数看成物品,每个数的值看成体积和价值,问题就变成了「01 背包问题」

我的代码:

class Solution {
public:
    int lastStoneWeightII(vector<int>& stones) {
        int m = stones.size();
        int sum = 0;
        for(auto& s : stones) sum += s;
        int k = sum / 2;

        vector<vector<int>> dp(m + 1, vector<int>(k + 1));

        //填表
        for(int i = 1; i <= m; i++)
        {
            for(int j = 0; j <= k; j++)
            {
                dp[i][j] = dp[i - 1][j];
                if(j - stones[i - 1] >= 0) dp[i][j] = max(dp[i][j], dp[i - 1][j - stones[i - 1]] + stones[i - 1]);
            }
        }

        return sum - 2 * dp[m][k];
    }
};

我的思路:
1.两个石头相撞,结果要么为x-y,要么为y-x
2.无论你怎么两两相碰,永远有的数字前为正号,有的为负号,因此你总可以把最终式化为一堆和减去另外一堆数字和
3.因此我们要找的是这个集合的两个子集之和的最小差
4.要想子集之和差最小,则两者应该尽量接近或者相等
5.这个时候我们就可以把sum/2作为背包容量,使用01背包来解题了

我的笔记:

  • 若总和为奇数,sum/2向下取整不影响结果,因为差值只关心两部分的和差距。
  • 空间优化时,内层循环需逆序遍历,防止覆盖未使用的上一状态

解决01背包问题的注意事项

C++语法细节
  1. 数组下标设计
    • 1开始存储物品信息,避免处理i-1的边界问题(如nums[i-1]对应第i个物品)。
    • 二维dp表可优化为一维数组,但需逆序更新防止覆盖。
  2. 数据类型选择
    • 若结果可能较大(如目标和方案数),使用long long避免溢出。
    • vector初始化时,默认值需根据问题设定(如-1表示非法状态,01表示初始方案数)。
  3. 空间优化实现
    • 一维dp数组更新时,内层循环必须逆序(从大到小),保证每个物品只选一次。
    • 初始化需单独处理dp[0],如dp[0] = 1(目标和问题)或dp[0] = 0(最大价值问题)。
算法思路核心
  1. 状态定义
    • 明确dp[i][j]的含义,例如“前i个物品中选出体积不超过j的最大价值”或“组成和为j的方案数”。
  2. 状态转移方程
    • 分“选”与“不选”两种情况讨论,注意合法性判断(如体积不足时不能选)。
    • 累加或取最大值需根据问题目标调整。
  3. 边界条件处理
    • 初始化时,dp[0][0]通常有特殊含义(如空集和为0),其他位置根据问题设定初始值。
    • 处理负值或非法状态(如dp[j] = -1表示无法凑出体积j)。
  4. 问题转化技巧
    • 将复杂问题转化为背包模型,如“分割等和子集”转化为求子集和等于sum/2
    • 利用数学公式简化问题,如“目标和”中推导出a = (sum + target)/2
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Aeonio

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值