【动态规划篇】:解析背包问题--动态规划塑造的算法利器

✨感谢您阅读本篇文章,文章内容是个人学习笔记的整理,如果哪里有误的话还请您指正噢✨
✨ 个人主页:余辉zmh–CSDN博客
✨ 文章所属专栏:动态规划篇–CSDN博客

在这里插入图片描述

文章目录

  • 一.01背包问题
    • 1.模板题
    • 2.例题
      • 1.分割等和子集
      • 2.目标和
      • 3.最后一块石头的重量
  • 二.完全背包问题
    • 1.模板题
    • 2.例题
      • 1.零钱兑换1
      • 2.零钱兑换2
      • 3.完全平方数
  • 三.二维费用背包问题
    • 例题
      • 1.一和零
      • 2.盈利计划

一.01背包问题

动态规划中的01背包问题其目标是在给定背包容量的情况下,选择物品以最大化总价值,其中每个物品个数有限制,只有一个,因此只能选或者不选两种情况,这里通过一道模板题来讲解:

1.模板题

题目

在这里插入图片描述

在这里插入图片描述

算法原理

如图所示:

在这里插入图片描述

代码实现

const int N = 1001;
//全局变量,默认值为0
int n, V, dp[N][N], v[N], w[N];

int main(){
    //输入物品个数和背包容量
    cin >> n >> V;

    //输入每个物品的体积和价值
    for (int i = 1; i <= n; i++){
        cin >> v[i] >> w[i];
    }

    //问题一状态表示 dp[i][j]表示挑选前i个物品,体积不超过j的最大价值
    //初始值全部设置为0
    for (int i = 1; i <= n; i++){
        for (int j = 1; j <= V; j++){
            //如果不挑选当前i物品,找挑选前i-1个物品不超过j的最大价值
            dp[i][j] = dp[i - 1][j];
            //如果挑选当前i物品,找挑选前i-1个物品不超过j-v[i]的最大价值然后加上当前物品的价值
            //两种情况取最大值
            if (j >= v[i]){
                dp[i][j] = max(dp[i - 1][j - v[i]] + w[i], dp[i][j]);
            }
        }
    }
    //输出结果
    cout << dp[n][V] << endl;

    //初始化状态表
    memset(dp, 0, sizeof dp);

    //问题二状态表示 dp[i][j]表示挑选前i个物品,提及恰好等于j的最大价值
    //第一行除第一个外,其他全初始值设置为-1
    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++){
            //如果不挑选当前i物品,找挑选前i-1个物品,体积恰好等于j的最大价值
            dp[i][j] = dp[i - 1][j];

            //如果挑选当前i物品,找挑选前i-1个物品,体积恰好等于j-v[i]的最大价值
            if (j >= v[i] && dp[i - 1][j - v[i]] != -1){
                dp[i][j] = max(dp[i - 1][j - v[i]] + w[i], dp[i][j]);
            }
        }
    }
    //输出结果
    cout << (dp[n][V] == -1 ? 0 : dp[n][V]) << endl;
    return 0;
}

空间优化

将原本的二位状态表优化为一维状态表,每行都在上一行的对应下标的位置上更新,注意填表顺序必须是每行从右往左。

const int N = 1001;
//全局变量,默认值为0
int n, V, dp[N], v[N], w[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;
}

2.例题

1.分割等和子集

题目

在这里插入图片描述

算法原理

根据题意,将数组分割成两个相等的子集,因此需要先求出整个数组的总和sum,然后除以2,就是每个子集的总和,然后从原数组中挑选元素使其总和为sum/2,如果能找到就表示能分割成两个相等的子集,如果不能找到就表示不能分割。因此就转换成01背包问题中的问题二,挑选物品使其恰好装满背包。

状态表示: dp[i][j]表示挑选前i个数,所有的选法中,能否恰好等于j。

状态转移方程:根据当前位置元素nums[i-1](下表映射需要减一)选或者不选分为两种情况,选:dp[i-1][j];不选:dp[i-1][j-nums[i-1]](前提条件:j>=nums[i-1]);两种情况满足其中一种即可。

初始化:添加第0行和第0列,除第一行外其余全部为true。

填表顺序:从第一行到最后一行,其中每一行从左往右。

返回值:dp[n][sum],从前n个数中挑选,所有的选法中能否恰好等于sum。

代码实现

