首页 > 其他分享 >【学习笔记】线段树合并

【学习笔记】线段树合并

时间:2024-01-17 18:59:27浏览次数:17  
标签:return int tr 线段 合并 笔记 merge ls

一. 普通线段树合并

线段树合并就是建立一棵新的线段树保存原有的两棵线段树的信息。

两棵线段树当前要合并的点所表示的区间是一样的。

线段树合并的过程很简单。

如果A有p位置,B没有,新的线段树p位置赋成A,返回 A;
如果B有p位置,A没有,新的线段树p位置赋成B,返回 A;
如果合并到叶子结点,按照所需合并A,B,把新线段树上的p位置赋成A,返回 A;
递归左子树,递归右子树;
更新A (当前节点);
返回 A;

以下是一份较为常见的 merge 代码实现。

//l,r表示当前所访问的节点的区间,x,y表示当前节点在A,B两棵线段树中的编号
//这里是将B合并到A上,返回的是A的节点
inline int merge(int x,int y,int l,int r){
	if(!x||!y)return x+y;//若A或B没有区间[l,r]这个节点就返回另一个
	if(l==r){//merge到了叶子结点
		tr[x]=tr[x]+tr[y];//执行合并操作
		return x;
	}
	int mid=(l+r)>>1;
	ls[x]=merge(ls[x],ls[y],l,mid);
	rs[x]=merge(rs[x],rs[y],mid+1,r);//递归左右子树
	pushup(x);
	return x;
}

注意:以上做法线段树合并的修改与合并不能同时。

假如对于两棵树 \(x,y\),某个节点 \(u\), 在 \(x\) 中未出现而在 \(y\) 中出现,那么由于最后是合并到 \(x\) 上,所以 \(fa_u\) 的儿子会直接指向 \(y\) 中的 \(u\)。

但是若我们修改了 \(u\),那么 \(y\) 的形态结构就会发生改变。

这种做法在许多题目都是对的是因为 update 与 merge 不同时,或者将询问离线下来。这样若保证了 merge 后不会 update,或者离线询问后从下往上 merge(保证合并后的子树不被修改)。

一个解决方法就是可持久化。

新建一个节点 \(p\) 来表示 \(u\),然后 merge。

这样就使得 \(y\) 子树不会被改变。

inline int merge(int x,int y){
	if(!x||!y)return x|y;
	int p=++id;
	ls[p]=merge(ls[x],ls[y]);
	rs[p]=merge(rs[x],rs[y]);
	pushup(p);
	return p;
}

考虑对于每个点建一棵权值线段树。

树上差分,对于在从 \(u\) 到 \(v\) 的路径加上颜色 \(col\),直接在 \(u,v\) 处加上 \(col\),然后在 \(\text{lca}(u,v)\) 与 \(fa_{\text{lca}(u,v)}\) 的 \(col\) 减一。

这样对于点 \(u\),其所有的 \(col\) 就在其子树中,可以使用线段树合并。

由于是动态开点,所以用一个一维数组 tr[p] 表示动态开点后编号为 \(p\) 的节点。

但是这样无法分清哪些节点是哪棵树,所以加入 rt[u] 表示 \(u\) 所在的权值线段树的根。

维护一个 kx[p] 表示权值线段树中编号为 \(p\) 的节点中出现最多的粮食的编号。

tr[p] 则表示出现最多的粮食的出现次数。

统计颜色时遍历树,从下到上,将 \(u\) 与它儿子 \(v\) 的权值线段树合并,并记录答案。

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

const int N=1e5+6;
const int M=8e6+7;
const int lim=1e5;
const int lg=18;

struct edge{
	int to,nxt;
}e[N<<1];

int n,m,head[N],tot,f[N][lg+1],dep[N],tr[M],kx[M],ls[M],rs[M],ans[N],rt[N],id;

inline void addedge(int u,int v){
	e[++tot].to=v;
	e[tot].nxt=head[u];
	head[u]=tot;
	return;
}

inline void predfs(int u,int fa){
	dep[u]=dep[fa]+1;
	f[u][0]=fa;
	for(int i=1;i<=lg;i++)f[u][i]=f[f[u][i-1]][i-1];
	for(int i=head[u];i;i=e[i].nxt){
		int v=e[i].to;
		if(v==fa)continue;
		predfs(v,u);
	}
	return;
}

inline int lca(int u,int v){
	if(dep[u]>dep[v])swap(u,v);
	for(int i=lg;i>=0;i--)if(dep[f[v][i]]>=dep[u])v=f[v][i];
	if(u==v)return u;
	for(int i=lg;i>=0;i--){
		if(f[u][i]!=f[v][i]){
			u=f[u][i];
			v=f[v][i];
		}
	}
	return f[u][0];
}

