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

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

✨感谢您阅读本篇文章,文章内容是个人学习笔记的整理,如果哪里有误的话还请您指正噢✨
✨ 个人主页:余辉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的最大价值//初始值全部设置为0for (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的最大价值//第一行除第一个外,其他全初始值设置为-1for (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个数,所有的选法中,能否恰好等于jvector<vector<bool>> dp(n + 1, vector<bool>(sum + 1,true));//初始化 添加第0行和第0列,除第一行外其余全部为truefor (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,选法个数为1for (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];
}

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

相关文章:

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

✨感谢您阅读本篇文章&#xff0c;文章内容是个人学习笔记的整理&#xff0c;如果哪里有误的话还请您指正噢✨ ✨ 个人主页&#xff1a;余辉zmh–CSDN博客 ✨ 文章所属专栏&#xff1a;动态规划篇–CSDN博客 文章目录 一.01背包问题1.模板题2.例题1.分割等和子集2.目标和3.最后…...

ok113i平台——多媒体播放器适配

1. 视频播放支持 1.1 在Linux平台交叉编译ffmpeg动态库&#xff0c;详情查看《ok113i平台——交叉编译音视频动态库》 提取如下动态库&#xff1a; libavcodec.so.58.134.100 libavdevice.so.58.13.100 libavfilter.so.7.110.100 libavformat.so.58.76.100 libavutil.so.56.…...

【保姆级教程】DeepSeek R1+RAG,基于开源三件套10分钟构建本地AI知识库

一、总体方案 目前在使用 DeepSeek 在线环境时&#xff0c;页面经常显示“服务器繁忙&#xff0c;请稍后再试”&#xff0c;以 DeepSeek R1 现在的火爆程度&#xff0c;这个状况可能还会持续一段时间&#xff0c;所以这里给大家提供了 DeepSeek R1 RAG 的本地部署方案。最后实现…...

线程与进程的深入解析及 Linux 线程编程

在操作系统中&#xff0c;进程和线程是进行并发执行的两种基本单位。理解它们的区别和各自的特点&#xff0c;能够帮助开发者更好地进行多任务编程&#xff0c;提高程序的并发性能。本文将探讨进程和线程的基础概念&#xff0c;及其在 Linux 系统中的实现方式&#xff0c;并介绍…...

微信问题总结(onpageshow ,popstate事件)

此坑描述 订单详情某按钮点击&#xff0c;通过window.location.href跳转到&#xff08;外部&#xff09;第三方链接后&#xff0c;回退后&#xff0c;在ios中生命周期和路由导航钩子都失效了&#xff0c;无法触发。 在安卓中无视此坑&#xff0c; 回退没有问题 解决 原因&am…...

自己安装一台DeepSeek的服务器

找一台还可以的Linux服务器&#xff0c;登录后执行&#xff1a; curl -fsSL https://ollama.com/install.sh | sh 等待安装完成&#xff1a; 执行命令&#xff0c;根据服务器能力安装不同版本的AI模型&#xff1a; ollama run llama3.2 下一步就开始对话吧&#xff1a; llam…...

面阵工业相机提高餐饮业生产效率

餐饮行业是一个快节奏、高要求的领域&#xff0c;该领域对生产过程中每一个阶段的效率和准确性都有很高的要求。在食品加工、包装、质量控制和库存管理等不同生产阶段实现生产效率的优化是取得成功的关键步骤。面阵工业相机能够一次性捕捉对象的二维区域图像&#xff0c;并支持…...

Java 中 HTTP 协议版本使用情况剖析

Java 中 HTTP 协议版本使用情况剖析 一、HTTP/1.1 与 HTTP/2 概述 (一)HTTP/1.1 HTTP/1.1 是广泛应用且成熟的 HTTP 协议版本,它在互联网发展历程中扮演了重要角色。其特点主要包括: 连接方式:默认采用短连接,即每次请求都要建立新的 TCP 连接,请求完成后断开。不过也…...

计算机网络之物理层——基于《计算机网络》谢希仁第八版

(꒪ꇴ꒪ )&#xff0c;Hello我是祐言QAQ我的博客主页&#xff1a;C/C语言&#xff0c;数据结构&#xff0c;Linux基础&#xff0c;ARM开发板&#xff0c;网络编程等领域UP&#x1f30d;快上&#x1f698;&#xff0c;一起学习&#xff0c;让我们成为一个强大的攻城狮&#xff0…...

