【排序 + 状态压缩背包】掷出重围
【排序 + 状态压缩背包】掷出重围
关键性质
对于已经选定的一组球,投掷顺序不会改变最终被占据的位置集合。
考虑交换两个相邻且落点逆序的球,它们的初始落点分别为 。设从 开始的第一个空位置为 。
- 若 ,从 投出的球停在 ,不会影响从 开始寻找空位。
- 若 ,那么区间 已经全部被占据。无论哪个球先投,第一个球都会停在 ,第二个球都会停在 后面的第一个空位。
因此可以不断交换逆序的投掷,相当于按照 从小到大投掷。
将选择的球按照初始落点排序。假设上一个球最终停在 ,当前球的初始落点为 ,那么当前球的最终位置为
如果 ,它直接停在 。
如果 ,上一个球的初始落点不超过 ,而它能够滚动到 ,说明从 到 的位置都已被占据,所以当前球停在 。
朴素动态规划
可以把已经使用的体力和上一个球的最终位置都放入状态。
但 最大达到 ,不能直接将实际坐标作为数组下标。
关键在于:处理到当前落点 时,只需要知道上一个球比 多滚出了多少步。这个偏移量最多为选中球的数量,因此不超过 。
这种把最后落点表示成相对当前 的偏移,并在相邻落点之间平移状态的做法,可以将复杂度控制在 。(洛谷)
状态定义
首先把球按照 从小到大排序。
在处理第 个球之前,定义 表示:
- 已经处理了前 个球;
- 总体力消耗恰好为 ;
- 当前所有已选球的坐标和最大值;
- 上一个被投出的球相对于 的位置状态为 。
其中:
- :没有投出过球,或者上一个球的最终位置小于 ;
- :上一个球的最终位置为
只记录最大坐标和,因为状态相同的两个方案中,坐标和较小的方案不会更优。
初始状态为
其他状态均为负无穷。
投出当前球
设当前球消耗体力 。
如果原状态为 ,当前球停在 。
如果原状态为 ,上一个球停在 ,当前球停在
两种情况可以统一写成:当前球最终停在 ,新的偏移状态为 。
因此转移为
$$f\_{j+x\_i,k+1} \max \left( f\_{j+x\_i,k+1}, f\_{j,k}+w\_i+k \right). $$为了保证每个球最多选择一次,体力 必须从大到小枚举。
不投出当前球时,状态不需要修改。
将基准移动到下一个落点
处理完第 个球后,状态使用的坐标基准还是 。下一个球的基准为 。
记
若当前 ,最后一个球的位置为
相对于新的基准 ,新的偏移量为
如果这个值不大于 ,说明最后一个球已经位于 左侧,具体位置不再重要,可以统一压缩为状态 。
因此平移后的状态为
对于多个状态平移到同一个 的情况,只保留坐标和最大的状态。
正确性说明
按照前面的交换论证,任意投掷方案都可以调整为按照 非递减的顺序投掷,同时最终占据的位置集合不变,因此排序不会遗漏最优方案。
处理第 个球时,状态 准确记录上一个球与当前初始落点的相对关系。
- 选择当前球时,其最终位置必然为 ,所以选择转移准确模拟了滚动过程。
- 不选择当前球时,原方案仍然合法。
- 移动到下一个初始落点后,公式 准确描述最后位置相对于新基准的偏移。当最后位置位于新基准左侧时,所有这类状态对后续球的影响完全相同,因此可以合并。
每个球只有选择与不选择两种转移,所有体力不超过 的子集都会被处理。相同状态只保留最大坐标和不会丢失最优答案,因此最终得到的最大值就是答案。
参考实现
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 505;
const ll inf = (1LL << 60);
int n, s;
pair<ll, int> a[N];
ll f[N][N], g[N];
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> n >> s;
for (int i = 1; i <= n; i++) {
cin >> a[i].first;
}
for (int i = 1; i <= n; i++) {
cin >> a[i].second;
}
sort(a + 1, a + n + 1);
a[n + 1].first = a[n].first;
for (int j = 0; j <= s; j++) {
for (int k = 0; k <= n; k++) {
f[j][k] = -inf;
}
}
f[0][0] = 0;
for (int i = 1; i <= n; i++) {
ll w = a[i].first;
int x = a[i].second;
for (int j = s; j >= x; j--) {
for (int k = 0; k < i; k++) {
if (f[j - x][k] == -inf) {
continue;
}
f[j][k + 1] = max(
f[j][k + 1],
f[j - x][k] + w + k
);
}
}
ll d = a[i + 1].first - a[i].first;
for (int j = 0; j <= s; j++) {
for (int k = 0; k <= n; k++) {
g[k] = -inf;
}
for (int k = 0; k <= i; k++) {
if (f[j][k] == -inf) {
continue;
}
int t = (int)max(0LL, k - d);
g[t] = max(g[t], f[j][k]);
}
for (int k = 0; k <= n; k++) {
f[j][k] = g[k];
}
}
}
ll ans = 0;
for (int j = 0; j <= s; j++) {
for (int k = 0; k <= n; k++) {
ans = max(ans, f[j][k]);
}
}
cout << ans << '\n';
return 0;
}
复杂度分析
排序的时间复杂度为 。
动态规划中,每个球枚举体力和偏移量,选择转移与基准平移的复杂度均为 ,因此总时间复杂度为
动态规划数组大小为 ,空间复杂度为
京公网安备11010802045784号