inline void pushup(int p){
	if(tr[ls[p]]>=tr[rs[p]]){
		tr[p]=tr[ls[p]];
		kx[p]=kx[ls[p]];
	}
	else{
		tr[p]=tr[rs[p]];
		kx[p]=kx[rs[p]];
	}
	return;
}

inline void update(int &p,int l,int r,int col,int val){
	if(!p)p=++id;
	if(l==r){
		tr[p]+=val;
		kx[p]=col;
		return;
	}
	int mid=(l+r)>>1;
	if(col<=mid)update(ls[p],l,mid,col,val);
	else update(rs[p],mid+1,r,col,val);
	pushup(p);
	return;
}

inline int merge(int x,int y,int l,int r){
	if(!x||!y)return x+y;
	if(l==r){
		tr[x]=tr[x]+tr[y];
		return x;
	}
	int mid=(l+r)>>1;
	ls[x]=merge(ls[x],ls[y],l,mid);
	rs[x]=merge(rs[x],rs[y],mid+1,r);
	pushup(x);
	return x;
}

inline void dfs(int u,int fa){
	for(int i=head[u];i;i=e[i].nxt){
		int v=e[i].to;
		if(v==fa)continue;
		dfs(v,u);
		rt[u]=merge(rt[u],rt[v],1,lim);
	}
	ans[u]=kx[rt[u]];
	if(!tr[rt[u]])ans[u]=0;
	return;
}

int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<n;i++){
		int u,v;
		scanf("%d%d",&u,&v);
		addedge(u,v);
		addedge(v,u);
	}
	predfs(1,0);
	for(int i=1;i<=m;i++){
		int u,v,o,col;
		scanf("%d%d%d",&u,&v,&col);
		o=lca(u,v);
		update(rt[u],1,lim,col,1);
		update(rt[v],1,lim,col,1);
		update(rt[o],1,lim,col,-1);
		update(rt[f[o][0]],1,lim,col,-1);
	}
	dfs(1,0);
	for(int i=1;i<=n;i++)printf("%d\n",ans[i]);
	return 0;
}

这道题就是之前所说的可持久化 merge 方法。

首先 \(a\) 和 \(b\) 都是 \(c\)的祖先。

考虑 \(a,b\) 之间的祖先关系。

  1. \(b\) 是 \(a\) 的祖先。

那么 \(b\) 就是 \(a\) 的父亲往上的一条链,有 \(\min\{k,dep_a-1\}\) 个节点。\(c\) 在 \(a\) 子树中随便取。总方案数 \(\min(k,dep_a-1)*(sz_a-1)\)。

  1. \(b\) 是 \(a\) 的后代。

那么 \(b\) 在 \(a\) 的子树中随便取,\(c\) 继续在 \(b\) 的子树中随便取。

方案数为 \(\displaystyle \sum_{dep_a+1\le dep_u\le dep_a+k}(sz_u-1)\)。

那么考虑建权值线段树下标表示深度。

对于本题,dfs 中同时 update 与 merge,那么就需要将 merge 可持久化。

这种写法好处是绝对不会错,劣处是空间加大,一般会多个常数。所以能不可持久化就别可持久化。

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

#define int long long

const int N=3e5+6;
const int M=6e6+7;
const int lim=1e9;

int n,m,dep[N],sz[N],tr[M],ls[M],rs[M],rt[N],head[N],id,tot=1;

struct edge{
	int to,nxt;
}e[N<<1];

inline void addedge(int u,int v){
	e[++tot].to=v;
	e[tot].nxt=head[u];
	head[u]=tot;
	return;
}

inline void pushup(int p){
	tr[p]=tr[ls[p]]+tr[rs[p]];
	return;
}

inline void update(int &p,int l,int r,int now,int val){
	if(!p)p=++id;
	if(l==r){
		tr[p]+=val;
		return;
	}
	int mid=(l+r)>>1;
	if(now<=mid)update(ls[p],l,mid,now,val);
	else update(rs[p],mid+1,r,now,val);
	pushup(p);
	return;
}

inline int merge(int x,int y){
	if(!x||!y)return x|y;
	int p=++id;
	tr[p]=tr[x]+tr[y];
	ls[p]=merge(ls[x],ls[y]),rs[p]=merge(rs[x],rs[y]);
	return p;
}

inline int query(int p,int l,int r,int s,int t){
	if(s<=l&&r<=t)return tr[p];
	int mid=(l+r)>>1,res=0;
	if(s<=mid)res+=query(ls[p],l,mid,s,t);
	if(t>mid)res+=query(rs[p],mid+1,r,s,t);
	return res;
}

