动态规划——背包问题

概述

背包问题简而言之:你有一个背包 这个背包有一定的容积 要装入物品进去 不同物品体积不同 价值不同

背包的情况有两种 :01背包就是每种物品只有一个选了之后就是1不选就是0 完全背包就是每种物品个数可能有多个

装入的情况也有两种:恰好将背包装满的最大物品价值 背包可以不装满的最大价值

示例题目:https://www.nowcoder.com/share/jump/3925989451770378237514

先研究第一种情况 也就是不需要装满

很明显 物品的价值和体积分别要用一个一维数组记录 w、v

若是定义dp[i] 从前i个物品中选择 此时的最大价值 这样是不行的 因为没有东西表示你的背包容积 不知道背包容积去选择物品肯定不行

所以定义dp[i][j] 从前i个物品中选择 并且选择的物品的总体积不超过j 此时的最大价值 现在看能不能推导状态转移方程 

i位置物品可以选也可以不选 不选 那么dp[i][j] = dp[i-1][j] 选那么dp[i][j] = w[i] + dp[i-1][j-v[i]] 因为选择了i 那么前面的方程容积肯定不能超过j-v[i] 但是j-v[i] 可能小于0 那么就在选择i物品这种情况之前判断一下j-v[i] >= 0 只有条件满足才可以选 最终dp[i][j] 是这两者之间的最大值

初始化 因为dp存在i-1 所以w、v最好也多开一个空间 让物品的下标从1开始标号 dp也多开一行一列 dp[0][0] = 0 因为其意思为从前0个物品中选 总体积不超过0的最大价值 价值就是0 第一行后面的位置 表示从前0个物品中选总体积不超过j 价值肯定也是0 因为选不到 第一列其他位置 物品存在 但是体积不可能为0 所以价值也是0

返回值返回dp[n][V] V表示背包的容积

第二种情况 背包恰好装满

相比于第一种情况只需要改动一部分即可

状态表示 定义dp[i][j] 从前i个物品中选择 并且选择的物品的总体积恰好为j 此时的最大价值

状态转移方程 这里有一个重点区别 若是选不到体积恰好为j的dp[i][j]应该填什么 显然这种情况是一种不可能的情况 也就是错误情况 这里用-1表示 -1表示错误 。若是不选i位置dp[i][j] = dp[i-1][j] 此时若是 dp[i-1][j] 为-1 也不影响 因为前面选不到体积恰好为 j 的 那么不选 i 之后的 dp[i][j] 肯定也选不到体积恰好为j的 所以也填入-1 若是选 i位置 区别于不需要将背包填满的情况 只需要添加一下条件 原本是dp[i][j] = w[i] + dp[i-1][j-v[i]] 但是dp[i-1][j-v[i]] 可能为-1 若是让-1进来这个方程 物品的价值就被改变了 很显然不合逻辑 并且从状态表示出发 若是 前面dp[i-1][j-v[i]]==-1 这里选了i体积也不能凑成为j的 因此方程不能为dp[i][j] = w[i] + dp[i-1][j-v[i]] 所以想要方程为这个 要加一个条件 就是 dp[i-1][j-v[i]] != -1 从这个两个中选最大值

初始化 dp[0][0] = 0 第一行后面的从前0个物品中选 全部选不到容积恰好为j的 都填-1 第一列下面的 为全部为0

返回值 返回dp[n][V]

代码:

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

int main() 
{
    int n, V;
    cin >> n >> V;
    vector<int> v(n+1, 0), w(n+1, 0);
    vector<vector<int>> dp(n+1, vector<int>(V+1, 0));
    for (int i = 1; i <= n; i++)
        cin >> v[i] >> w[i];
    
    for (int i = 1; i <= n; i++)
    {
        for (int j = 1; j <= V; j++)
        {
            dp[i][j] = dp[i-1][j];
            if (j - v[i] >= 0) dp[i][j] = max(dp[i][j], w[i] + dp[i-1][j-v[i]]);
        }
    }
    cout << dp[n][V] << endl;

    for (int j = 1; j <= V; j++) dp[0][j] = -1;
    for (int i = 1; i <= n; i++)
    {
        for (int j = 1; j <= V; j++)
        {
            dp[i][j] = dp[i-1][j];
            if (j - v[i] >= 0 && dp[i-1][j-v[i]] != -1) dp[i][j] = max(dp[i][j], w[i] + dp[i-1][j-v[i]]);
        }
    }
    cout << (dp[n][V] == -1 ? 0 : dp[n][V]) << endl;
    return 0;
}

