Codeforces Round 279 (Div. 2) F. Treeland Tour(线段树合并)

原题链接:F. Treeland Tour


题目大意:


给出一棵 n n n 个点, n − 1 n - 1 n1 条边的树,第 i i i 个点带点权 r i r_{i} ri,让你求出树上 严格 最长上升子序列( L I S LIS LIS)的长度。

解题思路:


这题的数据范围 n ≤ 6000 n \leq 6000 n6000 ,可以暴力 O ( n 2 log ⁡ n ) O(n^{2} \log n) O(n2logn) 过。

这里主要讲讲 n ≤ 2 ⋅ 1 0 5 n \leq 2\cdot 10^{5} n2105 怎么做。

比如考虑如下的这一棵树,我们现在在 5 5 5 号点,且 { 3 , 2 , 8 } \{ 3,2,8\} {3,2,8} 号点为我们的子树,它们的信息在回溯到 5 5 5 号点前就已经处理好了。(这里我们简化一下,把点的编号看成点权,但本题点权可以是任意值)

这时候想一下我们的的答案会从哪里来:

  • 我们当前的答案直接是我们其中一个子树内的答案。
  • 将 其中一个子树 → \rightarrow 另一个子树 拼起来。
  • 将 其中一个子树 → \rightarrow 根节点 → \rightarrow 另一个子树 三者拼起来。

先想一想我们怎么把两棵子树拼起来。

比如我们选了绿色子树,和紫色子树拼起来,我们有很多种方法可以选择,但本质上就是从一个子树中选最长上升子序列和另一个子树中选最长下降子序列拼起来。

比如我们选择将绿色子树的上升子序列 { 1 , 3 } \{1,3\} {1,3} 和紫色子树的下降子序列 { 8 , 9 } \{8,9\} {8,9} 拼起来,那么我们的 L I S LIS LIS 长度就是 2 + 2 = 4 2+2=4 2+2=4

我们要想能将它们拼接起来,前提是最长上升子序列的结尾要比最长下降子序列的开头要小才可以。

这启发我们去做一件事:维护子树中 所有点 作为 L I S LIS LIS 的结尾 和 L D S LDS LDS 的开头,能够获得的最长长度。

我们这里考虑的 所有点 u u u 作为结尾和开头,指的是考虑点 u u u 的点权作为结尾和开头。

什么意思呢?比如我们从随便选一个点 3 3 3 ,我们考虑将它的子树和其他子树拼起来。

如果我们要选出 3 3 3 的子树其中一个点为 L I S LIS LIS 的结尾,选的点点权为 r u r_{u} ru,那我们就去找其他子树的 L D S LDS LDS 且点权范围在 [ r u + 1 , + ∞ ] [r_{u}+1,+\infty] [ru+1,+] 的才合法,而且只需要知道区间 L D S LDS LDS max ⁡ \max max 值即可。

而且,还只是各个子树内的 max ⁡ \max max 值,想到是个 R M Q RMQ RMQ

假设我们对每个点 u u u 都开了一棵权值线段树,维护点 u u u 子树内点 v v v 的权值 r v r_{v} rv 作为 L I S LIS LIS 的开头, L D S LDS LDS 的结尾 所能得到的最长长度。

比如点 3 3 3 开的线段树维护的是整颗绿色子树的信息。我们查询这个线段树点权在 [ 3 , 3 ] [3,3] [3,3] 区间内得到的 L I S LIS LIS 值为 2 ( { 1 , 3 } ) 2(\{1,3\}) 2({1,3}),得到的 L D S LDS LDS 值为 2 ( { 3 , 4 } , { 3 , 7 } ) 2(\{3,4\},\{3,7\}) 2({3,4},{3,7})

比如点 8 8 8 开的线段树维护的是整颗紫色子树的信息。我们查询这个线段树点权在 [ 8 , 8 ] [8,8] [8,8] 区间内得到的 L I S LIS LIS 值为 1 ( { 8 } ) 1(\{8\}) 1({8}),得到的 L D S LDS LDS 值为 2 ( { 8 , 9 } ) 2(\{8,9\}) 2({8,9})