DeepSeek 助力 Vue 开发:打造丝滑的缩略图列表(Thumbnail List)

前言&#xff1a;哈喽&#xff0c;大家好&#xff0c;今天给大家分享一篇文章&#xff01;并提供具体代码帮助大家深入理解&#xff0c;彻底掌握&#xff01;创作不易&#xff0c;如果能帮助到大家或者给大家一些灵感和启发&#xff0c;欢迎收藏关注哦 &#x1f495; 目录 Deep…...

day56 第十一章:图论part06

108.冗余连接 注意init初始化 改进&#xff1a; 其实只有一条边冗余&#xff0c;改为&#xff0c;如果两条边在同一个集合里&#xff0c;就输出&#xff0c;不然加入。 #include <iostream> #include <vector> using namespace std;int n 1005; vector<int>…...

conda 配置源

无论是Anaconda vs Miniconda vs Miniforge 中的哪个&#xff0c;只要使用conda就涉及源&#xff0c;换源的目的是为了加速包的获取 修改配置文件 通过修改用户目录下的 .condarc 文件来使用 不同系统下的 .condarc 目录如下&#xff1a; Linux: ${HOME}/.condarcmacOS: ${…...

LangChain大模型应用开发:多模态输入与自定义输出

介绍 大家好&#xff0c;博主又来给大家分享知识了。今天给大家分享的内容是使用LangChain进行大模型应用开发中的多模态输入与自定义输出。 LangChain中的多模态数据输入是指将多种不同形式的数据作为输入提供给基于语言模型的应用程序或系统&#xff0c;以丰富交互内容和提…...

HarmonyOS 开发套件 介绍 ——上篇

HarmonyOS 开发套件 介绍 ——上篇 在当今科技飞速发展的时代&#xff0c;操作系统作为智能设备的核心&#xff0c;其重要性不言而喻。而HarmonyOS&#xff0c;作为华为推出的全新操作系统&#xff0c;正以其独特的魅力和强大的功能&#xff0c;吸引着越来越多的开发者和用户的…...

【Unity Shader编程】之图元装配与光栅化

执行方式&#xff1a;自动完成 图元装配自动化流程 顶点坐标存入装配区 → 按绘制模式连接顶点 → 生成完整几何图元 示例&#xff1a;gl.drawArrays(gl.TRIANGLES, 0, 3)自动生成三角形 会自动自动裁剪超出屏幕范围&#xff08;NDC空间外&#xff09;的三角形&#xff0c;仅保…...

小游戏-记忆卡牌

1、游戏开始4张卡牌&#xff0c;每次过关后新增两张&#xff0c;总共64张卡&#xff0c;可以修改数组EMOJIS&#xff0c;添加表情&#xff0c;增加卡牌数量 2、新建txt文件&#xff0c;将代码粘贴进去&#xff0c;保存后&#xff0c;将txt修改后缀名为html的格式 <!DOCTYPE…...

数据结构——二叉树经典习题讲解

各位看官早安午安晚安呀 如果您觉得这篇文章对您有帮助的话 欢迎您一键三连&#xff0c;小编尽全力做到更好 欢迎您分享给更多人哦 大家好&#xff0c;我们今天来学习java数据结构的二叉树 递归很重要的一些注意事项&#xff1a; 1&#xff1a;递归你能不能掌握在于&#xff1…...

centos 9 时间同步服务

在 CentOS 9 中&#xff0c;默认的时间同步服务是 chrony&#xff0c;而不是传统的 ntpd。 因此&#xff0c;建议使用 chrony 来配置和管理时间同步。 以下是使用 chrony 配置 NTP 服务的步骤&#xff1a; 1. 安装 chrony 首先&#xff0c;确保系统已安装 chrony。 在 CentOS…...

视觉应用工程师(面试)

视觉应用工程师&#xff08;面试&#xff09; 1.自我介绍、会的技能、项目 2.相机和机械手调试过程 检查硬件&#xff0c;看软件驱动是否链接&#xff0c;调节相机和镜头保证能够识别这个物料&#xff0c;看接口和通讯是否正常&#xff0c;如&#xff1a;波特率&#xff0c;数…...