空间优化 滚动数组

原dp是二维的 但是每次填表需要的两个位置在同一行 那么可以这样优化空间 开两个一维数组 大小为V+1 第一个数组记录i-1那一行 也就是此时填表需要的一行 第二个数组表示当前行 填完当前行之后将第一个数组赋值为当前数组 那么填后面的dp时 填到第二个数组里面 参照第一个数组 这样向下滚动的过程就是滚动数组 完成了空间的优化 这是最基本的滚动数组

本题用另一种更佳的方式优化 填当前位置的时候 其实只需要两个位置 i-1行的两个位置 那么可以只开一个V+1的一维数组 这个数组记录的是i-1行的信息 此时为i行开始填表 从后往前填 这样可以保证i-1行的我们需要的两个位置没有被覆盖 也就是确保i行的内容是正确的 这样完成空间优化

在上述代码上的修改实际上就是要消去dp表的行保存列 并且改一下填表顺序即可

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

int main() 
{
    int n, V;
    cin >> n >> V;
    vector<int> v(n+1, 0), w(n+1, 0);
    vector<int> dp(V+1, 0);
    for (int i = 1; i <= n; i++)
        cin >> v[i] >> w[i];
    
    for (int i = 1; i <= n; i++)
    {
        for (int j = V; j >= 1; j--)
        {
            // dp[j] = dp[j]; // 去掉行之后就是这样的 那可以直接省略
            if (j - v[i] >= 0) dp[j] = max(dp[j], w[i] + dp[j-v[i]]);
        }
    }
    cout << dp[V] << endl;

    for (int j = 1; j <= V; j++) dp[j] = -1;
    for (int i = 1; i <= n; i++)
    {
        for (int j = V; j >= 1; j--)
        {
            // dp[j] = dp[j];
            if (j - v[i] >= 0 && dp[j-v[i]] != -1) dp[j] = max(dp[j], w[i] + dp[j-v[i]]);
        }
    }
    cout << (dp[V] == -1 ? 0 : dp[V]) << endl;
    return 0;
}

题目

416. 分割等和子集 - 力扣(LeetCode)

class Solution 
{
public:
    bool canPartition(vector<int>& nums) 
    {
        int n = nums.size();
        int sum = 0;
        for (auto& num : nums) sum += num;
        if (sum % 2 != 0) return false;


        vector<vector<bool>> dp(n+1, vector<bool>(sum/2+1, false));
        for (int i = 1; i <= n; i++) dp[i][0] = true;

        for (int i = 1; i <= n; i++)
        {
            for (int j = 1; j <= sum / 2; j++)
            {
                dp[i][j] = dp[i-1][j];
                if (j - nums[i-1] >= 0) dp[i][j] = dp[i-1][j] || dp[i-1][j-nums[i-1]];
            }
        }
        return dp[n][sum/2];
    }
};

// 先分析 我们只需要从数组中选则一部分元素 使之和为sum / 2 sum为nums中元素总和
// 那么每个元素就面临着选与不选的问题 从此我们想到01背包 这是物品没有价值的01背包 只需要使体积为sum / 2

// 状态表示
// 定义dp[i][j] : 从前i个元素中选 看能否等于j dp中存的值为true或者false

// 状态转移方程
// 不选i位置 为dp[i-1][j] 选i位置 为dp[i-1][j-nums[i]] 当然要有条件限制 就是j - nums[i] >= 0