inline void dfs(int u,int fa){
	dep[u]=dep[fa]+1;
	sz[u]=1;
	for(int i=head[u];i;i=e[i].nxt){
		int v=e[i].to;
		if(v==fa)continue;
		dfs(v,u);
		sz[u]+=sz[v];
		rt[u]=merge(rt[u],rt[v]);
	}
	update(rt[u],1,n,dep[u],sz[u]-1);
	return;
}

signed main(){
	scanf("%lld%lld",&n,&m);
	for(int i=1;i<n;i++){
		int u,v;
		scanf("%lld%lld",&u,&v);
		addedge(u,v);
		addedge(v,u);
	}
	dfs(1,0);
	while(m--){
		int u,k,ans;
		scanf("%lld%lld",&u,&k);
		printf("%lld\n",min(k,dep[u]-1)*(sz[u]-1)+query(rt[u],1,n,dep[u]+1,min(n,dep[u]+k)));
	}
	return 0;
}

习题

模板题。

交换左右子树对当前节点和前面的所有节点没有影响,所以产生逆序对当且仅当 \(u,v\) 在左右子树中。

那么在合并线段树的过程中统计交换子树的逆序对个数和不交换子树的逆序对个数取 min。

逆序对个数显然就是左子树大小乘上右子树大小。

一个节点 \(u\) 的 \(k\) 级 cousin 个数相当于 \(u\) 的 \(k\) 辈父亲 \(fa\) 的 \(k\) 级儿子个数减一(减去 \(u\))。

先将询问离线,然后倍增得到每个点的 \(k\) 辈父亲。

对于每个节点 \(fa\),查询 \(fa\) 字数内有多少节点 \(u\) 满足 \(dep_{fa}+k=dep_i\)

那么给每个点建立权值线段树后合并。

二. 线段树合并与其他结合

对于每一个节点开一棵线段树维护其重要度。

那么一开始对于每个节点都是一棵树,当 \(x\) 到 \(y\) 加入一条边的时候,将它们的线段树合并。同时使用并查集合并两个点,这样可以处理出对于每一个点,其所在的连通块。

查询点 \(u\) 所在的连通块的根的权值线段树,在权值线段树上找到第 \(k\) 小。这个不熟悉可以去看权值线段树部分。

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

const int N=1e5+6;
const int M=4e6+7;

int tr[M],ls[M],rs[M],id[M],n,m,Q,fa[N],rt[N],lim,tot;

inline void pushup(int p){
	tr[p]=tr[ls[p]]+tr[rs[p]];
	return;
}

inline void update(int &p,int l,int r,int now,int idx){
	if(!p)p=++tot;
	if(l==r){
		tr[p]++;
		id[p]=idx;
		return;
	}
	int mid=(l+r)>>1;
	if(now<=mid)update(ls[p],l,mid,now,idx);
	else update(rs[p],mid+1,r,now,idx);
	pushup(p);
	return;
}

inline int merge(int x,int y,int l,int r){
//	cout<<x<<" "<<y<<" "<<l<<" "<<r<<"&\n";
	if(!x||!y)return x|y;
	if(l==r){
		if(!id[x])id[x]=id[y];
		tr[x]+=tr[y];
		return x;
	}
	int mid=(l+r)>>1;
	ls[x]=merge(ls[x],ls[y],l,mid);
	rs[x]=merge(rs[x],rs[y],mid+1,r);
	pushup(x);
	return x;
}

inline int query(int p,int l,int r,int k){
//	cout<<p<<" "<<l<<" "<<r<<" "<<k<<" "<<tr[p]<<"*\n";
	if(!p||tr[p]<k)return -1;
	if(l==r)return id[p];
	int mid=(l+r)>>1;
	if(tr[ls[p]]>=k)return query(ls[p],l,mid,k);
	else return query(rs[p],mid+1,r,k-tr[ls[p]]);
}

inline int find(int x){
	if(fa[x]==x)return x;
	return fa[x]=find(fa[x]);
}

inline void addedge(int u,int v){
	u=find(u),v=find(v);
	if(u==v)return;
	fa[v]=u;
	rt[u]=merge(rt[u],rt[v],1,lim);
	return;
}

int main(){
	scanf("%d%d",&n,&m),lim=n;
	for(int i=1;i<=n;i++){
		int x;
		fa[i]=i;
		scanf("%d",&x);
		update(rt[i],1,lim,x,i);
	}
	for(int i=1;i<=m;i++){
		int u,v;
		scanf("%d%d",&u,&v);
		addedge(u,v);
	}
	scanf("%d",&Q);
	while(Q--){
		int u,v,k;
		char op;
//		scanf("%c",&op);
		cin>>op;
		if(op=='Q'){
			scanf("%d%d",&u,&k);
			printf("%d\n",query(rt[find(u)],1,lim,k));
		}
		else{
			scanf("%d%d",&u,&v);
			addedge(u,v);
		}
	}
	return 0;
}

