当前位置: 首页 > news >正文

【算法】动态规划:背包问题

头像
⭐️个人主页:@小羊
⭐️所属专栏:算法
很荣幸您能阅读我的文章,诚请评论指点,欢迎欢迎 ~

动图描述

目录

    • 1、01背包
      • 【模板】01背包
      • 分割等和子集
      • 目标和
      • 最后一块石头的重量 II
      • 一和零
      • 盈利计划
    • 2、完全背包
      • 【模板】完全背包
      • 零钱兑换
      • 零钱兑换 II
      • 完全平方数
    • 3、似包非包
      • 组合总和 Ⅳ
      • 不同的二叉搜索树


1、01背包

【模板】01背包

  • 【模板】01背包

请添加图片描述

(1) 定义状态 dp[i][j] 表示在前 i 个物品中挑选,总体积不超过 j 的所有选法中,最大的价值。

在这里插入图片描述

(2) 定义状态 dp[i][j] 表示在前 i 个物品中挑选,总体积刚好等于 j 的所有选法中,最大的价值。

在这里插入图片描述

#include <bits/stdc++.h>
using namespace std;

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

int main()
{
    cin >> n >> V;
    for (int i = 1; i <= n; i++)
        cin >> v[i] >> w[i];
    // (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]) 
                dp[i][j] = max(dp[i][j], dp[i - 1][j - v[i]] + w[i]);
        }
    }
    cout << dp[n][V] << endl;
    // (2)
    memset(dp, 0, sizeof dp);
    for (int i = 1; i <= V; i++) dp[0][i] = -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] && 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;
}

| 利用滚动数组做优化:

通过观察状态转移方程不难发现,在每次填dp表的时候都只依赖上一行的数据,因此可以用两个数组滚动来完成填表的工作,而如果我们从右往左填表从而避免数据被覆盖的话,用一个数组就能轻松完成任务。

所以以上面的代码利用滚动数组做空间优化只需要两步:

  1. 删除掉dp表的横坐标;
  2. 修改填表顺序。
#include <bits/stdc++.h>
using namespace std;

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

int main()
{
    cin >> n >> V;
    for (int i = 1; i <= n; i++)
        cin >> v[i] >> w[i];
    // (1)
    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;

    // (2)
    memset(dp, 0, sizeof dp);
    for (int i = 1; i <= V; i++) dp[i] = -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;
}

分割等和子集

  • 分割等和子集

在这里插入图片描述

题目本质就是能否从数组中找一个元素和为原数组和一半的子集,属于01背包问题。
定义状态 dp[i][j] 表示能否从前 i 个元素中找若干个和刚好为 j 的子集。

在这里插入图片描述

优化前代码:

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int n = nums.size(), sum = 0;
        for (int e : nums) sum += e;
        if (sum % 2) return false;
        int m = sum / 2;
        vector<vector<bool>> dp(n + 1, vector<bool>(m + 1));
        for (int i = 0; i <= n; i++) dp[i][0] = true;
        for (int i = 1; i <= n; i++)
            for (int j = 1; j <= m; 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][m];
    }
}; 

滚动数组优化后代码:

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int n = nums.size(), sum = 0;
        for (int e : nums) sum += e;
        if (sum % 2) return false;
        int m = sum / 2;
        vector<bool> dp(m + 1);
        dp[0] = true;
        for (int i = 1; i <= n; i++)
            for (int j = m; j >= nums[i - 1]; j--)
                dp[j] = dp[j] || dp[j - nums[i - 1]];
        return dp[m];
    }
}; 

目标和

  • 目标和

在这里插入图片描述
int m = (target + sum) / 2;
dp[i][j] 表示前 i 个数中选总和正好等于 j,总共有多少种选法。

