动态规划——分组背包(附带经典例题3个)

分组背包:

1.定义:给定一个整数m表示背包的容量,有n个货物可供挑选。每个货物有自己的体积,价值,组号。同一个组的物品只能挑选一件,所有挑选物品的体积总和不能超过背包容量。

怎么挑选货物能达到价值最大,返回最大价值。

2.dp[i][j]表示1~i组上,每组只能选一件商品(注意:i表示的是组,不是商品)容量不超过j的条件下的最大价值。

1)不要i组商品就满足条件——dp[i-1][j]

2)要i组商品,考虑要哪一件?全试!!!

a:dp[i-1][j-a的体积]+a的价值

b:dp[i-1][j-b的体积]+b的价值

c:dp[i-1][j-c的体积]+c的价值

(注意:a,b,c是i组中的商品)

关键是给物品分好组,这样才能进行后面的计算。

3.时间复杂度:

for(组的数量)

    for(背包容量)

        for(组内物品数量)

组1:m*组1物品数量

组2:m*组2物品数量

……

组i:m*组i物品数量

o(m*物品总个数)

例题一:通天之分组背包

题目描述

自 01 背包问世之后,小 A 对此深感兴趣。一天,小 A 去远游,却发现他的背包不同于 01 背包,他的物品大致可分为 k 组,每组中的物品相互冲突,现在,他想知道最大的利用价值是多少。

输入格式

两个数 m,n,表示一共有 n 件物品,背包能承受的最大重量为 m。

接下来 n 行,每行 3 个数 ai​,bi​,ci​,表示物品的重量,利用价值,所属组数。

输出格式

一个数,最大的利用价值。

输入

45 3 10 10 1 10 5 1 50 400 2

输出

10

说明/提示

0≤m≤1000,1≤n≤1000,1≤k≤100,ai​,bi​,ci​ 在 int 范围内。

代码实现:

#include<stdio.h> #define MAXN 1010 #define MAXM 1010 typedef struct{ int w; int v; int k; } Node; Node a[MAXN]; int dp[MAXN][MAXM]; // dp[i][j]表示前i组,容量为j时的最大价值 int max(int a, int b) { return a > b ? a : b; } int compute(int m, int n) { // 统计有多少组 int teams = 1; for(int i = 2; i <= n; i++) { if(a[i].k != a[i-1].k) { teams++; } } int start = 1; // 当前组的起始位置 int end = 1; // 当前组的结束位置 int i = 1; // 组索引,表示第一组 // 按组处理物品 for(start = 1, end = 2, i = 1; start <= n; i++) { // 找到当前组的结束位置 while(end <= n && a[end].k == a[start].k) { end++; } // 此时end指向下一组的第一个元素 //完成第i组的归纳后,就要对这一组进行处理。找到一件价值最大的商品 // 处理第i组 for(int j = 0; j <= m; j++) {//对所有重量进行遍历。先取一个重量,对所有物品进行遍历,找到最大的那个。再取其他重量,最后找到的肯定是所有物品中价值最大的。 // 不选当前组的任何物品 dp[i][j] = dp[i-1][j]; // 尝试选择当前组中的每个物品 for(int g = start; g < end; g++) { if(j >= a[g].w) { dp[i][j] = max(dp[i][j], dp[i-1][j - a[g].w] + a[g].v); }//上述取最大值刚好符合我们分析的部分背包的最大价值取法。 } } // 移动到下一组,再开始进行下一组的处理 start = end; // 下一组的起始位置是当前的end end++; // end指向下一组的第二个位置 } return dp[i-1][m];//因为先处理i++,然后才发现下一组不能循环了,所以最终返回的是dp[i-1][m] } int main() { int m, n; scanf("%d%d", &m, &n); // 输入从1开始,方便处理 for(int i = 1; i <= n; i++) { scanf("%d%d%d", &a[i].w, &a[i].v, &a[i].k); } // 根据组号进行排序(按组号升序) for(int i = 1; i < n; i++) { for(int j = 1; j <= n - i; j++) { if(a[j].k > a[j+1].k) { Node temp = a[j]; a[j] = a[j+1]; a[j+1] = temp; } } } int sum = compute(m, n); printf("%d\n", sum); return 0; }

例题二:摆花

题目描述

小明的花店新开张,为了吸引顾客,他想在花店的门口摆上一排花,共 m 盆。通过调查顾客的喜好,小明列出了顾客最喜欢的 n 种花,从 1 到 n 标号。为了在门口展出更多种花,规定第 i 种花不能超过 ai​ 盆,摆花时同一种花放在一起,且不同种类的花需按标号的从小到大的顺序依次摆列。

试编程计算,一共有多少种不同的摆花方案。

输入格式

