题解:P14251 [集训队互测 2025] Everlasting Friends?
\(tp=1\)
考虑枚举 \(S\) 在 \(T_{\max}\) 中的根 \(\max(S)=x\)。观察到 \(sub_{T_{\max}}(x)\) 本身在 \(T\) 中的导出子图就是连通的,其对应着 \(T\) 中包含 \(x\) 的节点编号均 \(\leq x\) 的极大连通块 \(S_x\)。不难发现,在构建 \(T_{\max}\) 加入 \(u\) 的时刻,每条有效的原树边 \((u,v)\) 中的 \(v\) 必然处于 \(T_{\max}\) 不同的连通块中。挖掘性质:
- \(S_x\) 中的每条边都覆盖了 \(sub_{T_{\max}}(x)\) 内的一条祖先链。
- \(sub_{T_{\max}}(x)\) 内的每条边都会被至少覆盖 \(1\) 次。
注意到 \(T_{\max}\) 中每个满足 \(\max(S)=x\) 的连通块 \(S\) 唯一对应一个极小的断边集合,满足在 \(sub_{T_{\max}}(x)\) 中断掉集合中的边即可得到 \(S\)。
为了刻画 \(T\) 中的连通块,不妨计算 \(e_T(S)\) 表示 \(S\) 在 \(T\) 中导出子图的边数。考虑到断边又相当于删去若干不交子树 \(sub_{T_{\max}}(v_1),\cdots,sub_{T_{\max}}(v_k)\),分析这会导致在 \(T\) 中损失哪些边:
- 删去的每一棵子树在 \(T\) 中同样构成连通块,这些连通块内部会删去 \(\sum\limits_{i=1}^k (sz_{T_{\max}}(v_1)-1)\) 条边。
- 删去的连通块和剩余部分之间的连边同样会被删去,这些连边的总数,就是所有 \(v_i\) 和父节点的连边的总覆盖次数 \(\sum\limits_{i=1}^k c(v_i,fa_{T_{\max}}(v_i))\)。
因此可以得到
显然要使 \(S\) 在 \(T\) 中的导出子图连通,必须要有 \(e_T(S)=|S|-1\),因此
根据前面的性质,\(c(v_i,fa_{T_{\max}}(v_i))\geq 1\),于是 \(\forall 1\leq i\leq k,c(v_i,fa_{T_{\max}}(v_i))=1\)。
我们得到了一个很强的充要条件:\(S\) 合法当且仅当其在 \(T_{\max}\) 中断掉的边覆盖次数恰好为 \(1\)。
容易得到一个 \(\mathcal{O}(n^2)\) 的暴力:枚举 \(x\),在 \(T_{\max}\) 上树形 DP,设 \(f_u\) 为以 \(u\) 为根的合法连通块个数,转移就是
进一步优化,考虑 DFS 过程中自底向上处理 \(x\),相当于引出若干条以 \(x\) 为链顶的祖先链 \(\operatorname{path}(x,y_i)\),而链中不与 \(x\) 相连的边,在处理 \(x\) 之前覆盖次数就已经 \(\geq 1\) 了,所以被这条链覆盖后必然变得不合法。那么我们自底向上取出链中的每条覆盖次数为 \(1\) 的边 \((u,v)\),令 \(f_u\gets \dfrac{f_u}{f_v+1}\times f_v\) 即可。使用树上并查集把不合法的边缩起来即可。需要注意在取模意义下有可能出现乘除 \(0\) 的情况,扩域记录 \(a\times 0^b\) 即可。
时间复杂度为 \(\mathcal{O}(n\log{n}+n\log{P})\)。
\(tp=2\)
先给出 \(T_{\max}\) 的一个性质(\(T_{\min}\) 同理): 在 \(T_{\max}\) 中,\(\operatorname{lca}(u,v)\) 恰为 \(\operatorname{path}_T(u,v)\) 中编号最大的节点。由重构树的构建过程,不难理解其正确性。
猜测若 \(S\) 在 \(T_{\max}\) 与 \(T_{\min}\) 中的导出子图均为连通块,则其在 \(T\) 中的导出子图同样是一个连通块。
证明
使用反证法。假设存在 \(S\) 在 \(T_{\max}\) 与 \(T_{\min}\) 中的导出子图均为连通块,但在 \(T\) 中的导出子图不连通,那么必然存在 \(u,v\in S\),使得 \(\operatorname{path}_T(u,v)\setminus \{u,v\}\) 中的点都不属于 \(S\)。不妨设 \(u>v\),由前文中的性质,在 \(T_{\max}\) 中 \(u\) 是 \(v\) 的祖先,且 \(u,v\) 分别是 \(\operatorname{path}_T(u,v)\) 中编号最大和编号最小的节点。考察 \(v\) 在 \(u\) 方向上的邻点 \(w\),不难推出,在 \(T_{\max}\) 中 \(w\) 是 \(v\) 的祖先,\(u\) 是 \(w\) 的祖先,这说明 \(w\in S\),矛盾。\(\Box\)
枚举 \(\min(S)=y,\max(S)=x\),则 \(\operatorname{path}_T(x,y)\) 上的点都在 \(S\) 中。初始化 \(S^*=\operatorname{path}_T(x,y)\),考虑这样的扩展过程:
- 若存在 \(u\in S^*\) 满足 \(u\neq x\land fa_{T_{\max}}(u)\notin S^*\),则将 \(fa_{T_{\max}}(u)\) 加入 \(S^*\) 中。
- 同理,若存在 \(u\in S^*\) 满足 \(u\neq y\land fa_{T_{\min}}(u)\notin S^*\),则将 \(fa_{T_{\min}}(u)\) 加入 \(S^*\) 中。
一直扩展直到不存在满足上述条件的点,得到最终集合 \(S^*\)。若 \(S^*\) 在 \(T_{\max}\) 或 \(T_{\min}\) 上的导出子图不连通,则不存在满足 \(\min(S)=y,\max(S)=x\) 的集合 \(S\);否则,\(S^*\) 必然是每个满足条件的 \(S\) 的子集,此时我们指出,满足条件的 \(S\) 有且仅有一个,恰好就是 \(S^*\)。
证明
使用反证法。假设存在满足条件的 \(S\) 且 \(S^*\subsetneqq S\),由于两个集合在 \(T\) 上的导出子图都是连通的,因此必然存在一条 \(T\) 中的树边 \((u,v)\),满足 \(u\in S^*\) 且 \(v\in S\setminus S^*\)。若 \(u>v\),则 \(u\) 在 \(T_{\max}\) 上是 \(v\) 的祖先,显然 \(v\neq x\),那么 \(u\) 应当在扩展过程中被加入 \(S^*\),矛盾。若 \(u<v\),可以同理在 \(T_{\min}\) 上导出矛盾。\(\Box\)
进一步挖掘性质:若 \(S^*\) 合法,则 \(S^*=sub_{T_{\max}}(x)\cap sub_{T_{\min}}(y)\)。
证明
不难发现若 \(S^*\) 合法,则 \(S^*\subseteq sub_{T_{\max}}(x)\cap sub_{T_{\min}}(y)\)。
接下来证明若 \(S^*\) 合法,则每个 \(u\in sub_{T_{\max}}(x)\cap sub_{T_{\min}}(y)\) 都会被加入 \(S^*\) 中。
考察一个点 \(u\in sub_{T_{\max}}(x)\cap sub_{T_{\min}}(y)\) 和 \(\operatorname{path}_T(x,y)\) 中离 \(u\) 最近的点 \(v\)。设 \(v\) 到 \(u\) 这条路径上的点依次为 \(v=p_0,p_1,\cdots,p_k=u\)。由于 \(u\in sub_{T_{\max}}(x)\cap sub_{T_{\min}}(y)\),每个 \(p_i\) 都满足 \(p_i\in [y,x]\)。对路径上的点依次归纳:
- 已知 \(p_0\in S^*\)。
- 假设 \(p_{i-1}\in S^*\),若 \(p_{i-1}<p_i<x\),则 \(p_i\) 在 \(T_{\max}\) 上是 \(p_{i-1}\) 的祖先,可以得出 \(p_i\in S^*\);若 \(p_{i-1}>p_i>y\),则 \(p_i\) 在 \(T_{\min}\) 上是 \(p_{i-1}\) 的祖先,同样可以得出 \(p_i\in S^*\)。
因此我们证明了路径上所有点都 \(\in S^*\)。\(\Box\)
问题转化为,求有多少 \(1\leq y\leq x\leq n\),满足:
- \(x\in anc_{T_{\max}}(y)\)。
- \(y\in anc_{T_{\min}}(x)\)。
- \(V=sub_{T_{\max}}(x)\cap sub_{T_{\min}}(y)\) 在 \(T_{\max}\) 和 \(T_{\min}\) 上的导出子图均为一个连通块。
容易想到把条件 \(3\) 拆成点数减边数,即 \(2|V|-e_{T_{\max}}(V)-e_{T_{\min}}(V)=2\)。分析具体的贡献方式:
- 对于任意一个 \(u\in V\),若 \(u\in sub_{T_{\max}}(x)\) 且 \(u\in sub_{T_{\min}}(y)\),则有 \(+2\) 的贡献。
- 对于 \(T_{\max}\) 中的任意一条边 \((u,fa_{T_{\max}}(u))\),若 \(fa_{T_{\max}}(u)\in sub_{T_{\max}}(x)\) 且 \(\operatorname{lca}_{T_{\min}}(u,fa_{T_{\max}}(u))\in sub_{T_{\min}}(y)\),则有 \(-1\) 的贡献。
- 对于 \(T_{\min}\) 中的任意一条边 \((u,fa_{T_{\min}}(u))\),若 \(fa_{T_{\min}}(u)\in sub_{T_{\min}}(y)\) 且 \(\operatorname{lca}_{T_{\max}}(u,fa_{T_{\min}}(u))\in sub_{T_{\max}}(x)\),则有 \(-1\) 的贡献。
考虑对 \(x\) 扫描线,数据结构维护 \(y\) 对应的点减边权值。注意到贡献条件都是子树形式的,容易想到对 \(x\) 按 DFS 序倒序扫描线。
对于上面的三种贡献方式,将它们对应成三种操作:
- 对于每个点 \(u\),在 \(u\) 点挂一个 \(T_{\min}\) 上 \(u\) 的根链 \(+2\) 的操作。
- 对于 \(T_{\max}\) 中的每条边 \((u,fa_{T_{\max}}(u))\),在 \(fa_{T_{\max}}(u)\) 点挂一个 \(T_{\min}\) 上 \(\operatorname{lca}_{T_{\min}}(u,fa_{T_{\max}}(u))\) 的根链 \(-1\) 的操作。
- 对于 \(T_{\min}\) 中的每条边 \((u,fa_{T_{\min}}(u))\),在 \(\operatorname{lca}_{T_{\max}}(u,fa_{T_{\min}}(u))\) 点挂一个 \(fa_{T_{\min}}(u)\) 的根链 \(-1\) 的操作。
在 \(T_{\min}\) 的 DFS 序上建立线段树。初始时将所有点的权值置为 \(+\infty\),扫描线处理到点 \(x\) 时,把 \(x\) 的权值置为 \(0\),然后树剖处理挂在 \(x\) 上的操作,儿子节点的操作用线段树合并处理即可。需要标记永久化,时空复杂度均为 \(\mathcal{O}(n\log^2{n})\)。
空间复杂度还可以进一步优化。考虑类似 DSU On Tree 的方式,先递归处理重儿子 \(hson_u\),然后将当前点 \(u\) 的线段树设为 \(hson_u\) 的线段树,再依次合并轻儿子的线段树。这样每个时刻只会保留轻边条数棵,也就是 \(\mathcal{O}(\log{n})\) 棵线段树,加上节点回收即可做到 \(\mathcal{O}(n\log{n})\) 空间复杂度。
主要代码
int tp, n, ans, rt[MAXN];
vector<int> T[MAXN];
vector<pii> op[MAXN];
struct DSU {
int fa[MAXN];
void init() { for (int i = 1; i <= n; ++i) fa[i] = i; }
int find(int x) { return x == fa[x] ? x : fa[x] = find(fa[x]); }
void unite(int x, int y) { fa[find(x)] = find(y); }
} dsu;
int qpow(int a, int b) {
int res = 1;
for (; b; b >>= 1) {
if (b & 1) res = (ll)res * a % mod;
a = (ll)a * a % mod;
}
return res;
}
struct DP {
int a, b;
DP() : a(1), b(0) {}
DP(int x, int y) : a(x), b(y) {}
friend DP operator*(const DP &lhs, int rhs) {
return rhs ? DP((ull)lhs.a * rhs % mod, lhs.b) : DP(lhs.a, lhs.b + 1);
}
friend DP operator/(const DP &lhs, int rhs) {
return rhs ? DP((ull)lhs.a * qpow(rhs, mod - 2) % mod, lhs.b) : DP(lhs.a, lhs.b - 1);
}
int val() const { return b ? 0 : a; }
} f[MAXN];
struct Tree {
vector<int> T[MAXN];
int fa[MAXN], sz[MAXN], hson[MAXN], top[MAXN];
int stmp, dfn[MAXN];
int f[LOGN][MAXN];
void ins(int x, int y) { T[x].emplace_back(y), fa[y] = x; }
void dfs1(int u) {
sz[u] = 1;
for (int v : T[u]) {
dfs1(v);
sz[u] += sz[v];
if (sz[v] > sz[hson[u]]) hson[u] = v;
}
}
void dfs2(int u, int tp) {
top[u] = tp, f[0][dfn[u] = ++stmp] = fa[u];
if (hson[u]) dfs2(hson[u], tp);
for (int v : T[u]) if (v != hson[u]) dfs2(v, v);
}
int get(int x, int y) { return dfn[x] < dfn[y] ? x : y; }
void init(int rt) {
dfs1(rt), dfs2(rt, rt);
for (int i = 1; 1 << i <= n; ++i) {
for (int j = 1; j <= n - (1 << i) + 1; ++j) {
f[i][j] = get(f[i - 1][j], f[i - 1][j + (1 << i - 1)]);
}
}
}
int lca(int x, int y) {
auto query = [&](int l, int r) {
int k = 31 ^ __builtin_clz(r - l + 1);
return get(f[k][l], f[k][r - (1 << k) + 1]);
};
if (x == y) return x;
if (dfn[x] > dfn[y]) swap(x, y);
return query(dfn[x] + 1, dfn[y]);
}
} TMax, TMin;
struct SegTree {
static const int MAXSZ = MAXN * LOGN * 2;
int top, tot, stk[MAXSZ];
int ls[MAXSZ], rs[MAXSZ], tg[MAXSZ];
struct Node {
int mn, cnt;
friend Node operator+(const Node &lhs, const Node &rhs) {
if (lhs.mn < rhs.mn) return lhs;
else if (lhs.mn > rhs.mn) return rhs;
else return {lhs.mn, lhs.cnt + rhs.cnt};
}
Node &operator+=(const Node &rhs) { return *this = *this + rhs; }
} nd[MAXSZ];
int create() {
int p = top ? stk[top--] : ++tot;
ls[p] = rs[p] = tg[p] = 0;
return p;
}
void del(int x) { stk[++top] = x; }
void pushUp(int p, int l, int r) {
int mid = l + r >> 1;
Node ndL = ls[p] ? nd[ls[p]] : Node{inf, mid - l + 1};
Node ndR = rs[p] ? nd[rs[p]] : Node{inf, r - mid};
nd[p] = ndL + ndR;
nd[p].mn += tg[p];
}
void makeTg(int p, int v) { tg[p] += v, nd[p].mn += v; }
void add(int &p, int l, int r, int x, int y, int v) {
if (!p) p = create(), nd[p] = {inf, r - l + 1};
if (x <= l && y >= r) return makeTg(p, v);
int mid = l + r >> 1;
if (x <= mid) add(ls[p], l, mid, x, y, v);
if (y > mid) add(rs[p], mid + 1, r, x, y, v);
pushUp(p, l, r);
}
Node query(int p, int l, int r, int x, int y, int sum) {
if (!p) {
int L = max(l, x), R = min(r, y);
return {inf + sum, R - L + 1};
}
if (x <= l && y >= r) return {nd[p].mn + sum, nd[p].cnt};
int mid = l + r >> 1;
Node res = {inf, 0};
if (x <= mid) res = query(ls[p], l, mid, x, y, sum + tg[p]);
if (y > mid) res += query(rs[p], mid + 1, r, x, y, sum + tg[p]);
return res;
}
int merge(int p, int q, int l, int r) {
if (!p || !q) return p ^ q;
tg[p] += tg[q];
if (l == r) return nd[p] = {inf + tg[p], 1}, del(q), p;
int mid = l + r >> 1;
ls[p] = merge(ls[p], ls[q], l, mid);
rs[p] = merge(rs[p], rs[q], mid + 1, r);
return pushUp(p, l, r), del(q), p;
}
} sgt;
void dfs1(int u) {
for (int v : TMax.T[u]) dfs1(v);
for (int v : T[u]) {
if (v > u) continue;
v = dsu.find(v);
int tmp = f[v].val();
while (1) {
int faV = dsu.find(TMax.fa[v]);
if (faV == u) break;
int tmp2 = f[faV].val();
f[faV] = f[faV] / add(tmp, 1) * f[v].val();
dsu.unite(v, faV), v = faV, tmp = tmp2;
}
}
for (int v : TMax.T[u]) f[u] = f[u] * add(f[v].val(), 1);
cadd(ans, f[u].val());
}
void dfs2(int u) {
if (TMax.hson[u]) {
dfs2(TMax.hson[u]);
rt[u] = rt[TMax.hson[u]];
for (int v : TMax.T[u]) {
if (v == TMax.hson[u]) continue;
dfs2(v);
rt[u] = sgt.merge(rt[u], rt[v], 1, n);
}
}
sgt.add(rt[u], 1, n, TMin.dfn[u], TMin.dfn[u], -inf);
for (auto [x, d] : op[u]) {
while (x) {
sgt.add(rt[u], 1, n, TMin.dfn[TMin.top[x]], TMin.dfn[x], d);
x = TMin.fa[TMin.top[x]];
}
}
SegTree::Node nd = {inf, 0};
int x = u;
while (x) {
nd += sgt.query(rt[u], 1, n, TMin.dfn[TMin.top[x]], TMin.dfn[x], 0);
x = TMin.fa[TMin.top[x]];
}
cadd(ans, nd.cnt);
}

浙公网安备 33010602011771号