虚树

模板:

struct edge {
    int v, w;
};
struct HLD {
    int n;
    vector<int> siz, top, parent, l, r, hson, dep;
    vector<vector<edge>> adj;
    int idx;
    vector<int> mn;//1-u的最小边权

    HLD() {}
    HLD(int n) {
        init(n);
    }
    void init(int n) {
        this->n = n;
        siz.resize(n + 1), hson.resize(n + 1), top.resize(n + 1);
        parent.resize(n + 1);
        l.resize(n + 1), r.resize(n + 1);
        idx = 0;
        adj.resize(n + 1), dep.resize(n + 1);
        // 根据题目要求加数据结构
        mn.resize(n + 1, 1e9);
    }
    void addEdge(int u, int v, int w) {
        adj[u].push_back({v, w});
    }
    void work(int root = 1) {
        top[root] = root;
        parent[root] = -1;
        dep[root] = 1;
        dfs1(root, -1);
        dfs2(root, root);
    }
    void dfs1(int u, int f) {  // 搞fa,dep,son
        siz[u] = 1;
        for (auto [v, w] : adj[u]) {
            if (v == f)
                continue;
            mn[v] = min(mn[u], w);
            parent[v] = u;
            dep[v] = dep[u] + 1;
            dfs1(v, u);
            siz[u] += siz[v];
            if (siz[hson[u]] < siz[v])
                hson[u] = v;
        }
    }
    void dfs2(int u, int t) {  // 搞top
        top[u] = t;            // 记录链头
        l[u] = ++idx;
        if (!hson[u]) {
            r[u] = idx;
            return;
        }  // 无重儿子
        dfs2(hson[u], t);  // 搜重儿子
        for (auto [v, w] : adj[u]) {
            if (v == parent[u] || v == hson[u])
                continue;
            dfs2(v, v);  // 搜轻儿子
        }
        r[u] = idx;
    }
    int lca(int u, int v) {
        while (top[u] != top[v]) {
            if (dep[top[u]] > dep[top[v]]) {
                u = parent[top[u]];
            } else {
                v = parent[top[v]];
            }
        }
        return dep[u] < dep[v] ? u : v;
    }
    bool isAncester(int u, int v) {  // 判断u是不是v的祖先
        return l[u] <= l[v] && r[v] <= r[u];
    }
};
void solve() {
    int n;
    cin >> n;
    HLD hld(n);
    for (int i = 1; i <= n - 1; i++) {
        int u, v, w;
        cin >> u >> v >> w;
        hld.addEdge(u, v, w);
        hld.addEdge(v, u, w);
    }
    auto cmp = [&](int i, int j) {
        return hld.l[i] < hld.l[j];
    };
    hld.work(1);
    auto buildvt = [&](vector<int>& node, vector<vector<edge>>& e) {
        node.push_back(1);//保证根节点在虚树中存在
        sort(alls(node), cmp);
        node.erase(unique(alls(node)), node.end());
        set<int> tmp;
        for (auto x : node) tmp.insert(x);
        for (int i = 1; i < (int)node.size(); i++) tmp.insert(hld.lca(node[i - 1], node[i]));
        node.clear();
        for (auto x : tmp) node.push_back(x);
        sort(alls(node), cmp);
        vector<int> st;  // 维护一个栈
        for (auto v : node) {
            while (!st.empty() && !hld.isAncester(st.back(), v))
                st.pop_back();
            if (!st.empty())
                e[st.back()].push_back({v, hld.mn[v]});
            st.push_back(v);
        }
    };
    int q;
    cin >> q;
    vector<vector<edge>> e(n + 1);
    vector<ll> dp(n + 1);  // 使得u子树内关键点与u不连通的代价
    vector<bool> vis(n + 1);
    auto cal = [&](auto self, int u, int fa) -> void {//计算答案
        for (auto [v, w] : e[u]) {
            if (v == fa)
                continue;
            self(self, v, u);
            if (vis[v])
                dp[u] += w;
            else
                dp[u] += min((ll)w, dp[v]);
        }
    };
    auto clear = [&](vector<int>& node) {//清空本次用的点的信息
        for (auto x : node) {
            vis[x] = 0;
            dp[x] = 0;
            e[x].clear();
        }
    };
    for (int i = 1; i <= q; i++) {
        int num;
        cin >> num;
        vector<int> node;
        for (int j = 1; j <= num; j++) {
            int x;
            cin >> x;
            node.push_back(x);
            vis[x] = 1;
        }
        buildvt(node, e);
        cal(cal, 1, 1);
        cout << dp[1] << endl;
        clear(node);
    }
}