// 初始化 
// dp表多开一行一列 此时要注意下标的映射关系
// dp[0][0] = true 第一行后面的全为false 因为不可能从0中选出不为0的数 第一列下面全为true 每一次都不选即可

// 返回值 返回dp[n][sum/2] 要注意只有sum为偶数才能这样动态规划 为奇数一定不能分割 直接返回false

// 空间优化
class Solution 
{
public:
    bool canPartition(vector<int>& nums) 
    {
        int n = nums.size();
        int sum = 0;
        for (auto& num : nums) sum += num;
        if (sum % 2 != 0) return false;


        vector<bool> dp(sum / 2 + 1, false);
        dp[0] = true;

        for (int i = 1; i <= n; i++)
        {
            for (int j = sum / 2; j >= 1; j--)
            {
                dp[j] = dp[j];
                if (j - nums[i-1] >= 0) dp[j] = dp[j] || dp[j-nums[i-1]];
            }
        }
        return dp[sum/2];
    }
};

494. 目标和 - 力扣(LeetCode)

// class Solution
// {
// public:
//     int findTargetSumWays(vector<int>& nums, int target)
//     {
//         int sum = 0;
//         for (auto& i : nums)
//             sum += i;
//         int a = (sum + target) / 2;
//         if ((sum + target) % 2 || a < 0)
//             return 0;

//         int n = nums.size();
//         vector<vector<int>> dp(n+1, vector<int>(a+1, 0));
//         dp[0][0] = 1;
//         for (int i = 1; i <= n; i++)
//         {
//             for (int j = 0; j <= a; j++) // j从0开始因为没有初始化第一列
//             {
//                 dp[i][j] = dp[i-1][j];
//                 if (j - nums[i-1] >= 0)
//                     dp[i][j] += dp[i-1][j - nums[i-1]];
//             }
//         }
//         return dp[n][a];
//     }
// };
// 空间优化
class Solution
{
public:
    int findTargetSumWays(vector<int>& nums, int target)
    {
        int sum = 0;
        for (auto& i : nums)
            sum += i;
        int a = (sum + target) / 2;
        if ((sum + target) % 2 || a < 0)
            return 0;

        int n = nums.size();
        vector<int> dp(a+1);
        dp[0] = 1;
        for (int i = 1; i <= n; i++)
        {
            for (int j = a; j >= 0; j--) // j从0开始因为没有初始化第一列
            {
                if (j - nums[i-1] >= 0)
                    dp[j] += dp[j - nums[i-1]];
            }
        }
        return dp[a];
    }
};
// 转换:将nums中的数分为正负两个部分(a、b表示正负两部分绝对值的和)并且a-b=target(按照提议反推,假设a、b就是我们最终分出来的正负部分)
// 又因为a+b=sum,那么消去b,a = (sum + target)/ 2

// 这样一来就是01背包了,意思就是选择物品,使之总体积恰好为一个数
// 定义dp[i][j] 从前i个物品中挑选,体积恰好我j的选法

// 状态转移方程:
// 不选nums[i],那么dp[i][j]有dp[i-1][j]种选法;选nums[i],那么dp[i][j]有dp[i-1][j-nums[i]]种选法(要判断j-nums[i] >= 0)
// 最终dp[i][j]为两者相加

// 初始化
// 多开一行一列
// 第一行除了dp[0][0]全部初始化为0,dp[0][0]=1
// 第一列除了dp[0][0],看是什么情况:因为nums[i]可能为0,那么从前i个位置选体积为0就可能出现多种情况了,比如dp[1][0],但是nums[1]=0,后面也可能会出现,这样一来不能简单地初始化0或者1了。事实上这一行不用初始化,通过观察,选择这个位置要先判断j-nums[i] >= 0,而j为0,nums[i]>=0,只有nums[i]==0的时候第一列才会填表,但是现在使用的上一列的值(j-nums[i] = 0),没使用左上角,所以不用初始化

// 返回值
// dp[n][a]

1046. 最后一块石头的重量 - 力扣(LeetCode)

