题解: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))\)

因此可以得到

\[\begin{align*} e_T(S)&=sz_{T_{\max}}(x)-1-\sum_{i=1}^k (sz_{T_{\max}}(v_1)-1)-\sum_{i=1}^k c(v_i,fa_{T_{\max}}(v_i))\\ &=|S|-1+k-\sum_{i=1}^k c(v_i,fa_{T_{\max}}(v_i)) \end{align*} \]

显然要使 \(S\)\(T\) 中的导出子图连通,必须要有 \(e_T(S)=|S|-1\),因此

\[\sum_{i=1}^k c(v_i,fa_{T_{\max}}(v_i))=k \]

根据前面的性质,\(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\) 为根的合法连通块个数,转移就是

\[f_u=\prod_{v\in son_{T_{\max}}(u)}(f_v+[c(v,u)=1]) \]

进一步优化,考虑 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);
}
posted @ 2026-04-11 23:51  P2441M  阅读(4)  评论(0)    收藏  举报