线段树合并
题目一 线段树合并+第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 的岛屿的排名 。
接下来 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,为一个 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≤≤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座山峰,每座山峰有他的高度 。有些山峰之间有双向道路相连,共 m 条路径,每条路径有一个困难值,这个值越大表示越难走。
现在有 q 组询问,每组询问询问从点 v 开始只经过困难值小于等于 x 的路径所能到达的山峰中第 k 高的山峰,如果无解输出 -1。
输入格式
第一行三个数 n,m,q。 第二行 n 个数,第 i 个数为 。
接下来 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;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了