class Solution
{
public:
    int lastStoneWeightII(vector<int>& stones)
    {
        int n = stones.size(), sum = 0;
        for (int i = 0; i < n; i++)
            sum += stones[i];
        int target = sum / 2;
        vector<int> dp(target + 1, 0);
        for (int i = 1; i <= n; i++)
        {
            for (int j = target; j >= 1; j--)
            {
                if (j - stones[i - 1] >= 0)
                    dp[j] = max(dp[j], dp[j - stones[i - 1]] + stones[i - 1]);
            }
        }
        return sum - 2 * dp[target];
    }
};

// 问题转换:
// 本质上就是给数字前面加上+、-号然后相加求出最小和,和目标和类似
// 将整个数组中数字分为正负两部分,求出两者绝对值的最小差值
// 一个数字只有将其分为接近这个数字一半的两部分,这两部分的差值才是最小的

// 01背包
// 背包容积sum/2,物品体积、价值都是数字数值

完全背包_牛客题霸_牛客网

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

int main()
{
    int n, V;
    cin >> n >> V;
    vector<int> v(n), w(n);
    for (int i = 0; i < n; i++)
    {
        cin >> v[i] >> w[i];
    }
    vector<vector<int>> dp(n + 1, vector<int>(V + 1, 0));
    for (int i = 1; i <= n; i++)
    {
        for (int j = 1; j <= V; j++)
        {
            dp[i][j] = dp[i - 1][j];
            if (j - v[i - 1] >= 0)
                dp[i][j] = max(dp[i][j], dp[i][j - v[i - 1]] + w[i - 1]);
        }
    }
    cout << dp[n][V] << endl;
    for (int j = 1; j <= V; j++)
        dp[0][j] = -1;
    for (int i = 1; i <= n; i++)
    {
        for (int j = 1; j <= V; j++)
        {
            dp[i][j] = dp[i - 1][j];
            if (j - v[i - 1] >= 0 && dp[i][j - v[i - 1]] != -1)
                dp[i][j] = max(dp[i][j], dp[i][j - v[i - 1]] + w[i - 1]);
        }
    }
    cout << (dp[n][V] == -1 ? 0 : dp[n][V]) << endl;
    return 0;
}

// 第一问
// 状态表示
// 定义dp[i][j]:从前i个物品中挑选,总体积不超过j的最大价值

// 状态转移方程
// dp[i][j] : 不选i位置为dp[i-1][j], 选i位置可以选多个i即max(dp[i-1][j-v[i]]+w[i], dp[i-1][j-2*v[i]]+2*w[i]......, dp[i-1][j-k*v[i]]+k*w[i])
// 所以dp[i][j] = max(dp[i-1][j], dp[i-1][j-v[i]]+w[i], dp[i-1][j-2*v[i]+2*w[i]]......, dp[i-1][j-k*v[i]]+k*w[i]);
// 出现这种很多个方程的情况下,需要考虑简化,利用数学规律
// dp[i][j-v[i]] = max(dp[i-1][j-v[i]], dp[i-1][j-2*v[i]]+w[i], dp[i-1][j-3*v[i]]+2*w[i], ........, dp[i-1][j-x*v[i]]+(x-1)*w[i]);
// 观察第一个公式的右边部分除了第一个的后面部分,和下面公式的右边部分就差一个w[i]
// 所以dp[i][j] = max(dp[i-1][j], dp[i][j-v[i]]+w[i]);

// 初始化
// 多开一行一列,第一行全部为0,第一列全部为0

// 填表顺序
// 只能从上往下,从左往右,因为dp[i][j-v[i]]+w[i]需要用到同行中前面的值

// 返回值
// dp[n][V]

// 第二问

// 状态标识
// 定义dp[i][j]:从前i个物品中挑选,总体积恰好为j的最大价值

// 状态转移方程
// dp[i][j] = max(dp[i-1][j], dp[i][j-v[i]]+w[i]);
// 但是要注意一个点,就是dp[i][j-v[i]]可能存在这种情况,就是不能恰好填满,这个时候设置为-1,所以除了要判断j-v[i]>=0,还需要dp[i][j-v[i]]!=-1

