闲话
草 感觉写闲话时浑身不自在(
溜了溜了
今日推歌:临川浮梦
链接自行搜吧(
但是好听!
模拟赛题解
感觉最近好颓啊……
T1 神奇纸牌
我怎么写不出容斥式子了?
srds 感觉验题人做法好理解 写验题人的做法(
设颜色种类 \(k = 4\)。我们可以转化成 \(n\times k\) 的网格涂黑,计数联通方案。
考虑每一行 \(k\) 个格子的涂色方案只有 \(2^k\) 种。然后我们可以将方案按出现哪些行涂色方案归类,可以发现只有 \(2^{2^k}\) 种。我们可以暴力筛出来每种行涂色方案是否联通,假设这种方案里有 \(m\) 种行的涂色方案,那贡献就是 \(n\) 个彼此不同的小球放在 \(m\) 个彼此不同的盒子里,每个盒子至少放一个,根据十二重计数法答案显然是 \(\sum_{i\ge 0} (-1)^{m - i} \binom{m}{i} i^n\)。由于 \(m\le 2^k\),可以做到 \(O(2^k \log n)\) 求每种可能。
总时间复杂度 \(O(2^{2^k + 1} + 2^k \log n)\),瓶颈在预处理 \(m\) 对应的方案数。
如果毒瘤出题人把 \(k\) 开到 5,可以预处理每个 \(m\) 有多少种合法方案,打个 \(2^k\) 大小的表,最后只需要 \(O(2^k \log n)\) 统计答案即可。
k 再大还能做吗?
code
#include <bits/stdc++.h>
using namespace std;
#define int long long
int mod, phi, ans, C[20][20], g[20], cnt[20] = {0,15,95,453,1595,4079,7775,11325,12838,11436,8008,4368,1820,560,120,16,1};
ll n;
int qp(int a, int b) {
int ret = 1;
while (b) {
if (b & 1) ret = 1ll * ret * a % mod;
a = 1ll * a * a % mod;
b >>= 1;
} return ret;
}
signed main() {
cin >> n >> mod;
rep(i,0,16) C[i][0] = 1;
rep(i,1,16) rep(j,1,i) C[i][j] = (C[i - 1][j] + C[i - 1][j - 1]) % mod;
rep(i,1,16) g[i] = qp(i, n);
rep(i,2,16) rep(j,1,i-1) g[i] = (g[i] - 1ll * C[i][j] * g[j] % mod + mod) % mod;
rep(i,0,16) ans = (ans + 1ll * g[i] * cnt[i]) % mod;
cout << ans + 1 << '\n';
}
T2 凌乱平衡树
可以知道 \(\sum dep = \sum size\)。因此我们可以转化成对子树大小之和的求解。
这样转化的好处是只有 treap 一侧的链在合并时的贡献要增加,增加的答案外就是两棵 treap 的字数大小之和,可以每次旋转时 \(O(1)\) 计算。
然后只需要查看增加的贡献。我们拉出来两条边链上节点的 size 自上而下组成的序列,左边 treap 的右链记作 \(A\),右边 treap 的左链记作 \(B\),这两个序列都单减。
观察合并过程。
合并是维护两个初始为 \(1\) 的指针 \(i,j\)。如果 \(A_i \ge B_j\) 则 \(i\) 自增,答案增加 \(B_j\),反之 \(j\) 自增,答案增加 \(A_i\)。
可以发现,\(A_i\) 的贡献次数是落在 \((A_i, A_{i - 1}]\) 里的 \(B\) 的个数,\(B_j\) 的贡献次数是落在 \([B_i, B_{i - 1})\) 里的 \(A\) 的个数。
由于这个相对关系,我们可以让 \(A_i = 2A_i + 1\),\(B_j = 2B_j\),这样就能得到严格的大小关系,好维护了。
可以发现如果将 \(A,B\) 放在值域上,一个值对答案的贡献就是大于它且和他奇偶性不同的极长连续段长度。这个可以用权值线段树上维护连续段得到答案。由于每次旋转只会对边链上 \(O(1)\) 个节点作修改,直接单点修改后维护区间答案,最后提出根节点答案就是增加的答案。
总时间复杂度 \(O(n\log n)\)。
code
#include <bits/stdc++.h>
using namespace std;
int n, m, t, t1, t2;
ll ans;
struct SegmentBeats {
#define ls (p << 1)
#define rs (p << 1 | 1)
struct node {
int lmst[2], rmst[2], hd[2], tl[2], cnt;
ll ans;
inline friend node operator + (const node& l, const node& r) {
if (!l.cnt or !r.cnt) return l.cnt ? l : r;
node ret;
rep(i,0,1) ret.lmst[i] = l.lmst[i] + (l.lmst[i] == l.cnt ? r.lmst[i] : 0);
rep(i,0,1) ret.rmst[i] = r.rmst[i] + (r.rmst[i] == r.cnt ? l.rmst[i] : 0);
rep(i,0,1) ret.hd[i] = l.hd[i] ? l.hd[i] : r.hd[i];
rep(i,0,1) ret.tl[i] = r.tl[i] ? r.tl[i] : l.tl[i];
ret.cnt = l.cnt + r.cnt; ret.ans = l.ans + r.ans;
int bt = r.lmst[0] ? 0 : 1;
ret.ans += r.lmst[bt] * l.tl[bt ^ 1];
return ret;
}
inline void reset(int typ = -1) {
if (typ == -1) memset(this, 0, sizeof *this);
else lmst[typ] = rmst[typ] = hd[typ] = tl[typ] = 0;
}
} seg[N << 3];
void update(int p, int l, int r, int pos, int va) {
if (l == r) {
if (va == -1) seg[p].reset();
else {
int bt = pos & 1;
seg[p].lmst[bt] = seg[p].rmst[bt] = 1;
seg[p].hd[bt] = seg[p].tl[bt] = pos >> 1;
seg[p].reset(bt ^ 1); seg[p].cnt = 1, seg[p].ans = 0;
} return;
} int mid = l + r >> 1;
if (pos <= mid) update(ls, l, mid, pos, va);
else update(rs, mid + 1, r, pos, va);
seg[p] = seg[ls] + seg[rs];
}
} Tr;
int rt1, rt2;
struct node {
int fa, son[2], siz;
} tr[N];
#define ps_p(p) tr[p].siz = tr[tr[p].son[0]].siz + tr[tr[p].son[1]].siz + 1
int vis[N];
void dfs1(int u, bool f) {
vis[u] = f;
if (tr[u].son[0]) dfs1(tr[u].son[0], 0);
if (tr[u].son[1]) dfs1(tr[u].son[1], f);
ps_p(u); ans += tr[u].siz;
if (vis[u]) Tr.update(1, 1, t, tr[u].siz << 1 | 1, 1);
}
void dfs2(int u, bool f) {
vis[u] = f;
if (tr[u].son[0]) dfs2(tr[u].son[0], f);
if (tr[u].son[1]) dfs2(tr[u].son[1], 0);
ps_p(u); ans += tr[u].siz;
if (vis[u]) Tr.update(1, 1, t, tr[u].siz << 1, 1);
}
void rotate1(int u) {
int f = tr[u].fa, g = tr[f].fa;
bool sn = tr[f].son[1] == u, fsn = tr[g].son[1] == f;
if (vis[u]) Tr.update(1, 1, t, tr[u].siz << 1 | 1, -1), vis[f] = 0;
tr[f].son[sn] = tr[u].son[sn ^ 1], tr[tr[u].son[sn ^ 1]].fa = f;
ans -= tr[f].siz; ps_p(f); ans += tr[f].siz;
tr[u].son[sn ^ 1] = f, tr[f].fa = u;
ans -= tr[u].siz; ps_p(u); ans += tr[u].siz;
if (vis[f]) Tr.update(1, 1, t, tr[f].siz << 1 | 1, 1), vis[u] = 1;
if (g == 0) rt1 = u, tr[u].fa = 0;
else tr[g].son[fsn] = u, tr[u].fa = g;
}
void rotate2(int u) {
int f = tr[u].fa, g = tr[f].fa;
bool sn = tr[f].son[1] == u, fsn = tr[g].son[1] == f;
if (vis[u]) Tr.update(1, 1, t, tr[u].siz << 1, -1), vis[f] = 0;
tr[f].son[sn] = tr[u].son[sn ^ 1], tr[tr[u].son[sn ^ 1]].fa = f;
ans -= tr[f].siz; ps_p(f); ans += tr[f].siz;
tr[u].son[sn ^ 1] = f, tr[f].fa = u;
ans -= tr[u].siz; ps_p(u); ans += tr[u].siz;
if (vis[f]) Tr.update(1, 1, t, tr[f].siz << 1, 1), vis[u] = 1;
if (g == 0) rt1 = u, tr[u].fa = 0;
else tr[g].son[fsn] = u, tr[u].fa = g;
}
signed main() {
cin >> n >> m; t = max(n << 1 | 1, m << 1);
rep(i,1,n) {
cin >> t1 >> t2;
tr[i].son[0] = t1, tr[i].son[1] = t2;
tr[t1].fa = i, tr[t2].fa = i;
} rep(i,1,n) if (tr[i].fa == 0) rt1 = i;
rep(i,n+1,n+m) {
cin >> t1 >> t2; t1 += (t1 > 0) * n, t2 += (t2 > 0) * n;
tr[i].son[0] = t1, tr[i].son[1] = t2;
tr[t1].fa = i, tr[t2].fa = i;
} rep(i,n+1,n+m) if (tr[i].fa == 0) rt2 = i;
dfs1(rt1, 1); dfs2(rt2, 1);
cout << ans + Tr.seg[1].ans << '\n';
multi {
cin >> t1 >> t2;
if (t1 == 1) rotate1(t2);
else rotate2(t2 + n);
cout << ans + Tr.seg[1].ans << '\n';
}
}
T3 打扫笛卡尔
说实在的我第一眼以为这是 DS 题(
根据期望线性性,考虑拆解成每个点的答案的加和。然后观察一个深度为 \(d\) 的点。
由于我们计算的是所有可能的 dfs 方法,由祖先向对应方向递归的可能性显然是 \(1/2\),则这个点对答案的期望贡献是 \(d / 2^{d - 1}\)。因此如果记 \(F(i, j)\) 为标号为 \(i\) 的节点深度为 \(j\) 的笛卡尔树的计数,有答案即为
我反正不会通过分析得到 \(F\) 的通项,因此考虑 dp 笛卡尔树的形态。设 \(f(i, j)\) 为前 \(i\) 个数里 \(1\to i\) 能拉一条长度为 \(j\) 的左链的排列的计数,我们可以直接分两部分 dp。首先选出 \(i\) 个数来,方案数是组合数。然后前一半的方案数就是 \(2^{j - 1}\times f(i, j)\),考虑到笛卡尔树的链有左右之分。后一半的方案数就是 \((n - i)!\),由于没有限制。也就是
\[F(i, j) = \binom{n}{i} 2^{j - 1} \times f(i, j) \times (n - i)! \]然后我们就只需要看这条链了。考虑枚举父亲是 \(k\),那 \(\in (k, i)\) 的数不能放在 \(k\sim i\) 之间,而 \(\in [1, k)\) 的可以。可以写出
\[f(i, j) = \sum_{k = 1}^{i - 1}f(k, j - 1)\times \binom{i - 2}{k - 1}\times (i - k - 1)! \]显然 \(f(1, 1) = 1\)。直接做得到 \(O(n^3)\),前缀和优化 \(O(n^2)\)。
重写答案如下:
\[\sum_{i\ge 1}\sum_{j\ge 1} \binom{n}{i} \times f(i, j) \times (n - i)! \times j \]听说可以记录 \(g(i, j) = f(i, j)\times j\),然后这两个的前缀和可以不依赖 \(j\) 转移,总时间复杂度能得到 \(O(n)\)。不太懂。
可以发现
\[\sum_{i\ge 1}\sum_{j\ge 1} \binom{n}{i} \times f(i, j) \times (n - i)! \]即 \(1\to i\) 是一条左链的笛卡尔树的计数。也就是说,任意小于 \(i\) 的数需要在 \(i\) 的右侧,答案就是
\[\sum_{i\ge 0} \binom{n}{i} (i - 1)! (n - i)! = n!\times H_n \]我们需要的就是一条链的贡献为链长度时的答案。这不太好做,不妨转化链长度为枚举祖先,得到 \(j = 1 + \sum_u [u\ 是\ i\ 的祖先]\)。\(1+\) 的部分如上处理即可,我们现在只需考虑钦定 \(u\) 是 \(i\) 的祖先时如何做上面的计数。
设 \(g(u, i)\) 为 \(1\to i\) 是一条左链且 \(u\) 在其上的笛卡尔树的计数,可以知道这部分的答案就是 \(\sum_{i} \sum_{u < i} g(u, i)\)。我们仍然可以做如上的划分值域限制的讨论,能得到 \(\in [1, u)\) 的数只能在 \(u\) 的右侧,\(\in (u, i)\) 的数只能在 \(i\) 的右侧,其余的数随意。作计数就是
\[g(u, i) = \binom{n}{i}(n - i)! \binom{i - 1}{u} (i - u - 1)! (u - 1)! = \frac{n}{ui} \]这部分的答案就是
\[n! \sum_{i\ge 0} H_{i - 1} \]可以将 \(n!\) 乘进去,第一部分部分是前缀积乘后缀积的形式,而这一部分可以记录一个积之和,算 \(i\) 的答案乘入后缀积,然后加一个 \(i - 1\) 位置的前缀积即可。
总时间复杂度 \(O(n)\)。
code
#include <bits/stdc++.h>
using namespace std;
int n, ans, sum, mod, prf[N], suf[N];
signed main() {
iot; file(cartesian);
cin >> n >> mod;
prf[0] = suf[n + 1] = 1;
rep(i,1,n) prf[i] = 1ll * prf[i - 1] * i % mod;
pre(i,n,1) suf[i] = 1ll * suf[i + 1] * i % mod;
rep(i,1,n) {
ans = (ans + 1ll * prf[i - 1] * suf[i + 1]) % mod;
ans = (ans + 1ll * sum * suf[i + 1]) % mod;
sum = (1ll * sum * i + prf[i - 1]) % mod;
} cout << ans << '\n';
}