好了,接下来想想怎么拼起来,子树信息和答案的维护才能不重不漏:

  • 1. 1. 1. 最初时点 u u u 的线段树里面是空的,所有节点的 L I S LIS LIS L D S LDS LDS 全是 0 0 0 。(点 u u u 可以看成我们当前的 5 5 5 号点)
  • 2. 2. 2. 枚举点 u u u 的一个子树点 v v v,暴力 f o r for for 一遍点 v v v 线段树的每个区间 [ i , i ] [i,i] [i,i] 查询它的 L I S , L D S LIS,LDS LIS,LDS 值,同时在点 u u u 的线段树查 [ − ∞ , i − 1 ] [-\infty,i -1] [,i1] L I S ′ LIS' LIS,和 [ i + 1 , + ∞ ] [i+1,+\infty] [i+1,+] L D S ′ LDS' LDS,那么一个合法答案就是 L I S + L D S ′ LIS+LDS' LIS+LDS 以及 L I S ′ + L D S LIS'+LDS LIS+LDS 。同时,在这一步维护 u [ − ∞ , r u − 1 ] → r u → v [ r u + 1 , + ∞ ] u_{[-\infty,r_{u} -1]} \rightarrow r_{u} \rightarrow v_{[r_{u}+1,+\infty]} u[,ru1]ruv[ru+1,+] L I S LIS LIS L D S LDS LDS 拼起来再 + 1 +1 +1 就是点 u u u 作为中间节点的合法答案。
  • 3. 3. 3. 取完答案之后再暴力 f o r for for 一遍点 v v v 线段树的每个区间 [ i , i ] [i,i] [i,i] 查询它的 L I S , L D S LIS,LDS LIS,LDS 值,然后和点 u u u 线段树的 [ i , i ] [i,i] [i,i] L I S , L D S LIS,LDS LIS,LDS 值取 max ⁡ \max max ,合并子树线段树的信息。
  • 4. 4. 4. 将所有子树 v v v 的线段树都暴力完之后,点 u u u 中维护的线段树就是点 u u u 子树中所有点 v v v 线段树的信息集合的 max ⁡ \max max 值,但还不包含点 u u u 的信息(点 u u u L I S LIS LIS L D S LDS LDS 还没求出来)。
  • 5. 5. 5. 再在点 u u u 的线段树查 [ − ∞ , r u − 1 ] [-\infty,r_{u}-1] [,ru1] L I S ′ LIS' LIS,和 [ r u + 1 , + ∞ ] [r_{u}+1,+\infty] [ru+1,+] L D S ′ LDS' LDS,那么点 u u u L I S = L I S ′ + 1 LIS=LIS'+1 LIS=LIS+1 L D S = L D S ′ + 1 LDS=LDS'+1 LDS=LDS+1 ,单点更新点 u u u 线段树 [ r u , r u ] [r_{u},r_{u}] [ru,ru] 然后取 max ⁡ \max max 即可。

(当然,按照题目的信息来说 − ∞ = 1 , + ∞ = 1 0 6 -\infty=1,+\infty=10^{6} =1,+=106)

这样,我们的答案维护完的同时,线段树对整颗子树的信息也维护完了。

注意到每个点 u u u 的信息很少,而且我们在 2 , 3 2,3 2,3 的操作本质上等价于跑一个线段树合并,并且合并过程中对答案的实时维护也很简单。

具体上,我们点 v v v 的线段树要合并到点 u u u 的线段树。

  • 当前合并到的区间是 [ l , r ] [l,r] [l,r],我们求出了一个 m i d = l + r 2 mid=\frac{l+r}{2} mid=2l+r
  • 我们的 v v v 树在 [ l , m i d ] [l,mid] [l,mid] L I S LIS LIS u u u 树在 [ m i d + 1 , r ] [mid+1,r] [mid+1,r] L D S LDS LDS 合并就是一个合法答案
  • 我们的 u u u 树在 [ l , m i d ] [l,mid] [l,mid] L I S LIS LIS v v v 树在 [ m i d + 1 , r ] [mid+1,r] [mid+1,r] L D S LDS LDS 合并就是一个合法答案
  • 递归合并 u , v u,v u,v [ l , m i d ] [l,mid] [l,mid] [ m i d + 1 , r ] [mid+1,r] [mid+1,r] 节点

