角色属性树

GitHub跳转原题关系图返回列表

把“有相同萌元素”转成“有公共质因子”,每次修改后整树 DFS,沿根路径按质因子维护最近祖先栈即可回答所有查询。

OJ: luogu

题目 ID: P2441

难度:提高+/省选-

标签:树形结构dfs思维

日期: 2026-06-19 21:10

题意

给出一棵组织树和每个节点当前的属性值。

查询 1 x 要求回答:离 x 最近、并且和 x 有公共萌元素的上司是谁。 这里“有公共萌元素”等价于两个属性值有公共质因子,也就是 gcd>1\gcd > 1

修改 2 x y 则把节点 x 的属性值改成 y

思路

最直接的办法是每次查询都沿父链往上找第一个 gcd>1\gcd > 1 的祖先。

先看一个可以直接验证想法的朴素解:

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

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n, q;
    cin >> n >> q;

    vector<int> value(n + 1);
    for (int i = 1; i <= n; ++i) {
        cin >> value[i];
    }

    vector<vector<int>> tree(n + 1);
    for (int i = 1; i <= n - 1; ++i) {
        int u, v;
        cin >> u >> v;
        tree[u].push_back(v);
        tree[v].push_back(u);
    }

    vector<int> parent(n + 1, 0);
    vector<int> order;
    order.push_back(1);
    for (int i = 0; i < (int)order.size(); ++i) {
        int u = order[i];
        for (int v : tree[u]) {
            if (v == parent[u]) {
                continue;
            }
            parent[v] = u;
            order.push_back(v);
        }
    }

    while (q--) {
        int type;
        cin >> type;
        if (type == 1) {
            int x;
            cin >> x;
            int cur = parent[x];
            int ans = -1;

            // 直接沿父链向上找第一个 gcd 大于 1 的祖先。
            while (cur != 0) {
                if (std::gcd(value[cur], value[x]) > 1) {
                    ans = cur;
                    break;
                }
                cur = parent[cur];
            }
            cout << ans << '\n';
        } else {
            int x, y;
            cin >> x >> y;
            value[x] = y;
        }
    }
    return 0;
}

brute.cpp 每次查询都暴力爬父链,适合帮助理解和对拍。但如果树很深、查询很多,这样最坏会到 O(nq)O(nq)

真正的突破口在于:修改次数最多只有 50 次。

样例路径

这张图展示样例树,节点里同时写出编号和属性值:

graph TD
  A["1 / 10"] --> B["2 / 8"]
  B --> C["3 / 4"]
  C --> D["4 / 3"]

例如查询节点 3 时,它的属性值是 4=224 = 2^2。 沿路径往上看,节点 22 的属性值 8=238 = 2^3 也含质因子 2,而且它比节点 1 更近,所以答案是 2。 这说明题目的关键不在 gcd 本身,而在“当前路径上,哪个祖先最近含有某个公共质因子”。

所以可以把问题改写成:

  1. 先把每个点的属性值分解成不同质因子
  2. DFS 整棵树时,对每个质因子维护一个“当前路径出现栈”
  3. 到达节点 u 时,枚举 u 的所有质因子
  4. 每个质因子的栈顶,都是一个候选最近祖先
  5. 在这些候选里选深度最大的那个,就是答案

由于修改次数很少,我们不必做复杂在线结构。每次修改后,重新分解该点质因子,再整树 DFS 重算一遍 answer[u] 即可。

代码

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

const int MAXP = 46340;

struct Operation {
    int type;
    int x;
    int y;
};

struct Frame {
    int u;
    int parent;
    int state;
};

static vector<int> primes;
static unordered_map<int, int> prime_id;
static vector<vector<int>> prime_stacks;

int get_prime_id(int p) {
    auto it = prime_id.find(p);
    if (it != prime_id.end()) {
        return it->second;
    }
    int id = (int)prime_stacks.size();
    prime_id[p] = id;
    prime_stacks.push_back(vector<int>());
    return id;
}

