线段树合并
线段树合并
1 权值线段树
1.1 权值线段树的基本思想
权值线段树其实比较简单。
正常的线段树是维护区间上每一个点的值,而权值线段树则是维护每一个数字出现的次数(可以类比为桶)。
例如原本的 \(1-4\) 表示区间 \([1,4]\) 上数字的和(或差、最大值等等),现在就表示数字 \(1-4\) 的出现次数之和。
1.2 实现
基本的权值线段树可以实现如下操作:
- 添加一个数(对应单点修改)
- 查找一个数出现的次数(对应单点查询)
- 查找一段区间内数字出现的次数(对应区间查询)
- 寻找第 \(k\) 大(小)元素
前三个操作都很简单,下面着重来看第四个操作。
int kth(int p, int k) {//找第 k 大
if(t[p].l == t[p].r) {
return t[p].l;
}
int mid = (t[p].l + t[p].r) >> 1;
int s1 = t[lp].num;//左区间元素个数
int s2 = t[rp].num;//右区间元素个数
if(k <= s2) {//说明第 k 大元素在右区间
return kth(rp, k);
}
else if {//在左区间
return kth(lp, k - s2);//在左区间内找第 k-s2 大的元素
}
}
2 动态开点线段树
在权值线段树中,我们的值域可能在 \([0,10^9]\),如果在线段树上提前把每一个点都开好,那是必然的 MLE。
于是我们便有了一种新的东西:动态开点。
2.1 动态开点的基本思想
对于每一个线段树上的节点,记录下他的左儿子与右儿子编号。
如此,每一次只需要再使用一个节点时判断该节点是否存在即可,如果不存在就新建节点,同时记录儿子即可。
2.2 实现
首先是树的结构体定义:
struct node{
int l, r, sum, lazy;//注意这里的 l,r 不是区间 [l,r] 而是左右儿子编号
}t[Maxn];
接下来是 update
函数:
void update(int p) {
t[p].sum = t[t[p].l].sum + t[t[p].r].sum;
}
然后接下来是最重要的 pushdown
函数:
void pushdown(int p, int l, int r) {//节点为 p,区间为 [l,r]
if(t[p].lazy) {
if(!t[p].l) t[p].l = ++cnt;
if(!t[p].r) t[p].r = ++cnt;//如果没有节点就新建节点
int mid = (l + r) >> 1;//以中点分割
t[t[p].l].lazy += t[p].lazy;
t[t[p].r].lazy += t[p].lazy;
t[t[p].l].sum += (mid - l + 1) * t[p].lazy;
t[t[p].r].sum += (r - mid) * t[p].lazy;//和正常线段树类似
t[p].lazy = 0;
}
}
接下来是区间修改与区间查询:
void mdf(int &p, int l, int r, int L, int R, int v) {//p 要传实参是因为要动态开点,修改左右儿子的值
if(r < L || l > R) {
return ;
}
if(!p) {//当前节点没有,新建节点
p = ++cnt;
}
if(L <= l && r <= R) {
t[p].lazy += v;
t[p].sum += (r - l + 1) * v;
return ;
}
int mid = (l + r) >> 1;
pushdown(p, l, r);
mdf(t[p].l, l, mid, L, R, v);
mdf(t[p].r, mid + 1, r, L, R, v);
update(p);
}
int query(int p, int l, int r, int L, int R) {
if(!p) return 0;//没有当前节点,跳过
if(r < L || l > R) return 0;
if(L <= l && r <= R) {
return t[p].sum;
}
int mid = (l + r) >>1;
pushdown(p, l, r);
return query(t[p].l, l, mid, L, R) + query(t[p].r, mid + 1, r, L, R);
}
接下来将它们拼在一起,加上一点细节,我们就能用动态开点 A 掉线段树模版 P3372 【模板】线段树 1。
代码如下:
#include <bits/stdc++.h>
#define int long long
using namespace std;
typedef long long LL;
const int Maxn = 2e5 + 5;
int n, m;
int root = 1, cnt = 1;//root 用于传实参,cnt 是已用节点数
struct node{
int l, r, sum, lazy;
}t[Maxn];
void update(int p) {
t[p].sum = t[t[p].l].sum + t[t[p].r].sum;
}
void pushdown(int p, int l, int r) {
if(t[p].lazy) {
if(!t[p].l) t[p].l = ++cnt;
if(!t[p].r) t[p].r = ++cnt;
int mid = (l + r) >> 1;
t[t[p].l].lazy += t[p].lazy;
t[t[p].r].lazy += t[p].lazy;
t[t[p].l].sum += (mid - l + 1) * t[p].lazy;
t[t[p].r].sum += (r - mid) * t[p].lazy;
t[p].lazy = 0;
}
}
void mdf(int &p, int l, int r, int L, int R, int v) {
if(r < L || l > R) {
return ;
}
if(!p) {
p = ++cnt;
}
if(L <= l && r <= R) {
t[p].lazy += v;
t[p].sum += (r - l + 1) * v;
return ;
}
int mid = (l + r) >> 1;
pushdown(p, l, r);
mdf(t[p].l, l, mid, L, R, v);
mdf(t[p].r, mid + 1, r, L, R, v);
update(p);
}
int query(int p, int l, int r, int L, int R) {
if(!p) return 0;
if(r < L || l > R) return 0;
if(L <= l && r <= R) {
return t[p].sum;
}
int mid = (l + r) >>1;
pushdown(p, l, r);
return query(t[p].l, l, mid, L, R) + query(t[p].r, mid + 1, r, L, R);
}
//函数见上面解释
signed main() {
ios::sync_with_stdio(0);
cin >> n >> m;
for(int i = 1; i <= n; i++) {
int p;
cin >> p;
root = 1;
mdf(root, 1, n, i, i, p);//以单点修改来建树
}
while(m--) {
int opt, x, y, z;
cin >> opt >> x >> y;
if(opt == 1) {
cin >> z;
root = 1;
mdf(root, 1, n, x, y, z);
}
else {
cout << query(1, 1, n, x, y) << '\n';
}
}
return 0;
}
3 线段树合并
3.1 线段树合并的基本思想
顾名思义,线段树合并就是将多颗线段树的信息合并起来,用一颗线段树保存。
常有两种方式实现,一种是新建一颗线段树来存储,另一种是将一颗线段树直接合并到另一个上面去(相当于 c=a+b
和 a+=b
),第二种方法则更节省空间,缺点是丢失了一颗线段树的原始信息。
3.2 实现
线段树合并常常基于权值线段树以及动态开点线段树来实现。
采用第二种方法:
//a 是第一棵树的节点,b 是第二棵树的节点
int merge(int a, int b, int l, int r) {
if(!a) return b;
if(!b) return a;//如果有一颗线段树该位置是空的,那就返回另一个节点,然后用动态开点存储左右儿子
if(l == r) {//叶子节点
t[a].sum += t[b].sum;//合并
return a;
}
int mid = (l + r) >> 1;
t[a].l = merge(t[a].l, t[b].l, l, mid);
t[a].r = merge(t[a].r, t[b].r, mid + 1, r);//动态开点
update(a);
return a;
}