第一行包含两个正整数 n 和 m,中间用一个空格隔开。

第二行有 n 个整数,每两个整数之间用一个空格隔开,依次表示 a1​,a2​,⋯,an​。

输出格式

一个整数,表示有多少种方案。注意:因为方案数可能很多,请输出方案数对 10^6+7 取模的结果。

输入

2 4 3 2

输出

2

说明/提示

【数据范围】

对于 20% 数据,有 0<n≤8,0<m≤8,0≤ai​≤8。

对于 50% 数据,有 0<n≤20,0<m≤20,0≤ai​≤20。

对于 100% 数据,有 0<n≤100,0<m≤100,0≤ai​≤100。

代码实现:

#include <stdio.h> #include <string.h> #define maxn 105 #define mod 1000007 int main() { int n, m; int a[maxn]; int f[maxn][maxn]; // 读取输入 scanf("%d %d", &n, &m); for (int i = 1; i <= n; i++) { scanf("%d", &a[i]); } // 初始化 DP 数组 memset(f, 0, sizeof(f)); f[0][0] = 1; // 动态规划计算方案数 for (int i = 1; i <= n; i++) { for (int j = 0; j <= m; j++) { int limit = (a[i] < j) ? a[i] : j; // min(j, a[i]) for (int k = 0; k <= limit; k++) { f[i][j] = (f[i][j] + f[i-1][j-k]) % mod; } } } // 输出结果 printf("%d\n", f[n][m]); return 0; }

解题思路:

相当于是已经分好组了。还是从一个组中取物品。

dp[i][j]表示从1~i组中取花摆成j盆花的方案数。根据上述dp数组含义,说明这是要累加算方案数的。

1)不用第i组的花就可以满足j盆花的要求

2)用第i组的花才能满足j盆花的要求

可能用一朵花,也可能两朵花,还可能n朵花……

因为组已经分好了,所以直接从第一组开始遍历,在最外层的循环。然后就是遍历j,也就是从0~4遍历花盆,最后在最里面的循环是遍历每一组的花,因为每一组的花是一样的,所以遍历的是每一组的花的数量,就像上面分析的。有多少朵花就遍历多少次,但不能超出j的最大值。

最后的递推公式是累加,因为所有符合题意的dp数组都属于方案数。这就是为什么不是把dp数组作为标志变量最后用另一个变量累加的原因,因为所有的dp数组都是符合题意的。

为什么在砝码称重问题中dp数组作为标志变量,有的成立,有的不成立呢?

因为dp数组含义决定这个问题。要求的是可以称出的重量,遍历的是所有可能达到的重量,但有些重量不一定达到,所以不能直接累加,而是将dp数组作为标志变量,i那样摆放是否可以达到称出j的作用,然后最后才计算累加。

为什么上述题目可以直接累加呢?

因为dp数组一定成立,所有计算出的dp[i][j]都是成立的方案数。主要是由dp数组的含义决定,表示的就是方案数。而且初始化保证了基础成立,状态转移方程都是根据成立的初始化来的,所以肯定是成立的方案数。

还有一个和摆花问题一样的“砝码来组合”在01背包专题中。

例题三:从栈中取出k个硬币的最大面值和

主要看解题思路。很巧妙。

一张桌子上总共有 n 个硬币  。每个栈有 正整数 个带面值的硬币。

每一次操作中,你可以从任意一个栈的 顶部 取出 1 个硬币,从栈中移除它,并放入你的钱包里。

给你一个列表 piles ,其中 piles[i] 是一个整数数组,分别表示第 i 个栈里 从顶到底 的硬币面值。同时给你一个正整数 k ,请你返回在 恰好 进行 k 次操作的前提下,你钱包里硬币面值之和 最大为多少 。

示例 1:

输入:piles = [[1,100,3],[7,8,9]], k = 2 输出:101 解释: 上图展示了几种选择 k 个硬币的不同方法。 我们可以得到的最大面值为 101 。

示例 2:

输入:piles = [[100],[100],[100],[100],[100],[100],[1,1,1,1,1,1,700]], k = 7 输出:706 解释: 如果我们所有硬币都从最后一个栈中取,可以得到最大面值和。

提示:

  • n == piles.length
  • 1 <= n <= 1000
  • 1 <= piles[i][j] <= 105
  • 1 <= k <= sum(piles[i].length) <= 2000

主要解决的是怎么转化成分组背包的类型:

第一组:

  • 进行一次操作,取走1,面值为1
  • 进行两次操作,取走1和100,面值为101
  • 进行三次操作,取走1,100和3,面值为104

也就是可以将上述分成类似分组背包中的第一个组有三个物品。