虚数建树模板题[P245 SDOI2011] 消耗战 - 洛谷 | 计算机科学教育新生态

题意:给定一颗树,q次查询,每次查询k个点,每个边有砍断的代价,求最少砍断多少条边使得这k个点都无法到达1号根节点。保证 $\sum k \le2e5$

Sol:对于这样的询问点集总和有$\sum$保证的,可以考虑虚树。对于k个关键点,虚数最多2k个点,这样总和还是$O(\sum k)$级别的,复杂度有了保证。我们应该考虑问题的核心是如何dp。考虑$dp[u]$表示以u为根的子树中没有关键点与u连通的最小代价。

  • 转移如下:对于u的儿子v,如果v是关键节点,那u和v之间的边必须切断。如果v不是关键节点,那考虑可以递归到dp[v]也可也选择下面的不动,切断这里,也就是$min(dp[v],w)$

实现细节:不能每次动态开边集vector和vis,而是要选择动态清空保证复杂度,实现clear函数。

代码z

应用:Kingdom and its Cities - 洛谷 | 计算机科学教育新生态

1.题意:给定一棵树。q次查询,每次查询给出k个关键点,现在需要消灭一些非关键点使得这k个点两两不连通,求最小消灭点的数量满足需求。($\sum k \le 1e5$)

Sol:还是可以建立虚数,然后我们考虑怎么dp。考虑$dp[u]$表示考虑u的子树内关键点互相都不连通的代价。

  • u本身是关键点,则它的任意一个儿子里面有剩余未处理干净的关键点的话 ,必须选择切断下面的这个儿子。说到这里自然会考虑如果儿子也是关键点呢,我们只能消灭非关键点,所以这种情况我们是无法处理的,无解。
    • 所以无解的情况就是存在两个关键点直接有边相连
  • u不是关键点,那不能出现u内有两个儿子内部通过u连通,一旦出现就需要把u消灭掉,同时把dp[u]=0表示u的子树已经处理完了,上面考虑的时候不会重复计算这部分。
void solve() {
    int n;
    cin >> n;
    HLD hld(n);
    for (int i = 1; i <= n - 1; i++) {
        int u, v;
        cin >> u >> v;
        hld.addEdge(u, v);
        hld.addEdge(v, u);
    }
    auto cmp = [&](int i, int j) {
        return hld.l[i] < hld.l[j];
    };
    hld.work(1);
    auto buildvt = [&](vector<int>& node, vector<vector<int>>& e) {
        node.push_back(1);
        sort(alls(node), cmp);
        node.erase(unique(alls(node)), node.end());
        set<int> tmp;
        for (auto x : node) tmp.insert(x);
        for (int i = 1; i < (int)node.size(); i++) tmp.insert(hld.lca(node[i - 1], node[i]));
        node.clear();
        for (auto x : tmp) node.push_back(x);
        sort(alls(node), cmp);
        deb(node);
        vector<int> st;  // 维护一个栈
        for (auto v : node) {
            deb(st, v);
            while (!st.empty() && !hld.isAncester(st.back(), v))
                st.pop_back();
            if (!st.empty())
                e[st.back()].push_back(v);
            st.push_back(v);
        }
    };
    int q;
    cin >> q;
    int ans = 0;
    vector<int> sz(n + 1);
    vector<vector<int>> e(n + 1);
    // 考虑以u子树内所有关键点不连通的代价
    function<void(int, int)> dp = [&](int u, int fa) {
        if (sz[u]) {  // 本身是关键点
            for (auto v : e[u]) {
                if (v == fa)
                    continue;
                dp(v, u);
                if (sz[v]) {
                    ans++;
                    sz[v] = 0;
                }
            }
        } else {
            for (auto v : e[u]) {
                if (v == fa)
                    continue;
                deb(u, v);
                dp(v, u);
                sz[u] += sz[v];
                sz[v] = 0;  // 清空
            }
            if (sz[u] >= 2) {
                ans++;
                sz[u] = 0;
            }
        }
    };
    auto clear = [&](vector<int>& node) {
        for (auto x : node) e[x].clear(), sz[x] = 0;
        ans = 0;
    };
    for (int i = 1; i <= q; i++) {
        int num;
        cin >> num;
        vector<int> node;
        for (int j = 1; j <= num; j++) {
            int x;
            cin >> x;
            node.push_back(x);
            sz[x] = 1;
        }
        // 提前特判无解情况------------------
        bool flag = true;
        for (auto x : node) {
            if (x == 1)
                continue;
            int fa = hld.parent[x];
            if (sz[fa] > 0) {
                cout << -1 << endl;
                flag = false;
                break;
            }
        }
        if (flag == 0) {
            clear(node);
            continue;
        }
        //----------------------------------
        buildvt(node, e);
        dp(1, 1);
        cout << ans << endl;
        for (auto x : node) e[x].clear(), sz[x] = 0;
        clear(node);
    }
}