vector<int> factorize_ids(int x) {
    vector<int> ids;
    int value = x;
    for (int p : primes) {
        if (1LL * p * p > value) {
            break;
        }
        if (value % p == 0) {
            ids.push_back(get_prime_id(p));
            while (value % p == 0) {
                value /= p;
            }
        }
    }
    if (value > 1) {
        ids.push_back(get_prime_id(value));
    }
    return ids;
}

void build_primes() {
    vector<int> is_prime(MAXP + 1, 1);
    is_prime[0] = is_prime[1] = 0;
    for (int i = 2; i <= MAXP; ++i) {
        if (!is_prime[i]) {
            continue;
        }
        primes.push_back(i);
        if (1LL * i * i <= MAXP) {
            for (int j = i * i; j <= MAXP; j += i) {
                is_prime[j] = 0;
            }
        }
    }
}

void recompute_answers(const vector<vector<int>> &tree,
                       const vector<vector<int>> &factor_ids,
                       vector<int> &answer,
                       vector<int> &depth) {
    int n = (int)tree.size() - 1;
    vector<Frame> st;
    st.reserve(n * 2);
    st.push_back({1, 0, 0});
    depth[1] = 0;

    while (!st.empty()) {
        Frame cur = st.back();
        st.pop_back();

        int u = cur.u;
        int parent = cur.parent;

        if (cur.state == 0) {
            int best = -1;
            int best_depth = -1;

            // 枚举当前点的所有不同质因子,看看路径上最近的同类祖先是谁。
            for (int pid : factor_ids[u]) {
                if (!prime_stacks[pid].empty()) {
                    int candidate = prime_stacks[pid].back();
                    if (depth[candidate] > best_depth) {
                        best_depth = depth[candidate];
                        best = candidate;
                    }
                }
            }
            answer[u] = best;

            for (int pid : factor_ids[u]) {
                prime_stacks[pid].push_back(u);
            }

            st.push_back({u, parent, 1});
            for (int i = (int)tree[u].size() - 1; i >= 0; --i) {
                int v = tree[u][i];
                if (v == parent) {
                    continue;
                }
                depth[v] = depth[u] + 1;
                st.push_back({v, u, 0});
            }
        } else {
            for (int pid : factor_ids[u]) {
                prime_stacks[pid].pop_back();
            }
        }
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    build_primes();

    int n, q;
    cin >> n >> q;

    vector<int> value(n + 1);
    vector<vector<int>> factor_ids(n + 1);
    for (int i = 1; i <= n; ++i) {
        cin >> value[i];
    }

    vector<vector<int>> tree(n + 1);
    for (int i = 1; i <= n - 1; ++i) {
        int u, v;
        cin >> u >> v;
        tree[u].push_back(v);
        tree[v].push_back(u);
    }

    for (int i = 1; i <= n; ++i) {
        factor_ids[i] = factorize_ids(value[i]);
    }

    vector<int> answer(n + 1, -1);
    vector<int> depth(n + 1, 0);
    recompute_answers(tree, factor_ids, answer, depth);

    for (int i = 1; i <= q; ++i) {
        int type;
        cin >> type;
        if (type == 1) {
            int x;
            cin >> x;
            cout << answer[x] << '\n';
        } else {
            int x, y;
            cin >> x >> y;
            value[x] = y;
            factor_ids[x] = factorize_ids(value[x]);
            recompute_answers(tree, factor_ids, answer, depth);
        }
    }
    return 0;
}

复杂度

每次整树重算都只需 DFS 一遍,并枚举每个点有限个不同质因子,所以单次可视为 O(n)O(n)。修改次数最多 5050,因此总复杂度约为 O((c+1)n+q)O((c+1) \cdot n + q),空间复杂度是 O(n)O(n)

总结

这题的关键是抓住“更新少、查询多”这个结构特征。与其为每次查询单独向上找,不如每次修改后把整棵树的答案一次性重算出来。

一图流解析

这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。

一图流解析