背包
背包
前言:
可撤销背包,曾一次次洗刷着我对于背包的认知.
模拟赛考了一道背包神题:Log Set.
ABC 考了可撤销 01 背包的板子.
这三者是我想来一次彻底整理的源头吧。
多年之后,当我依稀听见这个陌生而熟悉的词,心中多少有一分怀念温存。
省流版前言:学动态规划不学背包,就像打二游不玩某二字开放世界游戏,看番剧不看进击的巨人,玩旮旯不玩千恋万花,只能度过一个相对失败的人生。
默认读者会基础的背包问题。
Pre-Definition
物品:物品。
背包:用来装物品的一个容器。
体积/重量:一件物品在背包中占用的体积/重量。
价值:将一件物品装入背包后获得的价值。
01背包:一件物品只能选一次或不选。
完全背包:一件物品可以选任意个。
多重背包:一件物品
01背包和完全背包在(一维)实现上的区别在于枚举顺序。
Tricks
换维
交换体积和价值,并改变 dp 数组的含义。
以 Knapsack 2 为例,体积是
由于要在限制的体积内求出价值总和最大值,因此转成记得到一个确定的价值,最少需要多少体积。
bitset优化判定性背包
CF1854B Earn or Unlock
当最终解锁的牌数确定时,最终价值也是确定的,所以只需要知道对于每一个
设计
注意要开
[ABC221G] Jumping sequence
单独考察一维,这个问题就是个 01 背包。麻烦的是前后两维是绑定在一起的。
如何解除这种绑定?转切比雪夫距离!
对应地,考察每一步的变化量也以同样的方式进行转化:
然后就可以分别考虑两个维度的背包问题了!
具体地,要求出一种分配正负号的方案,使
考虑两边同时加上
为了优化时空,可以采用 bitset 优化这种判断是否可以拼出一个数的背包问题。
很遗憾,如果只转化到了这一步,你会被出题人卡空间。你没有办法进行滚动优化,因为你需要根据之前的 dp 值倒推移动方案。
你发现如果
于是可以将左右同除
时空复杂度均为
多重背包的二进制优化
把一件物品的数量拆成
多重背包的前缀和优化
适用于计数类的多重背包。
考察一个物品数量为
记
对模
for(int i = 1; i <= n; ++i)
{
for(int j = 0; j + v[i] <= m; ++j)
Inc(f[j + v[i]], f[j]);
for(int j = m; j >= (s[i] + 1) * v[i]; --j)
Dec(f[j], f[j - (s[i] + 1) * v[i]]);
}
这样可以将原本为
[ARC104D] Multiset Mean
把每种数值看作一种物品,每种物品的数量为
这个价值为重量的
那么我们每选取一个数,就将这个数减
把数值本身塞进下标做多重背包,考虑到要对所有
注意减一以避免空集。
对于多重背包的部分,直接做是
但是可以用前缀和优化到
生成函数观点
这是个天坑,此处只介绍一道题。
LOJ556 「Antileaf's Round」咱们去烧菜吧
LOJ556 「Antileaf's Round」咱们去烧菜吧
考虑每一种物品的生成函数:
- 若
: 。 - 否则:
。
套路:把乘积形式通过取对数转为
仍然分为有限与无限两种情况讨论。
- 若无限:
。 - 若有限:
。
求
Others
在特殊限制下,我们可以用更加开放的思路去解决特定问题。
CF3B Lorry
当其中一种重量的物品数量确定时,另一种重量的物品数量上界可以被确定。
对于同一种重量的所有物品,当知道选择个数为
因此支持我们枚举重量为
可撤销背包
01背包的撤销操作
记未加入
考虑正向加入的更新过程,是
因此删除时,应该使
void insert(int x)
{
for(int i = n; i >= x; --i)
Inc(f[i], f[i - x]);
}
void remove(int x)
{
for(int i = x; i <= n; ++i)
Dec(f[i], f[i - x]);
}
[ABC321F] #(subset sum = K) with Add and Erase
关于可撤销背包,需要知道这个常用技巧:
可行性背包转方案数背包,以方便进行回退。
由于方案数很大,所以需要取模;而取模后为
[ABC056D] No Need
第一道例题对上面的技巧进行解释。
具体地,先整体做一遍正常的 01 背包,
对每个物品进行回退(
然后还要分析一下体积上限,因为我们只需要判断
CF981E Addition on Segments
仍然用到了上述技巧。
做可撤销背包,如果一个数能表示出来,则一定有办法让它成为一个所选子集中能表示出来的数的最大值。时间复杂度
此题有线段树分治的另解,作为线段树分治的讲解题目也很适合。
P6808 [BalticOI 2010 Day2] Candies
P6808 [BalticOI 2010 Day2] Candies
仍然用到了上述技巧。
感觉这是一个背包好题!但是不如 Log Set。
对于一个能表示出
所以可以将两个问分别考虑。下记
求
求 bitset
优化!
但是
看来要探究
神仙!把除
时间复杂度为 bitset
已经无所谓了。
注意加入负数时,01 背包的枚举顺序要变!并且应当先加入所有正数,再加入负数。
完全背包的撤销操作
void insert(int x)
{
for(int i = x; i <= n; ++i)
Inc(f[i], f[i - x]);
}
void remove(int x)
{
for(int i = n; i >= x; --i)
Dec(f[i], f[i - x]);
}
[AGC049D] Convex Sequence
可以用 差分 来理解式子,也可以把这个序列看成是 凸性 的,具体这道题是下凸。
考虑如何构造一个合法序列。
枚举第一个最小值的位置
- 整个数列加
。 - 选择
,给 分别加上 。由于我们限制 必须是第一个最小值,因此必须选择 进行一次这种操作。 - 选择
,给 分别加上 。
不同的
这一点很重要,因为这样我们就可以仅仅从操作序列求得答案。
考虑操作的过程相当于一个完全背包问题,从前往后枚举
考虑有效的背包物品数量为
(计数类)多重背包的撤销操作
[ARC028D] 注文の多い高橋商店
强制第
但是有多组询问。对于每个物品,预处理只考虑第
具体地:
记
这个过程不用枚举
可撤销背包中,删除一种物品有多种方法。
Question:为什么这里要讲这么多方法?
Answer:因为之前写这篇题解的时候就是这样的。
-
仿照上面的做法,倒着处理一遍 dp 数组
,统计答案时排除要求的物品即可。由于查询时还要枚举左右物品的数量分配,因此时间复杂度为
。 -
用生成函数表示多重背包的总方案数,即对于一种数量为
的物品,其贡献为 ,合起来的生成函数即为若干个分别的生成函数相乘。删除一种物品,即为除去该种物品对应的生成函数,多项式除法,但不用多项式工业,直接暴力即可, 。 -
采用分治思想,递归到叶节点表示删除该种物品。那么如果递归左边,右边就可以背包合并起来;反之。如此,每种物品会被合并
次,总时间复杂度为 。
Kami
gym100702D Log Set
版本 T0。
学背包不做 Log Set,就像打二游不玩某二字开放世界游戏,追星不追理塘王丁真珍珠,玩泣系旮旯不玩克拉纳的,只能度过一个相对失败的人生。
Problem
有一个大小为
现在以如下方式给定
给定
请求出符合条件的
Solution
模拟赛时的部分分设了一个
这个部分分给的太牛了,因为正解是每次按
记
记
若
否则,
对
简要说明:要么 加一个负数成为 ,要么 加一个正数成为 ,这里只讨论 的情况。
经过不断加数 成为 的过程,一定有 ,否则 不是最大值。 假设
, ,则 , 不是 中次大值,所以 至多加一个 中的数成为 。 又因为
, 是次大值,所以中间缺的这一块是 中(非 元素)的最小值。
现在我们尚且只知道了
01 背包中改变一个元素
神中神!这就是背包啊!
简要说明:只讨论
的情况,其它情况类似。 现在有一个未加入
的 dp 数组 ,考虑加入 的本质: 先将
向右平移 个单位,得到 dp 数组 ,再将 与 合并(对应下标的 与 值相加),得到 dp 数组 。 将
反号,再做一遍:先将 向左平移 个单位,得到 dp 数组 ,再将 与 合并(对应下标的 与 值相加),得到 dp 数组 。 你发现
和 唯一的区别就是 个单位的位置偏移!
那先强行钦定得到的
所以最大值和次大值的差是不变的,那么由上述算法得出的
现在可以进行定号了!
如果按照正确的
而我们的算法中,当
幸运的是,我们能够知道,一个错误的元素能够导致多少偏移量。
贪心地按绝对值从大到小判断当前值是否能够反号。
这又是一个背包问题:有
将
实现上,由于元素数值太大,需要用 map, set
等工具进行辅助。
做可撤销背包和求答案的时间复杂度为
Code
const int N = 1e4 + 5;
const int M = 65;
int T, n, m;
PLL a[N];
LL ans[M];
map<LL, LL> mp;
set<LL> f[M];
void work(int cas)
{
mp.clear(); LL sum = 0;
scanf("%d", &n);
for(int i = 1; i <= n; ++i)
scanf("%lld", &a[i].fi);
for(int i = 1; i <= n; ++i)
scanf("%lld", &a[i].se);
for(int i = 1; i <= n; ++i)
{
mp.insert(a[i]);
sum += a[i].se;
}
m = log2(sum);
for(int i = 1; i <= m + 1; ++i)
f[i].clear();
for(int i = 1; i <= m; ++i)
{
vector<PLL> vec; vec.clear();
LL mx1 = 7210721, mx2 = 7210721;
mx2 = mx1 = prev(mp.end())->fi;
if(prev(mp.end()) != mp.begin())
mx2 = prev(prev(mp.end()))->fi;
LL p = mx1 - mx2;
ans[i] = p;
for(auto now : mp)
{
LL x = now.fi, c = now.se;
if(c == 0)
{
vec.EB(MP(x, c));
continue;
}
if(mp.find(x + p) == mp.end())
continue;
if(p == 0) mp[x] >>= 1;
else mp[x + p] -= c;
}
for(auto now : vec)
mp.erase(now.fi);
}
LL delta = abs(mp.begin()->fi);
sort(ans + 1, ans + m + 1, [&](LL x, LL y){
return x > y;
});
f[m + 1].insert(0);
for(int i = m; i >= 1; --i)
for(auto x : f[i + 1])
{
f[i].insert(x);
f[i].insert(x + ans[i]);
}
for(int i = 1; i <= m; ++i)
if(f[i + 1].find(delta - ans[i]) != f[i + 1].end())
delta -= ans[i], ans[i] = -ans[i];
sort(ans + 1, ans + m + 1);
printf("Case #%d: ", cas);
for(int i = 1; i <= m; ++i)
printf("%lld ", ans[i]);
puts("");
}
int main()
{
scanf("%d", &T);
for(int cas = 1; cas <= T; ++cas)
work(cas);
return 0;
}
[ARC096F] Sweet Alchemy
将原题限制转化为父节点
这是一个显然的多重背包问题:选取一个子树
但是体积和物品数量都是
贪心
这个题最厉害的地方就是把背包和贪心结合在一起。
众所周知,按
尽可能地把绝大多数规模的问题划归到一个正确的贪心上以保证时间复杂度正确。
假设现在有物品
当选择的
记
借洛谷题解里的这个图:
可知中间部分的贪心是正确的,也即中间部分不可能有两个没填满的柱子。
把每个物品取
此时多重背包价值和的上界为
本文作者:Schucking-Sattin
本文链接:https://www.cnblogs.com/Schucking-Sattin/p/17726836.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步