bool canPartition(vector<int>& nums){
    int sum=0;
    for(auto num : nums){
        sum += num;
    }

    if (sum % 2 == 1){
        return false;
    }
    sum /= 2;
    int n = nums.size();

    //状态表示 dp[i][j]表示挑选前i个数,所有的选法中,能否恰好等于j
    vector<vector<bool>> dp(n + 1, vector<bool>(sum + 1,true));
    //初始化 添加第0行和第0列,除第一行外其余全部为true
    for (int j = 1; j <= sum; j++){
        dp[0][j] = false;
    }

    //填表
    for (int i = 1; i <= n; i++){
        for (int j = 1; j <= sum; j++){
            //当前位置i元素要么选要么不选,两种情况满足一种即可
            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][sum];
}

空间优化

bool canPartition(vector<int>& nums){
    int n = nums.size();

    int sum = 0;
    for(auto num : nums){
        sum += num;
    }
    if (sum % 2 == 1){
        return false;
    }
    sum /= 2;

    //状态表示
    vector<bool> dp(sum + 1);
    //初始化
    dp[0] = true;

    //填表
    for (int i = 1; i <= n; i++){
        for (int j = sum; j >= nums[i - 1]; j--){
            if (dp[j - nums[i - 1]] != false){
                dp[j] = dp[j] || dp[j - nums[i - 1]];
            }
        }
    }

    //返回值
    return dp[sum];
}

2.目标和

题目

在这里插入图片描述

算法原理

根据题意在原数组中每个元素前添加正好或者负号,相当于将原数组分成两堆,一堆为正数,一堆为负数,假设正数的和为a,负数的和的绝对值为b,原数组的和为sum,a-b=target && a+b=sum => a=(sum+target)/2,因此转化成从原数组中挑选几个数和为a,总共有多少种选法。

状态表示: dp[i][j]表示从前i个数中挑选,和恰好为j,总共有多少种选法。

初始化: dp[0][0]表示从前0个数中挑选,和恰好为0,有1种选法,第0行表示从前0个数中挑选,和恰好为j(j>=1),有0种选法。

状态转移方程:根据当前位置元素nums[i-1](下表映射需要减一)选或者不选分为两种情况,选:dp[i-1][j];不选:dp[i-1][j-nums[i-1]](前提条件:j>=nums[i-1]);因为要统计所有选法,所以两种情况选法个数相加。

填表顺序:从第一行到最后一行,其中每一行从左往右。

返回值:dp[n][a],从前n个数中挑选,和恰好为a,总共有多少种选法。

代码实现

int findTargetSumWays(vector<int>& nums, int target){
    int n = nums.size();
    int sum = 0;
    for(auto num : nums){
        sum += num;
    }
    if (sum + target < 0 || (sum + target) % 2 == 1){
        return 0;
    }
    int a = (sum + target) / 2;

    //状态表示 dp[i][j]表示从前i个数中挑选,和恰好为j,总共有多少种选法
    vector<vector<int>> dp(n + 1, vector<int>(a + 1));

    //初始化 dp[0][0]表示从前0个数中挑选,和恰好为0,有1种选法
    //第0行表示从前0个数中挑选,和恰好为j(j>=1),有0种选法
    dp[0][0] = 1;

    //填表
    for (int i = 1; i <= n; i++){
        for (int j = 0; j <= a; 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][a];
}

空间优化

int findTargetSumWays(vector<int>& nums, int target){
    int n = nums.size();
    int sum = 0;
    for(auto num :nums){
        sum += num;
    }
    if (sum + target < 0 || (sum + target) % 2 == 1){
        return 0;
    }
    int a = (sum + target) / 2;

    //状态表示
    vector<int> dp(a + 1);
    //初始化
    dp[0] = 1;

    //填表
    for (int i = 1; i <= n; i++){
        for (int j = a; j >= nums[i - 1]; j--){
            //状态转移方程
            dp[j] += dp[j - nums[i - 1]];
        }
    }

    //返回值
    return dp[a];
}

3.最后一块石头的重量

题目

在这里插入图片描述

算法原理

根据题意要求,假设现在有一堆石头[a,b,c,d,e],先挑选b和c两个石头,剩余[a,b-c,d,e],然后挑选d和e两个石头,剩余[a,b-c,d-e],然后挑选a和b-c两个石头,剩余[a-b+c,d-e],最后两个石头相减,剩余[a-b+c-d+e],最后剩余石头的质量就是每个石头的质量相加减,其实和上一题有点相似,最后这个表达式就是在每个数字前添加正号和负号,然后分成两堆,一堆为正数,一堆为负数。

假设所有正数的总和为a,所有负数的总和绝对值为b,所有数的和为a+b=sum,要使a-b的绝对值最小,只需a和b两个值无限接近总和sum的一半即可,这样两个数相减一定是最小的,因此本道题就是转换成从所有石头中挑选,使其总和不超过aim=sum/2的最大重量。

状态表示: dp[i][j]表示挑选前i个石头,使重量和接近j所有选法种,最大的重量。

初始化 :dp[0][0]表示挑选前0个石头,使重量和接近0的最大重量,就是0,第0行表示挑选前0个石头,使重量和接近j(j>=1)的最大重量,就是0,第0列表示挑选前i(i>=1)个石头,使重量和接近0的最大重量,还是0。

状态转移方程:根据当前石头选还是不选分情况讨论,选:dp[i-1][j];不选:dp[i-1][j-stones[i-1]](前提条件:j>=stones[i-1]);因为要统计最大值,所以两种情况取最大值。

返回值:因为找到当前一堆的石头重量为dp[n][aim],另一堆石头重量就是sum-dp[n][aim],根据上面的表达式,最后两堆石头相减就是最小值,因此返回结果就是sum - 2 * dp[n][aim]

代码实现

int lastStoneWeightII(vector<int>& stones){
    int n = stones.size();
    int sum = 0;
    for(auto num : stones){
        sum += num;
    }
    int aim = sum / 2;

    //状态表示 dp[i][j]表示挑选前i个石头,使重量和接近j所有选法种,最大的重量
    vector<vector<int>> dp(n + 1, vector<int>(aim + 1));
    //初始化 dp[0][0]表示挑选前0个石头,使重量和接近0的最大重量,就是0
    //第0行表示挑选前0个石头,使重量和接近j(j>=1)的最大重量,就是0
    //第0列表示挑选前i(i>=1)个石头,使重量和接近0的最大重量,还是0

    //填表
    for (int i = 1; i <= n; i++){
        for (int j = 1; j <= aim; j++){
            //不挑选当前i石头
            dp[i][j] = dp[i - 1][j];
            //挑选当前i石头
            if (j >= stones[i - 1]){
                dp[i][j] = max(dp[i - 1][j - stones[i - 1]] + stones[i - 1], dp[i][j]);
            }
        }
    }

    //返回值
    return sum - 2 * dp[n][aim];
}

空间优化

int lastStoneWeightII(vector<int>& stones){
    int n = stones.size();
    int sum = 0;
    for(auto num : stones){
        sum += num;
    }
    int aim = sum / 2;

    //状态表示
    vector<int> dp(aim + 1);

    //填表
    for (int i = 1; i <= n; i++){
        for (int j = aim; j >= stones[i - 1]; j--){
            //状态转移方程
            dp[j] = max(dp[j], dp[j - stones[i - 1]] + stones[i - 1]);
        }
    }

    //返回值
    return sum - 2 * dp[aim];
}

二.完全背包问题

动态规划中的完全背包问题其目标是在给定背包容量的情况下,选择物品以最大化总价值,而每个物品个数不限制,可以选0个,1个,2个等等多种情况,这里通过一道模板题来讲解:

1.模板题

题目

在这里插入图片描述

在这里插入图片描述

算法原理

如图所示:

在这里插入图片描述

代码实现

const int N = 1001;
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];
    }

    //问题一状态表示 dp[i][j]表示挑选前i个物品,背包体积不超过j的所有选法中,最大的价值
    //填表
    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 - v[i]] + w[i], dp[i][j]);
            }
        }
    }
    //输出结果
    cout << dp[n][V] << endl;

    //初始化状态表
    memset(dp, 0, sizeof dp);

    //问题二状态表示 dp[i][j]表示挑选前i个物品,背包体积恰好等于j的所有选法中,最大的价值
    //先初始化第一行为-1,表示不存在的情况
    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][j - v[i]] != -1){
                dp[i][j] = max(dp[i][j - v[i]] + w[i], dp[i][j]);
            }
        }
    }
    //输出结果
    cout << (dp[n][V] == -1 ? 0 : dp[n][V]) << endl;
    return 0;
}