// 初始化
// 多开一行一列,第一行第一个为0,剩下为-1(从前0个物品中选,总体积不可能恰好为一个非0的),第一列全部为0

// 返回值
// 要判断是不是-1

LCR 103. 零钱兑换 - 力扣(LeetCode)

// class Solution
// {
// public:
//     const int INF = 0x3f3f3f3f;
//     int coinChange(vector<int>& coins, int amount) 
//     {
//         int n = coins.size();
//         vector<vector<int>> dp(n+1, vector<int>(amount+1, INF));
//         dp[0][0] = 0;
//         for (int i = 1; i <= n; i++)
//             dp[i][0] = 0;
//         for (int i = 1; i <= n; i++)
//         {
//             for (int j = 1; j <= amount; j++)
//             {
//                 dp[i][j] = dp[i-1][j];
//                 if (j - coins[i-1] >= 0 && dp[i][j - coins[i-1]] < INF)
//                     dp[i][j] = min(dp[i][j], dp[i][j - coins[i-1]] + 1);
//             }
//         }
//         return dp[n][amount] >= INF ? -1 : dp[n][amount];
//     }
// };
// 空间优化
class Solution
{
public:
    const int INF = 0x3f3f3f3f;
    int coinChange(vector<int>& coins, int amount)
    {
        int n = coins.size();
        vector<int> dp(amount + 1, INF);
        dp[0] = 0;

        for (int i = 1; i <= n; i++)
        {
            for (int j = 1; j <= amount; j++)
            {
                if (j - coins[i - 1] >= 0 && dp[j - coins[i - 1]] < INF)
                    dp[j] = min(dp[j], dp[j - coins[i - 1]] + 1);
            }
        }
        return dp[amount] >= INF ? -1 : dp[amount];
    }
};

// 看到无限想到完全背包

// 状态表示
// 定义dp[i][j]:从前i个硬币里面选,总金额恰好为j的最小硬币个数

// 状态转移方程
// 和完全背包模板一样
// dp[i][j] = min(dp[i-1][j], dp[i][j-coins[i]] + 1); (只有完全背包可以用这个数学规律)

// 初始化
// 多开一行一列,不可能的地方填0x3f3f3f3f(已经很大了)

// 返回值
// 先判断是否>=0x3f3f3f3f,大于则返回-1,否则返回dp[n][amount];

518. 零钱兑换 II - 力扣(LeetCode)

// class Solution 
// {
// public:
//     int change(int amount, vector<int>& coins) 
//     {
//         int n = coins.size();
//         vector<vector<unsigned int>> dp(n + 1, vector<unsigned int>(amount + 1, 0)); // 超出范围直接截断
//         for (int i = 1; i <= n; i++)
//             dp[i][0] = 1;
//         for (int i = 1; i <= n; i++)
//         {
//             for (int j = 1; j <= amount; j++)
//             {
//                 dp[i][j] = dp[i-1][j];
//                 if (j - coins[i-1] >= 0)
//                     dp[i][j] += dp[i][j - coins[i-1]];
//             }
//         }
//         return dp[n][amount];
//     }
// };

// 空间优化
class Solution
{
public:
    int change(int amount, vector<int>& coins)
    {
        int n = coins.size();
        vector<unsigned int> dp(amount + 1, 0); // 超出范围直接截断
        dp[0] = 1;
        for (int i = 1; i <= n; i++)
        {
            for (int j = 1; j <= amount; j++)
            {
                dp[j] = dp[j];
                if (j - coins[i - 1] >= 0)
                    dp[j] += dp[j - coins[i - 1]];
            }
        }
        return dp[amount];
    }
};

// 组合有多少种?每一次选的硬币变换了都要加上这个选法的种类
// 选与不选都是选法,都要加上去

279. 完全平方数 - 力扣(LeetCode)

