P5363 [SDOI2019] 移动金币
Staircase-Nim 加计数。
首先怎么转化成 Staircase-Nim 呢,可以把每个金币右边到下一个金币中间那些空的地方看成这个金币的石子,那么每次金币的向左移动就是把石子从右边金币的堆移到左边金币的堆。注意最右侧也有可能有空的部分,这一部分不属于任何金币。
那么总共就有 \(m + 1\) 堆,\(n-m\) 个石子。根据 Staircase - Nim 的结论,当从右往左数第奇(从 0 开始)数堆上的 Nim 和不为 0 时,先手必胜,否则先手必败。
由于有限制的只有奇数堆(共 \(D = \lfloor \frac{m + 1}{2} \rfloor\) 个),偶数堆的分法直接插板,那么可以考虑先只对奇数堆 dp,最后再合并答案。异或和为 0 有很强的性质,所有我们对先手必败做计数,然后总的方案 \(n \choose m\) 减一下就行。异或和为 0 等价于每一位上 1 的个数都是偶数,那么考虑按位 dp。状态是 \(f_{i,j}\) 考虑前 \(i\) 位,当前石子个数是 \(j\) 的方案数。转移就是考虑这一位放几个 \(1\),那么就是 \(f_{i,j} {D \choose k} \to f_{i + 1, j + k \times 2 ^{i + 1}}, k \text{ is even}\)。
复杂度 \(O(nm \log n)\)
Code
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 1.5e5 + 5, M = 55, mod = 1e9 + 9;
ll fac[N], inv[N], ifac[N], dp[21][N];
int n, m;
ll C(int n, int m){return fac[n] * ifac[m] % mod * ifac[n - m] % mod;
}
int main(){cin.tie(nullptr)->sync_with_stdio(0);cin >> n >> m;fac[0] = ifac[0] = inv[1] = fac[1] = ifac[1] = 1;for(int i = 2; i <= n; ++i){fac[i] = (fac[i - 1] * i) % mod;inv[i] = ((mod - mod / i) * inv[mod % i]) % mod;ifac[i] = (ifac[i - 1] * inv[i]) % mod; }n -= m;dp[0][0] = 1;int D = __lg(n), B = (m + 1) / 2;for(int i = 0; i <= D; ++i){for(int j = 0; j <= n; j += 2){for(int k = 0; k <= B; k += 2){int nxt = j + k * (1 << i);if(nxt > n) break;(dp[i + 1][nxt] += dp[i][j] * C(B, k)) %= mod;}}}ll ans = 0;for(int j = 0; j <= n; j += 2){(ans += dp[D][j] * C(n - j + m - B, m - B)) %= mod;}cout << (C(n + m, m) + mod - ans) % mod;return 0;
}
P4689 [Ynoi Easy Round 2016] 这是我自己的发明
先考虑换根,我们发现,可以通过分讨把换根去掉。具体来说,我们先以 1 为根,把 dfs 序列建出来。记当前的根是 \(rt\), 对于 \(u\) 的子树该如何表示,有如下分讨:
- 如果 \(u = rt\),那么 \(u\) 的子树是整棵树 \(T\)。
- 如果 \(rt\) 在原来树上是 \(u\) 的子树中的某个点,记它来自 \(u\) 的儿子 \(v\) 的子树 \(S\)(这个可以倍增算)。那么这时 \(u\) 的子树是 \(T - S\)。
- 如果 \(rt\) 不在 \(u\) 的子树中,那么 \(u\) 的子树没有变化。
以上三种情况都对应着 dfs 序列上的一段(或几段的并),因此可以变成序列问题,就是这道题了。
那么对于序列上的询问 \(f(l_1, r_1, l_2, r_2)\) 怎么做呢,记 \(g(x, l, r)\) 表示 \([l,r]\) 这段中有多少个数的颜色是 \(x\)。那么 \(f(l_1, r_1, l_2, r_2) = \sum_x g(x,l_1, r_1) \times g(x, l_2, r_2)\),变成 \(\text{1-side}\) 的,也就是,\(\sum_x (g(x, 1, r_1) - g(x, 1, l_1 - 1)) \times (g(x, 1, r_2) - g(x, 1, l_2 - 1))\)。展开之后,发现我们就是要求 \(G(l, r) = \sum_x g(x, 1, l) \times g(x, 1, r)\)。
\((g(x, 1, l) + \Delta)g(x, 1, r)= g(x, 1, l)g(x, 1, r) + \Delta g(x, 1, r)\),因此 \(G(l, r)\) 可以 \(O(1)\) 拓展到相邻。考虑莫队。因为第一次写,就把一些莫队的东西也写这里了。莫队的基本思路是,按左端点所在的编号为第一关键字,右端点为第二关键字,升序排序。假设块长是 \(S\),右端点总的移动次数最大是块数乘 \(n\),左端点是块长乘询问数量 \(m\),也就是 \(n\times \frac{n}{S} + m \times S\),取块长是 \(S = \frac{n}{\sqrt m}\),复杂度是 \(n \sqrt m\)。
这个序列弱化版就做完了。但是由于我们在 dfs 序列上拆询问拆的特别多,所以有 16 倍常数。发现大部分的查询都是 \(G(x, l, n)\) 的形式,因此考虑预处理一个端点是 \(n\) 的询问。发现这样要插入的询问就是 4 倍常数了。
Code
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using pii = tuple<int, int>;
const int N = 1e5 + 5, M = 5e5 + 5;
vector<int> e[N];
int n, m, lg[N], a[N], V[N], S, cnt, f[N][21], dfn[N], siz[N], dep[N], tsp, _dfn[N];
ll g[N], sum[2][N], ans[M], nwans;
int getid(int x){ return (x - 1) / S + 1; }
void del(int op, int x){ x = a[_dfn[x]];nwans -= sum[op ^ 1][x];sum[op][x]--;
}
void add(int op, int x){x = a[_dfn[x]];nwans += sum[op ^ 1][x];sum[op][x]++;
}
struct node{int l, r, id, idx, k;
}q[M * 4];
void addq(int l, int r, int idx, int k){if(l > r) swap(l, r);if(l == 0) return;if(r == n) return ans[idx] += k * g[l], void();q[++cnt] = {l, r, getid(l), idx, k};
}
void ins(int l1, int r1, int l2, int r2, int idx){addq(r1, r2, idx, 1), addq(l1 - 1, r2, idx, -1),addq(r1, l2 - 1, idx, -1), addq(l1 - 1, l2 - 1, idx, 1);
}
void dfs(int u, int fa){_dfn[dfn[u] = ++tsp] = u;dep[u] = dep[fa] + 1;f[u][0] = fa;for(int i = 1; (1 << i) <= dep[u]; ++i) f[u][i] = f[f[u][i - 1]][i - 1];siz[u] = 1;for(int v : e[u]){if(v != fa){dfs(v, u);siz[u] += siz[v];}}
}
int find(int v, int u){for(int i = lg[dep[v] - dep[u]]; i >= 0; --i)if(dep[f[v][i]] > dep[u]) v = f[v][i];return v;
}
int main(){cin.tie(nullptr)->sync_with_stdio(0);cin >> n >> m;for(int i = 1; i <= n; ++i){cin >> a[i];V[i] = a[i];lg[i] = lg[i >> 1] + 1;}sort(V + 1, V + 1 + n);int sizV = unique(V + 1, V + 1 + n) - V - 1;for(int i = 1; i <= n; ++i){a[i] = lower_bound(V + 1, V + 1 + sizV, a[i]) - V;}for(int i = 1; i < n; ++i){int u, v;cin >> u >> v;e[u].emplace_back(v);e[v].emplace_back(u);}dfs(1, 0);S = ceil(n / sqrt(m));for(int i = 1; i <= n; ++i) add(1, i);for(int i = 1; i <= n; ++i){add(0, i);g[i] = nwans;}int rt = 1;for(int i = 1; i <= m; ++i){int op;cin >> op;if(op == 1) cin >> rt, ans[i] = -1;else{vector<pii> tmp[2];int ver[2];cin >> ver[0] >> ver[1];for(int op : {0, 1}){int u = ver[op];if(rt == u) tmp[op].emplace_back(1, n);else if(dfn[u] <= dfn[rt] && dfn[rt] <= dfn[u] + siz[u] - 1){int v = find(rt, u);// cout << i << ' ' << u << ' ' << v << ' ' << dfn[v] << ' ' << siz[v] << '\n';tmp[op].emplace_back(1, dfn[v] - 1);tmp[op].emplace_back(dfn[v] + siz[v], n);}else tmp[op].emplace_back(dfn[u], dfn[u] + siz[u] - 1);}for(auto [l1, r1] : tmp[0]){for(auto [l2, r2] : tmp[1])ins(l1, r1, l2, r2, i);}}}sort(q + 1, q + 1 + cnt, [](node x, node y){return (x.id == y.id ? x.r < y.r : x.id < y.id);});memset(sum, 0, sizeof(sum));nwans = 0;int l = 0, r = 0;for(int i = 1; i <= cnt; ++i){auto [pl, pr, _, idx, k] = q[i];while(l > pl) del(0, l), --l;while(r < pr) ++r, add(1, r);while(l < pl) ++l, add(0, l);while(r > pr) del(1, r), --r;ans[idx] += k * nwans;}for(int i = 1; i <= m; ++i){if(ans[i] != -1) cout << ans[i] << '\n';}return 0;
}
P7115 [NOIP2020] 移球游戏
超级棒的构造题(下面颜色的说明 0 -index 和 1 -index 混着说了)。
先考虑怎样优秀地做出 \(n = 2\) 的情况。假设我们让 1 柱放颜色是 0 的那些,2 柱同理。设一开始 1 柱有 \(a\) 个颜色是 0 的球。那么我们先从 2 柱上拿 \(a\) 个球当空柱子上,然后从上到下遍历 1 柱,如果是 0,放到 2 柱,否则放到空柱上。这时候,2 柱上面的 \(a\) 个都是 0,空柱上面的 \(m - a\) 个都是 1。然后先把 \(a\) 个 0 放回 1 柱,然后把 1 也放回去。然后把 2 柱剩下的 \(m - a\) 个放到空柱上。这时 2 柱是空的了,那么我们把 1 柱上面的 \(m - a\) 个 1 放到 2 柱上。然后从上至下遍历空柱,如果是 0 放 1 柱,1 放 2 柱。然后就做完了,并且空柱仍然是 \(n + 1\)。
操作次数是 \(a + m + m + (m - a) + (m - a) + m = 5m - a\)。取上界 \(5m\)。
然后是一个超级经典的 trick。0 - 1 值域分治(也就是 cdq)。我们还是让第 \(i\) 个柱子放颜色是 \(i\)。考虑分治,假设我们现在处理的是柱子 \([l, r]\),我们的目标是让 \(\le mid\) 的那些全部在 \([l, mid]\) 里,让 \(> mid\) 的全部在 \([mid + 1, r]\) 里。这时就可以吧 \(\le mid\) 的视为 0,否则视为 1,按类似 \(n = 2\) 的做法做了。
具体来说,每次挑出左边一个仍有 1 的柱子 \(i\),和右边一个仍有 0 的柱子进行配对 \(j\)。如果两个柱子 0 的个数加起来 \(\ge m\),那么我们可以按上面的方法让 \(i\) 变成全 0,否则可以让 \(j\) 变成全 1。这样每次配对总能让仍需处理的柱子减少 1,这样我们 \(solve(l, r)\) 的复杂度就是 \(O(r - l + 1)\)。加上分治,操作次数上界是 \(5nm \log n\),复杂度也是这个。
Code
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using pii = pair<int, int>;
const int N = 55, M = 405;
vector<pii> ans;
int n, m, a[N][M], tp[N], em;
void move(int x, int y){assert(tp[y] < m && tp[x] > 0);a[y][++tp[y]] = a[x][tp[x]], tp[x]--;ans.emplace_back(x, y);
}
void print(int x){cout << "Debug " << x << ",the size is " << tp[x] << '\n';for(int i = 1; i <= tp[x]; ++i) cout << a[x][i] << ' ';cout << '\n';
}
void merge(int x, int y, int k){// cout << x << ' ' << y << '\n';int cnt = 0;for(int i = 1; i <= m; ++i) if(a[x][i] <= k) ++cnt;for(int i = 1; i <= cnt; ++i) move(y, em); while(tp[x]) ((a[x][tp[x]] <= k) ? move(x, y) : move(x, em));// if(tp[x] != 0){// cout << x << ' ' << y << '\n';// print(x), print(y), print(em);// exit(0); // }assert(tp[x] == 0 && tp[y] == m);for(int i = 1; i <= cnt; ++i) move(y, x);for(int i = 1; i <= m - cnt; ++i) move(em, x);// assert(tp[y] == 0);while(tp[y]) move(y, em);for(int i = 1; i <= m - cnt; ++i) move(x, y);assert(tp[em] == m);for(int i = m; i >= 1; --i){if(a[em][i] <= k) {if(tp[x] < m) move(em, x);else move(em, y);}else{if(tp[y] < m) move(em, y);else move(em, x);}} assert(tp[x] == m), assert(tp[y] == m);assert(tp[em] == 0);
}
bool check(int x, int k, int op){for(int i = 1; i <= m; ++i) if((a[x][i] <= k) == op) return 0;return 1;
}
void solve(int l, int r){if(l == r) return;int mid = (l + r) >> 1;int x = l, y = mid + 1;// print(x), print(y);while(x <= mid && y <= r){merge(x, y, mid);if(check(x, mid, 0)) ++x;if(check(y, mid, 1)) ++y; }solve(l, mid);solve(mid + 1, r);
}
int main(){cin.tie(nullptr)->sync_with_stdio(0);cin >> n >> m;em = n + 1;for(int i = 1; i <= n; ++i){for(int j = 1; j <= m; ++j){cin >> a[i][j];}tp[i] = m;}solve(1, n);cout << ans.size() << '\n';for(auto [x, y] : ans) cout << x << ' ' << y << '\n';return 0;
}
P7518 [省选联考 2021 A/B 卷] 宝石
考虑如何处理跨过 \(rt\) 的询问 \((u, v)\),可以拆成 \((u, rt)\) 和 \((rt, v)\) 两部分。第一部分维护起点为 \(x\),开头是 1,最大能跳到数字为 \(f_x\) 的。第二部分维护末尾是数字 \(t\),最小能跳到的数字 \(g_t\)。然后把两边拼接起来就行。也就是找到到达 \(v\) 时,\(\max_{g_t \le f_u + 1} t\),可以 BIT,这样多一个 \(\log\)。但是我们发现这个 \(t\) 是可以二分的,那么直接做就行。
点分治什么的都是 trival 的了,注意回溯时各个数组也要回到上一个状态。
Code
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using pii = pair<int, int>;
using tpi = tuple<int, int, int>;
const int N = 2e5 + 5, inf = 1e9;
int n, m, T, c, p[N], f[N], g[N], h[N], idx[N], a[N], siz[N], mxsiz[N], rt, allsiz, ans[N], from[N];
bitset<N> done;
vector<int> e[N];
vector<pii> Q[N];
vector<tpi> query[N];
void chmax(int &x, int y){ x = max(x, y); }
void chmin(int &x, int y){ x = min(x, y); }
struct BIT{#define lb(i) (i & (-i))int tr[N], tp = 0;vector<pii> op[N];void upd(int x, int k){++tp; op[tp].clear();for(int i = x; i <= m; i += lb(i)){op[tp].emplace_back(i, tr[i]);chmax(tr[i], k);}}int qry(int x){int ret = 0;for(int i = x; i; i -= lb(i)) chmax(ret, tr[i]);return ret;}void back(){for(auto [x, y] : op[tp]) tr[x] = y;--tp;}
}tr;
void erase(int u){idx[a[u]] = idx[a[u] + 1] = 0;g[a[u]] = g[a[u] + 1] = inf;if(a[u]) g[a[u] - 1] = inf;
}
void clear(int u, int fa, int tp){erase(u);from[u] = tp;for(int v : e[u]){if(!done[v] && v != fa){clear(v, u, tp);} }
}
void init(int u, int fa){siz[u] = 1, mxsiz[u] = 0;for(int v : e[u]){if(!done[v] && v != fa){init(v, u);siz[u] += siz[v];chmax(mxsiz[u], siz[v]);}}chmax(mxsiz[u], allsiz - siz[u]);if(mxsiz[u] < mxsiz[rt]) rt = u;
}
void solve(int u, int fa){int bef = idx[a[u]];idx[a[u]] = u;h[u] = 1;chmax(h[u], h[idx[a[u] + 1]] + 1);f[u] = h[idx[1]];for(int v : e[u]){ if(!done[v] && v != fa){solve(v, u);}}idx[a[u]] = bef;
}
void getans(int u, int fa){int bef = g[a[u]]; if(a[u]) chmin(g[a[u]], min(a[u], g[a[u] - 1]));tr.upd(g[a[u]], a[u]);for(auto[v, idx] : Q[u]){if(f[v] <= 0) ans[idx] = tr.qry(1);else ans[idx] = max(f[v], tr.qry(f[v] + 1));}Q[u].clear();for(int v : e[u]){if(!done[v] && v != fa){getans(v, u);}}tr.back();g[a[u]] = bef;
}
void work(int u, vector<tpi> q){query[u].clear();for(int v : e[u]){if(done[v]) continue;query[v].clear(), clear(v, u, v);}from[u] = 0;erase(u);f[u] = 0;for(int v : e[u]) if(!done[v]) solve(v, u);for(auto [x, y, idx] : q){if(from[x] != from[y]) Q[y].emplace_back(x, idx);else query[from[x]].emplace_back(x, y, idx);}getans(u, 0);done[u] = 1;for(int v : e[u]){if(done[v]) continue;init(v, u); allsiz = siz[v];rt = 0, mxsiz[rt] = inf, init(v, u);work(rt, query[v]);}
}
int main(){cin.tie(nullptr)->sync_with_stdio(0);cin >> n >> m >> c;for(int i = 1; i <= c; ++i){int x; cin >> x;p[x] = i;}for(int i = 1; i <= n; ++i){cin >> a[i];a[i] = p[a[i]];}for(int i = 1; i < n; ++i){int u, v; cin >> u >> v;e[u].emplace_back(v);e[v].emplace_back(u);}allsiz = n, rt = 0, mxsiz[rt] = inf;init(1, 0);cin >> T;for(int i = 1; i <= T; ++i){int u, v;cin >> u >> v;query[rt].emplace_back(u, v, i);}work(rt, query[rt]);for(int i = 1; i <= T; ++i){cout << ans[i] << '\n';}return 0;
}
P7519 [省选联考 2021 A/B 卷] 滚榜
最终排名和公布排名的顺序正好相反。因此我们可以考虑 \(n!\) 的枚举所有排列。这时限制只有 \(b_i\) 降序序并且 \(a_i + b_i \ge a_j + b_j (j > i)\),和 \(a_i + b_i > a_j (j < i)\)。每次我们都取最小的 \(b_i\),如果最后的总和小于等于 \(m\) 那么这个排名就是可行的。复杂度是 \(O(n^2 \times n!)\)。进一步地,我们发现实际上只需要排名最后的那个满足 \(a_n + b_n > a_j (j < n)\) 就行了。其他只需要满足前面那个性质,这样可以做到 \(O(n\times n!)\)。
发现反过来按 \(b_i\) 升序考虑更加自然。考虑装压,\(f_{mask, i, b, sum}\) 表示已经考虑了 \(mask\) 中的队伍,最后一个队伍是 \(i\),最后放的 \(b_i\) 最小是 \(b\),此时 \(b\) 的总和是 \(sum\)。复杂度变成了 \(O(2 ^ n \times n \times m ^ 2)\),看起来更劣了?
一个重要的性质是 \(b_i\) 是升序的,因此如果我们考虑每次的增量 \(\delta\),这样需要满足的限制就只有 \(\delta \ge 0\),并且 \(a_j + (b_i + \delta) \ge a_i + b_i\) 了。\(delta\) 对于 \(sum\) 的贡献是一个简单的贡献提前计算 trick。也就是说,此时我们要记录的状态就变成了 \(f_{mask, i, sum}\)。
这告诉我们,当某个序列满足单调性时,有时候可以 dp 增量,然后贡献提前计算考虑 \(sum\)。
复杂度 \(O(2 ^ n \times n ^ 2 \times m)\)。
Code
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
#define int long long
const int N = 14, M = 505;
int n, m, c[N][N], a[N], f[8193][N][M];
signed main(){cin.tie(nullptr)->sync_with_stdio(0);cin >> n >> m;for(int i = 1; i <= n; ++i) cin >> a[i];for(int i = 1; i <= n; ++i){for(int j = 1; j <= n; ++j){if(j == i) continue;c[i][j] = max(0ll, a[i] - a[j] + (j > i));}}for(int i = 1; i <= n; ++i){int tmp = 0;for(int j = 1; j <= n; ++j){tmp = max(tmp, a[j] + (j < i));}int b = tmp - a[i];if(b * n <= m) f[1 << (i - 1)][i][b * n] = 1;}for(int i = 0; i < (1 << n); ++i){int cnt = __builtin_popcount(i); for(int j = 1; j <= n; ++j){if(i & (1 << (j - 1))){for(int sm = 0; sm <= m; ++sm){if(!f[i][j][sm]) continue;for(int k = 1; k <= n; ++k){if(!(i & (1 << (k - 1)))){int nxt = sm + c[j][k] * (n - cnt);if(nxt <= m) f[i | (1 << (k - 1))][k][nxt] += f[i][j][sm];}}}}}}int ans = 0, mask = (1 << n) - 1;for(int i = 1; i <= n; ++i){for(int j = 0; j <= m; ++j){ans += f[mask][i][j];}}cout << ans;return 0;
}