Re:从零开始的近世代数复习(hard)

Re:从零开始的近世代数复习(hard)

题目描述

期末考还有最后一门考试--近世代数,但是小明还在进行出题,他必须从零开始复习近世代数。近世代数是一门非常复杂的课,挂科率很高,小明决定找到最优的复习策略。

假设近世代数中有 $n$ 个不同的定理 $1$ 到 $n$,每个定理有不同的难度,导致每个定理复习需要时间不同,其中第 $i$ 个定理需要的复习时间是 $a_i$,且复习一个定理需要先知道它的前置定理,所有定理可以连接成一颗有根树,定理 $1$ 为根节点,一个除根以外的定理 $x$ 可以被复习当且仅当它的父节点已经被复习过了,根节点可以直接进行复习。

现在小明一个定理都还没有复习,但他有 $q$ 种复习策略,每种策略有 $k$ 个定理是重要考点,他希望能够复习到这 $k$ 个定理,不过时间不够了,他希望花费最少的时间去复习到这 $k$ 个定理,请你对每种策略求出复习到该策略中的 $k$ 个定理所需要的最短时间是多少。

本题与 easy 版本的唯一区别在于本题的 $k$ 范围是 $1 \leq k \leq 10^5$。

输入描述:

第一行输入定理个数 $n$。

第 $2$ 行输入 $n$ 个数,第 $i$ 个数代表定理 $i$ 的复习时间 $a_i$。

接下来 $n-1$ 行每行输入两个数 $u \, v$,代表 $u$ 是 $v$ 的父节点。

下一行输入一个数 $q$ 代表复习策略数。

对每种策略,先输入一个数 $k$ 代表该策略重要定理数。

再在下一行输入 $k$ 个数代表需要复习的重要定理。

输出描述:

对每种策略输出一行一个整数代表最短复习时间。

示例1

输入

5
1 2 3 4 5
1 2
2 3
2 4
2 5
2
2
1 3
3
3 4 5

输出

6
15

备注:

数据范围:

$1 \leq n,q \leq 10^5$

$1 \leq a_i \leq 10^9$

$1 \leq k \leq n$

保证单个测试文件 $\sum{k} \leq 2 \times 10^5$

 

解题思路

  如果要选择某个节点,那么它的所有祖先节点也必须选择。因此对于询问的 $k$ 个节点,设 $S_i$ 表示第 $i$ 个节点与其祖先构成的点集,问题的核心就是求这 $k$ 个点集的并集 $\bigcup\limits_{i=1}^{k}{S_i}$ 中所有节点的权值和。

  如果只有单次询问,为避免重复计算,可以通过树上差分标记所有询问节点及其祖先,最后通过 dfs 进行子树求和。如果某个节点被标记则将其点权累加到答案中。

  然而,以上方法对于每个询问都要遍历整个树,若询问数量较多会超时。注意到 $\sum{k_i} \leq 2 \times 10^5$,意味着每次询问中,并非所有节点都需要遍历。因此,我们可以思考能否只遍历有用的节点?我们可以建立一棵由询问点组成的虚树来解决这个问题,其中虚树中的节点由 $k$ 个询问点,所有询问点之间的 $\text{lca}$,以及根节点 $1$(因为根节点必选)构成,可以证明虚树的节点数量不超过 $2k$ 个。

  虚树中的每个节点的点权与原树相同,而每条边 $(u,v)$ 的边权则是原树中 $u$ 到 $v$ 路径上的点权和(不包括 $u$ 和 $v$)。所以在建立出虚树后,只需对这棵虚树进行 dfs 累加所有的点权与边权,并且过程中不会出现重复计算的问题。

  AC 代码如下,时间复杂度为 $O(n\log{n} + q + \sum{k_i}\log{n})$:

#include <bits/stdc++.h>
using namespace std;

typedef long long LL;

const int N = 1e5 + 5, M = N * 2;

int a[N];
int h[N], e[M], ne[M], idx;
LL wt[M];
int dfn[N];
LL s[N];
int fa[N][17], d[N];

void add(int u, int v, LL w = 0) {
    e[idx] = v, wt[idx] = w, ne[idx] = h[u], h[u] = idx++;
}

void dfs1(int u, int p) {
    dfn[u] = ++idx;
    s[u] = s[p] + a[u];
    d[u] = d[p] + 1;
    fa[u][0] = p;
    for (int i = 1; i <= 16; i++) {
        fa[u][i] = fa[fa[u][i - 1]][i - 1];
    }
    for (int i = h[u]; i != -1; i = ne[i]) {
        int v = e[i];
        if (v == p) continue;
        dfs1(v, u);
    }
}

int lca(int u, int v) {
    if (d[u] < d[v]) swap(u, v);
    for (int i = 16; i >= 0; i--) {
        if (d[fa[u][i]] >= d[v]) u = fa[u][i];
    }
    if (u == v) return u;
    for (int i = 16; i >= 0; i--) {
        if (fa[u][i] != fa[v][i]) u = fa[u][i], v = fa[v][i];
    }
    return fa[u][0];
}