习题

考虑将查询离线下来,然后按照 \(x\) 从小到大排序。对于加边也是同理,按照边权从小到大排序。

处理询问时,从边中找到边权小于 \(x\) 的并加入。这部分由于询问与边权均单调递增,故时间复杂度为 \(O(n)\)。

那么查询点 \(u\) 所在的连通块中就没有边权大于 \(x\) 的点了。那么这就与永无乡一模一样了。

标签:return,int,tr,线段,合并,笔记,merge,ls
From: https://www.cnblogs.com/trsins/p/17970746

相关文章

  • 【学习笔记】权值线段树
    一.权值线段树权值线段树即一种线段树,以序列的数值为下标。节点里所统计的值为节点所对应的区间\([l,r]\)中,\([l,r]\)这个值域中所有数的出现次数。举个例子,有一个长度为\(10\)的序列\(\{1,5,2,3,4,1,3,4,4,4\}\)。那么统计每个数出现的次数。易知\(1\)出现了\(2\)......
  • 【学习笔记】数论杂谈
    一.素数相关0.约定若无特殊说明,本部分做以下记号规定。\(p\in\mathbb{P}\),\(\mathbbP\)为质数集。\(\pi(n)\)表示\(1\)至\(n\)内的素数个数。\(P^{+}(n),P^-(n)\)分别表示\(n\)的最大/最小质因子。\(\nu_i\)表示\(i\)的可重质因子个数。1.素数定理\[\pi(......
  • 【学习笔记】数论函数与莫比乌斯反演
    一.数论函数基础数论函数:满足值域为整数的函数。本文下述的数若无特殊说明均为整数。若无特殊说明则钦定\(\displaystylen=\prod_{i=1}^kp_i^{e_i},p_i\in\mathbb{P}\)。\(\mathbb{P}\)表示质数集合,\(p_i\)互不相同。介绍几个常见的数论函数:\(I(n)\):恒等函数,无论\(n\)......
  • 【学习笔记】Segment Tree Beats/吉司机线段树
    一.区间最值操作本文对吉如一老师在\(2016\)年国家集训队论文中的线段树处理历史区间最值的问题的一些杂谈。区间最值笼统地指求区间的最值以及区间所有数对\(x\)取最值(即令\(a_i=\max/\min(a_i,x)\))这一类的查询与修改操作。HDU5306GorgeousSequence支持对区间......
  • 学习笔记6
    Scala匿名函数(函数字面量)Scala中的匿名函数也叫做函数字面量,既可以作为函数的参数使用,也可以将其赋值给一个变量,在匿名函数的定义中“=>”可理解为一个转换器,它使用右侧的算法,将左侧的输入数据转换为新的输出数据,使用匿名函数后,我们的代码变得更简洁了。valtest=(x:Int)=>......
  • Print linked list using recursion【1月17日学习笔记】
    点击查看代码//Printlinkedlistusingrecursion#include<iostream>usingnamespacestd;structnode{ intdata; node*next;};voidprint(node*p){ if(p==NULL)return;//递归中止条件 cout<<p->data<<""; print(p->next)......
  • Reverse a linked list【1月17日学习笔记·】
    点击查看代码//Reverssealinkedlist#include<iostream>usingnamespacestd;structnode{ intdata; node*next;};node*A;voidreverse(){ node*next;//用于保存下一个·节点地址以便索引 node*current=A;//当前索引 node*prev=NULL;//保存上一个节点......
  • Avalonia 11 学习笔记
    Avalonia学习之样式Avalonia学习之样式的选择器和伪类Avalonia学习之资源使用Avalonia学习之绑定Avalonia学习之属性Avalonia学习之自定义控件Avalonia实现一条虚线Avalonia使用d:DataContext实现设计时预览Avalonia实现滑动加载Avalonia通过ef操作sqlite......
  • Git - 合并本地未提交的 commit
    每次工作养成了保存commit的习惯,提交远程仓库时想合并这些commit保证历史信息干净整洁。通过rebase合并本地未提交的commit并修改消息。gitrebase-i[hash]如上图所示,我要将这两个3分钟前的commit合并在一起,那么就要执行gitrebase-i[第commit的hash值](第三个h......
  • Delete d node at nth position【1月17日学习笔记】
    点击查看代码//Deletednodeatnthposition#include<iostream>usingnamespacestd;structnode{ intdata; node*next;};node*A;voidinsert(intx){ node*temp=newnode; temp->data=x; temp->next=NULL; if(A==NULL){ A=temp;......