这样,我们也可以不重不漏的统计完答案。

于是我们的答案就在如上所有情况中取 max ⁡ \max max ,这题就做完了。

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

AC代码:

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

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

const int N = 1e6 + 1, M = 1e7;

struct SegTree {
    int l, r, lis, lds;
} seg[M];

#define lson(k) seg[k].l, l, mid
#define rson(k) seg[k].r, mid + 1, r
#define lis(k) seg[k].lis
#define lds(k) seg[k].lds
#define ls(k) seg[k].l
#define rs(k) seg[k].r

SegTree merge(SegTree& R, const SegTree& A, const SegTree& B) {
    R.lis = max(A.lis, B.lis);
    R.lds = max(A.lds, B.lds);
    return R;
}

int idx = 0;
int creat(int& pre) {
    seg[++idx] = seg[pre];
    return idx;
}

int Modify(int& pre, int l, int r, int& p, int& lis, int& lds) {
    int cur = creat(pre);
    if (l == r) {
        lis(cur) = max(lis(cur), lis);
        lds(cur) = max(lds(cur), lds);
        return cur;
    }
    int mid = l + r >> 1;
    if (p <= mid) {
        ls(cur) = Modify(lson(pre), p, lis, lds);
    } else {
        rs(cur) = Modify(rson(pre), p, lis, lds);
    }
    merge(seg[cur], seg[ls(cur)], seg[rs(cur)]);
    return cur;
}

SegTree Query(int& k, int l, int r, int x, int y) {
    if (!k) return {};
    if (l >= x && r <= y) {
        return seg[k];
    }
    int mid = l + r >> 1; SegTree res;
    if (y <= mid) return Query(lson(k), x, y);
    if (x > mid) return Query(rson(k), x, y);
    return merge(res, Query(lson(k), x, y), Query(rson(k), x, y));
}

int ans = 0;

int Merge(int& pre, int& cur, int l, int r) {
    if (!pre || !cur) return pre + cur;
    if (l == r) {
        lis(cur) = max(lis(cur), lis(pre));
        lds(cur) = max(lds(cur), lds(pre));
        return cur;
    }
    int mid = l + r >> 1;
    ans = max({ ans, lis(ls(pre)) + lds(rs(cur)), lis(ls(cur)) + lds(rs(pre)) });
    ls(cur) = Merge(ls(pre), lson(cur));
    rs(cur) = Merge(rs(pre), rson(cur));
    merge(seg[cur], seg[ls(cur)], seg[rs(cur)]);
    return cur;
}

void solve() {
    int n;
    cin >> n;

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

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

    vector<int> ver(n + 1);
    auto DFS = [&](auto self, int u, int ufa) -> void {
        int ulis = 0, ulds = 0;
        for (auto& v : g[u]) {
            if (v == ufa) continue;
            self(self, v, u);
            int lis = Query(ver[v], 0, N, 0, val[u] - 1).lis;
            int lds = Query(ver[v], 0, N, val[u] + 1, N).lds;

            ans = max({ ans, lis + ulds + 1, ulis + lds + 1 });
            ulis = max(ulis, lis), ulds = max(ulds, lds);
            ver[u] = Merge(ver[v], ver[u], 0, N);
        }
        ver[u] = Modify(ver[u], 0, N, val[u], ++ulis, ++ulds);
    };
    DFS(DFS, 1, 0);

    cout << ans << '\n';
}

signed main() {

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

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

    return 0;
}
  • 22
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

柠檬味的橙汁

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

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

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

打赏作者

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

抵扣说明:

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

余额充值