void build(vector<int> &p) {
    auto cmp = [&](int i, int j) {
        return dfn[i] < dfn[j];
    };
    sort(p.begin(), p.end(), cmp);
    for (int i = p.size() - 1; i; i--) {
        p.push_back(lca(p[i - 1], p[i]));
    }
    p.push_back(1);
    sort(p.begin(), p.end(), cmp);
    p.erase(unique(p.begin(), p.end()), p.end());
    idx = 0;
    for (auto &x : p) {
        h[x] = -1;
    }
    for (int i = 0; i + 1 < p.size(); i++) {
        int t = lca(p[i], p[i + 1]);
        add(t, p[i + 1], s[fa[p[i + 1]][0]] - s[t]);
    }
}

LL dfs2(int u, int p) {
    LL ret = a[u];
    for (int i = h[u]; i != -1; i = ne[i]) {
        int v = e[i];
        if (v == p) continue;
        ret += dfs2(v, u) + wt[i];
    }
    return ret;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int n, m;
    cin >> n;
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
    }
    memset(h, -1, sizeof(h));
    for (int i = 0; i < n - 1; i++) {
        int u, v;
        cin >> u >> v;
        add(u, v), add(v, u);
    }
    dfs1(1, 0);
    cin >> m;
    while (m--) {
        int c;
        cin >> c;
        vector<int> p;
        while (c--) {
            int x;
            cin >> x;
            p.push_back(x);
        }
        build(p);
        cout << dfs2(1, 0) << '\n';
    }
    
    return 0;
}

  直接上虚树多少有点无脑了,还有另外一种相对简单的做法。

  由于根节点必然会被选择,因此在下面的讨论中默认询问点包含根节点。对询问点按 dfs 序排序,从根节点开始 dfs 进行先序遍历,依次访问各询问点,最后再返回到根节点。该过程只会访问到所有询问点及其祖先,但每个节点会被遍历多次(为节点的度数)。而在这个过程中每条边只会被遍历两次,因此我们可以把点权转化为边权,即如果 $u$ 是 $v$ 的父节点,则定义边 $(u,v)$ 的边权为 $v$ 的点权 $a_v$,这也意味着不存在根节点所对应的边权。所以在 dfs 的过程中,只需累加边权,最后将结果除以 $2$ 并加上根节点的点权 $a_1$,即可得到最终答案。

  显然对于每个询问都进行 dfs 会超时,但在按 dfs 序排序后,相邻两个询问点之间的路径就是从前一个询问点到后一个询问点的 dfs 遍历路径。因此,我们可以通过枚举相邻两个询问点,计算它们的 lca,然后得到路径的边权和。这样,我们只需要 $O(k \log{n})$ 的复杂度即可完成原本需要 dfs 的计算。

  AC 代码如下,时间复杂度为 $O(n\log{n} + q + \sum{k_i}\log{n})$:

#include <bits/stdc++.h>
using namespace std;

typedef long long LL;

const int N = 1e5 + 5, M = N * 2;

int a[N];
int h[N], e[M], ne[M], idx;
int dfn[N];
LL s[N];
int fa[N][17], d[N];

void add(int u, int v) {
    e[idx] = v, ne[idx] = h[u], h[u] = idx++;
}

void dfs(int u, int p) {
    dfn[u] = ++idx;
    d[u] = d[p] + 1;
    fa[u][0] = p;
    for (int i = 1; i <= 16; i++) {
        fa[u][i] = fa[fa[u][i - 1]][i - 1];
    }
    for (int i = h[u]; i != -1; i = ne[i]) {
        int v = e[i];
        if (v == p) continue;
        s[v] = s[u] + a[v];
        dfs(v, u);
    }
}

int lca(int u, int v) {
    if (d[u] < d[v]) swap(u, v);
    for (int i = 16; i >= 0; i--) {
        if (d[fa[u][i]] >= d[v]) u = fa[u][i];
    }
    if (u == v) return u;
    for (int i = 16; i >= 0; i--) {
        if (fa[u][i] != fa[v][i]) u = fa[u][i], v = fa[v][i];
    }
    return fa[u][0];
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int n, m;
    cin >> n;
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
    }
    memset(h, -1, sizeof(h));
    for (int i = 0; i < n - 1; i++) {
        int u, v;
        cin >> u >> v;
        add(u, v), add(v, u);
    }
    dfs(1, 0);
    cin >> m;
    while (m--) {
        int c;
        cin >> c;
        vector<int> q({1});
        while (c--) {
            int x;
            cin >> x;
            q.push_back(x);
        }
        sort(q.begin(), q.end(), [&](int i, int j) {
            return dfn[i] < dfn[j];
        });
        LL ret = 0;
        for (int i = 0; i < q.size(); i++) {
            int p = lca(q[i], q[(i + 1) % q.size()]);
            ret += s[q[i]] + s[q[(i + 1) % q.size()]] - 2 * s[p];
        }
        cout << ret / 2 + a[1] << '\n';
    }
    
    return 0;
}

 

参考资料

  0-1 BFS 最短路 LCA【力扣周赛 450】:https://www.bilibili.com/video/BV1Z3JGzwEU9/

  河南萌新联赛2025第(五)场:信息工程大学”题解:https://www.cnblogs.com/lmmsblog/p/19036167

posted @ 2025-08-14 20:12  onlyblues  阅读(20)  评论(0)    收藏  举报
Web Analytics