class Solution
{
public:
    const int INF = 0x3f3f3f3f;
    int numSquares(int n)
    {
        int m = sqrt(n);
        vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
        for (int j = 1; j <= n; j++)
            dp[0][j] = INF; // 因为是最少个数,所以将不可能的选法设置为大的数
        for (int i = 1; i <= m; i++)
        {
            for (int j = 1; j <= n; j++)
            {
                dp[i][j] = dp[i - 1][j];
                if (j - i * i >= 0)
                {
                    dp[i][j] = min(dp[i][j], dp[i][j - i * i] + 1);
                }
            }
        }
        return dp[m][n];
    }
};

// 就相当于从1到根号n的数种找一些数的平方和恰好等于n,求选的数的最少个数
// 完全背包

// dp[i][j] = (dp[i-1][j], dp[i-1][j-i^2]+1, dp[i-1][j-2*i^2]+2........, dp[i-1][j-n*i^2]+n);
// dp[i][j-i^2] = (dp[i-1][j-i^2], dp[i-1][j-2*i^2]+1, dp[i-1][j-3*i^2]+2........, dp[i-1][j-n*i^2]+n);

474. 一和零 - 力扣(LeetCode)

// class Solution 
// {
// public:
//     int findMaxForm(vector<string>& strs, int m, int n) 
//     {
//         int len = strs.size();
//         vector<pair<int, int>> cnt01(len);
//         // 计算每个字符串的0、1个数
//         for (int i = 0; i < len; i++)
//         {
//             int cnt0 = 0, cnt1 = 0;
//             for (auto& c : strs[i])
//             {
//                 if (c == '0') cnt0++;
//                 else cnt1++;
//             }
//             cnt01[i] = make_pair(cnt0, cnt1);
//         }
//         vector<vector<vector<int>>> dp(len + 1, vector<vector<int>>(m + 1, vector<int>(n + 1, 0)));
//         for (int i = 1; i <= len; i++)
//         {
//             for (int j = 0; j <= m; j++)
//             {
//                 for (int k = 0; k <= n; k++)
//                 {
//                     dp[i][j][k] = dp[i - 1][j][k];
//                     int a = cnt01[i - 1].first, b = cnt01[i - 1].second;
//                     if (j - a >= 0 && k - b >= 0)
//                         dp[i][j][k] = max(dp[i][j][k], dp[i - 1][j - a][k - b] + 1);
//                 }
//             }
//         }    
//         return dp[len][m][n];
//     }
// };

// 实际上也是完全背包问题,只不过限定条件从一维变为了二维(可以理解为一维只需要保证体积不超,而现在重量也要保证):从数组中选一些字符串,这些字符串0、1的数量和不超过m、n

// 状态表示
// 定义dp[i][j][k]:从前i个字符串中选,0总和不超过j,1总和不超过k的选取字符串最大个数

// 状态转移方程
// 不选i位置,那么dp[i][j][k] = dp[i-1][j][k];
// 选i位置(a、b分别表示i位置字符串0、1的个数),那么dp[i][j][k] = dp[i-1][j-a][k-b] + 1(是最大个数,因此选一个是加1),但是需要保证下标合法
// 最终选两者最大值
// a、b可以提前计算

// 初始化
// 多开一个位置,全部初始化为0即可,从前0个中选使之0、1数量不超过j,k的最大长度明显选不到,所以初始化为0;从前i个中选,使之0,1数量不超过0,0因为没有空串,所以不可能选到,初始化为0即可,dp[0][0][0]这个位置显而易见是0

// 返回值dp[len][m][n]

// 空间优化需要从后往前,避免需要的数据被覆盖

