线段树合并

题目一 线段树合并+第k小

P3224 [HNOI2012]永无乡(https://www.luogu.com.cn/problem/P3224)

题目描述

永无乡包含 n 座岛,编号从 1 到 n ,每座岛都有自己的独一无二的重要度,按照重要度可以将这 n 座岛排名,名次用 1 到 n 来表示。某些岛之间由巨大的桥连接,通过桥可以从一个岛到达另一个岛。如果从岛 a 出发经过若干座(含 0 座)桥可以 到达岛 b ,则称岛 a 和岛 b 是连通的。

现在有两种操作:

B x y 表示在岛 x 与岛 y 之间修建一座新桥。

Q x k 表示询问当前与岛 x 连通的所有岛中第 k 重要的是哪座岛,即所有与岛 x 连通的岛中重要度排名第 k 小的岛是哪座,请你输出那个岛的编号。

输入格式

第一行是用空格隔开的两个整数,分别表示岛的个数 n 以及一开始存在的桥数 m。

第二行有 n 个整数,第 i 个整数表示编号为 ii 的岛屿的排名 pi

接下来 m 行,每行两个整数 u, v 表示一开始存在一座连接编号为 u 的岛屿和编号为 v 的岛屿的桥。

接下来一行有一个整数,表示操作个数 q。

接下来 q 行,每行描述一个操作。每行首先有一个字符 op,表示操作类型,然后有两个整数 x, y

若 op 为 Q,则表示询问所有与岛 x 连通的岛中重要度排名第 y 小的岛是哪座,请你输出那个岛的编号。
若 op 为 B,则表示在岛 x 与岛 y 之间修建一座新桥。

输出格式

对于每个询问操作都要依次输出一行一个整数,表示所询问岛屿的编号。如果该岛屿不存在,则输出 -1 。

输入

5 1
4 3 2 5 1
1 2
7
Q 3 2
Q 2 1
B 2 3
B 1 5
Q 2 1
Q 2 4
Q 2 3

输出

-1
2
5
1
2

说明/提示

数据规模与约定
对于 100% 的数据,保证1≤m≤n≤105,1≤q≤3×105,pi为一个 1∼n 的排列,op∈{Q,B} 1≤u,v,x,y≤n。

思路

开始为每个节建立应该线段树。每次合并把线段树的根赋值给并查集上根标号就可以了。
然后就是一个线段树二分。

#include <bits/stdc++.h>
#define LL long long
using namespace std;
const int N = 1e5+5;
int w[N], root[N*20], id[N];

struct M_Tree {
    int sum[N*20];
    int lc[N*20], rc[N*20], tot=0;
    void Insert(int &i, int l, int r, int x, int Id) { //建树
        if(r<l)
            return;
        i=++tot;//点
        if(l==r) {
            sum[i]++;
            return ;
        }
        int mid=(l+r)>>1;
        if(x<=mid)
            Insert(lc[i], l, mid, x, Id);
        if(x>mid)
            Insert(rc[i], mid+1, r, x, Id);
        sum[i]=sum[lc[i]]+sum[rc[i]];//权值在区间[l, r]的元素个数
    }

    int Merge(int x, int y, int l, int r) { //合并
        if(!x) return y;
        if(!y) return x;
        int mid=l+r>>1;
        if (l==r) {return sum[x]=sum[x]+sum[y], x;}
        lc[x]=Merge(lc[x], lc[y], l, mid);
        rc[x]=Merge(rc[x], rc[y], mid+1, r);
        sum[x]=sum[lc[x]]+sum[rc[x]];//节点信息合并

        return x;
    }
    int query(int root, int l, int r, int x) { //查询子树中第k大的节点
        if(!root)
            return -1;
        if(!x)
            return -1;
        if(l==r) {
            if(sum[root]>=x) {
                return l;
            }
            return -1;
        }
        int mid=(l+r)>>1;
        if(x>sum[lc[root]])
            return query(rc[root], mid+1, r, x-sum[lc[root]]);
        else
            return query(lc[root], l, mid, x);
    }
}T;

int f[N];
int fd(int x) {
    if(f[x]==x)
        return x;
    return f[x]=fd(f[x]);
}
int main() {

    int n, m, x, y, q;
    scanf("%d%d", &n, &m);
    for(int i=1; i<=n; i++) {
        scanf("%d", &w[i]);
        id[w[i]]=i;
        f[i]=i;
    }
    for(int i=1; i<=n; i++) {
        T.Insert(root[i], 1, n, w[i], i);
    }
    for(int i=1; i<=m; i++) {
        scanf("%d%d", &x, &y);
        x=fd(x), y=fd(y);
        f[y]=x;
        root[x]=T.Merge(root[x], root[y], 1, n);

    }
    scanf("%d", &q);
    while(q--) {
        char op[5];
        scanf("%s%d%d", op, &x, &y);
        x=fd(x);
        if(op[0]=='B') {
            y=fd(y);
            if(x==y)
                continue;
            f[y]=x;
            root[x]=T.Merge(root[x], root[y], 1, n);
            //cout<<root[x]<<" : "<<sum[root[x]]<<endl;
        } else {
            int ans=T.query(root[x], 1, n, y);
            printf("%d\n", ans==-1?-1:id[ans]);
        }
    }

    return 0;

}

题目二 线段树合并+求和

P3605 [USACO17JAN]Promotion Counting P(https://www.luogu.com.cn/problem/P3605)

题目

给出一颗树,每个点都有一个权值,最后对于每个点,输出在它的子树中,有多少个点的权值比它大。

输入

5
804289384
846930887
681692778
714636916
957747794
1
1
2
3

输出

2
0
1
0
0
对于 100% 的数据,1≤n≤105,1≤pi≤109。

#include <bits/stdc++.h>
#define LL long long
using namespace std;
const int N = 1e5+5;

int root[N*20];
struct M_Tree {
    int sum[N*20];
    int lc[N*20], rc[N*20], tot=0;
    void Insert(int &i, int l, int r, int x) { //建树
        if(r<l) return;
        i=++tot;//点
        if(l==r) {
            sum[i]++; return ;
        }
        int mid=(l+r)>>1;
        if(x<=mid) Insert(lc[i], l, mid, x);
        if(x>mid) Insert(rc[i], mid+1, r, x);
        sum[i]=sum[lc[i]]+sum[rc[i]];//权值在区间[l, r]的元素个数
    }

    int Merge(int x, int y, int l, int r) { //合并
        if(!x) return y;
        if(!y) return x;
        int mid=l+r>>1;
        if (l==r) {return sum[x]=sum[x]+sum[y], x;}
        lc[x]=Merge(lc[x], lc[y], l, mid);
        rc[x]=Merge(rc[x], rc[y], mid+1, r);
        sum[x]=sum[lc[x]]+sum[rc[x]];//节点信息合并

        return x;
    }

    int query(int root, int l, int r, int x) { //查询子树中权值<=x的节点个数
        if(!root) return 0;
        if(!x) return 0;
        if(x==r) return sum[root];

        int mid=(l+r)>>1;
        if(x>mid) return sum[lc[root]]+query(rc[root], mid+1, r, x);
        else return query(lc[root], l, mid, x);
    }
} T;

//下标1开始
struct LSH { //离散化
    int b[N];
    int lsh(int a[], int n) { //得到离散化后不同元素的个数
        for(int i=1; i<=n; i++)
            b[i]=a[i];
        sort(b+1, b+n+1);
        int cnt=unique(b+1, b+n+1)-b-1;
        for(int i=1; i<=n; i++) {
            a[i]=lower_bound(b+1, b+cnt+1, a[i])-b;
        }
        return cnt;
    }
    int id(int x) { //得到原数
        return b[x];
    }
} Lsh;

int w[N], ans[N], n;
vector<int> v[N];

void dfs(int u, int fa) {
    for(auto x: v[u]) {
        if(x!=fa) {
            dfs(x, u);
            root[u]=T.Merge(root[u], root[x], 1, n);
        }
    }
    ans[u]=T.query(root[u], 1, n, w[u]-1);
}

int main() {

    scanf("%d", &n);
    for(int i=1; i<=n; i++) {
        scanf("%d", &w[i]);
        w[i]=-w[i];
    }
    Lsh.lsh(w, n);
    for(int i=2; i<=n; i++) {
        int x;
        scanf("%d", &x);
        v[i].push_back(x);
        v[x].push_back(i);
    }
    for(int i=1; i<=n; i++) {
        T.Insert(root[i], 1, n, w[i]);//建树
    }
    dfs(1, -1);
    for(int i=1; i<=n; i++) {
        printf("%d\n", ans[i]);
    }

    return 0;
}

题目三 线段树合并+第k大

题目描述

在 Bytemountains 有n座山峰,每座山峰有他的高度 hi。有些山峰之间有双向道路相连,共 m 条路径,每条路径有一个困难值,这个值越大表示越难走。

现在有 q 组询问,每组询问询问从点 v 开始只经过困难值小于等于 x 的路径所能到达的山峰中第 k 高的山峰,如果无解输出 -1。

输入格式

第一行三个数 n,m,q。 第二行 n 个数,第 i 个数为 hi

接下来 m 行,每行三个整数 a,b,c表示从 a→b 有一条困难值为 c 的双向路径。 接下来 q 行,每行三个数 v,x,k表示一组询问。

输出格式

对于每组询问,输出一个整数表示能到达的山峰中第 k 高的山峰的高度。

输入

10 11 4
1 2 3 4 5 6 7 8 9 10
1 4 4
2 5 3
9 8 2
7 8 10
7 1 4
6 7 1
6 4 8
2 1 5
10 8 10
3 4 7
3 4 6
1 5 2
1 5 6
1 5 8
8 9 2

输出

6
1
-1
8

说明/提示

数据规模与约定
对于 100% 的数据,n≤10^5 0≤m,q≤5×10,
h_i, c,x≤10^9

思路

我们在线有Kruskal 重构树的写法。如果可以离线,我们考虑线段树合并。
我们对查询的困难度,从小到大处理,那么在处理困难为x的查询时,所有<x的边都可以使用。
用这些边构造最小生成树。读入用线段树维护每个连通块的信息。查询节点y时。找到y的连通块。
直接在线段树上查询就可以了。连边就合并线段树。并且在处理困难>x的查询时,可以直接在上一个查询上添边就可以了。

#include <bits/stdc++.h>
#define LL long long
using namespace std;
const int N = 2e5+5;

int root[N];
struct M_Tree {
    int sum[N*20];
    int lc[N*20], rc[N*20], tot=0;
    void Insert(int &i, int l, int r, int x) { //建树
        if(r<l) return;
        i=++tot;//点
        if(l==r) {
            sum[i]++; return ;
        }
        int mid=(l+r)>>1;
        if(x<=mid) Insert(lc[i], l, mid, x);
        if(x>mid) Insert(rc[i], mid+1, r, x);
        sum[i]=sum[lc[i]]+sum[rc[i]];//权值在区间[l, r]的元素个数
    }

    int Merge(int x, int y, int l, int r) { //合并
        if(!x) return y;
        if(!y) return x;
        int mid=l+r>>1;
        if (l==r) {return sum[x]=sum[x]+sum[y], x;}
        lc[x]=Merge(lc[x], lc[y], l, mid);
        rc[x]=Merge(rc[x], rc[y], mid+1, r);
        sum[x]=sum[lc[x]]+sum[rc[x]];//节点信息合并

        return x;
    }


    int query(int root, int l, int r, int x) { //查询子树第k大
        if(!root) return -1;
        if(!x) return -1;
        if(l==r) {
            if(sum[root]>=x) {
                return l;
            }
            return -1;
        }
        int mid=(l+r)>>1;
        if(x>sum[rc[root]]) return query(lc[root], l, mid, x-sum[rc[root]]);
        else return query(rc[root], mid+1, r, x);
    }
} T;

struct LSH { //离散化
    int b[N];
    int lsh(int a[], int n) { //得到离散化后不同元素的个数
        for(int i=1; i<=n; i++) b[i]=a[i];
        sort(b+1, b+n+1);
        int cnt=unique(b+1, b+n+1)-b-1;
        for(int i=1; i<=n; i++) {
            a[i]=lower_bound(b+1, b+cnt+1, a[i])-b;
        }
        return cnt;
    }
    int id(int x) { //得到原数
        return b[x];
    }
} Lsh;

int f[N];
int fd(int x){
    if(!f[x]) return x;
    return f[x]=fd(f[x]);
}
int h[N];

struct Edge{
    int from, to, w;
}E[2000005];
int cut=0;
void Add(int x, int y, int w){
    E[++cut]={x, y, w};
}

struct Qry{
    int v, x, k, i;
}q[500005];

int ans[500005];
int main() {

    int n, m, Q; scanf("%d%d%d", &n, &m, &Q);
    for(int i=1; i<=n; i++){
        scanf("%d", &h[i]);
    }
    Lsh.lsh(h, n);
    for(int i=1; i<=n; i++){
        T.Insert(root[i], 1, n, h[i]);
    }
    for(int i=1; i<=m; i++){
        int x, y, w; scanf("%d%d%d", &x, &y, &w);
        Add(x, y, w);
    }
    for(int i=1; i<=Q; i++){
        scanf("%d%d%d", &q[i].v, &q[i].x, &q[i].k);
        q[i].i=i;
    }
    sort(q+1, q+Q+1, [](Qry &a, Qry &b){return a.x<b.x;});
    sort(E+1, E+cut+1, [](Edge &a, Edge &b){return a.w<b.w;});
    int pos=1;
    for(int i=1; i<=cut; i++){
        int x=fd(E[i].from), y=fd(E[i].to);
        while(E[i].w>q[pos].x&&pos<=Q){//把<=x的边全部添加进生成树,再处理这个查询
            ans[q[pos].i]=T.query(root[fd(q[pos].v)], 1, n, q[pos].k);
            pos++;
        }
        if(x!=y){
            f[x]=y;
            root[y]=T.Merge(root[x], root[y], 1, n);
        }
    }
    while(pos<=Q){
        ans[q[pos].i]=T.query(root[fd(q[pos].v)], 1, n, q[pos].k);
        pos++;
    }
    for(int i=1; i<=Q; i++){
        printf("%d\n", ((ans[i]==-1)?-1:Lsh.id(ans[i])));
    }

    return 0;
}

题目四 Kruskal 重构树+可持久化线段树合并

F 红蓝图(https://ac.nowcoder.com/acm/contest/7745/F)

题目描述

有一张 n 个点,m 条边的无向图。点从 0 到 n-1 编号。边有边权和颜色,颜色为红色和蓝色中的一种。给定 q 组询问,每次给定两个参数 x,t。删除边权大于 t 的红色边和边权小于 t 的蓝色边。如果此时两个点 x,y 既有仅经过红色边的路径相连,又有仅经过蓝色边的路径相连,那么称这两个点连通。求与编号为 x 的点连通的点的数量(包括 x 本身)。询问间相互独立,每次询问的删除不会影响其他询问。

输入描述:

输入共 m+q+1 行。

第一行三个整数 n,m,q,表示图的点数、边数和询问数。

接下来 m 行,每行三个整数 x,y,c,表示点 x 和点 y 之间存在一条边,若这是输入的第 i 条边(从 1 开始数到 m),该边的边权是 i。若 c=0 则这条边为红色边,c=1 则这条边为蓝色边。可能存在自环和重边。

接下来 q 行,每行两个整数,表示一组询问的 x,t。

输出描述:

输出共 q 行,第 i 行表示第 i 组询问的答案。

输入

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

输出

2
3
2
1
3

备注:

2≤n≤200000, 2≤m≤400000, 1≤q≤400000
0≤x,y<n, 0≤t≤m+1, c∈{0,1}
本题的输入输出量可能很大,您需要使用快速的输入输出方式。您可以使用如下的模板:

https://paste.ubuntu.com/p/NYKBd45xwZ/

思路

我们分为红色边和蓝色边建立Kruskal 重构树,那么对于一个查询,我们可以在红树找到一个LCA=lx,我们可以在蓝树找到一个LCA=ly
问题转化是lx的子树的叶子节点有多少个同时是ly的子树的叶子节点。

如果我们把红树的dfs序进行重新编号。
那么对应到蓝树不就是区间查询了吗?
如图:对红树的dfs重新对每个节点进行编号后。每一个棵在红树的子树,对应就是一段连续的区间。
那么蓝树只只要在数上线段树合并就可以再区间查询就可以了,不过线段树合并的不同之处在:可持久化
就是合并时开新新节点,那么之前每个仍然有之前的信息。

#include <bits/stdc++.h>
#define LL long long
using namespace std;
const int N = 3e5+5;
const int maxn = 1e6+5;
const int maxm = 2e6+5;

int root[maxn];
struct M_Tree {
    int sum[N*35];
    int lc[N*35], rc[N*35], tot=0;
    void Insert(int &i, int l, int r, int x) { //建树
        if(r<l)
            return;
        i=++tot;//点
        if(l==r) {
            sum[i]++;
            return ;
        }
        int mid=(l+r)>>1;
        if(x<=mid)
            Insert(lc[i], l, mid, x);
        if(x>mid)
            Insert(rc[i], mid+1, r, x);
        sum[i]=sum[lc[i]]+sum[rc[i]];//权值在区间[l, r]的元素个数
    }

    int Merge(int x, int y, int l, int r) { //合并
        if(!x||!y)
            return y|x;
        if (l==r) {
            return sum[++tot]=sum[x]+sum[y], tot;
        }
        int mid=l+r>>1, node=++tot;
        lc[node]=Merge(lc[x], lc[y], l, mid);
        rc[node]=Merge(rc[x], rc[y], mid+1, r);
        sum[node]=sum[lc[node]]+sum[rc[node]];//节点信息合并

        return node;
    }

    int query(int root, int l, int r, int x) { //查询子树中权值<=x的节点个数
        if(!root||!x)
            return 0;
        if(x==r)
            return sum[root];
        int mid=(l+r)>>1;
        if(x>mid)
            return sum[lc[root]]+query(rc[root], mid+1, r, x);
        else
            return query(lc[root], l, mid, x);
    }
} Tree;

struct Edge {
    int from, to, nxt;
};

int pos[maxn][2], n;//树的dfs序
struct kruskal_Tree {
    Edge e[maxm], E[maxm];
    int cute=0, T=0;
    int head[maxn], cut=0;
    int w[maxn];//重构树每个点的点权
    int fa[maxn][21];//重构树每个节点父亲

    void Addedge(int x, int y) {
        E[++cut]= {x, y, head[x]};
        head[x]=cut;
    }
    int f[maxn], vis[maxn];
    int fd(int x) {
        if(!f[x])
            return x;
        return f[x]=fd(f[x]);
    }
    void add(int x, int y, int w) {
        e[++cute]= {x, y, w};
    }

    int Find(int x) { //每个连通块的根
        if(fa[x][0]==0) {
            return x;
        }
        return Find(fa[x][0]);
    }

    void DFS1(int u) {
        vis[u]=1;
        for(int i=1; i<=20; i++) {
            fa[u][i]=fa[fa[u][i-1]][i-1];
        }
        pos[u][0]=T;
        if(!head[u]) {
            pos[u][1]=++T;//对每个叶子节点重新编号
            return ;
        }
        for(int i=head[u]; i; i=E[i].nxt) {
            DFS1(E[i].to);
        }
        pos[u][1]=T;
    }

    void DFS2(int u) {
        vis[u]=1;
        for(int i=1; i<=20; i++) {
            fa[u][i]=fa[fa[u][i-1]][i-1];
        }
        for(int i=head[u]; i; i=E[i].nxt) {
            DFS2(E[i].to);
            root[u]=Tree.Merge(root[u], root[E[i].to], 1, n);
        }
        if(!head[u]){
            Tree.Insert(root[u], 1, n, pos[u][1]);
        }
    }

    void get_Tree(int n, int g) {
        if(g==1) {
            sort(e+1, e+cute+1, [](Edge &a, Edge &b) {
                return a.nxt<b.nxt;
            });
        } else {
            sort(e+1, e+cute+1, [](Edge &a, Edge &b) {
                return a.nxt>b.nxt;
            });
        }
        for(int i=1; i<=cute; i++) {
            int x=fd(e[i].from), y=fd(e[i].to);
            if(x==y)
                continue;
            w[++n]=e[i].nxt;
            f[x]=f[y]=n;
            fa[x][0]=fa[y][0]=n;
            Addedge(n, x);
            Addedge(n, y);//建立重构树
        }
        //ps:这个图不一定连通,要多次DFS
        if(g==1) {
            for(int i=1; i<=n; i++) {
                if(!vis[i]) DFS1(Find(i));
            }
        }
        else{
            for(int i=1; i<=n; i++) {
                if(!vis[i]){
                    DFS2(Find(i));
                }
            }
        }
    }
} kt1, kt2;

int main() {

    int m, q; scanf("%d%d%d", &n, &m, &q);
    for(int i=1; i<=m; i++){
        int x, y, c; scanf("%d%d%d", &x, &y, &c);
        x++, y++;
        if(!c){
            kt1.add(x, y, i);
        }
        else{
            kt2.add(x, y, i);
        }
    }
    kt1.get_Tree(n, 1);
    kt2.get_Tree(n, 2);
    while(q--){
        int x, y; scanf("%d%d", &x, &y);
        x++;
        int lx=x;
        for(int i=20; i>=0; i--){
            if(kt1.fa[lx][i]&&kt1.w[kt1.fa[lx][i]]<=y) lx=kt1.fa[lx][i];
        }
        int ly=x;
        for(int i=20; i>=0; i--){
            if(kt2.fa[ly][i]&&kt2.w[kt2.fa[ly][i]]>=y) ly=kt2.fa[ly][i];
        }
        printf("%d\n", Tree.query(root[ly], 1, n, pos[lx][1])-Tree.query(root[ly], 1, n, pos[lx][0]));
    }

    return 0;
}

posted @   liweihang  阅读(178)  评论(0编辑  收藏  举报
编辑推荐:
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
Live2D
欢迎阅读『线段树合并』
点击右上角即可分享
微信分享提示