2.https://atcoder.jp/contests/abc359/tasks/abc359_g

题意:树上每个点有自己的颜色,定义$f(i,j)=[color[i]==color[j]] \times dis(i,j)$

求$\displaystyle \sum _ {i=1}^{N-1}\sum _ {j=i+1}^N f(i,j)$(启发式合并做法待补,jiangly)

考虑相同颜色的点之间才有贡献,按照把相同颜色的点放一起建立虚树,虚数的边权为两个点的深度差。考虑一个边的贡献次数就是联通块两侧数量乘积。

void solve() {
    int n;
    cin >> n;
    HLD hld(n);
    for (int i = 1; i <= n - 1; i++) {
        int u, v;
        cin >> u >> v;
        hld.addEdge(u, v, 1);
        hld.addEdge(v, u, 1);
    }
    vector<int> a(n + 1);
    vector<vector<int>> q(n + 1);
    for (int i = 1; i <= n; i++) cin >> a[i], q[a[i]].push_back(i);
    auto cmp = [&](int i, int j) {
        return hld.l[i] < hld.l[j];
    };
    hld.work(1);
    auto buildvt = [&](vector<int>& node, vector<vector<edge>>& e, int rt) {
        node.push_back(rt);  // 保证根节点在虚树中存在
        sort(alls(node), cmp);
        node.erase(unique(alls(node)), node.end());
        set<int> tmp;
        for (auto x : node) tmp.insert(x);
        for (int i = 1; i < (int)node.size(); i++) tmp.insert(hld.lca(node[i - 1], node[i]));
        node.clear();
        for (auto x : tmp) node.push_back(x);
        sort(alls(node), cmp);
        vector<int> st;  // 维护一个栈
        for (auto v : node) {
            while (!st.empty() && !hld.isAncester(st.back(), v))
                st.pop_back();
            if (!st.empty())
                e[st.back()].push_back({v, hld.dep[v] - hld.dep[st.back()]});
            st.push_back(v);
        }
    };
    vector<vector<edge>> e(n + 1);
    vector<int> sz(n + 1);
    ll ans = 0;
    int sum = 0;
    function<void(int, int)> calsz = [&](int u, int fa) {
        for (auto [v, w] : e[u]) {
            if (v == fa)
                continue;
            calsz(v, u);
            sz[u] += sz[v];
        }
    };
    function<void(int, int)> cal = [&](int u, int fa) {
        for (auto [v, w] : e[u]) {
            if (v == fa)
                continue;
            cal(v, u);
            ans += (ll)sz[v] * (sum - sz[v]) * w;
        }
    };
    auto clear = [&](vector<int>& node) {
        for (auto x : node) {
            e[x].clear();
            sz[x] = 0;
        }
    };
    deb(a);
    for (int i = 1; i <= n; i++) {
        vector<int> node;
        for (auto x : q[i]) node.push_back(x), sz[x] = 1;
        deb(node);
        sum = node.size();
        buildvt(node, e, 1);
        calsz(1, 1);
        cal(1, 1);
        clear(node);
    }
    cout << ans << endl;
}

