【动态规划】二维费用的背包问题

二维费用的背包问题

在这里插入图片描述

点赞👍👍收藏🌟🌟关注💖💖
你的支持是对我最大的鼓励,我们一起努力吧!😃😃

1.一和零

题目链接: 474. 一和零

题目分析:

在这里插入图片描述

找出并返回 strs 的最大子集的长度,该子集中 最多 有 m 个 0 和 n 个 1 ,也就说找到的子集 字符 0 的个数 小于等于 m,字符 1 的个数 小于等于 n。

在这里插入图片描述

如果对背包问题敏感,这就是一个背包问题。背包问题问的是给一堆物品,让在这堆物品中选一些物品,在满足某个限定条件下,问一些东西,要么问最大价值,要么问多少种选法。。。。

这道题其实就是背包问题,就是在字符串数组中挑一些字符串出来,需要满足的是两个条件,字符 0 的个数小于等于5,字符 1 的个数小于等于3。像这种满足两个条件的背包问题,我们称之为二维费用背包问题。

二维费用背包问题:之前我们遇到的01背包问题,完全背包问题都只有一个限定条件,二维费用背包就是多了一个限制变成了两个限制条件。

二维费用背包问题也分为二维费用的01背包问题和二维费用的完全背包问题。

这这道题因为每个字符串都面临选or不选两种情况,所以是二维费用的01背包问题。

算法原理:

之前说过所有其余分类的背包问题都是从01背包问题哪里延申出来的。所以我们尝试用01背包问题分析思路来解决我们这里的问题。

1.状态表示

01背包状态表示:

dp[i][j] 表示:从前 i 个物品中挑选,总体积不超过 j,所有选法中,最大的价值

二维费用的01背包问题:其实就只是多加了一个限制条件,之前就只有体积限制,现在不仅有体积还有重量。所以多加一维。

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

在这里插入图片描述

2.状态转移方程

本质还是一个01背包问题,所以我们还可以借用之前01背包状态转移方程的思路,根据最后一个位置,划分情况

设最后一个位置的字符串中,字符 0 的个数为 a,字符 1 的个数为 b。

不选 i 位置这个字符串,那就从 0 ~ i - 1 区间内字符串中挑,因为 i 位置都没选,所以0 ~ i - 1 区间内字符串中选 字符 0 的个数不超过 j,字符 1 的个数不超过k,在这两个限制条件下要的最大的长度。就是dp[i-1][j][k]

在这里插入图片描述

选 i 位置这个字符串,整体要保证字符 0 的个数不超过 j,字符 1 的个数不超过k,又因为选了 i 位置这个字符串,接下去取 0 ~ i - 1挑的时候,就要去掉 i 这个字符串中 字符串 0 的个数 和 字符串 1 的个数。然后加上选 i 位置字符串的个数。

注意 j - a,k - b 不一定存在,所以用之前要判断一下。

在这里插入图片描述

我们要的是最大长度,因此是两种情况的最大值

在这里插入图片描述

如果01背包问题都熟悉了,二维费用的01背包问题就是多加一个限制条件其他都一样。

3.初始化

初始化和之前一样,仅需考虑 i = 0 的情况就可以了,k = 0,j = 0 我们会特判不会越界的。

当 i = 0 表示字符串数组里没有字符串,如果数组没有字符串,想凑成字符串 0 的个数不超过 j,字符串 1 的个数不超过 k ,根本不能做到,不能做到就是一个空集,如果是空集,那最大个数全都是0

在这里插入图片描述

4.填表顺序

填dp[i][j][k]状态,要保证填 i 这一面 , i - 1 那一面的值已经填好了,因此仅需保证 i 从小到大。j、k无论怎么变化什么顺序都可以。

在这里插入图片描述

5.返回值

dp[i][j][k] 表示:从前 i 个字符串中挑选,字符 0 的个数不超过 j,字符 1 的个数不超过 k,所有选法中,最长的长度。我们要的是从整个字符串中选,字符 0 的个数不超过 j,字符 1 的个数不超过 k,所有选法中,最长的长度。所以返回的是
dp[len][m][n],len是字符串数组长度。

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)
        {
        	//统计一下字符串中 0  1 的个数
            int a = 0, b = 0;
            for(auto ch : strs[i - 1])
            {
                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][j][k], dp[i - 1][j - a][k - b] + 1);
                }
        }
                    
        return dp[len][m][n];
    }
};

优化:

  1. 直接删掉第一维
  2. 修改 j、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 ch : strs[i - 1])
        //     {
        //         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][j][k], dp[i - 1][j - a][k - b] + 1);
        //         }
        // }
                    
        // return dp[len][m][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 ch : strs[i - 1])
                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][k], dp[j - a][k - b] + 1);
        }
                    
        return dp[m][n];
    }
};