空间优化

将原本的二维状态表优化为一维状态表,每行的状态值都在上一行对应的位置上填写,注意填写顺序每行必须是从左往右填写。

int dp[N];
int main(){
    //输入物品个数和背包体积
    cin >> n >> V;

    //输入每个物品的体积和价值
    for (int i = 1; i <= n; i++){
        cin >> v[i] >> w[i];
    }

    //问题一状态表示 dp[i][j]表示挑选前i个物品,背包体积不超过j的所有选法中,最大的价值
    //填表
    for (int i = 1; i <= n; i++){
        //和01背包不同的是从左往右填
        for (int j = v[i]; j <= V; j++){
            dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
        }
    }
    //输出结果
    cout << dp[V] << endl;

    //初始化状态表
    memset(dp, 0, sizeof dp);

    //问题二状态表示 dp[i][j]表示挑选前i个物品,背包体积恰好等于j的所有选法中,最大的价值
    //先初始化第一行为-1,表示不存在的情况
    for (int j = 1; j <= V; j++){
        dp[j] = -1;
    }
    //填表
    for (int i = 1; i <= n; i++){
        for (int j = v[i]; j <= V; 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;
}

2.例题

1.零钱兑换1

题目

在这里插入图片描述

算法原理

根据题意可以看出本道题就是典型的完全背包问题,对应模板题中的问题二,使其背包中恰好装满。

状态表示: dp[i][j]表示从前i个硬币中挑选,总和恰好等于j,所有的选法中最少的硬币个数。

状态转移方程:根据当前硬币coins[i-1](下标映射要减一)选0个,1个,2个等等分为多种情况,因为要找最少硬币个数,所以是所有情况取最小值,这里的推导过程和模板题一样,这里直接写结论:dp[i][j]=min(dp[i-1][j],dp[i][j-coins[i-1]+1])前提条件:j>=coins[i-1],(注意因为状态值表示的硬币个数,因此当前位置的硬币选多少个,后面要加上对应的个数,这就是为什么后面要+1)。

初始化:dp[0][0]表示从前0个硬币中挑选,总和恰好等于0,最少的硬币个数就是0;除第一行第一个外,其余全部初始化为INF(因为状态转移方程中要取最小值,所以用0x3f3f3f3f,最大值的一半),表示不存在的情况。

填表顺序:从第一行到最后一行,其中每一行从左往右。

返回值:dp[n][amount] >= INF ? -1 : dp[n][amount],需要先判断一下是否等于INF,如果等于说明从前n个数中挑选,使其总和恰好等于amount不存在,因此返回-1,如果不等于就是存在,返回最小硬币个数。

代码实现

const int INF = 0x3f3f3f3f;
int coinChange(vector<int>& coins, int amount){
    int n = coins.size();

    //状态表示 dp[i][j]表示从前i个硬币中挑选,总和恰好等于j,所有的选法中最少的硬币个数
    vector<vector<int>> dp(n + 1, vector<int>(amount + 1));
    //初始化第一行为INF,表示不存在的情况
    for (int j = 1; j <= amount; j++){
        dp[0][j] = INF;
    }

    //填表
    for (int i = 1; i <= n; i++){
        for (int j = 0; j <= amount; j++){
            dp[i][j] = dp[i - 1][j];
            if (j >= coins[i-1]){
                dp[i][j] = min(dp[i][j - coins[i-1]] + 1, dp[i][j]);
            }
        }
    }

    //返回值
    return dp[n][amount] >= INF ? -1 : dp[n][amount];
}

空间优化

int coinChange(vector<int>& coins, int amount){
    int n = coins.size();

    //状态表示
    vector<int> dp(amount + 1);

    //初始化
    for (int j = 1; j <= amount; j++){
        dp[j] = INF;
    }

    //填表
    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] == INF ? -1 : dp[amount];
}

2.零钱兑换2

题目

在这里插入图片描述

算法原理

本道题是上面一道题的变形,挑选硬币使其等于目标值,统计所有的选法。

状态表示:dp[i][j]表示挑选前i个硬币,总和恰好为j的选法总数。

状态转移方程:根据当前硬币coins[i-1](下标映射要减一)选0个,1个,2个等等分为多种情况,因为要统计所有的选法个数,所以这里是所有情况相加。这里的推导过程以及优化和模板题一样,这里直接写结论:dp[i][j]=dp[i-1][j] + dp[i][j-coins[i-1](前提条件:j>=coins[i-1])。

初始化:dp[0][0]表示挑选前0个硬币,总和恰好为0的选法总数,就是1;第一行除dp[0][0]外其余全为0,因为不存在满足条件的选法。

填表顺序:从第一行到最后一行,其中每一行从左往右。

返回值:dp[n][amount],挑选前n个硬币,总和恰好为amount的选法总数。

代码实现

int change(int amount, vector<int>& coins){
    int n = coins.size();

    //状态表示 dp[i][j]表示挑选前i个硬币,总和恰好为j的选法总数
    vector<vector<unsigned long long>> dp(n + 1, vector<unsigned long long>(amount + 1));
    //初始化
    dp[0][0] = 1;

    //填表
    for (int i = 1; i <= n; i++){
        for (int j = 0; j <= amount; j++){
            dp[i][j] = dp[i - 1][j];
            if (j >= coins[i - 1]){
                dp[i][j] += dp[i][j - coins[i - 1]];
            }
        }
    }

    //返回值
    return dp[n][amount];
}

空间优化

int change(int amount, vector<int>& coins){
    int n = coins.size();

    //状态表示
    vector<unsigned long long> dp(amount + 1);
    //初始化
    dp[0] = 1;

    //填表
    for (int i = 1; i <= n; i++){
        for (int j = coins[i-1]; j <= amount; j++){
            dp[j] = dp[j] + dp[j - coins[i - 1]];
        }
    }

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

3.完全平方数

题目

在这里插入图片描述

算法原理

根据题意挑选完全平方数使其等于目标和n,返回使用完全平方数最少的数量。属于完全背包问题中的问题二,并且和零钱兑换1那道题相似,也是找最少数量。

状态表示 :dp[i][j]表示从前i个数中挑选,平方总和恰好等于j的所有选法中,使用数字最小的个数。

状态转移方程:根据当前完全平方数i*i选0个,1个,2个等等分为多种情况,因为要找最少数量,所以是所有情况取最小值,这里的推导过程和模板题一样,这里直接写结论:dp[i][j]=min(dp[i-1][j],dp[i][j-i*i]+1)前提条件:j>=i*i,(注意因为状态值表示的是个数,因此当前位置的完全平方数选多少个,后面要加上对应的个数,这就是为什么后面要+1)。

初始化:dp[0][0]表示从前0个完全平方数中挑选,总和恰好等于0,最少的个数就是0;除第一行第一个外,其余全部初始化为INF(因为状态转移方程中要取最小值,所以用0x3f3f3f3f,最大值的一半),表示不存在的情况。

填表顺序:从第一行到最后一行,其中每一行从左往右。

返回值:dp[m][n] >= INF ? -1 : dp[m][n],需要先判断一下是否等于INF,如果等于说明从前m个数中挑选,使其总和恰好等于n不存在,因此返回-1,如果不等于就是存在,返回最少个数。

代码实现


int numSquares(int n){
    //m表示可以取得完全平方数范围
    int m = sqrt(n);

    //状态表示 dp[i][j]表示从前i个数中挑选,平方总和恰好等于j的所有选法中,使用数字最小的个数
    vector<vector<int>> dp(m + 1, vector<int>(n + 1));
    //初始化
    for (int j = 1; j <= n; j++){
        dp[0][j] = INF;
    }

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

    //返回值
    return dp[m][n] == INF ? 0 : dp[m][n];
}

空间优化

int numSquares(int n){
    int m = sqrt(n);

    //状态表示
    vector<int> dp(n + 1);
    //初始化
    for (int j = 1; j <= n; j++){
        dp[j] = INF;
    }

    //填表
    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] == INF ? 0 : dp[n];
}

三.二维费用背包问题

例题

1.一和零

题目

在这里插入图片描述

算法原理

根据题意要求,从二进制字符串数组中挑选字符串,每个字符串中包含0和1,挑选最多的字符串,使其0的个数不超过m,1的个数不超过n,和01背包问题相比,其实就是多了一个限制条件,因此属于二维费用的01背包问题。

状态表示: dp[i][j][k]表示从前i个字符串中挑选,数字0不超过m个,数字1不超过n个的所有选法中,最长子集的长度。

状态转移方程:根据当前字符串strs[i-1],选还是不选分情况讨论,如果不选:找到挑选前i-1个字符串,数字0不超过j个,数字1不超过k个的所有选法中,最长的子集长度(用状态表示就是dp[i-1][j][k]);如果选:假设当前字符串中的0有a个,1有b个,挑选前i-1个字符串时,数字0不超过j-a(其中j>=a),数字1不超过k-b(其中k>=b),(用状态表示就是dp[i-1][j-a][k-b]),因为要找的是最长长度,所以两种情况要取最大值。

初始化:当i=0时,无论怎么选,最长的长度一定为0,所以初始值全部设置为0即可。

返回值:dp[x][m][n]x表示的是一共有多少个字符串,从前x个字符串中挑选,数字0不超过m个,数字1不超过n个的所有选法中,最长子集的长度。

代码实现

int findMaxForm(vector<string>& strs, int m, int n){
    int x = strs.size();

    //状态表示 dp[i][j][k]表示从前i个字符串中挑选,数字0不超过m个,数字1不超过n个的所有选法中,最长子集的长度
    vector<vector<vector<int>>> dp(x + 1, vector<vector<int>>(m + 1, vector<int>(n + 1)));

    //初始化 初始值设置为0即可

    //填表
    for (int i = 1; i <= x; i++){
        //统计当前字符串中0和1的个数
        string cur = strs[i - 1];
        int a = 0, b = 0;
        for(auto ch : cur){
            if (ch == '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 - 1][j - a][k - b] + 1, dp[i][j][k]);
                }
            }
        }
    }

    //返回值
    return dp[x][m][n];
}

空间优化

int findMaxForm(vector<string>& strs, int m, int n){
    int x = strs.size();

    //状态表示
    vector<vector<int>> dp(m + 1, vector<int>(n + 1));

    //初始化 初始值全部设置为0

    //填表
    for (int i = 1; i <= x; i++){
        //统计当前字符串中的数字0和1个数
        string cur = strs[i - 1];
        int a = 0, b = 0;
        for(auto ch : cur){
            if (ch == '0'){
                a++;
            }
            else{
                b++;
            }
        }

        for (int j = m; j >= a; j--){
            for (int k = n; k >= b; k--){
                //状态转移方程
                dp[j][k] = max(dp[j - a][k - b] + 1, dp[j][k]);
            }
        }
    }

    //返回值
    return dp[m][n];
}

2.盈利计划

题目

在这里插入图片描述

算法原理

根据题意要求,假设数组的长度为x,从前x个工作中挑选,使其需要的员工个数不超过n个,盈利不低于m(minProfit),当前工作i对应的所选要的员工是group[i-1],盈利为profit[i-1](下标从0开始),统计一共有多少种选法。还是属于两个限制条件的01背包问题。

状态表示:dp[i][j][k]表示从前i个计划中挑选,员工个数不超过j,盈利不低于k,一共有多少种选法。

状态转移方程:根据当前的工作i选还是不选分两种情况讨论,如果不选:就是从前i-1个工作中挑选,使其员工个数不超过j,盈利不低于k,用状态表示就是dp[i-1][j][k];如果选:就是从前i-1个工作中挑选,使其员工个数不超过j-group[i-1],(前提条件,j>=group[i-1]),盈利不低于k-profit[i-1](注意,因为这里的盈利是不低于,所以即使k<profit[i-1],也依然是可以的,但是因为不存在负数的情况,所以如果当k-profit[i-1]小于0时,取0即可)。因为要统计所有的选法,所以两种情况相加。

初始化:当i=0k=0时,dp[0][j][0]表示从前0个工作中挑选,员工个数不超过j,盈利不低于0,保证不选即可,因此选法个数就是1。

返回值:dp[x][n][m],从前x个工作中挑选,使其员工个数不超过n,盈利不低于m,一共有多少种选法。

代码实现

const int N = 1000000007;
int profitableSchemes(int n, int m, vector<int>& group, vector<int>& profit){
    int x = group.size();

    //状态表示 dp[i][j][k]表示从前i个计划中挑选,员工个数不超过j,盈利不低于k,总的选法个数
    vector<vector<vector<int>>> dp(x + 1, vector<vector<int>>(n + 1, vector<int>(m + 1)));

    //初始化 dp[0][j][0]表示从前0个计划中挑选,员工个数不超过j,盈利不低于0,选法个数为1
    for (int j = 0; j <= n; j++){
        dp[0][j][0] = 1;
    }

    //填表
    for (int i = 1; i <= x; i++){
        for (int j = 0; j <= n; j++){
            for (int k = 0; k <= m; k++){
                //状态转移方程 根据当前计划选还是不选分情况讨论
                //不选
                dp[i][j][k] = dp[i - 1][j][k];

                //选 
                if (j >= group[i - 1]){
                    dp[i][j][k] += dp[i - 1][j - group[i - 1]][max(0, k - profit[i - 1])];
                }

                dp[i][j][k] %= N;
            }
        }
    }

    //返回值
    return dp[x][n][m];
}

空间优化

int profitableSchemes(int n, int m, vector<int>& group, vector<int>& profit){
    int x = group.size();

    //状态表示
    vector<vector<int>> dp(n + 1, vector<int>(m + 1));

    //初始化
    for (int j = 0; j <= n; j++){
        dp[j][0] = 1;
    }

    //填表
    for (int i = 1; i <= x; i++){
        for (int j = n; j >= group[i - 1]; j--){
            for (int k = m; k >= 0; k--){
                dp[j][k] += dp[j - group[i - 1]][max(0, k - profit[i - 1])];
                dp[j][k] %= N;
            }
        }
    }

    //返回值
    return dp[n][m];
}

以上就是关于动态规划中背包问题练习题的讲解,如果哪里有错的话,可以在评论区指正,也欢迎大家一起讨论学习,如果对你的学习有帮助的话,点点赞关注支持一下吧!!!
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/974956.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

量子计算驱动的金融衍生品定价革命:突破传统蒙特卡洛模拟的性能边界

引言&#xff1a;金融计算的算力困局 某国际投行采用128量子位处理器对亚洲期权组合定价时&#xff0c;其量子振幅估计算法在2.7秒内完成传统GPU集群需要68小时的计算任务。在蒙特卡洛路径模拟实验中&#xff0c;量子随机游走算法将10,000维衍生品的价格收敛速度提升4个数量级…

【C++篇】树影摇曳,旋转无声:探寻AVL树的平衡之道

文章目录 从结构到操作&#xff1a;手撕AVL树的实现一、AVL树介绍1.1 什么是AVL树1.2 平衡因子的定义1.3 平衡的意义1.4 AVL树的操作 二、AVL树的节点结构2.1 节点结构的定义&#xff1a; 三、插入操作3.1 插入操作概述3.2 步骤1&#xff1a;按二叉查找树规则插入节点3.3 步骤2…

DeepSeek、微信、硅基流动、纳米搜索、秘塔搜索……十种不同方法实现DeepSeek使用自由

为了让大家实现 DeepSeek 使用自由&#xff0c;今天分享 10 个畅用 DeepSeek 的平台。 一、官方满血版&#xff1a;DeepSeek官网与APP 首推&#xff0c;肯定是 DeepSeek 的官网和 APP&#xff0c;可以使用满血版 R1 和 V3 模型&#xff0c;以及联网功能。 网址&#xff1a; htt…

推荐几款SpringBoot项目手脚架

作为程序员、一般需要搭建项目手脚架时、都会去Gitee或Github上去找、但是由于Github在国内并不稳定、所以就只能去Gitee去上查找。 不同语言检索方式不一样、但是也类似。 Gitee WEB应用开发 / 后台管理框架 芋道源码 ELADMIN 后台管理系统 一个基于 Spring Boot 2.7.1…

【VSCode】MicroPython环境配置

【VSCode】MicroPython环境配置 RT-Thread MicroPython 插件安装MicroPython 库文件配置结束语 RT-Thread MicroPython 插件安装 在 VSCode 拓展中搜索 “RT-Thread MicroPython” 并安装&#xff0c;详细配置步骤&#xff08;修改 VSCode 默认终端、MicroPython 代码补全&…

Moonshot AI 新突破:MoBA 为大语言模型长文本处理提效论文速读

前言 在自然语言处理领域&#xff0c;随着大语言模型&#xff08;LLMs&#xff09;不断拓展其阅读、理解和生成文本的能力&#xff0c;如何高效处理长文本成为一项关键挑战。近日&#xff0c;Moonshot AI Research 联合清华大学、浙江大学的研究人员提出了一种创新方法 —— 混…

大语言模型推理能力从何而来?

前言 DeepSeek R1采用强化学习进行后训练&#xff0c;通过奖励机制和规则引导模型生成结构化思维链&#xff08;CoT&#xff09;&#xff0c;从而显著提升了推理能力。这一创新方法使得DeepSeek R1能够在无需大量监督数据的情况下&#xff0c;通过自我进化发展出强大的推理能力…

最新本地部署 DeepSeekR1 蒸馏\满血量化版 + WebOpenUI 完整教程(Ubuntu\Linux系统\Ollama)

测试机为6133CPU(40Cores)256G D44*4090D 24G 一种方法是部署蒸馏版Distill模型。一种是部署Huggingface上unsloth的量化版模型 Ollama及模型安装 1.下载并安装ollama curl -fsSL https://ollama.com/install.sh | sh如果下载不动可以试试挂梯子或者再试几次 挂代理代码&…

PySide6学习专栏(四):用多线程完成复杂计算任务

如果计程序中要处理一个非常庞大的数据集中的数据&#xff0c;且数据处理计算很复杂&#xff0c;造成数据处理占用大量时间和CPU资源&#xff0c;如果不用多线程&#xff0c;仅在主进程中来处理数据&#xff0c;将会使整个程序卡死&#xff0c;必须采用多线程来处理这些数据是唯…

路由基本配置

学习目标 • 根据拓扑图进行网络布线。 • 清除启动配置并将路由器重新加载为默认状态。 • 在路由器上执行基本配置任务。 • 配置并激活以太网接口。 • 测试并检验配置。 • 思考网络实施方案并整理成文档。 任务 1&#xff1a;网络布线 使用适当的电缆类型连接网络设备。…

STM32MP157A单片机移植Linux驱动深入版

需求整理 在Linux设备树中新增leds节点&#xff0c;其有3个gpio属性&#xff0c;分别表示PE10对应led1&#xff0c;PF10对应led2&#xff0c;PE8对应led3&#xff0c;设备树键值对如下&#xff1a; leds { led1-gpio <&gpioe 10 0>; led2-gpio &l…

瑞芯微RV1126部署YOLOv8全流程:环境搭建、pt-onnx-rknn模型转换、C++推理代码、错误解决、优化、交叉编译第三方库

目录 1 环境搭建 2 交叉编译opencv 3 模型训练 4 模型转换 4.1 pt模型转onnx模型 4.2 onnx模型转rknn模型 4.2.1 安装rknn-toolkit 4.2.2 onn转成rknn模型 5 升级npu驱动 6 C++推理源码demo 6.1 原版demo 6.2 增加opencv读取图片的代码 7 交叉编译x264 ffmepg和op…

如何为自己的 PDF 文件添加密码?在线加密 PDF 文件其实更简单

随着信息泄露和数据安全问题的日益突出&#xff0c;保护敏感信息变得尤为重要。加密 PDF 文件是一种有效的手段&#xff0c;可以确保只有授权用户才能访问或修改文档内容。本文将详细介绍如何使用 CleverPDF 在线工具为你的 PDF 文件添加密码保护&#xff0c;确保其安全性。 为…

蓝桥杯核心内容

核心内容 数学 质数与筛质数&#xff0c;分解质因数 分解质因数 所有的数都可以写成有限个数相乘质数&#xff1a;可以写成1✖本身&#xff08;如131✖13&#xff09;合数&#xff1a;ab1✖...✖bn-》把乘数里面是合数的再分&#xff08;如b3是合数-》b3c1✖c2&#xff09;进…

七星棋牌源码高阶技术指南:6端互通、200+子游戏玩法深度剖析与企业级搭建实战(完全开源)

在棋牌游戏行业高速发展的今天&#xff0c;如何构建一个具备高并发、强稳定性与多功能支持的棋牌游戏系统成为众多开发者和运营团队关注的焦点。七星棋牌全开源修复版源码 凭借其 六端互通、200子游戏玩法、多省区本地化支持&#xff0c;以及 乐豆系统、防沉迷、比赛场、AI智能…

【学习笔记】【SpringCloud】MybatisPlus 基础使用

目录 一、使用 MybatisPlus 基本步骤 1. 引入 MybatisPlus 依赖 2. 定义Mapper接口并继承BaseMapper 二、MybatisPlus 常用配置 三、自定义SQL 四、IService 接口 1. 批量新增的效率问题 2. 配置方式 五、插件功能 1. 分页插件 一、使用 MybatisPlus 基本步骤 1. 引…

QT 引入Quazip和Zlib源码工程到项目中,无需编译成库,跨平台,压缩进度

前言 最近在做项目时遇到一个需求&#xff0c;需要将升级的文件压缩成zip&#xff0c;再进行传输&#xff1b; 通过网络调研&#xff0c;有许多方式可以实现&#xff0c;例如QT私有模块的ZipReader、QZipWriter&#xff1b;或者第三方库zlib或者libzip或者quazip等&#xff1…

在高流量下保持WordPress网站的稳定和高效运行

随着流量的不断增加&#xff0c;网站的稳定和高效运行变得越来越重要&#xff0c;特别是使用WordPress搭建的网站。流量过高时&#xff0c;网站加载可能会变慢&#xff0c;甚至崩溃&#xff0c;直接影响用户体验和网站正常运营。因此&#xff0c;我们需要采取一些有效的措施&am…

linux 安装启动zookeeper全过程及遇到的坑

1、下载安装zookeeper 参考文章&#xff1a;https://blog.csdn.net/weixin_48887095/article/details/132397448 2、启动失败 1、启动失败JAVA_HOME is not set and java could not be found in PATH 已安装 JAVA 配置了JAVA_HOME,还是报错解决方法&#xff1a;参考&#xf…

投资组合风险管理

投资组合风险管理 市场风险 信用风险流动性风险风险指标收益率波动率最大回撤 α \alpha α&#xff08;詹森指数&#xff09;&#xff0c; β \beta β卡玛比率月胜率上/下行捕获比夏普比率索提诺比率经风险调整的收益率&#xff08;&#x1d440;2&#xff09;特雷诺比率信息…