nums[i] 有可能为0,因此要特别注意初始化。

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        int n = nums.size(), sum = 0;
        for (int e : nums) sum += e;
        int m = (target + sum) / 2;
        if (m < 0 || (target + sum) % 2) return 0;
        vector<vector<int>> dp(n + 1, vector<int>(m + 1));
        dp[0][0] = 1;
        for (int i = 1; i <= n; i++)
        {
            for (int j = 0; j <= m; 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][m];
    }
};
class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        int n = nums.size(), sum = 0;
        for (int e : nums) sum += e;
        int m = (target + sum) / 2;
        if (m < 0 || (target + sum) % 2) return 0;
        vector<int> dp(m + 1);
        dp[0] = 1;
        for (int i = 1; i <= n; i++)
            for (int j = m; j >= nums[i - 1]; j--)
                dp[j] += dp[j - nums[i - 1]];
        return dp[m];
    }
};

最后一块石头的重量 II

  • 最后一块石头的重量 II

在这里插入图片描述

本质就是尽可能地按照石头的重量两等分,返回最小的差值。
定义状态 dp[i][j] 表示在前 i 个石头中挑选,重量不超过 j 的最大和。

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

一和零

  • 一和零

在这里插入图片描述

dp[i][j][k] 表示从前 i 个字符串中挑选0的个数不超过 j,1的个数不超过 k 的最大子集的长度。

class Solution {
public:
    int findMaxForm(vector<string>& strs, int m, int n) {
        int len = strs.size();
        vector<vector<vector<int>>> dp(len + 1, vector<vector<int>>(m + 1, vector<int>(n + 1)));
        for (int i = 1; i <= len; i++)
        {
            int a = 0, b = 0;
            for (auto e : strs[i - 1])
                if (e == '0') a++;
                else b++;
            for (int j = 0; j <= m; j++)
            {
                for (int k = 0; k <= n; k++)
                {
                    dp[i][j][k] = dp[i - 1][j][k];
                    if (j >= a && k >= b)
                        dp[i][j][k] = max(dp[i][j][k], dp[i - 1][j - a][k - b] + 1);
                }
            }
        }
        return dp[len][m][n];
    }
};

优化后的代码:

class Solution {
public:
    int findMaxForm(vector<string>& strs, int m, int n) {
        int len = strs.size();
        vector<vector<int>> dp(m + 1, vector<int>(n + 1));
        for (int i = 1; i <= len; i++)
        {
            int a = 0, b = 0;
            for (auto e : strs[i - 1])
                if (e == '0') a++;
                else b++;
            for (int j = m; j >= a; j--)
                for (int k = n; k >= b; k--)
                    dp[j][k] = max(dp[j][k], dp[j - a][k - b] + 1);
        }
        return dp[m][n];
    }
};

盈利计划

  • 盈利计划

在这里插入图片描述

dp[i][j][k] 表示从前 i 个计划中选,总人数不超过 j,总利润至少为 k 的选法。

关于初始化,没有任务没有利润的选择只有一种。

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

2、完全背包

【模板】完全背包

  • 【模板】完全背包

在这里插入图片描述

(1)定义状态 dp[i][j] 表示在前 i 个物品中挑选总体积不超过 j 的最大价值。

在这里插入图片描述
在这里插入图片描述

(2)定义状态 dp[i][j] 表示在前 i 个物品中挑选总体积刚好等于 j 的最大价值。

在这里插入图片描述

#include <bits/stdc++.h>
using namespace std;

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

int main()
{
    // (1)
    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][j - v[i]] + w[i]);
        }
    }
    cout << dp[n][V] << endl;
    // (2)
    memset(dp, 0, sizeof dp);
    for (int i = 1; i <= V; i++) dp[0][i] = -0x3f3f3f3f;
    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][j - v[i]] + w[i]);
        }
    }
    cout << (dp[n][V] < 0 ? 0 : dp[n][V]) << endl;
    return 0;
}

注意:一维数组优化01背包是从右往左填表,因为根据状态转移方程可知在填表的过程中依赖的是上一行的数据;而优化完全背包是从左往右填表,根据状态转移方程可知在填表的过程中依赖的是同行的数据。

#include <bits/stdc++.h>
using namespace std;

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

int main()
{
    // (1)
    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[i]; j <= V; j++)
            dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
    cout << dp[V] << endl;
    // (2)
    memset(dp, 0, sizeof dp);
    for (int i = 1; i <= V; i++) dp[i] = -0x3f3f3f3f;
    for (int i = 1; i <= n; i++)
        for (int j = v[i]; j <= V; j++)
            dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
    cout << (dp[V] < 0 ? 0 : dp[V]) << endl;
    return 0;
}