3.给定一棵树,树上每个点有自己的颜色。求满足条件1和2的树的导出子图数量

  • 导出子图还是树
  • 导出子图所有度为1的点都是相同颜色的

Sol:颜色相同想到虚树,枚举 导出子图T叶子的颜色 C , T 一定是这个颜色的点构成的虚树的子图。

dp统计答案。$f[u]$表示 u 作为 当前子图树的根的时候有$ f[u] $种合法方案。

  • 考虑转移:对于儿子v,选有$f[v]$种方案,不选是1种方案。$f[u]^{*}=f[v]+1$。写到这里发现需要讨论,因为如果都不选就剩u一个点的话,u必须是颜色C才行。
  • u就是C颜色,那可以向答案直接贡献$f[u]$
  • u不是c颜色,首先u不能作为度数为1的点。首先就是不能只选u一个点,$f[u]-=1$。其次再考虑就是不能只选一个儿子,这时候u的度数也只有1.

为了更加清晰,不妨引入$g[u]$,它表示u只选一个儿子的方案数。

$f_{u}=∏{v∈sonu}(f{v}+1)−[coloru=C],$

$g_{u}=∑{v∈son[u]}f{v}$

void solve() {
    int n;
    cin >> n;
    HLD hld(n);
    vector<vector<int>> q(n + 1);
    vector<int> col(n + 1);
    for (int i = 1; i <= n; i++) {
        cin >> col[i];
        q[col[i]].push_back(i);
    }
    for (int i = 1; i <= n - 1; i++) {
        int u, v;
        cin >> u >> v;
        hld.addEdge(u, v, 1);
        hld.addEdge(v, u, 1);
    }
    vector<int> a(n + 1);
    auto cmp = [&](int i, int j) {
        return hld.l[i] < hld.l[j];
    };
    hld.work(1);
    auto buildvt = [&](vector<int>& node, vector<vector<edge>>& e, int rt) {
        node.push_back(rt);  // 保证根节点在虚树中存在
        sort(alls(node), cmp);
        node.erase(unique(alls(node)), node.end());
        set<int> tmp;
        for (auto x : node) tmp.insert(x);
        for (int i = 1; i < (int)node.size(); i++) tmp.insert(hld.lca(node[i - 1], node[i]));
        node.clear();
        for (auto x : tmp) node.push_back(x);
        sort(alls(node), cmp);
        vector<int> st;  // 维护一个栈
        for (auto v : node) {
            while (!st.empty() && !hld.isAncester(st.back(), v))
                st.pop_back();
            if (!st.empty())
                e[st.back()].push_back({v, hld.dep[v] - hld.dep[st.back()]});
            st.push_back(v);
        }
    };
    vector<vector<edge>> e(n + 1);
    vector<int> f(n + 1), g(n + 1);
    auto clear = [&](vector<int>& node) {
        for (auto x : node) {
            e[x].clear();
            f[x] = g[x] = 0;
        }
    };
    int cur = 0;
    int ans = 0;
    function<void(int, int)> cal = [&](int u, int fa) {
        f[u] = 1;
        g[u] = 0;
        for (auto [v, w] : e[u]) {
            if (v == fa)
                continue;
            cal(v, u);
            f[u] *= f[v] + 1;

            f[u] %= mod;
            g[u] += f[v];
        }
        if (col[u] == cur) {
            ans += f[u];
        } else {
            f[u] = (f[u] + mod - 1) % mod;
            ans += (f[u] + mod - g[u]) % mod;
            ans %= mod;
        }
    };
    for (int i = 1; i <= n; i++) {
        vector<int> node;
        for (auto x : q[i]) node.push_back(x);
        deb(node);
        buildvt(node, e, 1);
        cur = i;
        cal(1, 1);
        clear(node);
    }
    cout << ans << endl;
}

4.https://www.luogu.com.cn/problem/P4103题意:给定一棵树,每次选k个点,问这个k个点构成的$\binom{2}{k}$条路径的最长路径,最短路径,以及所有路径的权值和。