第二组:

  • 进行一次操作,取走7,面值为7
  • 进行两次操作,取走7和8,面值为15
  • 进行三次操作,取走7,8和9,面值为24

第二个组也有三个物品。

按上述就可以将上述问题转化成分组背包问题。

代码实现:一维数组

#include <stdio.h> #include <string.h> #include <stdlib.h> #define MAX(a, b) ((a) > (b) ? (a) : (b)) // 计算每个栈取不同数量硬币时的最大价值 int* getStackValues(int* pile, int pileSize, int k) {//只是将每个组的几种情况计算出来 int* values = (int*)calloc(k + 1, sizeof(int)); int sum = 0; // 取 t 个硬币的价值就是前 t 个硬币的和 for (int t = 1; t <= pileSize && t <= k; t++) { sum += pile[t - 1]; // pile[0]是栈顶 values[t] = sum;//这种将和赋值给数组每个元素的方法要会 } return values; } int maxValueOfCoins(int** piles, int pilesSize, int* pilesColSize, int k) { // dp[j] 表示恰好取 j 个硬币时的最大面值和 int* dp = (int*)calloc(k + 1, sizeof(int)); // 遍历每个栈(每个组) for (int i = 0; i < pilesSize; i++) { // 获取当前栈取不同数量硬币的价值 int* values = getStackValues(piles[i], pilesColSize[i], k); // 逆序更新dp,保证每个栈只取一次 for (int j = k; j >= 0; j--) { // 尝试从当前栈取 t 个硬币 for (int t = 1; t <= pilesColSize[i] && t <= j; t++) { dp[j] = MAX(dp[j], dp[j - t] + values[t]); } } free(values); } int result = dp[k]; free(dp); return result; }

二维数组:

#include <stdio.h> #include <string.h> #include <stdlib.h> #define MAX(a, b) ((a) > (b) ? (a) : (b)) int maxValueOfCoins(int** piles, int pilesSize, int* pilesColSize, int k) { // dp[i][j] 表示从前i个栈中恰好取j个硬币时的最大面值和 int** dp = (int**)malloc((pilesSize + 1) * sizeof(int*)); for (int i = 0; i <= pilesSize; i++) { dp[i] = (int*)calloc(k + 1, sizeof(int)); }//将数组全初始化为0 // 遍历每个栈 for (int i = 1; i <= pilesSize; i++) { int pileIndex = i - 1; // 当前栈在piles中的索引 int pileSize = pilesColSize[pileIndex]; // 先复制不取当前栈的情况 for (int j = 0; j <= k; j++) { dp[i][j] = dp[i - 1][j]; } // 计算当前栈的前缀和,表示取t个硬币的总价值 int sum = 0; // 尝试从当前栈取t个硬币 for (int t = 1; t <= pileSize && t <= k; t++) { sum += piles[pileIndex][t - 1]; // piles[pileIndex][0]是栈顶 // 从当前栈取t个,那么前面i-1个栈取j-t个 for (int j = t; j <= k; j++) { dp[i][j] = MAX(dp[i][j], dp[i - 1][j - t] + sum); } } } int result = dp[pilesSize][k]; // 释放内存 for (int i = 0; i <= pilesSize; i++) { free(dp[i]); } free(dp); return result; }

解析上述代码:

1.// dp[i][j] 表示从前i个栈中恰好取j个硬币时的最大面值和
    int** dp = (int**)malloc((pilesSize + 1) * sizeof(int*));
    for (int i = 0; i <= pilesSize; i++) {
        dp[i] = (int*)calloc(k + 1, sizeof(int));
    }

// 第一步:分配指针数组(第一维)
int** dp = (int**)malloc((pilesSize + 1) * sizeof(int*));
// 此时:
// dp[0] 是未初始化的指针(指向垃圾值)
// dp[1] 是未初始化的指针
// ...
// dp[pilesSize] 是未初始化的指针

// 第二步:为每一行分配实际数据空间(第二维)
for (int i = 0; i <= pilesSize; i++) {
    dp[i] = (int*)calloc(k + 1, sizeof(int));
}
// 此时:
// dp[0] 指向一个大小为 k+1 的int数组
// dp[1] 指向另一个大小为 k+1 的int数组
// ...

第一步后: dp → [ptr0][ptr1][ptr2]...[ptr_pilesSize] (每个ptr都是未初始化的) ↓ ↓ ↓ ↓ ? ? ? ? (指向哪里?不知道!) 第二步后: dp → [ptr0][ptr1][ptr2]...[ptr_pilesSize] ↓ ↓ ↓ ↓ [0,0,0] [0,0,0] [0,0,0] ... [0,0,0] (现在都指向合法的数组)

2.上述代码中主要有一个不一样的点是:

将在该栈中不取硬币的情况单独放到一个循环中,先算完该栈中不取任何硬币的面值,然后再开始循环,计算从栈中取一个,两个……的情况,取两个就是进行加和,将和作为一个物品进行计算面值。但也可以将不取的情况和取的情况放在一起,只是循环条件发生变化。

注意这个地方最容易因题而异!!!

Read more

Flutter for OpenHarmony: Flutter 三方库 cached_query 为鸿蒙应用打造高性能声明式数据缓存系统(前端缓存终极方案)

Flutter for OpenHarmony: Flutter 三方库 cached_query 为鸿蒙应用打造高性能声明式数据缓存系统(前端缓存终极方案)

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net 前言 在进行 OpenHarmony 应用开发时,网络请求的响应速度直接决定了用户体验(体验 UX)。如果用户每次切换页面都必须等待加载动画,应用会显得非常低级。我们不仅需要处理异步数据请求,更需要一套精密的机制来解决以下痛点: 1. 自动缓存:第二次访问时应瞬间展示历史数据。 2. 过期失效(Stale-while-revalidate):在展示旧数据的同时,后台静默拉取新数据。 3. 无限滚动:简单地处理分页与数据追加内容逻辑。 cached_query 是一个类似于 Web 端 React Query 的 Dart 状态管理库。它专注于数据获取与同步,让你的鸿蒙应用具备顶级的数据缓存表现。 一、核心缓存驱动机制 cached_query 在内存与数据源之间建立了一层“智能感知”缓存。 数据过期/缺失 返回新数据 发射流

By Ne0inhk
【踩坑记录】使用 Layui 框架时解决 Unity WebGL 渲染在 Tab 切换时黑屏问题

【踩坑记录】使用 Layui 框架时解决 Unity WebGL 渲染在 Tab 切换时黑屏问题

【踩坑记录】使用 Layui 框架时解决 Unity WebGL 渲染在 Tab 切换时黑屏问题 在开发 Web 应用时,尤其是集成了 Unity WebGL 内容的页面,遇到一个问题:当 Unity WebGL 渲染内容嵌入到一个 Tab 中时,切换 Tab 后画面会变黑,直到用户点击黑屏区域,才会恢复显示。 这个问题通常是因为 Unity 渲染在 Tab 切换时被暂停或未能获得焦点所致。 在本文中,我们将介绍如何在使用 Layui 框架时,通过监听 Tab 切换事件并强制 Unity WebGL 渲染恢复,来解决这一问题。 1. 问题描述 当 Unity WebGL 内容嵌入到页面中的多个

By Ne0inhk
JavaScript 中 var、let、const 的核心区别与实战应用

JavaScript 中 var、let、const 的核心区别与实战应用

要理解 const、var、let 的区别,我们可以从 作用域、变量提升、可重复声明、可修改性 这几个核心维度展开,这些也是新手最容易混淆的点。 一、核心概念铺垫 首先明确两个基础概念,能帮你更好理解区别: * 函数作用域:变量只在声明它的函数内部可访问(var 是函数作用域)。 * 块级作用域:变量只在声明它的 {} 内部可访问(let/const 是块级作用域,{} 包括 if/for/while/ 普通代码块)。 * 变量提升:JS 引擎在执行代码前,会把变量声明 “提升” 到当前作用域顶部(但赋值不会提升)。 二、逐个拆解 + 对比 1. var(ES5 语法) var 是 ES5 中声明变量的方式,特性如下:

By Ne0inhk
从 XMLHttpRequest 到 Fetch API:现代前端网络请求的演进与迁移指南

从 XMLHttpRequest 到 Fetch API:现代前端网络请求的演进与迁移指南

🧑 博主简介:ZEEKLOG博客专家,历代文学网(PC端可以访问:https://literature.sinhy.com/#/?__c=1000,移动端可关注公众号 “ 心海云图 ” 微信小程序搜索“历代文学”)总架构师,16年工作经验,精通Java编程,高并发设计,分布式系统架构设计,Springboot和微服务,熟悉Linux,ESXI虚拟化以及云原生Docker和K8s,热衷于探索科技的边界,并将理论知识转化为实际应用。保持对新技术的好奇心,乐于分享所学,希望通过我的实践经历和见解,启发他人的创新思维。在这里,我希望能与志同道合的朋友交流探讨,共同进步,一起在技术的世界里不断学习成长。 🤝商务合作:请搜索或扫码关注微信公众号 “ 心海云图 ” 从 XMLHttpRequest 到 Fetch API:现代前端网络请求的演进与迁移指南 引言:为什么我们需要新的网络请求方案? 在前端开发领域,XMLHttpRequest (XHR) 长期统治着浏览器端的网络请求。然而,随着 Web

By Ne0inhk