20250731
T1
交朋友
容易发现两个人最终连通等价于其间任意存在一条路径使得这条路径的路径 \(\max\) 小于这两个人编号的 \(\min\)。于是从小往大加入每个点,维护每个连通块周围的所有编号大于当前枚举的人的编号的点。set 维护,合并连通块时启发式合并,合并完了删掉当前 set 里编号 \(\le\) 当前点的。然后把 set 的 size 加到答案里即可。
代码
#include <iostream>
#include <vector>
#include <set>
#define int long long
using namespace std;
int n, m;
vector<int> G[200005];
set<int> st[200005];
int fa[200005];
int getf(int x) { return (fa[x] == x ? x : (fa[x] = getf(fa[x]))); }
void Merge(set<int> &x, set<int> &y) {
if (x.size() < y.size())
swap(x, y);
for (auto v : y) x.insert(v);
y.clear();
}
signed main() {
freopen("friends.in", "r", stdin);
freopen("friends.out", "w", stdout);
cin >> n >> m;
for (int i = 1; i <= n; i++) fa[i] = i;
for (int i = 1; i <= m; i++) {
int u, v;
cin >> u >> v;
if (u > v)
swap(u, v);
st[u].insert(v);
G[v].emplace_back(u);
}
int ans = 0;
for (int i = 1; i <= n; i++) {
for (auto v : G[i]) {
int x = getf(v);
if (x == i)
continue;
Merge(st[i], st[x]);
fa[x] = i;
}
while (st[i].size() && *st[i].begin() <= i) st[i].erase(st[i].begin());
ans += st[i].size();
}
cout << ans - m << "\n";
return 0;
}
T2
魔塔
枚举 A 走到哪,动态对每个 B 走到的位置维护此时 C 最多走到哪。那么根据 A 新走到的位置所需的钥匙数量分类,每次相当于把一段 B 上的值与给定值 chkmin(因为之后的地方走不到了)。那么把每个 B 位置自身的权值作为 \(x\),走到的 C 的位置的权值作为 \(y\),相当于要维护区间 \(y\) chkmin,查询区间(因为 A 走到一个地方之后 B 可能也要有走不到的地方) \(x + y\) 最大值。注意到 \(y\) 不增,因此区间 chkmin,在找到到底哪些位置要被改掉之后,就相当于区间推平。这个过程需要再写一个线段树二分,来找到区间第一个比给定值小的数。之后就是好维护的。
代码
#include <iostream>
#include <string.h>
using namespace std;
#define getchar() p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 1 << 21, stdin), p1 == p2) ? EOF : *p1++
char buf[1<<21], *p1, *p2, ch;
long long read() {
long long ret = 0, neg = 0; char c = getchar(); neg = (c == '-');
while (c < '0' || c > '9') c = getchar(), neg |= (c == '-');
while (c >= '0' && c <= '9') ret = ret * 10 + c - '0', c = getchar();
return ret * (neg ? -1 : 1);
}
char _51_;
int tc;
int n;
int a[100005];
int clr[3][100005], val[3][100005];
int _c[3][100005];
int B[100005];
struct Segment_Tree {
int mx0[400005], mx[400005], mn[400005], tg[400005];
void tag(int o, int v) {
mn[o] = tg[o] = v;
mx[o] = mx0[o] + v;
}
void pushdown(int o) {
if (tg[o] == -1)
return;
tag(o << 1, tg[o]);
tag(o << 1 | 1, tg[o]);
tg[o] = -1;
}
void pushup(int o) {
mx0[o] = max(mx0[o << 1], mx0[o << 1 | 1]);
mx[o] = max(mx[o << 1], mx[o << 1 | 1]);
mn[o] = min(mn[o << 1], mn[o << 1 | 1]);
}
void Build(int o, int l, int r) {
tg[o] = -1;
if (l == r) {
mx0[o] = val[1][l];
mx[o] = val[1][l] + B[r];
mn[o] = B[r];
return;
}
int mid = (l + r) >> 1;
Build(o << 1, l, mid);
Build(o << 1 | 1, mid + 1, r);
pushup(o);
}
void Cover(int o, int l, int r, int L, int R, int v) {
if (L <= l && r <= R)
return tag(o, v);
pushdown(o);
int mid = (l + r) >> 1;
if (L <= mid)
Cover(o << 1, l, mid, L, R, v);
if (R > mid)
Cover(o << 1 | 1, mid + 1, r, L, R, v);
pushup(o);
}
int Search(int o, int l, int r, int L, int R, int v) {
if (l == r)
return mn[o] < v ? l : R + 1;
if (L <= l && r <= R) {
if (mn[o] >= v)
return R + 1;
}
pushdown(o);
int mid = (l + r) >> 1;
if (R <= mid)
return Search(o << 1, l, mid, L, R, v);
if (L > mid)
return Search(o << 1 | 1, mid + 1, r, L, R, v);
int t = Search(o << 1, l, mid, L, R, v);
if (t != R + 1)
return t;
else
return Search(o << 1 | 1, mid + 1, r, L, R, v);
}
int Query(int o, int l, int r, int L, int R) {
if (L <= l && r <= R)
return mx[o];
pushdown(o);
int mid = (l + r) >> 1;
if (R <= mid)
return Query(o << 1, l, mid, L, R);
if (L > mid)
return Query(o << 1 | 1, mid + 1, r, L, R);
return max(Query(o << 1, l, mid, L, R), Query(o << 1 | 1, mid + 1, r, L, R));
}
} seg;
int main() {
freopen("tower.in", "r", stdin);
freopen("tower.out", "w", stdout);
tc = read();
while (tc--) {
n = read();
for (int i = 1; i <= n; i++) a[i] = read();
for (int i = 1; i <= n; i++) clr[0][i] = read();
for (int i = 1; i <= n; i++) clr[1][i] = read(), _c[1][clr[1][i]] = i;
for (int i = 1; i <= n; i++) clr[2][i] = read(), _c[2][clr[2][i]] = i;
for (int i = 1; i <= n; i++) val[0][i] = read(), val[0][i] += val[0][i - 1];
for (int i = 1; i <= n; i++) val[1][i] = read(), val[1][i] += val[1][i - 1];
for (int i = 1; i <= n; i++) val[2][i] = read(), val[2][i] += val[2][i - 1];
for (int i = 0, p = n; i <= n; i++) {
if (i && a[clr[1][i]] == 1)
p = min(_c[2][clr[1][i]] - 1, p);
B[i] = val[2][p];
}
seg.Build(1, 0, n);
int ans = 0;
for (int i = 0, pos = n; i <= n; i++) {
int c = clr[0][i], p = _c[1][c], q = _c[2][c];
if (a[c] == 1) {
int x = seg.Search(1, 0, n, 0, p - 1, val[2][q - 1]);
if (x > 0)
seg.Cover(1, 0, n, 0, x - 1, val[2][q - 1]);
} else if (a[c] == 2) {
int x = seg.Search(1, 0, n, p, n, val[2][q - 1]);
if (p < x)
seg.Cover(1, 0, n, p, x - 1, val[2][q - 1]);
}
if (i && a[c] == 1)
pos = min(_c[1][c] - 1, pos);
ans = max(ans, val[0][i] + seg.Query(1, 0, n, 0, pos));
}
cout << ans << "\n";
}
return 0;
}
T3
星光交汇
\(m\) 为奇数时只有一个重心,枚举这个重心。容斥,减去至少一棵子树大小爆炸的情况。一个大小为 \(s\) 的子树大小爆炸的方案数为:\(f_s = \sum\limits_{i = \lceil \frac{m}{2} \rceil}^m\binom{i + s - 1}{s - 1}\binom{n - i + n - s - 1}{n - s - 1}\)。我们希望对每个 \(s\) 求这个方案数。
发挥想象力,想象我们往 \(n\) 个节点里扔 \(m\) 个权值。如果一个权值被扔到了前 \(s\) 个节点里,这个权值就要被纳入这棵子树的权值和。然后我们希望求出前 \(s\) 个节点里的权值和 \(\ge \lceil \frac{m}{2} \rceil\) 的方案数。那么,就会发现如果第 \(\lceil \frac{m}{2} \rceil\) 个权值被扔到了前 \(s\) 个节点里,那么这个方案一个就是我们想要的方案。于是我们只需要枚举第 \(\lceil \frac{m}{2} \rceil\) 个权值被扔到了哪个节点里,然后算一下方案数的前缀和即可。这样我们就求出了 \(f\)。于是我们就做完了 \(m\) 为奇数的情况。
接下来考虑 \(m\) 为偶数。这个时候注意到所有重心构成一条链,于是考虑对着这个东西点分治。对于每个点,维护它到分治中心这些点的最小编号,以及在 以分治中心为根这个点的完整子树(而不是被曾经的分治中心划开的连通块) 中选出 \(\frac{m}{2}\) 个点的方案数。为了保证这个点真的是链的端点,还需要令它的子树中至少有两个是非空的。对每条链求出这个之后,不同子树间答案的合并是容易的。随便上个数据结构即可。需要特殊考虑只从根到一棵子树的链。
但是这样我们只算了重心是一条链的贡献,还没有算重心为单点的贡献。这个时候我们还是枚举这个重心,然后先把存在子树大小超过 \(\frac{m}{2}\) 的方案数减掉。这一步可以对上面那个往结点上扔权值的东西稍作改动,如果理解了是平凡的。接下来减掉有两个子树大小都为 \(\frac{m}{2}\) 的情况。这个按顺序扫一遍每棵子树,容易在过程中维护。接下来减掉只有一棵子树大小为 \(\frac{m}{2}\),剩下的权值分散在自己和另外至少两棵子树中的情况。
这样就做完了。时间复杂度 \(\mathcal{O}(n \log^2 n)\)。能过。
其实还有一种并查集做法。具体是你考虑偶数时也枚举重心 \(x\) 然后容斥,需要减掉自己有一个大小 \(\frac{m}{2}\) 的子树,而且重心编号比自己小的情况。那么考虑这种情况,设那个编号更小的重心为 \(v\),那么容易发现 \(x\) 和 \(v\) 的路径上不可以有编号比 \(v\) 还小的点,否则重心就不是 \(v\) 而是那个编号更小的点了。那这个过程就可以像今天 T1 那样,从大往小枚举点,加入一个点时合并周围连通块,然后把那些连通块当中记录的与它相邻的点的方案数减掉。这个时候我们发现一个重心可能会有两个大小为 \(\frac{m}{2}\) 的子树,于是得加回来。这个过程也容易在合并连通块的过程中维护。于是也做完了。我们在并查集里维护每个连通块与其相邻点的贡献和,要合并的时候就把这个连通块对当前点算的贡献减掉,然后合并过来即可。这个做法的时间复杂度是 \(\mathcal{O}(n \alpha(n))\)。但我没写这个做法。
代码
#include <iostream>
#include <string.h>
#include <vector>
#define lowbit(x) ((x) & (-(x)))
#define int long long
using namespace std;
const int P = 998244353;
inline void Madd(int &x, int y) { (x += y) >= P ? (x -= P) : 0; }
int fac[6000005], ifac[6000005], inv[6000005];
void Cpre(int n) {
fac[0] = fac[1] = ifac[0] = ifac[1] = inv[0] = inv[1] = 1;
for (int i = 2; i <= n; i++) {
fac[i] = fac[i - 1] * i % P;
inv[i] = (P - P / i) * inv[P % i] % P;
ifac[i] = ifac[i - 1] * inv[i] % P;
}
}
inline int C(int n, int m) { return n < 0 || m < 0 || n < m ? 0 : fac[n] * ifac[m] % P * ifac[n - m] % P; }
int n, m;
int head[200005], nxt[400005], to[400005], ecnt;
void add(int u, int v) { to[++ecnt] = v, nxt[ecnt] = head[u], head[u] = ecnt; }
int ans = 0;
int sz[200005];
int f[200005];
int F[200005];
int dfn[200005], ncnt;
void dfs(int x, int fa) {
dfn[x] = ++ncnt;
sz[x] = 1;
for (int i = head[x]; i; i = nxt[i]) {
int v = to[i];
if (v != fa) {
dfs(v, x);
sz[x] += sz[v];
Madd(f[x], P - F[sz[v]]);
}
}
Madd(f[x], P - F[n - sz[x]]);
Madd(f[x], C(m + n - 1, n - 1));
}
int Sz(int x, int r) { return x == r ? n : (((dfn[r] <= dfn[x] && dfn[x] < dfn[r] + sz[r]) ? sz[x] : n - sz[r])); }
namespace P_DC {
struct BIT {
int bit[200005];
void add(int x, int y) { for (; x <= n; x += lowbit(x)) Madd(bit[x], y); }
int query(int x) {
int ret = 0;
for (; x; x -= lowbit(x)) Madd(ret, bit[x]);
return ret;
}
} bit1, bit2;
int rt, msz, all, cr;
int sz[200005];
bool mark[200005];
int af[200005];
vector<pair<int, int> > vec, av;
void getroot(int x, int fa, int mn = -1) {
int mx = 0;
sz[x] = 1;
for (int i = head[x]; i; i = nxt[i]) {
int v = to[i];
if (!mark[v] && v != fa) {
getroot(v, x, min(mn, v));
sz[x] += sz[v];
mx = max(mx, sz[v]);
}
}
mx = max(mx, all - sz[x]);
if (mx < msz)
rt = x, msz = mx;
if (mn != -1) {
int tmp = C(m / 2 + Sz(x, fa) - 1, Sz(x, fa) - 1);
for (int i = head[x]; i; i = nxt[i]) {
int v = to[i], t = Sz(v, x);
if (v != fa)
Madd(tmp, P - C(m / 2 + t - 1, t - 1));
}
vec.emplace_back(mn, tmp);
}
}
int rec[200005];
void dfs(int x) {
vector<int> G;
mark[x] = 1;
cr = x;
getroot(x, 0);
int sub = 0, s = 0;
for (int i = head[x]; i; i = nxt[i]) Madd(sub, P - C(Sz(to[i], x) + m / 2 - 1, Sz(to[i], x) - 1));
for (int i = head[x]; i; i = nxt[i]) {
int v = to[i], vv = C(Sz(v, x) + m / 2 - 1, Sz(v, x) - 1);
Madd(sub, vv);
int vt = C(n - Sz(v, x) + m / 2 - 1, n - Sz(v, x) - 1);
Madd(vt, sub);
Madd(f[x], P - vv * vt % P);
Madd(f[x], P - vv * s % P);
Madd(s, vv);
if (!mark[v]) {
all = sz[v], msz = n + 1;
vec.clear();
getroot(v, x, min(x, v));
G.emplace_back(rt);
for (auto v : vec) {
Madd(ans, v.second * v.first % P * bit1.query(n - v.first + 1) % P);
Madd(ans, v.second * bit2.query(v.first - 1) % P);
Madd(ans, v.second * v.first % P * vt % P);
}
for (auto w : vec) {
bit1.add(n - w.first + 1, w.second);
bit2.add(w.first, w.first * w.second % P);
av.emplace_back(w);
}
}
Madd(sub, P - C(Sz(v, x) + m / 2 - 1, Sz(v, x) - 1));
}
for (auto v : av) {
bit1.add(n - v.first + 1, P - v.second);
bit2.add(v.first, P - v.first * v.second % P);
}
av.clear();
for (auto v : G) dfs(v);
}
void work() {
all = msz = n;
getroot(1, 0);
dfs(rt);
}
}
signed main() {
freopen("star.in", "r", stdin);
freopen("star.out", "w", stdout);
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
Cpre(5200000);
cin >> n >> m;
for (int i = 1; i < n; i++) F[i] = C(m / 2 + i - 1, i - 1) * C((m - 1) / 2 + n - i, n - i) % P;
for (int i = 1; i < n; i++) Madd(F[i], F[i - 1]);
for (int i = 1; i < n; i++) {
int u, v;
cin >> u >> v;
add(u, v);
add(v, u);
}
dfs(1, 0);
if (!(m & 1))
P_DC::work();
for (int i = 1; i <= n; i++) Madd(ans, i * f[i] % P);
cout << ans << "\n";
return 0;
}
T4
微光
考虑 \(l\) 固定,\(r\) 大的时候连通块的形态,发现,如果设 \(g_l\) 为最大的 \(x\) 满足 \(f_x < i\) 的话,则所有连通块为以 \([l, g_l]\) 中点为根的若干连通子树。然后如果 \(r\) 比较小,则根就是 \([l, r]\),也就是根为 \([l, min(r, g_l)]\) 中所有点。然后显然当且仅当两个点在同一棵子树的时候他们的 LCA 满足要求,于是变成只保留 \([l, r]\) 中的点时,求每个连通块里点权和的平方和。然后会发现保留 \(l\) 之前的点并不会影响答案。因为这些点一定不会在 \([l, min(r, g_l)]\) 的子树中。于是考虑离线扫描线,每次加入一个右端点,并维护所有点的子树点权和及其平方和,然后每次询问只需要求一个区间和即可。接下来我们考虑根号重构,即每次加入 \(S\) 个右端点,并重构一次所有点的子树和及其平方前缀和,然后查询时加入那些被问到的,但没有被加入的右端点,只需要求出这些右端点到底在 \([l, min(r, g_l)]\) 中哪个点的子树里。由于 \([l, min(r, g_l)]\) 中的点最多只有两种深度,我们可以先把当前右端点跳到 \(dep_l + 1\),假设跳到了 \(t\),那如果 \(t > g_l\),说明它实际上在 \(f_t\) 的子树中,否则就是在 \(t\) 的子树中。于是接下来只需要一个 \(k\) 级祖先。我们不会长剖,考虑倍增,但是以 \(32\) 为底。具体地,维护每个点的 \(1, 2, 3, 4 \cdots\),\(32, 64, 96, 128, \cdots\),\(1024, 2048, 3072, 4096\) 级祖先,然后就可以做到 \(\mathcal{O}(\log_{32}n)\) 查询一个点的 \(k\) 级祖先了。几乎就是线性。总时间复杂度 \(\mathcal{O}(n\sqrt{n})\)。
代码
#include <iostream>
#include <algorithm>
#include <cassert>
#include <vector>
#include <math.h>
using namespace std;
const int P = 998244353;
void Madd(int &x, int y) { (x += y) >= P ? (x -= P) : 0; }
int n, q, S = 700, T;
int a[250005], f[250005], g[250005], dep[250005];
int anc[4][250005][33];
int pre[360][250005], sum[360][250005];
int Kanc(int x, int K) {
x = anc[0][x][K & 31], K >>= 5; if (!K) return x;
x = anc[1][x][K & 31], K >>= 5; if (!K) return x;
x = anc[2][x][K & 31], K >>= 5; if (!K) return x;
x = anc[3][x][K & 31], K >>= 5;
return x;
}
int R[605];
vector<pair<int, int> > tmp;
signed main() {
freopen("slight.in", "r", stdin);
freopen("slight.out", "w", stdout);
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin >> n >> q;
for (int i = 1; i <= n; i++) cin >> a[i];
for (int i = 2; i <= n; i++) cin >> f[i];
for (int x = 1; x <= n; x++) {
for (int i = 0; i < 4; i++) {
anc[i][x][0] = x;
for (int j = 1; j <= 32; j++)
anc[i][x][j] = (i ? anc[i - 1][anc[i][x][j - 1]][32] : f[anc[i][x][j - 1]]);
}
dep[x] = dep[f[x]] + 1;
}
for (int i = 1; i <= n; i += S) {
++T; R[T] = min(i + S - 1, n);
for (int x = R[T]; x; x--) {
Madd(sum[T][x], a[x]);
Madd(sum[T][f[x]], sum[T][x]);
}
for (int j = 1; j <= R[T]; j++) pre[T][j] = 1ll * sum[T][j] * sum[T][j] % P, Madd(pre[T][j], pre[T][j - 1]);
}
for (int i = 1, j = 1; i <= n; i++) {
while (j <= n && f[j] < i) ++j;
g[i] = j - 1;
}
int lans = 0;
while (q--) {
int l, r;
cin >> l >> r;
l ^= lans, r ^= lans;
int p = (r - 1) / S;
lans = (P + pre[p][min({ R[p], g[l], r })] - pre[p][min(R[p], l - 1)]) % P;
tmp.clear();
for (int i = max(l, p * S + 1), x; i <= r; i++) {
if (dep[i] > dep[l]) {
x = Kanc(i, dep[i] - dep[l] - 1);
(x > g[l]) ? (x = f[x]) : 0;
} else
x = i;
tmp.emplace_back(x, a[i]);
Madd(lans, 2ll * a[i] * sum[p][x] % P);
Madd(lans, 1ll * a[i] * a[i] % P);
Madd(sum[p][x], a[i]);
}
for (auto v : tmp) Madd(sum[p][v.first], P - v.second);
cout << lans << "\n";
}
return 0;
}
T3,想到重心为链,但没想到点分治;想到偶数时枚举中心,并减掉重心比它小的方案,但没往下想。不懂啊。是不是被吓到了。
范德蒙德卷积算一半:\(\sum\limits_{i = \lceil \frac{m}{2} \rceil}^m\binom{i + s - 1}{s - 1}\binom{n - i + n - s - 1}{n - s - 1}\),考虑组合意义,枚举第 \(\frac{m}{2}\) 个东西放在哪。当然可以是广义的一半,做法也是一样。
底数不为 \(2\) 的倍增,预处理复杂度高,询问复杂度低,可用于平衡复杂度。