Sol:对于权值和已经在例题2叙述过了。最大值和最小值总体思路一样。先预处理 maxdep[u]表示距离u的子树里距离u最远的关键点。再考虑resmn[u]表示u的子树里最长路径,找到u的最深链mx1,次深链mx2(考虑叶子一定是关键点)。$resmn[u]=max(resmn[v],mx1+mx2)$.

说到这里我们需要思考最小路径不一定是从叶子开始,我们不能完全模仿同样维护mn1和mn2.因为最短路径没有必要从叶子开始。所以需要提前处理的是对于关键节点keynode打标记,而在虚树上的lca非关键节点不标记。具体来说$mindep[keynode]=0以及最小链mn1=0$

实现方面:加入chmax便于取max,同时要注意维护次大值。MLE是因为#define int long long

image-20241023140830861

bool chmax(int& fir, int sec) {
    if (sec > fir) {
        fir = sec;
        return true;
    }
    return false;
}
bool chmin(int& fir, int sec) {
    if (sec < fir) {
        fir = sec;
        return true;
    }
    return false;
}
struct edge {
    int v, w;
};
struct HLD {
    int n;
    vector<int> siz, top, parent, l, r, hson, dep;
    vector<vector<edge>> adj;
    int idx;
    vector<int> mn;  // 1-u的最小边权

