Codeforces Round 881 (Div. 3) F2. Omsk Metro (hard version)(倍增+最大子段和)

原题链接:F2. Omsk Metro (hard version)


题目大意:


最初开始时,你有一个根节点 1 1 1 且权值为 1 1 1

接下来会有 n n n 个操作,每次操作按照如下格式给出:

设操作开始前节点总数为 c n t cnt cnt

  • + + + v v v x x x ,增加一个编号为 c n t + 1 cnt+1 cnt+1 ,且权值为 x ∈ { − 1 , 1 } x\in\{-1,1\} x{1,1} 的节点,同时和节点 v v v 连上一条边(保证节点 v v v 存在)。
  • ? ? ? x x x y y y k k k ,询问从 x x x y y y 的路径上,是否可以从某个位置开始,选出一些连续节点,并且使得点权之和为 k k k

解题思路:


注意到形成的结构是一棵树,并且还有一个性质: x ∈ { − 1 , 1 } x\in\{-1,1\} x{1,1}

我们引入一个结论:如果一个区间的和 s u m ≥ 0 sum\geq 0 sum0 ,那么我们可以通过删除最左边或最右边的一些元素,获得所有在区间 [ 0 , s u m ] [0,sum] [0,sum] 内的值,同样的, s u m ≤ 0 sum \leq 0 sum0 也满足这个条件。

感性证明一下为什么是对的:

首先我们肯定要从序列的一边慢慢删数,否则就不连续了。

  • 下面只考虑 s u m ≥ 0 sum \geq 0 sum0 的情况, s u m ≤ 0 sum\leq0 sum0 的情况也同理。
  • 假设序列的元素全为 1 1 1 ,结论显然是正确的。
  • 假设序列包含至少一个 − 1 -1 1 ,又因为 s u m = a × ( − 1 ) + b × 1 sum = a \times(-1)+b\times1 sum=a×(1)+b×1 ,对于每一个 − 1 -1 1,我们一定都能和某一个正 1 1 1 抵消掉这个 − 1 -1 1 的贡献,即删除了这个 − 1 -1 1 再删除一个 1 1 1 ,会使得 s u m sum sum 值不变,我们发现 − 1 -1 1 的存在本质上是没有意义的,这样问题就转化成了序列元素全是 1 1 1 的情况了。

引入这个结论有什么用呢?

假设我们可以知道这个序列 最小的负值和 S 1 S_{1} S1 以及 最大的正值和 S 2 S_{2} S2 ,按照这个结论,我们一定能用 S 1 S_{1} S1 凑出 [ S 1 , 0 ] [S_{1}, 0] [S1,0] 的任意值,用 S 2 S_{2} S2 凑出 [ 0 , S 2 ] [0,S_{2}] [0,S2] 的任意值。

所以判断是否能选出一个子数组,使得和为 k k k ,即判断 S 1 ≤ k ≤ S 2 S_{1} \leq k \leq S_{2} S1kS2 即可。

回到这题上,能否在 x → y x \rightarrow y xy 路径形成的序列上凑成 k k k ,本质就转化成了求 x → y x \rightarrow y xy 路径的最小子段和,以及最大子段和,这是一个典型问题。

假设根为 1 1 1 ,我们要求 x → y x \rightarrow y xy 的路径。

l c a lca lca x , y x,y x,y 的最近公共祖先。

假设 l c a lca lca x , y x,y x,y 的其中一个,那么答案就直接是 x → y x \rightarrow y xy 路径上的最大 / / /小子段和。

否则就是 x → l c a x \rightarrow lca xlca l c a → y lca \rightarrow y lcay 的路径拼起来,考虑怎么拼起来。

我们维护 7 7 7 个信息:

  • 严格包含 区间最左 / / /右端点的最大子段和: m x l , m x r mxl,mxr mxl,mxr
  • 严格包含 区间最左 / / /右端点的最小子段和: m n l , m n r mnl,mnr mnl,mnr
  • 只看这个区间内部的最大 / / /小子段和: m x s u m , m n s u m mxsum,mnsum mxsum,mnsum
  • 区间的和: s u m sum sum

