Omsk Metro的题解

Omsk Metro的题解

题意

维护一颗树,支持加点,维护 \(u\)\(v\) 的子段和是否有为 \(k\)

分析题意

显然这里动态加点很假,因为不修改值,且查询的点一定添加过了

所以我们在输入的时候直接分类询问和建边

一个重要的结论

观察到 \(x_i \in \{-1, 1\}\) , 这就很可疑了

假设一个区间和为sum,无论我们怎么拓展区间都只有 \(sum + 1\)\(sum - 1\) 两种情况

也就是值域是连续的

所以只要k的值在最大子段和和最小子段和之间,就可以被拼凑

维护

维护树上最大子段和和最小子段和

不难想到树链剖分

怎么用SGT维护上最大子段和和最小子段和呢?

显然我是问了数据结构大神ZXT才知道的

struct node {
    int sum, lmax, rmax, mmax, lmin, rmin, mmin;
	// sum: 区间总权值和
    // lmax: 以区间左端点为起点的最大子段和
    // rmax: 以区间右端点为终点的最大子段和
    // mmax: 区间内的最大子段和
    // lmin: 以区间左端点为起点的最小子段和
    // rmin: 以区间右端点为终点的最小子段和
    // mmin: 区间内的最小子段和
    node() : sum(0), lmax(-inf), rmax(-inf), mmax(-inf), lmin(inf), rmin(inf), mmin(inf) {}
    node(int x) {
        sum = x;
        lmax = rmax = mmax = x;
        lmin = rmin = mmin = x;
    }

    node operator + (const node& other) const {
        node res;
        res.sum = sum + other.sum;

        res.lmax = max(lmax, sum + other.lmax);
        res.rmax = max(other.rmax, other.sum + rmax);
        res.mmax = max({mmax, other.mmax, rmax + other.lmax});

        res.lmin = min(lmin, sum + other.lmin);
        res.rmin = min(other.rmin, other.sum + rmin);
        res.mmin = min({mmin, other.mmin, rmin + other.lmin});
        return res;
    }
} T[N * 4];

假设合并左区间 A 和右区间 B 得到区间 C:

总权值和\(C.sum=A.sum+B.sum\)

左起点最大子段和:要么是 A 的左起点最大子段和,要么是 A 全段和 + B 的左起点最大子段和 \(C.lmax=max(A.lmax,A.sum+B.lmax)\)

右终点最大子段和:要么是 B 的右终点最大子段和,要么是 B 全段和 + A 的右终点最大子段和 \(C.rmax=max(B.rmax,B.sum+A.rmax)\)

区间最大子段和:三者取最大值(A 的最大子段和、B 的最大子段和、A 的右终点最大 + B 的左起点最大);

最小子段和相关:逻辑与最大子段和对称,仅将 max 改为 min。

合并

在树链剖分时,整个区间其实是被划分成了很多个小区间的

因为合并必须从左到右(反正我的+号是这样重载的),所以必须遵从这样的顺序:

重点来了(我调了很久)

u → LCA 的合并顺序(query 结果 + res_u

u 深度 > LCA 深度,u 在重链的「下方」,LCA 在「上方」。线段树查询的是「重链顶端 → u」(父→子),但我们需要「u → 重链顶端 → LCA」(子→父),因此需将新查询的父→子段「前置」,即反向(rev_node

v → LCA 的合并顺序(res_v + query 结果

v 深度 > LCA 深度,线段树查询的是「重链顶端 → v」(父→子),但我们需要「v → 重链顶端 → LCA」(子→父),且这部分路径要接在 LCA 之后,因此需将新查询的父→子段「后置」

这个注释不是我加的

bool check(int u, int v, int k, int max_dfn) {
    // 空子段的和为0,题目规定空子段合法,直接返回true
    if (k == 0) return true;

    // 第一步:找到u和v的最近公共祖先(LCA),将路径拆分为 u→LCA 和 v→LCA 两部分
    int l = lca(u, v);
    
    // L:存储 u→LCA 路径的线段树节点(子→父顺序)
    // R:存储 v→LCA 路径的线段树节点(子→父顺序,不含LCA)
    node L, R;

    // 第二步:处理 u → LCA 的路径(从u向上跳重链到LCA)
    while (top[u] != top[l]) { // u和LCA不在同一条重链上
        // 查询当前重链的区间:top[u](重链顶端)→ u(父→子顺序,DFS序从小到大)
        node range = sgt.query(1, 1, max_dfn, dfn[top[u]], dfn[u]);
        // rev_node:反转节点的顺序(父→子 → 子→父),保证路径是u→top[u]→LCA
        // 合并顺序:L + 反转后的区间 → 把当前重链的子→父段追加到L末尾
        L = L + rev_node(range);
        // u跳到当前重链顶端的父节点,继续向上找LCA
        u = fath[top[u]];
    }
    // 处理最后一段:u和LCA在同一条重链上,查询 LCA→u 的区间(父→子)
    { // 花括号仅为限定range变量作用域,无逻辑意义
    node range = sgt.query(1, 1, max_dfn, dfn[l], dfn[u]);
    // 反转后合并到L,此时L完整存储 u→LCA 的子→父路径
    L = L + rev_node(range);
    }
    
    // 第三步:处理 v → LCA 的路径(从v向上跳重链到LCA,不含LCA)
    vector<node> ranges; // 临时存储v→LCA路径上的所有重链区间(父→子顺序)
    while (top[v] != top[l]) { // v和LCA不在同一条重链上
        // 查询当前重链的区间:top[v]→v(父→子),存入临时数组
        ranges.push_back(sgt.query(1, 1, max_dfn, dfn[top[v]], dfn[v]));
        // v跳到当前重链顶端的父节点,继续向上找LCA
        v = fath[top[v]];
    }
    // 处理最后一段:v和LCA在同一条重链上,查询 LCA+1→v 的区间(跳过LCA,避免重复计算)
    if (dfn[v] > dfn[l]) {
        ranges.push_back(sgt.query(1, 1, max_dfn, dfn[l] + 1, dfn[v]));
    }
    // 逆序合并ranges数组:将父→子的区间转为子→父的顺序
    // 例如:ranges存储 [top[v1]→v1, top[v2]→v2](父→子),逆序后合并为 v→v1→v2→LCA(子→父)
    for (int i = (int)ranges.size() - 1; i >= 0; --i) {
        R = R + ranges[i];
    }

    // 第四步:合并 u→LCA(L)和 LCA→v(R),得到完整路径 u→LCA→v 的节点信息
    node total = L + R;
    // 核心结论:权值仅为±1时,k在[mmin, mmax]之间则一定存在对应子段和
    return (total.mmin <= k && k <= total.mmax);
}

题外话

貌似还可以LCA Splay 疑似std? 和 倍增

posted @ 2026-03-15 22:36  Aojun  阅读(0)  评论(0)    收藏  举报