    HLD() {}
    HLD(int n) {
        init(n);
    }
    void init(int n) {
        this->n = n;
        siz.resize(n + 1), hson.resize(n + 1), top.resize(n + 1);
        parent.resize(n + 1);
        l.resize(n + 1), r.resize(n + 1);
        idx = 0;
        adj.resize(n + 1), dep.resize(n + 1);
        // 根据题目要求加数据结构
    }
    void addEdge(int u, int v, int w) {
        adj[u].push_back({v, w});
    }
    void work(int root = 1) {
        top[root] = root;
        parent[root] = -1;
        dep[root] = 1;
        dfs1(root, -1);
        dfs2(root, root);
    }
    void dfs1(int u, int f) {  // 搞fa,dep,son
        siz[u] = 1;
        for (auto [v, w] : adj[u]) {
            if (v == f)
                continue;
            parent[v] = u;
            dep[v] = dep[u] + 1;
            dfs1(v, u);
            siz[u] += siz[v];
            if (siz[hson[u]] < siz[v])
                hson[u] = v;
        }
    }
    void dfs2(int u, int t) {  // 搞top
        top[u] = t;            // 记录链头
        l[u] = ++idx;
        if (!hson[u]) {
            r[u] = idx;
            return;
        }  // 无重儿子
        dfs2(hson[u], t);  // 搜重儿子
        for (auto [v, w] : adj[u]) {
            if (v == parent[u] || v == hson[u])
                continue;
            dfs2(v, v);  // 搜轻儿子
        }
        r[u] = idx;
    }
    int lca(int u, int v) {
        while (top[u] != top[v]) {
            if (dep[top[u]] > dep[top[v]]) {
                u = parent[top[u]];
            } else {
                v = parent[top[v]];
            }
        }
        return dep[u] < dep[v] ? u : v;
    }
    bool isAncester(int u, int v) {  // 判断u是不是v的祖先
        return l[u] <= l[v] && r[v] <= r[u];
    }
};
void solve() {
    int n;
    cin >> n;
    HLD hld(n);
    for (int i = 1; i <= n - 1; i++) {
        int u, v;
        cin >> u >> v;
        hld.addEdge(u, v, 1);
        hld.addEdge(v, u, 1);
    }
    auto cmp = [&](int i, int j) {
        return hld.l[i] < hld.l[j];
    };
    hld.work(1);
    auto buildvt = [&](vector<int>& node, vector<vector<edge>>& e, int rt) {
        node.push_back(rt);  // 保证根节点在虚树中存在
        sort(alls(node), cmp);
        node.erase(unique(alls(node)), node.end());
        set<int> tmp;
        for (auto x : node) tmp.insert(x);
        for (int i = 1; i < (int)node.size(); i++) tmp.insert(hld.lca(node[i - 1], node[i]));
        node.clear();
        for (auto x : tmp) node.push_back(x);
        sort(alls(node), cmp);
        vector<int> st;  // 维护一个栈
        for (auto v : node) {
            while (!st.empty() && !hld.isAncester(st.back(), v))
                st.pop_back();
            if (!st.empty())
                e[st.back()].push_back({v, hld.dep[v] - hld.dep[st.back()]});
            st.push_back(v);
        }
    };
    vector<vector<edge>> e(n + 1);
    vector<int> sz(n + 1);
    vector<bool> vis(n + 1);
    vector<int> ressz(n + 1), resmn(n + 1, inf), resmx(n + 1);  // u子树内最大值,最小值
    ll ans = 0;
    int sum = 0;
    vector<int> maxdep(n + 1), mindep(n + 1, inf);
    function<void(int, int)> precal = [&](int u, int fa) {
        for (auto [v, w] : e[u]) {
            if (v == fa)
                continue;
            deb(u, v);
            precal(v, u);
            sz[u] += sz[v];
            chmin(mindep[u], mindep[v] + w);
            chmax(maxdep[u], maxdep[v] + w);
        }
        deb(u, sz[u]);
        deb(u, mindep[u]);
        deb(u, maxdep[u]);
    };
    function<void(int, int)> cal = [&](int u, int fa) {
        int mn1 = inf, mn2 = inf;
        int mx1 = 0, mx2 = 0;
        if (vis[u])
            mn1 = 0;
        for (auto [v, w] : e[u]) {
            if (v == fa)
                continue;
            cal(v, u);
            ans += (ll)sz[v] * (sum - sz[v]) * w;
            int tmp1 = mn1, tmp2 = mx1;
            if (chmin(mn1, mindep[v] + w) == 0)
                chmin(mn2, mindep[v] + w);
            else {
                mn2 = tmp1;
            }
            if (chmax(mx1, maxdep[v] + w) == 0)
                chmax(mx2, maxdep[v] + w);
            else {
                mx2 = tmp2;
            }
            chmin(resmn[u], resmn[v]);
            chmax(resmx[u], resmx[v]);
            deb(u, v, mn1, mn2);
        }
        deb(u, mn1, mn2);
        deb(u, mx1, mx2);
        if (vis[u] || (mn1 < inf && mn2 < inf))
            chmin(resmn[u], mn1 + mn2);
        if (vis[u] || (mx1 > 0 && mx2 > 0))
            chmax(resmx[u], mx1 + mx2);
    };
    auto clear = [&](vector<int>& node) {
        for (auto x : node) {
            e[x].clear();
            sz[x] = 0;
            resmn[x] = inf;
            resmx[x] = 0;
            maxdep[x] = 0;
            mindep[x] = inf;
            vis[x] = 0;
        }
        ans = 0;
    };
    int q;
    cin >> q;
    for (int i = 1; i <= q; i++) {
        vector<int> node;
        int num;
        cin >> num;
        for (int j = 1; j <= num; j++) {
            int x;
            cin >> x;
            node.push_back(x);
            sz[x] = 1;
            vis[x] = 1;
            mindep[x] = 0;
        }
        deb(node);
        sum = node.size();
        buildvt(node, e, 1);
        precal(1, 1);
        cal(1, 1);
        cout << ans << " " << resmn[1] << " " << resmx[1] << endl;
        clear(node);
        deb("end");
    }
}

5.https://codeforces.com/gym/104128/problem/E