假设我们已经知道 a → b a \rightarrow b ab b → c b \rightarrow c bc 的信息,我们要怎么将这两个区间合并成 a → c a \rightarrow c ac 的信息:

注意,这里的 a → b a \rightarrow b ab 指的是 左闭右开区间 [ a , b ) [a,b) [a,b) ,转化为下标可以理解为闭区间 [ a , b − 1 ] [a,b-1] [a,b1]

在这里插入图片描述
下面只考虑最大子段和,最小子段和也是同理:

按照上图将 a → b a \rightarrow b ab 统称为左边, b → c b \rightarrow c bc 统称为右边。

  • m x s u m a → c = max ⁡ { m x s u m a → b , m x s u m b → c , m x r a → b + m x l b → c } mxsum_{a \rightarrow c}=\max\{mxsum_{a \rightarrow b},mxsum_{b \rightarrow c}, mxr_{a \rightarrow b}+mxl_{b \rightarrow c}\} mxsumac=max{mxsumab,mxsumbc,mxrab+mxlbc}
    a → c a \rightarrow c ac 的最大子段和 m x s u m mxsum mxsum ,要么是左边或者右边其一的最大子段和,要么是左边 m x r mxr mxr 和右边 m x l mxl mxl 拼起来的中间那一段的和)

  • m x r a → c = max ⁡ { m x r b → c , m x r a → b + s u m b → c } mxr_{a \rightarrow c}=\max\{mxr_{b \rightarrow c},mxr_{a \rightarrow b}+sum_{b \rightarrow c}\} mxrac=max{mxrbc,mxrab+sumbc}
    a → c a \rightarrow c ac 包含右端点的最大子段和 m x r mxr mxr,要么是右边的 m x r mxr mxr,要么是左边的 m x r mxr mxr 再加上右边一整段区间的和)

  • m x l a → c = max ⁡ { m x l b → c , s u m a → b + m a x l b → c } mxl_{a \rightarrow c}=\max\{mxl_{b \rightarrow c},sum_{a \rightarrow b}+maxl_{b \rightarrow c}\} mxlac=max{mxlbc,sumab+maxlbc}
    a → c a \rightarrow c ac 包含左端点的最大子段和 m x l mxl mxl,要么是左边的 m x l mxl mxl,要么是左边一整段区间的和再加上右边的 m x l mxl mxl

  • s u m a → c = s u m a → b + s u m b → c sum_{a \rightarrow c} = sum_{a \rightarrow b} + sum_{b \rightarrow c} sumac=sumab+sumbc
    a → c a \rightarrow c ac 的区间的和直接就是左边的和和右边的和相加)

这样,我们就可以将树的每一条链分段维护,然后对每一段进行如上的信息合并,就能够实时求出 a → b a \rightarrow b ab 这一整条链上的信息了。

对于这一题而言,我们可以用倍增表做到在线加点维护信息,在线询问。

注意到我们的信息是自底向上维护的,且询问区间是 [ x , y ) [x,y) [x,y) 的左闭右开区间,因此我们除了 l c a lca lca x , y x,y x,y 的其中一个的情况,我们询问的答案都是 [ x , l c a ) , [ y , l c a ) [x,lca),[y,lca) [x,lca),[y,lca) 的信息。

这时我们要翻转一下一个区间,变成 [ x , l c a ) + [ l c a ] + ( l c a , y ] [x,lca)+[lca]+(lca,y] [x,lca)+[lca]+(lca,y] 的信息,才是 [ x , y ] [x,y] [x,y] 的信息。

细节比较多,可以看代码注释。

时间复杂度: O ( n log ⁡ n ) O(n \log n) O(nlogn)

AC代码:


#include <bits/stdc++.h>
#define YES return void(cout << "Yes\n")
#define NO return void(cout << "No\n")
using namespace std;

using u64 = unsigned long long;
using PII = pair<int, int>;
using i64 = long long;

