## 器材整理
#暴力 #枚举
本题我赛时第一次提交可以通过,后面改了代码重新提交导致 TLE。我的算法不能保证正确性。
首先看到题目,就很容易想到二分,因为我们很难寻找最小的答案,但是我们可以很简单地判断一个答案是否合法。不过之前 [[2025.7.30 模拟赛]] 的最后一题和这个很相像,那题答案是没有单调性的。
至于如何判断答案合法性,我们整一个 set 存储目前尚未选择的所有器材,然后每次在 set 中寻找小于等于剩余空间的器材,如果没找到就新开一个箱子装,最后判断需要的箱子数量是否小于等于 $k$。
写完二分,果不其然地 wa 了,答案偏大。然后我就不会了,但是过了二十分钟我灵感爆发:既然我二分出来的答案虽然不对但是离正确的答案很接近,那我为什么不以二分出来的答案为参考进行枚举呢?
于是我就往二分出来的答案下面枚举 $1000$ 个数,取满足条件的最小值,就这样过大样例了。但是在我写其它题目的时候,我仍然忧心忡忡,害怕我的算法会 wa。后来在比赛结束的前十分钟,我把 $1000$ 改成了 $2000$ 重新交了一遍。然后我就 TLE 了。
以下是我初版不会 TLE 可以 AC 的代码:
```cpp
#include <iostream>
#include <set>
using namespace std;
using LL = long long;
const LL kMaxN = 1005;
LL a[kMaxN], t, n, k, mx, res, ans;
multiset<LL> s;
bool check(LL x) {
s.clear();
for (LL i = 1; i <= n; i++) {
s.insert(a[i]);
}
LL now = 0, cnt = 1;
for (LL i = 1; i <= n; i++) {
auto p = s.upper_bound(x - now);
if (p != s.begin()) {
p--;
now += *p;
s.erase(p);
} else {
now = 0, cnt++;
auto p = s.upper_bound(x - now);
if (p == s.begin()) {
return 0;
}
p--;
now += *p;
s.erase(p);
}
}
return cnt <= k;
}
int main() {
cin.tie(0)->sync_with_stdio(0);
for (cin >> t; t; t--) {
cin >> n >> k;
mx = 0;
for (LL i = 1; i <= n; i++) {
cin >> a[i];
mx = max(mx, a[i]);
}
for (LL l = mx, r = 1e9; l <= r; ) {
LL m = (l + r) / 2;
if (check(m)) {
res = m;
r = m - 1;
} else {
l = m + 1;
}
}
for (LL i = res; i >= max(mx, res - 1000); i--) {
if (check(i)) {
ans = i;
}
}
cout << ans << '\n';
}
return 0;
}
```
## 简单题
#贪心 #排序
首先这道题目对子串进行操作之后,它会变成 `#`,这就代表着这个子串在后面不会参与其它的操作,而我们要求的是最后的 `#` 的数量,因此 $f(s)$ 的值其实就是 $s$ 中 $0/1$ 段的数量。不难想到时间复杂度为 $\mathcal O(n^2)$ 的 dp。设 $dp_{i,j,0/1}$ 表示为前 $i$ 个字符中,有 $j$ 个 `1`,$f(s)$ 的最小值,按照题意转移即可。
但是本题 $2\le n\le 2\times 10^5$,dp 会 TLE 掉,并且状态似乎无法简化,这时候就要想到贪心。在贪心前,我们需要改变思路。我们可以先将所有的 `?` 设为 `0`,然后一个一个地将原来是 `?` 的 `0` 变成 `1`,在变化的过程中维护 $0/1$ 段的数量。
为了方便,以下我们将“把原来的 `?` 化成的 `0` 变成 `1`”的操作称为“升级”操作。
考虑一种简单的 **鼠目寸光** 的贪心是:维护每一个原来是 `?` 的 `0` 升级到 `1` 所带来的贡献(贡献是答案 $0/1$ 段的增量),每次对贡献值最小位置进行升级。贡献值只和左右邻居有关,因此我们进行操作之后还要顺带维护邻居的贡献值。每次取最小+快速删除插入指定元素,set 是一个不错的选择。
```cpp
// 计算最开始所有 ? 都是 0 的段数
int calc() {
int cnt = 1;
for (int i = 2; i <= n; i++) {
cnt += b[i] != b[i - 1];
}
return cnt;
}
// 计算一个 0 升级到 1 的贡献,即新段数-旧段数
int delta(int x) {
return ((b[x - 1] != '1') + (b[x + 1] != '1')) -
((b[x - 1] != '0') + (b[x + 1] != '0'));
}
// 统计最初 1 的数量,记录 ? 的位置
for (int i = 1; i <= n; i++) {
if (s[i] == '1') {
ori++;
} else if (s[i] == '?') {
v.push_back(i);
}
}
// b 是将所有 ? 变成 0 的版本
b = s;
for (int i : v) {
b[i] = '0';
}
fill(ans, ans + n + 1, -1); // 初始化答案为 -1
now = calc(); // 最初的答案
ans[ori] = now;
// 放入所有贡献
for (int i : v) {
q.emplace(cur[i] = delta(i), i);
}
// 进行 0 到 1 的升级操作
for (int i = 1; i <= v.size(); i++) {
auto p = *q.begin(); // 取贡献值最小的
now += p.first; // 累加贡献
ans[ori + i] = now; // 记录答案
b[p.second] = s[p.second] = '1'; // 标记以被升级
// 对左邻居的贡献重计算
if (s[p.second - 1] == '?') {
q.erase(make_pair(cur[p.second - 1], p.second - 1));
q.insert(make_pair(cur[p.second - 1] = delta(p.second - 1), p.second - 1));
}
// 对右邻居的贡献重计算
if (s[p.second + 1] == '?') {
q.erase(make_pair(cur[p.second + 1], p.second + 1));
q.insert(make_pair(cur[p.second + 1] = delta(p.second + 1), p.second + 1));
}
q.erase(p);
}
```
这个算法当然是错误的,譬如 `0?0???0` 的数据,此时先选择右边的 `?` 进行升级是更优的,因为段数最开始 $+2$ 之后就一直不变直到右边耗完;而如果我们先选择左边的 `?` 进行升级了话,段数 $+2$ 了之后还要选择右边的,这时段数会重新 $+2$,当然是不优的。
此时我们就思考了,是不是如果贡献值一样的话,所在的连续的一段 `?` 长度越长越优呢?也不一定,比如 `1?1???1` 的数据,此时先升级左边的段数立马就能 $-2$,而若是先升级右边的得升级 $3$ 次才能让段数 $-2$。
那我们是不是要进行分类讨论,讨论一段两侧是都是 $1$,还是都是 $0$,还是一边是 $1$ 一边是 $0$ 呢?真是太复杂了,好渴鹅肯定不会写。但是,最近我们都一直在强调“一段连续的 `?`”,我们的最优操作是否是围绕着一段一段进行的呢?
---
**证明——最优操作一定是一段一段进行升级的**:(这里默认每一段长度大于 $1$)当我们对于全 `0` 的一段原本是 `?` 的未升级的段首次进行升级时,段数一定不会减少的[^1];初次升级之后接下来对同一段升级的若干次,如果没有把这一段填满的话,段数是不会改变的[^2];如果将这一段填满成 $1$ 了,段数是一定不会增加的[^3]。经过我们的讨论,不难发现:开新的一段不会当前段数不会更小,逮着一段专门升级,升级完了段数不会更大。因此最优操作一定是一段一段进行升级的。
[^1]: 如果段左边和右边都是 $0$ 时,初次升级段数会 $+2$;如果段左边和右边一 $0$ 一 $1$,选择 $1$ 的那边升级,初次升级段数不会改变;如果段左边和右边都是 $1$ 时,初次升级段数不会改变。
[^2]: 每次都升级和上一次升级的相邻的那个,段数不会改变。
[^3]: 如果段左边和段右边都是 $1$,填满后两侧会连起来,段数 $-2$;否则段数不变。
接下来我们就需要考虑对于每一段,应该按照什么样的方式升级才是最优的。
我们对于每一段的升级,搞三个变量 `fir`、`len` 和 `las`,分别表示初次升级的贡献、段的长度和升级完毕后的贡献。贡献计算方式在角标中优详细说明。
然后对于每一段进行排序,首先按照 `fir` 排,越小越好,因为这是第一次升级之后可以立马带来的贡献;其次按照 `las` 排,越小越好,因为这是升级完毕之后可以延迟带来的贡献;最后是按照 `len` 排,如果 `las` 是负数那么当然是越先满足越好,`len` 小的排前面,否则 `len` 大的排前面让 `las` 的增量尽量在后面加上,保证最优。
最后按照排序后的顺序枚举每一段进行升级即可。时间复杂度 $\mathcal O(n\log_2 n)$。
```cpp
#include <iostream>
#include <algorithm>
#include <string>
#include <vector>
using namespace std;
const int kMaxN = 2e5 + 5;
struct Node {
int fir, len, las;
};
int ans[kMaxN], t, n, ori, now;
string s, b;
vector<int> v;
vector<Node> c;
// 计算初始答案
int calc() {
int cnt = 1;
for (int i = 2; i <= n; i++) {
cnt += b[i] != b[i - 1];
}
return cnt;
}
// 获取每一段的信息
void getLen() {
for (int i = 1, j; i <= n; ) {
if (s[i] != '?') { // 如果不是 ? 就不用找了
i++;
continue;
}
for (j = i; s[j] == '?'; j++) { } // 找到第一个不是 ? 的下标,[i,j) 就是段
// 计算该段贡献
c.push_back(Node{2 * (s[i - 1] == '0' && s[j] == '0'),
j - i,
2 * -(s[i - 1] == '1' && s[j] == '1')});
i = j;
}
}
void solve() {
cin >> n >> s;
s = " " + s; // 下标从 1 开始
c.clear(); // 清空段
getLen(); // 获取段
ori = count(s.begin() + 1, s.begin() + n + 1, '1'); // 获取原始 1 数量
fill(ans, ans + n + 1, -1); // 初始化答案为 -1
b = s; // b 是将所有 ? 变成 0 的版本
for (int i = 1; i <= n; i++) {
if (s[i] == '?') {
b[i] = '0';
}
}
now = calc(); // 初始贡献
ans[ori] = now; // 记录答案
// 对段进行排序
sort(c.begin(), c.end(), [](const Node &a, const Node &b) {
if (a.fir != b.fir) {
return a.fir < b.fir;
}
if (a.las != b.las) {
return a.las < b.las;
}
return a.las < 0 ? a.len < b.len : a.len > b.len;
});
// 按照排序后顺序枚举
for (auto &i : c) {
now += i.fir; // 升级一次就有的贡献
for (int j = 1; j < i.len; j++) { // 记录答案
ans[++ori] = now;
}
now += i.las; // 升级完才有的贡献
ans[++ori] = now; // 记录答案
}
// 输出答案
for (int i = 0; i <= n; i++) {
cout << ans[i] << ' ';
}
cout << '\n';
}
int main() {
cin.tie(0)->sync_with_stdio(0);
for (cin >> t; t; t--) {
solve();
}
return 0;
}
```
## 异或可达
#并查集 #线段树
不会正解,所以我打的是暴力。
用并查集维护连通块,对于每个询问枚举每一条边,若满足条件就进行并查集合并,最后单个连通块内的任意两个元素都是可以到达的,计算组合数并累加。
## ABBA
#序列dp
本来写了一个 dp 的,但是会 wa,遂改成大爆搜吗,然后第一个点都能 TLE,于是就趋势了。
## 总结
总分:$55+100+20+0=175$。
花了太多时间在调 T2 的错误的贪心上,结果最后十分钟就重写出了正确的贪心;第一题本来能过的最后乱改导致 TLE;数据结构掌握不过关,不会 T3;时间搭配不合理,没时间写 T4 $\mathcal O(n^3)$ dp。总结:代码能跑不要乱动。