macos sequoia 禁用 ctrl+enter 打开鼠标右键菜单功能

macos sequoia默认ctrlenter会打开鼠标右键菜单&#xff0c;使得很多软件有冲突。关闭方法&#xff1a; end...

【后端基础】布隆过滤器原理

文章目录 一、Bloom Filter&#xff08;布隆过滤器&#xff09;概述1. Bloom Filter 的特点2. Bloom Filter 的工作原理 二、示例1. 添加与查询2. 假阳性 三、Bloom Filter 的操作1、假阳性概率2、空间效率3、哈希函数的选择 四、应用 Bloom Filter 是一种非常高效的概率型数据…...

cs*n 网页内容转为html 加入 onenote

csdn上有好用的内容&#xff0c;我们怎么将它们加到 onenote 里吃灰呢。 一、创建 新html create_html.py import sysdef create_html_file(filename):# 检查是否提供了文件名if not filename:print("请提供HTML文件名")return# 创建HTML内容html_content f"…...

输入搜索、分组展示选项、下拉选取,全局跳转页,el-select 实现 —— 后端数据处理代码,抛砖引玉展思路

详细前端代码写于上一篇&#xff1a;输入搜索、分组展示选项、下拉选取&#xff0c;el-select 实现&#xff1a;即输入关键字检索&#xff0c;返回分组选项&#xff0c;选取跳转到相应内容页 —— VUE项目-全局模糊检索 【效果图】&#xff1a;分组展示选项 【去界面操作体验】…...

蓝桥杯 Java B 组之岛屿数量、二叉树路径和(区分DFS与回溯)

Day 3&#xff1a;岛屿数量、二叉树路径和&#xff08;区分DFS与回溯&#xff09; &#x1f4d6; 一、深度优先搜索&#xff08;DFS&#xff09;简介 深度优先搜索&#xff08;Depth-First Search&#xff0c;DFS&#xff09;是一种用于遍历或搜索树或图的算法。它会沿着树的分…...

【论文阅读】SAM-CP:将SAM与组合提示结合起来的多功能分割

导言 近年来&#xff0c;视觉基础模型的快速发展推动了多模态理解的进步&#xff0c;尤其是在图像分割任务中。例如&#xff0c;Segment Anything模型&#xff08;SAM&#xff09;在图像Mask分割上表现出色&#xff0c;但在语义及实例分割方面仍存在局限。本文提出的SAM-CP&am…...

JUC并发—9.并发安全集合四

大纲 1.并发安全的数组列表CopyOnWriteArrayList 2.并发安全的链表队列ConcurrentLinkedQueue 3.并发编程中的阻塞队列概述 4.JUC的各种阻塞队列介绍 5.LinkedBlockingQueue的具体实现原理 6.基于两个队列实现的集群同步机制 4.JUC的各种阻塞队列介绍 (1)基于数组的阻塞…...

爱普生 SG-8101CE 可编程晶振在笔记本电脑的应用

在笔记本电脑的精密架构中&#xff0c;每一个微小的元件都如同精密仪器中的齿轮&#xff0c;虽小却对整体性能起着关键作用。如今的笔记本电脑早已不再局限于简单的办公用途&#xff0c;其功能愈发丰富多样。从日常轻松的文字处理、网页浏览&#xff0c;到专业领域中对图形处理…...

k8s网络插件详解(flannel)

1、介绍 Flannel 是一个轻量级、易于配置的网络插件&#xff0c;旨在简化 Kubernetes 集群中 Pod 网络的管理。Flannel 的核心功能是提供一个虚拟的网络&#xff0c;允许每个 Pod 获取一个独立的 IP 地址&#xff0c;并实现不同节点间的 Pod 之间的通信 2、网络模式 vxlan&am…...