2.盈利计划

题目链接: 879. 盈利计划

题目分析:

在这里插入图片描述

这道题读起来很难读,但是结合示例我们能明白这道题的意思:

集团有n名员工,还有一些计划,每种计划都有对应的利润以及人数。问从这些计划种挑选一些计划,要求人数不超过n,利润至少为minProfit,一共有多少种计划可以选择。

在这里插入图片描述

当明白这道题的意思之后,我们就应该想到这是一道背包问题。背包问题就是给我们一些东西,让我们在这些东西里挑一些东西出来,在满足某个限定条件下,问一些东西。这道题问的是有多少计划可以选择。

这道题有两个限定条件,因此是一个二维费用的背包问题,又因为每种工作只能面临选or不选两个情况,所以这是一个二维费用的01背包问题。

算法原理:

1.状态表示

dp[i][j][k] 表示:从前 i 个计划中挑选,总人数不超过 j,总利润至少为 k,一共有多少种选法。

不超过j -> 小于等于j,至少为k -> 大于等于k,等会状态转移方程你会发现不超过和至少状态转移方程是不一样的。

在这里插入图片描述

2.状态转移方程

还是根据最后一个位置,划分情况

不选 i ,相当于去 0 ~ i - 1 去看看总人数不超过 j,利润不超过 k,在这两种情况下,一共有多少种情况

在这里插入图片描述

选 i,要求总人数不超过 j,如果选 i 位置的计划,i 位置里面有个人数就是group[i]里面存着,那去 0 ~ i - 1里面挑的时候总人数是不能超过 j - group[i]的,

在这里插入图片描述

先别着急分析下一个,这里思考一下 j - g[i] 能小于0吗?不能!如果小于那就相当于 j < g[i],也就是 第 i 个计划里面人数就已经超过 j 了,但是dp里面表示总人数是不能超过 j 的,如果单单这一个计划就已经超过总人数了,那前面在怎么挑也不行。

在这里插入图片描述

在分析第二个限定条件,总利润至少为k,选 i 位置的时候,i 有一个利润在profit[i]里存着,也就是说在 0 ~ i -1 里面去选总利润至少为 k - p[i]就够了,看似和上面一样,但是k - p[i] 能小于 0 吗?

在这里插入图片描述

但是要注意,刚才的状态表示,一个是不超过 j,一个是至少为 k。其实 k - p[i] 能小于 0!也就相当于 k < p[i] ,也就是说在 0 ~ i 里挑总利润大于等于K,最后 i 位置就已经大于k了那不就正好吗。所以这里可以小于0的。

在这里插入图片描述

但是问题来了dp是一个数组,数组下标能小于0吗?不能!也就是说 k - p[i] 是可以存在的,但是在数组中找不到。所以我们要根据实际情况想一个折中的想法来表示这种情况。i 位置利润就已经超过k了,那说明前面 0 ~ i -1 里面的利润挑多少都可以,所以我们可以认为前面利润只要大于等于0就可以了。

在这里插入图片描述

问一共多少选法,所以这两种情况加起来

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

3.初始化

这里不在画图了,可以根据实际情况来初始化。

这里我们仅需出来当没有任务的时候,dp[i][j][k] 应该等于多少。

如果没有任务,那也没有利润,此时不管有多少人,只要不选就好了,也就是选出来一个空集也就是一种选法,因此dp[0][j][0] = 1 (0 <= j <= n)

在这里插入图片描述

4.填表顺序

填 i 需要 i - 1,所以仅需要保证 i 从小到大

在这里插入图片描述

5.返回值

dp[i][j][k] 表示:从前 i 个计划中挑选,总人数不超过 j,总利润至少为 k,一共有多少种选法。我们要的是从所有计划中挑,总人数不超过 j,总利润至少为 k,一共有多少种选法。因此返回 dp[len][n][m], len是数组的长度

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<vector<int>>> dp(len + 1, vector<vector<int>>(n + 1, vector<int>(m + 1)));
        for(int j = 0; j <= n; ++j) dp[0][j][0] = 1;
        for(int i = 1; i <= len; ++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 >= g[i - 1])
                        dp[i][j][k] += dp[i - 1][j - g[i - 1]][max(0,k - p[i - 1])];
                    dp[i][j][k] %= MOD;
                }
        return dp[len][n][m];
    }
};

优化

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 j = 0; j <= n; ++j) dp[j][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 - g[i - 1]][max(0, k - p[i - 1])];
                    dp[j][k] %= MOD;
                }
        return dp[n][m];

    }
};
评论 182
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值