线段树合并

线段树合并

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+ba+=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; 
}
posted @ 2024-02-27 17:52  UKE_Automation  阅读(22)  评论(0编辑  收藏  举报