//这里是我们的信息结构体
struct Info {
    int mnl, mnr, mxl, mxr, sum;
    int mxsum, mnsum;
    Info() { mnl = mnr = mxl = mxr = sum = mxsum = mnsum = 0; };
    Info(int _) {
        //初始时全为 0
        mnl = mnr = mxl = mxr = sum = mxsum = mnsum = 0;
        //分类讨论点 u 权值为 > 0 和 < 0 的情况
        if (_ > 0) {
            mxl = mxr = sum = mxsum = _;
        } else {
            mnl = mnr = sum = mnsum = _;
        }
    }
    //维护最大子段和
    friend Info merge(const Info& a, const Info& b) {
        Info res;
        res.mxsum = max({ a.mxsum, b.mxsum, a.mxr + b.mxl });
        res.mnsum = min({ a.mnsum, b.mnsum, a.mnr + b.mnl });
        res.mxl = max(a.mxl, a.sum + b.mxl);
        res.mxr = max(b.mxr, a.mxr + b.sum);
        res.mnl = min(a.mnl, a.sum + b.mnl);
        res.mnr = min(b.mnr, a.mnr + b.sum);
        res.sum = a.sum + b.sum;
        return res;
    }
    //区间反转操作
    void reverse() {
        swap(mnl, mnr);
        swap(mxl, mxr);
    }
};

void solve() {

    int n;
    cin >> n;

    const int logn = __lg(n);
    vector<vector<int>> nxt(n + 2, vector<int>(logn + 1));
    vector<vector<Info>> pre(n + 2, vector<Info>(logn + 1));

    vector<int> dep(n + 2);
    vector<tuple<int, int, int>> ask;

    //这里懒得把代码改了,就离线了,本质上是可以做到在线的
    int Node = 1;
    pre[Node][0] = Info(1);
    char op; int u, v, k;
    for (int i = 1; i <= n; ++i) {
        cin >> op >> u >> v;
        if (op == '+') {
            nxt[++Node][0] = u;
            dep[Node] = dep[u] + 1;
            pre[Node][0] = Info(v);
        } else {
            cin >> k;
            ask.emplace_back(u, v, k);
        }
    }

    //对每个点跳 2^j 步的信息都做一个如上的合并
    //注意信息是 [x, y) 的信息
    for (int j = 1; j <= logn; ++j) {
        for (int i = 1; i <= Node; ++i) {
            nxt[i][j] = nxt[nxt[i][j - 1]][j - 1];
            pre[i][j] = merge(pre[i][j - 1], pre[nxt[i][j - 1]][j - 1]);
        }
    }

    //求 LCA 模板
    auto LCA = [&](int u, int v) {
        if (dep[u] < dep[v]) swap(u, v);
        for (int j = logn; j >= 0; --j) {
            if (dep[u] - (1LL << j) >= dep[v]) {
                u = nxt[u][j];
            }
        }
        if (u == v) return u;
        for (int j = logn; j >= 0; --j) {
            if (nxt[u][j] != nxt[v][j]) {
                u = nxt[u][j];
                v = nxt[v][j];
            }
        }
        return nxt[u][0];
    };

    //cal函数 就是通过倍增计算 [x, a) + [a, b) + ... + [z, lca) 的信息从而得到 [x, lca) 的信息
    //特别要注意的是如果 u == lca 会返回一个全为 0 的信息,不会对答案产生影响,具体看代码即可明白原因
    auto cal = [&](int u, int lca) {
        Info res;
        for (int j = logn; j >= 0; --j) {
            if (dep[u] - (1LL << j) >= dep[lca]) {
                res = merge(res, pre[u][j]);
                u = nxt[u][j];
            }
        }
        return res;
    };

    //处理每个离线出来的询问
    for (auto [u, v, k] : ask) {
        int lca = LCA(u, v);
        Info A = cal(u, lca), B = cal(v, lca);
        A = merge(A, pre[lca][0]); //将 [u, lca) + [lca, lca]
        A.reverse(); //翻转 [u, lca]
        Info T = merge(B, A); //合并 [v, lca) + [lca, u];
        if (T.mnsum <= k && k <= T.mxsum) {
            cout << "Yes\n";
        } else {
            cout << "No\n";
        }
    }
}

signed main() {

    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);

    int t = 1; cin >> t;
    while (t--) solve();

    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

柠檬味的橙汁

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值