使用bitset优化0/1背包的时空复杂度。(100320. 执行操作可获得的最大总奖励 II)
被leetcode401单周赛狠狠打败了。就此记录下bitset优化0-1背包。
利用数字的二进制操作对整体进行转移
问题描述
求解思路
状态定义
定义\(dp[i][j]\)表示能否从前i个数中得到总奖励j。每次转移对前一个状态的j有特殊要求。
状态转移方程
考虑第i个物品选或不选:
- 不选第i个物品:\(dp[i][j] = dp[i-1][j]\) 。
- 选择第i个物品:\(dp[i][j] = dp[i-1][j-w[i]]\) ,需要满足\(j \geq w[i]\) 且\(j - w[i] < w[i]\) 。
状态为能否从前i个数中得到奖励,即$$dp[i][j] = dp[i-1][j]\ \cup\ dp[i-1][j-w[i]] $$
初始化方式
初始值\(dp[0][0] = true\) 。
所求答案为\(dp[n][j] = true\)中最大的j。
优化方式
对应的状态个数为\(n^{2}\),当然是过不了当前\(n = 2 \times 10^{5}\) 的数据。需要优化掉其中一个维度。
-
由于状态转移中第\(i\)层一定从第\(i - 1\)层转移过来,可以两个数组或者原地滚动进行优化。
但只省下空间,总状态数还是那么多,没啥用 -
注意到\(dp[i][j]\)一定是从\(dp[i-1][j]\)和\(dp[i-1][j-w[i]]\)转移过来,对应的\(0 \le j-w[i] < w[i]\)。也就是存在一一对应的关系,并且转移方式为两者取或。使用bitset对优化当前0/1背包的状态转移。
用二进制数表示每个状态,二进制数位j上的数字表示当前状态下的j能否被取到。 对应的状态转移为了满足限制条件,需要先将\(x\)中数位大于\(j\)的位置设置为0,再将当前的二进制数\(x\)与左移\(w[i]\)位的\(x^{*}\)取或。初始状态设置为1,代表开始只有0满足要求。
bitset介绍
该部分内容搬运自 -Wallace-的文章
它类似于 bool 数组,每个位置只有两种值:0 或 1。
bitset 的实现方式是压位,那么一个大小为 n𝑛 的 bitset 的空间复杂度为 \(O(\frac{1}{ω}n)\)。其中 ω=32 或 64(系统位数)。
基本操作
bitset<N> f;//定义一个大小为N的bitset,下标范围[0, N)
f.set(i);//在下标i设为1
f.reset(i);//将下标i设置为0
f.test(i);//判断下标i处是否为1
f[i];//取值
除了构造函数,其余操作均为\(O(1)\)。
进阶操作
f.set(); // 全部置为 1
f.reset(); // 全部置为 0
f = g; // bitset 赋值
f &= g; // 将 f 对 g 做按位与操作
f |= g; // 将 f 对 g 做按位或操作
f ^= g; // 将 f 对 g 做按位异或操作
// 以及各种位运算操作
f.count(); // 计算 bitset 中 1 的个数
这些范围操作都是\(O(\frac{1}{ω}n)\)的时间复杂度。bitset时空复杂度中的常数\(\frac{1}{w}\) 是bitset优化的关键。
代码案例
源自灵茶山艾府
bitset<100000>f{1};//初始化下标0的位置为1
for (int v : rewardValues) {
int shift = f.size() - v;
//bitset的区间操作带有常数1/w
f |= f << shift >> shift << v;
}
for (int i = rewardValues.back() * 2 - 1; ; i--) {
//获取最大的下标为1的位置
if (f.test(i)) {
return i;
}
}
使用bitset优化后,对应的时间复杂度为\((\frac{1}{w} n^{2})\) ,\(w\)对应为32或64,满足数据范围\(n = 2\times 10^{5}\)的要求。