## 缺零分治
#贪心 #二分 #前缀和
首先可以先对原序列进行处理。$a$ 肯定是要变成从 $0$ 开始慢慢递增的(每次增加 $1$),后面的都可以不要了,反正放到哪个可重集都不会改变 $\text{mex}$。$b$ 的话,对于 ${} (i,j)$ 满足 $a_i>a_j$,那么也要 $b_i\le b_j$,否则 $b_i$ 多的那一部分不可能会改变 $\text{mex}$ 的大小(直观理解就是 $a_i$ 要连续递增,$b_i$ 要连续单调不增(增了就用前缀 $\min$ 解决))。
肯定是先要进行贪心的。对于 $m$,最简单的模拟方式是每次选择最大可能构造出来但是大小不超过 $m$ 的 $\text{mex}$。显然 $m$ 的值非常大,一步一步减肯定会 TLE。
最浅显的优化方式就是每次选择最大 $\text{mex}$ 之后直接选择多次,这样单次询问就可以做到 $\mathcal O(n)$。多组询问仍然不好搞。考虑离线下来排序,用单调性一起做,但是发现使用了更高的 $\text{mex}$ 值所有更小的数量都要减掉,然而维护了这个就不能批量搞了(因为大家都不一样)。
所以考虑快速做减法的操作。维护后缀和 $p_i$ 表示把 $[i,n]$ 中所有 $a_i$ 可以表示出的 $\text{mex}=a_i+1$ 都拿来用,能搞出的最大和。那么有递推式 $p_i=p_{i+1}+(a_i+1)\times (b_i-b_{i+1})$。${} a_i+1 {}$ 表示拿 $[0,a_i]$ 前缀能搞出来的 $\text{mex}$,$b_i-b_{i+1}$ 是因为 $a_{i+1}$ 和后面的用了 $b_{i+1}$ 个 $a_i$。
这样,我们可以通过二分快速进行减法操作,找到最小的 $x$ 满足 $p_x\le m$。现在的 $m\gets m-p_x$ 后,已经无力减掉 $x-1$ 的全部 $\text{mex}$ 了,但是可以减去部分。计算 $\left\lfloor\cfrac{m}{a_{x-1}+1}\right\rfloor$ 并累加至答案当中。最后 $m$ 连 $x-1$ 都减不了了,如果还 $\ne 0$ 的话,直接答案 $+1$ 即可。
时间复杂度 $\mathcal O(q\log_2 n)$。
```cpp
const LL kMaxN = 1e5 + 5;
LL a[kMaxN], b[kMaxN], sub[kMaxN], t, n, q;
int main() {
cin.tie(0)->sync_with_stdio(0);
for (cin >> t; t; t--) {
cin >> n >> q;
for (LL i = 1; i <= n; i++) {
cin >> a[i] >> b[i];
}
for (LL i = 1; i <= n; i++) {
if (i == 1 && a[i] != 0 ||
i != 1 && a[i] != a[i - 1] + 1) {
n = i - 1;
break;
}
}
b[0] = 1e18;
for (LL i = 1; i <= n; i++) {
b[i] = min(b[i - 1], b[i]);
}
b[n + 1] = sub[n + 1] = 0;
for (LL i = n; i >= 1; i--) {
sub[i] = sub[i + 1] + (a[i] + 1) * (b[i] - b[i + 1]);
}
for (LL m; q; q--) {
cin >> m;
if (n == 0) {
cout << (m == 0 ? 1 : -1) << '\n';
continue;
}
if (m == 0) {
cout << "-1\n";
continue;
}
if (m <= a[n] + 1) {
cout << 1 + (m != a[n] + 1) << '\n';
continue;
}
if (m - sub[1] > 0) {
cout << "-1\n";
continue;
}
LL p = -1;
for (LL l = 1, r = n + 1; l <= r; ) {
LL mid = (l + r) / 2;
if (sub[mid] <= m) {
p = mid;
r = mid - 1;
} else {
l = mid + 1;
}
}
LL use = b[p];
m -= sub[p];
if (m > 0) {
use += m / (a[p - 1] + 1);
m %= a[p - 1] + 1;
if (m > 0) {
use++;
}
}
cout << use << '\n';
}
}
return 0;
}
```
## 猜数游戏
#背包dp #序列dp
- [c] 赛时半个小时没读懂题,后面思考得过于复杂了,实际上很简单。$n\le 10^5$ 真是太有诱惑性了,小可爱出题人居然给时间复杂度 $\mathcal O\left(Tn^2\right)$(而且还是标解)。
赛后发现就是 ZRR 题目。
首先看到数据范围就很像 DP。设 $dp_i$ 表示长度为 $i$ 的子问题的答案(原问题是长度为 $n$,这里即 $1$ 跳到 $i$),那么有 $dp_i=\min\limits_{j=1}^{i-1}\max(dp_{j},dp_{i-j})+v_j$,其中 $j$ 表示跳一步长度为 $j$ 的最小花费。直观来看,就是枚举 $j$ 表示从 $[1,i]$ 左边或右边掏掉 $j$ 的,然后用 $v_j$ 的代价补上。
$v_j$ 求的方式可以通过完全背包搞。注意需要用到负数下标,所以需要设置偏移。时间复杂度 $\mathcal O(Tn^2)$。
```cpp
const LL kMaxN = 2e4 + 5, kM = 1e4;
LL a[kMaxN], b[kMaxN], wyy[kMaxN], zrr[kMaxN], c, t, n, m;
int main() {
for (cin >> c >> t; t; t--) {
cin >> n >> m;
for (LL i = 1; i <= m; i++) {
cin >> a[i];
}
for (LL i = 1; i <= m; i++) {
cin >> b[i];
}
fill(begin(wyy), end(wyy), 1e18);
fill(begin(zrr), end(zrr), 1e18);
wyy[kM] = 0;
for (LL i = 1; i <= m; i++) {
for (LL j = a[i]; j <= 2 * kM; j++) {
wyy[j] = min(wyy[j], wyy[j - a[i]] + b[i]);
}
for (LL j = 2 * kM - a[i]; j >= 0; j--) {
wyy[j] = min(wyy[j], wyy[j + a[i]] + b[i]);
}
}
zrr[1] = 0;
for (LL i = 2; i <= n; i++) {
for (LL j = 1; j < i; j++) {
zrr[i] = min(zrr[i], max(zrr[j], zrr[i - j]) + wyy[kM + j]);
}
}
cout << (zrr[n] == 1e18 ? -1 : zrr[n]) << '\n';
}
return 0;
}
```
## 树上求值
#字典树 #启发式合并
采用暴力。遍历每个节点 $x$,对 $\text{lca}$ 为 $x$ 的节点对累加贡献。有两种可能的 $\text{lca}$ 存在方式:
- $(x,y)$ 当中一个存在于另一个的子树当中。
- $(x,y)$ 当中 $x$ 和 $y$ 分别存在于 $\text{lca}$ 的不同儿子的子树当中。
这些贡献是很好累加的。需要注意 $(x,x)$ 的贡献,会被加两次,需要减掉。时间复杂度 $\mathcal O(n^2)$,成功获得 $20$ 分。
```cpp
LL calc(LL x) {
LL res = 1;
for (LL i = 0; i <= 20; i++) {
res = res * a[i][x & 1] % mod;
x >>= 1;
}
return res;
}
LL summary(LL lca, LL x, LL f) {
LL res = val[x + d[lca]];
for (LL i : e[x]) {
if (i != f) {
res = (res + summary(lca, i, x)) % mod;
}
}
return res;
}
void add(LL x, LL f, LL s) {
sum[x] = (sum[x] + s) % mod;
for (LL i : e[x]) {
if (i != f) {
add(i, x, s);
}
}
}
void dfs(LL x, LL f) {
d[x] = d[f] + 1;
for (LL i : e[x]) {
if (i != f) {
dfs(i, x);
}
}
add(x, f, val[x + d[x]]);
sum[x] = (sum[x] + summary(x, x, f)) % mod;
sum[x] = (sum[x] - val[x + d[x]] + mod) % mod;
for (LL i : e[x]) {
if (i == f) {
continue;
}
LL s = summary(x, i, x);
for (LL j : e[x]) {
if (j == f || j == i) {
continue;
}
add(j, x, s);
}
}
}
```
## 夜里亦始终想念着你
#数学
我采用的暴力+特殊性质,r可以得到 $36$ 分。
- 对于 $n\le 20$,采用暴力 `dfs` 计算所有可能得到的最终局面,再做子集 DP 即可。时间复杂度 $\mathcal O(2^nq)$。
- 对于保证奇数位置必有棋子的,偶数位置可以随便跳。对于最终的集合,我们枚举它有多少个选中了偶数位置,奇数位置就直接用 $2$ 的幂算即可。需要配合前缀和处理。
```cpp
void dfs(LL x) {
// 记忆化搜索
}
LL dp() {
// 子集 DP
}
f (n <= 20) {
dfs(mask(s));
cout << dp() << '\n';
for (LL x; q; q--) {
cin >> x;
s[x] = ((s[x] - '0') ^ 1) + '0';
fill(begin(f), end(f), 0);
dfs(mask(s));
cout << dp() << '\n';
}
return 0;
}
init();
LL wyy = 0, zrr = 0;
for (LL i = 1; i <= n; i++) {
if (i % 2 == 0) {
wyy++;
cnt += s[i] == '1';
} else {
zrr++;
}
}
ans[0] = 1;
for (LL i = 1; i <= wyy; i++) {
ans[i] = (ans[i - 1] + comb(wyy, i)) % kMod;
}
LL base = pow(2, zrr);
cout << ans[cnt] * base % kMod << '\n';
for (LL x; q; q--) {
cin >> x;
s[x] = ((s[x] - '0') ^ 1) + '0';
cnt -= s[x] == '0';
cnt += s[x] == '1';
cout << ans[cnt] * base % kMod << '\n';
}
```
## 总结
**总分**:$100+0+20+36=156$。
T2 想得太复杂了,下次应该多思考不同的思路。