长剖做法: [【2022icpc Regional 南京 E】Color the Tree 题解 | Four’s](http://kqp.world/【2022icpc Regional 南京 E】Color the Tree 题解/index.html)

虚数题解:Color the Tree-虚树(2022南京区域赛) - 知乎

题意:给你一棵根树,根节点为 1 ,起初树的所有节点的颜色都是白色,你需要将每个节点染成黑色,你可以执行以下操作任意次:

  • 从树中任选一个点 u
  • 选定一个数字 $d(0≤d≤n−1) $,将 u 的子树中与其距离为 d 的点都涂黑

一次操作的代价为 a[d] , a 数组已经给出,求将所有点涂黑的最小花费。

Sol:具体更详细可以见sua——wiki。考虑染黑每一层是独立的,所以我们对每一层单独考虑贡献。这一层下面的点此时就没有用,我们考虑对这一层的点建立虚树。核心是考虑怎么dp?考虑染黑第cur层

  • $dp[u]$表示考虑以u为根的子树染黑这一层的贡献。u本身花费$a[cur-dep[u]]$的代价如果染,就会完成所有任务,任何儿子都不参与是最优的。不染的话就全都交给子节点去做,然后由于是虚树,相邻两点的边本质上是一条链,所以我们需要$tmp+=min(dp[v],min[a[cur-dep[v]],a[cur-dep[u]-1]])$
  • 实现上需要特判叶子的情况,主要是对于tmp的赋初始值
void solve() {//此处省略状压rmq板子和h
    int n;
    cin >> n;
    HLD hld(n);
    vector<int> a(n);
    for (int i = 0; i < n; i++) cin >> a[i];
    RMQ<int> qmn(a);
    for (int i = 1; i <= n - 1; i++) {
        int u, v;
        cin >> u >> v;
        hld.addEdge(u, v, 1);
        hld.addEdge(v, u, 1);
    }
    auto cmp = [&](int i, int j) {
        return hld.l[i] < hld.l[j];
    };
    hld.work(1);
    auto buildvt = [&](vector<int>& node, vector<vector<edge>>& e, int rt) {
        node.push_back(rt);  // 保证根节点在虚树中存在
        sort(alls(node), cmp);
        node.erase(unique(alls(node)), node.end());
        set<int> tmp;
        for (auto x : node) tmp.insert(x);
        for (int i = 1; i < (int)node.size(); i++) tmp.insert(hld.lca(node[i - 1], node[i]));
        node.clear();
        for (auto x : tmp) node.push_back(x);
        sort(alls(node), cmp);
        vector<int> st;  // 维护一个栈
        for (auto v : node) {
            while (!st.empty() && !hld.isAncester(st.back(), v))
                st.pop_back();
            if (!st.empty())
                e[st.back()].push_back({v, hld.dep[v] - hld.dep[st.back()]});
            st.push_back(v);
        }
    };
    vector<vector<edge>> e(n + 1);
    vector<vector<int>> q(n + 1);
    int maxdep = 0;
    for (int i = 1; i <= n; i++) q[hld.dep[i]].push_back(i), maxdep = max(maxdep, hld.dep[i]);
    ll ans = 0;
    vector<ll> dp(n + 1, 1e18);
    int cur = 0;
    function<void(int, int)> cal = [&](int u, int fa) {
        ll tmp = 0;
        if (e[u].size() == 0)
            tmp = 1e18;
        for (auto [v, w] : e[u]) {
            if (v == fa)
                continue;
            deb(u, v);
            cal(v, u);
            int ql = cur - hld.dep[v];
            int qr = cur - hld.dep[u] - 1;
            tmp += min(dp[v], (ll)qmn(ql, qr + 1));
        }
        dp[u] = min(tmp, (ll)a[cur - hld.dep[u]]);  // +1是深度偏移
    };
    auto clear = [&](vector<int>& node) {
        for (auto x : node) {
            e[x].clear();
            dp[x] = 1e18;
        }
    };
    for (int i = 1; i <= maxdep; i++) {
        vector<int> node;
        for (auto x : q[i]) node.push_back(x);
        cur = i;
        deb(i, node);
        buildvt(node, e, 1);
        cal(1, 1);
        deb(dp[1]);
        ans += dp[1];
        clear(node);
        deb("end");
    }
    cout << ans << endl;
}

6.Problem - 1681F - Codeforces题意:给定一棵树,边有边权。定义$f(u,v)为u到v简单路径上只出现一次的边权的权值数量$

参考学习:https://www.luogu.com.cn/article/dad36cgq(待补可撤销并查集以及线段树分治)

Sol:先考虑朴素做法。把所有颜色c的边砍去,答案就是相邻联通块大小乘积。这个是因为如果一条路径进入了大于两个「联通块」,那么就会出现两条颜色为 c的边。但这样枚举边的颜色,再dp统计的时间复杂度不对。

考虑颜色相关的均摊,想到虚树做法。每个值贡献独立,枚举边颜色cur,把边权两端的点全部抽出来建立虚树。

  • $sf[i] 代表 i 子树内和 i 在同个「联通块」的有几个点。$
  • $ot[i]代表 i 子树内与 i相邻的「联通块」的大小总和。$

$$ {转移如下:}
\begin{align*}
转移:
sf_u &= \sum sf_v [w = c] + \text{size}_u - \sum \text{size}_v \
ot_u &= \sum ot_v [w \neq c] + \sum sf_v [w = c]
\end{align*}
$$

  • sf的前面部分是虚树上所有和它在同个「联通块」的儿子的子树贡献

  • ss 的后面部分是不在虚树上的点的个数的贡献

  • $ot_{u}$的前面部分是和它在同个「联通块」的儿子的子树贡献,考虑u和v在同一联通块,则要统计与自己不在一起的联通块大小递归到和儿子不在一起的$ot_{v}$。

  • $ot_{u}$的后面部分是和它在不同「联通块」的儿子的联通块贡献。考虑u和v不在同一联通块,则要统计与自己不在一起的联通块大小递归到和儿子在一起的$sf_{v}$。

最后统计答案,应该是每个「联通块」的根(最高点),把自己的「联通块」的大小乘上身边「联通块」的大小总和,形式化地,

$res=∑sf_{u}ot_{u}[u=rt \or ⁡w(u,fau)=c]$

实现细节:建立虚树的时候需要加边,这关系到在哪里统计答案。需要开set或者map去记录这两点之间存不存在颜色为c的边,因为是虚树,两个点之间可能没什么关系,所以要支持快速随机访问。

void solve() {
    int n;
    cin >> n;
    HLD hld(n);
    vector<vector<int>> col(n + 1);
    vector<set<pii>> findedge(n + 1);
    for (int i = 1; i <= n - 1; i++) {
        int u, v, w;
        cin >> u >> v >> w;
        hld.addEdge(u, v, w);
        hld.addEdge(v, u, w);
        col[w].push_back(u);
        col[w].push_back(v);
        findedge[w].insert({min(u, v), max(u, v)});
    }
    auto cmp = [&](int i, int j) {
        return hld.l[i] < hld.l[j];
    };
    hld.work(1);
    int cur = 0;
    vector<vector<edge>> e(n + 1);
    vector<int> sf(n + 1), ot(n + 1);
    ll ans = 0;
    auto buildvt = [&](vector<int>& node, vector<vector<edge>>& e, int rt) {
        node.push_back(rt);  // 保证根节点在虚树中存在
        sort(alls(node), cmp);
        node.erase(unique(alls(node)), node.end());
        deb(cur);
        deb(node);
        set<int> tmp;
        for (auto x : node) tmp.insert(x);
        for (int i = 1; i < (int)node.size(); i++) tmp.insert(hld.lca(node[i - 1], node[i]));
        node.clear();
        for (auto x : tmp) node.push_back(x);
        sort(alls(node), cmp);
        vector<int> st;  // 维护一个栈
        for (auto v : node) {
            while (!st.empty() && !hld.isAncester(st.back(), v))
                st.pop_back();
            if (!st.empty()) {
                int w = 0;
                int mn = min(st.back(), v), mx = max(st.back(), v);
                if (findedge[cur].count({mn, mx}))
                    w = cur;
                e[st.back()].push_back({v, w});
            }
            st.push_back(v);
        }
    };
    function<void(int, int)> cal = [&](int u, int fa) {
        sf[u] = hld.siz[u];
        for (auto [v, w] : e[u]) {
            if (v == fa)
                continue;
            cal(v, u);
            if (w == cur) {
                ans += (ll)sf[v] * ot[v];
            }
            sf[u] += (w != cur) * sf[v] - hld.siz[v];
            ot[u] += (w == cur) * sf[v] + (w != cur) * ot[v];
        }
    };
    auto clear = [&](vector<int>& node) {
        for (auto x : node) {
            e[x].clear();
            sf[x] = ot[x] = 0;
        }
    };

    for (int i = 1; i <= n; i++) {
        if (col[i].size() == 0)
            continue;
        vector<int> node;
        for (auto x : col[i]) node.push_back(x);
        cur = i;
        buildvt(node, e, 1);
        cal(1, 1);
        ans += (ll)sf[1] * ot[1];
        clear(node);
    }
    cout << ans << endl;
}