零钱兑换

  • 零钱兑换

在这里插入图片描述
定义状态 dp[i][j] 表示从前 i 个硬币中挑选,总和正好等于 j 的所有选法中,最少的硬币个数。

在这里插入图片描述

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

零钱兑换 II

  • 零钱兑换 II

在这里插入图片描述

class Solution {
public:
    int change(int amount, vector<int>& coins) {
        vector<unsigned int> dp(amount + 1);
        dp[0] = 1;
        for (int e : coins)
            for (int j = e; j <= amount; j++)
                dp[j] += dp[j - e];
        return dp[amount];
    }
};

完全平方数

  • 完全平方数

在这里插入图片描述
dp[i][j] = min(dp[i - 1][j], dp[i][j - i * i] + 1);

class Solution {
public:
    int numSquares(int n) {
        int m = sqrt(n);
        vector<int> dp(n + 1, 0x3f3f3f3f);
        dp[0] = 0;
        for (int i = 1; i <= m; i++)
            for (int j = i * i; j <= n; j++)
                dp[j] = min(dp[j], dp[j - i * i] + 1);
        return dp[n];
    }
};

3、似包非包

组合总和 Ⅳ

  • 组合总和 Ⅳ

在这里插入图片描述

虽然题目说是求组合,但根据示例可知实际让我们求的是排列数。因为背包问题解决的是组合问题,所以这道题只能用类似背包问题的解法解决。

我们需要从nums中找一些排列刚好组成target,则对于某个排列,假设最后一个位置是nums[i],我们只需要从nums[i]中找一些排列刚好组成target-nums[i]即可。

定义状态 dp[i] 表示总和为 i 时的排列数。

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

不同的二叉搜索树

  • 不同的二叉搜索树

在这里插入图片描述

定义状态 dp[i] 表示当节点为 i 时一共有多少种二叉搜索树。

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[j - 1] * dp[i - j];
        return dp[n];
    }
};

本篇文章的分享就到这里了,如果您觉得在本文有所收获,还请留下您的三连支持哦~

头像
http://www.dtcms.com/a/99023.html

相关文章:

  • HTTP---基础知识
  • python实现股票数据可视化
  • 【电子通识】案例:为什么电子产品制造过程中使用马克笔在FFC/FPC连接器打点进行标记
  • 去噪算法大比拼
  • 手撕string
  • 【C#】ForEach vs foreach
  • swift-7-汇编分析闭包本质
  • 蓝桥杯省赛 棋盘 3533 二维差分+二维前缀和
  • Ruoyi-Vue拆解:优雅实现Vue页面过渡动画
  • 消息队列篇--通信协议篇--SSL/TLS协议
  • 【教学类-58-16】黑白三角拼图14——黑白三角图连接部分的白线(2*2宫格)
  • AI大模型底层技术——Multi-LoRA Combination Methods
  • 【免费】2007-2019年各省地方财政科学技术支出数据
  • leetcode 2360 图中最长的环 题解
  • 明天该穿哪件内衣出门?
  • 数据结构(并查集,图)
  • pip install cryptacular卡住,卡在downloading阶段
  • 嵌入式硬件篇---嘉立创PCB绘制
  • 【密码学】一文了解密码学的基本
  • 爱普生FC-135晶振5G手机的极端温度性能守护者
  • Ditto-Talkinghead:阿里巴巴数字人技术新突破 [特殊字符]️
  • Vue3组件响应式优化方法
  • Visual Studio 2022静态库与动态库创建及使用完全指南
  • Gerbv 与 Python 协同:实现 Gerber 文件智能分析与制造数据自动化
  • 知能行每日综测
  • vue.js前端条件渲染指令相关知识点
  • AI 时代,我们该如何写作?
  • MySQL———作业实验
  • Java进阶
  • 记录vite引入sass预编译报错error during build: [vite:css] [sass] Undefined variable.问题