基于flask+vue框架的的医院预约挂号系统i1616(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。

系统程序文件列表 项目功能:用户,医生,科室信息,就诊信息,医院概况,挂号信息,诊断信息,取消挂号 开题报告内容 基于FlaskVue框架的医院预约挂号系统开题报告 一、研究背景与意义 随着医疗技术的不断进步和人们健康意识的日益增强&#xff0c;医院就诊量逐年增加。传统的现场…...

JUC并发—8.并发安全集合一

大纲 1.JDK 1.7的HashMap的死循环与数据丢失 2.ConcurrentHashMap的并发安全 3.ConcurrentHashMap的设计介绍 4.ConcurrentHashMap的put操作流程 5.ConcurrentHashMap的Node数组初始化 6.ConcurrentHashMap对Hash冲突的处理 7.ConcurrentHashMap的并发扩容机制 8.Concu…...

Linux 内核网络设备驱动编程:私有协议支持

一、struct net_device的通用性与私有协议的使用 struct net_device是Linux内核中用于描述网络设备的核心数据结构,它不仅限于TCP/IP协议,还可以用于支持各种类型的网络协议,包括私有协议。其原因如下: 协议无关性:struct net_device的设计是通用的,它本身并不依赖于任何…...

机器学习的数学基础(三)——概率与信息论

目录 1. 随机变量2. 概率分布2.1 离散型变量和概率质量函数2.2 连续型变量和概率密度函数 3. 边缘概率4. 条件概率5. 条件概率的链式法则6. 独立性和条件独立性7. 期望、方差和协方差7.1 期望7.2 方差7.3 协方差 8. 常用概率分布8.1 均匀分布 U ( a , b ) U(a, b) U(a,b)8.2 Be…...

使用Docker Desktop部署GitLab

1. 环境准备 确保Windows 10/11系统支持虚拟化技术&#xff08;需在BIOS中开启Intel VT-x/AMD-V&#xff09;内存建议≥8GB&#xff0c;存储空间≥100GB 2. 安装Docker Desktop 访问Docker官网下载安装包安装时勾选"Use WSL 2 instead of Hyper-V"&#xff08;推荐…...

推理模型时代:大语言模型如何从对话走向深度思考?

一、对话模型和推理模型的区别概述 对话模型是专门用于问答交互的语言模型,符合人类的聊天方式,返回的内容可能仅仅只是一个简短的答案,一般模型名称后面会带有「chat」字样。 推理模型是比较新的产物,没有明确的定义,一般是指输出过程中带有<think>和</think&…...

GESP2024年3月认证C++七级( 第三部分编程题(1)交流问题)

参考程序&#xff1a; #include <iostream> #include <vector> #include <unordered_map> using namespace std;// 深度优先搜索&#xff0c;给每个节点染色&#xff0c;交替染色以模拟两校同学的划分 void dfs(vector<vector<int>>& graph…...

DeepSeek:AI商业化的新引擎与未来蓝图

摘要 在人工智能迅猛发展的浪潮中&#xff0c;DeepSeek以其卓越的技术实力和高超的商业化能力崭露头角。作为一款现象级AI产品&#xff0c;它不仅在算法性能上位居行业前列&#xff0c;还通过灵活的定制解决方案渗透到金融、医疗、零售等多个领域。DeepSeek以创新的商业模式和场…...

2025年度福建省职业院校技能大赛中职组“网络建设与运维”赛项规程模块三

模块三&#xff1a;服务搭建与运维 任务描述&#xff1a; 随着信息技术的快速发展&#xff0c;集团计划把部分业务由原有的 X86 服 务器上迁移到ARM 架构服务器上&#xff0c;同时根据目前的部分业务需求进行 了部分调整和优化。 一、X86 架构计算机操作系统安装与管理 1&…...

Python----数据结构(队列,顺序队列,链式队列,双端队列)

一、队列 1.1、概念 队列(Queue)&#xff1a;也是一种基本的数据结构&#xff0c;在队列中的插入和删除都遵循先进先出&#xff08;First in First out&#xff0c;FIFO&#xff09;的原则。元素可以在任何时刻从队尾插入&#xff0c;但是只有在队列最前面 的元素才能被取出或…...

【自学笔记】Spring Boot框架技术基础知识点总览-持续更新

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 Spring Boot框架技术基础知识点总览一、Spring Boot简介1.1 什么是Spring Boot&#xff1f;1.2 Spring Boot的主要特性 二、Spring Boot快速入门2.1 搭建Spring Boo…...

神经网络剪枝技术的重大突破:sGLP-IB与sTLP-IB

神经网络剪枝技术的重大突破:sGLP-IB与sTLP-IB 在人工智能飞速发展的今天,深度学习技术已经成为推动计算机视觉、自然语言处理等领域的核心力量。然而,随着模型规模的不断膨胀,如何在有限的计算资源和存储条件下高效部署这些复杂的神经网络模型,成为了研究者们亟待解决的…...

Django-Vue 学习-VUE

主组件中有多个Vue组件 是指在Vue.js框架中&#xff0c;主组件是一个父组件&#xff0c;它包含了多个子组件&#xff08;Vue组件&#xff09;。这种组件嵌套的方式可以用于构建复杂的前端应用程序&#xff0c;通过拆分功能和视图&#xff0c;使代码更加模块化、可复用和易于维…...

【Gin】2:快速上手Gin框架(模版、cookie、session)

本文目录 一、模版渲染二、自定义模版函数三、cookie四、Session五、cookie、session区别六、会话攻击 一、模版渲染 在 Gin 框架中&#xff0c;模板主要用于动态生成 HTML 页面&#xff0c;结合 Go 语言的模板引擎功能&#xff0c;实现数据与视图的分离。 模板渲染是一种动态…...

Linux修改主机名称

hostnamectl set-hostname 主机名称 exit 退出登录重新进入即可...

亲测Windows部署Ollama+WebUI可视化

一. Ollama下载 登录Ollama官网(Ollama)点击Download进行下载 如果下载很慢可用以下地址下载&#xff1a; https://github.com/ollama/ollama/releases/download/v0.5.7/OllamaSetup.exe 在DeepSeek官网上&#xff0c;你可以直接点击【model】 到达这个界面之后&#xff0c;…...

Java四大框架深度剖析:MyBatis、Spring、SpringMVC与SpringBoot

目录 前言&#xff1a; 一、MyBatis框架 1. 概述 2. 核心特性 3. 应用场景 4. 示例代码 二、Spring框架 1. 概述 2. 核心模块 3. 应用场景 4. 示例代码 三、SpringMVC框架 1. 概述 2. 核心特性 3. 应用场景 4. 示例代码 四、SpringBoot框架 1. 概述 2. 核心…...

ubuntu部署小笔记-采坑

ubuntu部署小笔记 搭建前端控制端后端前端nginx反向代理使用ubuntu部署nextjs项目问题一 如何访问端口号配置后台运行该进程pm2 问题二 包体过大生产环境下所需文件 问题三 部署在vercel时出现的问题需要魔法访问后端api时&#xff0c;必须使用https协议电脑端访问正常&#xf…...

23. AI-大语言模型-DeepSeek简介

文章目录 前言一、DeepSeek是什么1. 简介2. 产品版本1. 类型2. 版本3. 参数规模与模型能力 3. 特征4. 三种访问方式1. 网页端和APP2. DeepSeek API 二、DeepSeek可以做什么1. 应用场景2. 文本生成1. 文本创作2. 摘要与改写3. 结构化生成 3. 自然语言理解与分析1. 语义分析2. 文…...

基于SpringBoot的智慧家政服务平台系统设计与实现的设计与实现(源码+SQL脚本+LW+部署讲解等)

专注于大学生项目实战开发,讲解,毕业答疑辅导&#xff0c;欢迎高校老师/同行前辈交流合作✌。 技术范围&#xff1a;SpringBoot、Vue、SSM、HLMT、小程序、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、安卓app、大数据、物联网、机器学习等设计与开发。 主要内容&#xff1a;…...

Java NIO与传统IO性能对比分析

Java NIO与传统IO性能对比分析 在Java中&#xff0c;I/O&#xff08;输入输出&#xff09;操作是开发中最常见的任务之一。传统的I/O方式基于阻塞模型&#xff0c;而Java NIO&#xff08;New I/O&#xff09;引入了非阻塞和基于通道&#xff08;Channel&#xff09;和缓冲区&a…...

基于YOLO11深度学习的果园苹果检测与计数系统设计与实现【python源码+Pyqt5界面+数据集+训练代码】

《------往期经典推荐------》 一、AI应用软件开发实战专栏【链接】 项目名称项目名称1.【人脸识别与管理系统开发】2.【车牌识别与自动收费管理系统开发】3.【手势识别系统开发】4.【人脸面部活体检测系统开发】5.【图片风格快速迁移软件开发】6.【人脸表表情识别系统】7.【…...