class Solution 
{
public:
    int findMaxForm(vector<string>& strs, int m, int n) 
    {
        int len = strs.size();
        vector<pair<int, int>> cnt01(len);
        // 计算每个字符串的0、1个数
        for (int i = 0; i < len; i++)
        {
            int cnt0 = 0, cnt1 = 0;
            for (auto& c : strs[i])
            {
                if (c == '0') cnt0++;
                else cnt1++;
            }
            cnt01[i] = make_pair(cnt0, cnt1);
        }
        vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
        for (int i = 1; i <= len; i++)
        {
            for (int j = m; j >= 0; j--)
            {
                for (int k = n; k >= 0; k--)
                {
                    int a = cnt01[i - 1].first, b = cnt01[i - 1].second;
                    if (j - a >= 0 && k - b >= 0)
                        dp[j][k] = max(dp[j][k], dp[j - a][k - b] + 1);
                }
            }
        }    
        return dp[m][n];
    }
};

879. 盈利计划 - 力扣(LeetCode)

class Solution 
{
public:
    const int mode = 1e9 + 7; 
    int profitableSchemes(int n, int minProfit, vector<int>& group, vector<int>& profit) 
    {
        int m = group.size();
        vector<vector<vector<int>>> dp(m + 1, vector<vector<int>>(n + 1, vector<int>(minProfit + 1, 0)));
        for (int j = 0; j <= n; j++)
            dp[0][j][0] = 1;
        for (int i = 1; i <= m; i++)
        {
            for (int j = 0; j <= n; j++)
            {
                for (int k = 0; k <= minProfit; k++)
                {
                    dp[i][j][k] = dp[i - 1][j][k];
                    if (j - group[i - 1] >= 0)
                        dp[i][j][k] += dp[i - 1][j - group[i - 1]][max(0, k - profit[i - 1])];
                    dp[i][j][k] %= mode;
                }
            }
        }
        return dp[m][n][minProfit];
    }
};

// 二维01背包问题

// 状态表示
// 选任务,人数不超过n,并且利润超过minProfit的所有选法

// 状态转移方程
// 定义dp[i][j][k]: 从前i个任务中选,人数不超过j,利润超过k的选法
// 不选i位置任务,则dp[i][j][k] = dp[i-1][j][k]
// 选i位置任务,dp[i][j][k] += dp[i-1][j-group[i]][k-profit[i]]
// 其中j-group[i]不能小于0,小于0说明人数超过限制
// k - profit[i]可以小于0,小于0说明从前i-1任务中选利润超过负数的,显然随便选,但是下标可能越界因此要这样写max(0, k-profit[i]),
// 因为从前i-1个中选利润大于0的也是随便选

// 初始化
// 每一维多开一行
// dp[0][j][0] = 1,无论j是几都只有一种选法,就是不选

39. 组合总和 - 力扣(LeetCode)

class Solution 
{
public:
    int combinationSum4(vector<int>& nums, int target) 
    {
        int n =nums.size();
        vector<unsigned int> dp(target + 1);
        dp[0] = 1;
        for (int i = 1; i <= target; i++)
        {
            for (int j = 0; j < n; j++)
            {
                if (i - nums[j] >= 0)
                    dp[i] += dp[i - nums[j]];
            }
        }
        return dp[target];
    }
};

// 似包非包
// 看似是完全背包问题,但是这题每一种选法可以排列,完全背包解决不了
// 需要找重复子问题(多刷题总结经验)

// 状态表示
// dp[i]:凑成数i的组合总数

// 状态转移方程
// dp[i] += dp[i - nums[j]]

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

// 返回值
// dp[target]

96. 不同的二叉搜索树 - 力扣(LeetCode)

class Solution 
{
public:
    int numTrees(int n) 
    {
        vector<int> dp(n + 1);
        dp[0] = 1;
        for (int i = 1; i <= n; i++)
            for (int j = 1; j <= i; j++)
                dp[i] += dp[i - j] * dp[j - 1];
        return dp[n];
    }
};

// 需要找重复子问题(多刷题总结经验)

// 状态表示
// dp[i]:以i为根节点能组成几种不同的二叉搜索树
// 选定一个数i(1~n)作为根节点,找以这个值为根节点的二叉搜素树,那么左、右子树的二叉搜索树个数就是重复子问题

// 状态转移方程
// 以i为根节点,左子树的根节点变化在1~i-1之间,右子树根节点变化在i+1~n之间

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值