权值线段树,动态开点与线段树合并

不好吃的线段树半家桶。

权值线段树

本质是叶子节点下标意义的变化。

一般情况下,线段树的每条线段负责维护区间 \(a_l\sim a_r\) 的信息。存在两个叶子节点 \(tree_l,tree_r\),有 \(tree_l.l=tree_l.r=l,tree_r.l=tree_r.r=r\)。权值线段树表示为:\(tree_l.l=tree_l.r=a_l,tree_r.l=tree_r.r=a_r\)

翻译成人话:用线段树维护桶。

进而可以实现以下操作:

  • \(\Theta(logn)\) 求区间第k大数。
  • \(\Theta(logn)\) 求区间内元素 \(a_i\) 个数。
  • \(\Theta(logn)\) 求区间内多个元素 \(a_i,a_{i+1}\dots a_j\) 个数和。

之类的东西。

struct Segment_Tree{
	#define leftson(p) p<<1
	#define rightson(p) p<<1|1
	#define push_up(p) tree[p].val=tree[leftson(p)].val+tree[rightson(p)].val
	struct TREE{
		int l,r;
		int val;//l,r:当前节点所管辖的元素l~r。val:区间内的元素总数
	}tree[MAXN<<2];
	void build(int l,int r,int p){
		tree[p].l=l,tree[p].r=r,tree[p].val=0;
		if(l==r){return;}
		int mid=l+r>>1;
		build(l,mid,leftson(p));
		build(mid+1,r,rightson(p));
		push_up(p);
	}
	void update(int x,int v,int p){//将元素x增加v个
		if(tree[p].l==tree[p].r){
			tree[p].val+=v;
			return;
		}
		int mid=tree[p].l+tree[p].r>>1;
		if(x<=mid)update(x,v,leftson(p));
		else update(x,v,rightson(p));
	}//就是单点修改
	int query(int l,int r,int p){//查询元素l~r内的总元素个数
		if(tree[p].l>=l&&tree[p].r<=r)return tree[p].val;
		int mid=tree[p].l+tree[p].r>>1;
		int res=0;
		if(l<=mid)res+=query(l,r,leftson(p));
		if(r>mid)res+=query(l,r,rightson(p));
		return res;
	}//就是区间查询
	int kth(int k,int p){//查询序列中第k大的元素是哪个
		if(tree[p].l==tree[p].r)return tree[p].l;//找到了返回元素
		int mid=tree[p].l+tree[p].r>>1;
		int s1=tree[leftson(p)].val,s2=tree[rightson(p)].val;//我们令线段树管辖的序列是递增的,右儿子的元素普遍较大
		if(k<=s2)return kth(k,rightson(p));//如果k就在右儿子里就返回
		else return kth(k-s2,leftson(p));//如果k在左儿子里,就找左儿子中第k-s2大的元素。
	}
}ST;

动态开点线段树

我称其为和面流线段树,水多加面面多加水。

一般线段树要开 \(4N\) 的空间,事实上需要的点在 \(1\sim 2N\)是稠密的,在\(2N\sim4N\) 是稀疏的。并且我们根据进制思想,一棵维护区间 \(a_1\sim a_n\) 的线段树实际需要的空间为:\(\sum_{i=0}^{logn}2^i<2n\),因此每棵线段树都浪费了至少 \(2N\) 的空间。

观察语句:

#define leftson(p) p<<1

#define rightson(p) p<<1|1

这样可以使每个节点 \(p\) 的左右儿子编号都是 \(2p,2p+1\) 且不出现重复。同时也导致开了大量闲置的叶子节点。

节点编号是可以做到连续的,我们只需指定每个节点的左右儿子编号即可。

至于“水多加面”:只有我们要更新或查询一个先前还没有指定儿子节点变好的节点时,我们才去更新这个点的子节点信息,而不是在 ST.build() 函数中全部写好。

2024.2.5 updated:

可见动态开点线段树是现场钦定儿子的管辖范围的。由于修改一般是单点修改,所以添加一个点一定只会落在一个子节点中,所以在开点时会导致一个节点缺少一个儿子,因为目前那个儿子所管辖的区间还没有元素被加入。

对空间的节省使得动态开点权值线段树拥有天然的离散性,进而如果使用了此数据结构是不需要对值域离散化的。

以下是实现。

struct Segment_Tree{//线段树维护区间修改和单点查询。
	int tot=1,root=1;//tot代表节点编号,root指向树根,因为形如(&p)的函数是无法向其中填入常量(例如1)的。 
	struct TREE{
		int lson,rson;//一个节点所管理的区间和自己的编号没有关系,只和父节点管理的区间有关。
        //如此追溯直到 tree[1].l=1,tree[1].r=n。进而每个节点是不必要也不能定义l,r的。
		int val,tag;
	}tree[MAXN<<1];//只需要二倍空间。然而线段树合并时得开20倍,原因之后说。
	inline void push_up(int p){
		tree[p].val=tree[tree[p].lson].val+tree[tree[p].rson].val;
		return;//此时左右子节点就不是p*2之类的了,是自己定义出来的儿子下标。
	}
	inline void spread(int p,int l,int r){//与一般线段树最大的不同在下放操作。
    //p为父节点的下标,l,r为父节点管辖的区间,由tree[1]递推得来。
		if(!tree[p].lson)tree[p].lson=++tot;
		if(!tree[p].rson)tree[p].rson=++tot;//如果子节点还没用就不管,用了发现没定义就就地定义。
		int mid=l+r>>1;
		tree[tree[p].lson].val+=tree[p].tag*(mid-l+1);//就地赋予子节点管辖区间并作修改。
		tree[tree[p].rson].val+=tree[p].tag*(r-mid);//lson管l~mid,rson为mid+1~r。
		tree[tree[p].lson].tag+=tree[p].tag;
		tree[tree[p].rson].tag+=tree[p].tag;
		tree[p].tag=0;
	}
	inline void update(int l,int r,int k,int ul,int ur,int &p){//由于节点管辖区间不是固定的,所以每次要从[1,n]推得。
    //l,r为当前节点管辖区间,ul,ur为修改区间。
    //当前节点p可能是没开过的,如果真没开过那就就地给父亲节点的儿子赋值就行,所以是&p。
		if(!p)p=++tot;
		if(l>=ul&&r<=ur){
			tree[p].val+=k*(r-l+1);
			tree[p].tag+=k;//和一般线段树一样。
			return;
		}
		spread(p,l,r);//每次下放的父亲节点要指明其管辖区间。
		int mid=l+r>>1;
		if(ul<=mid)update(l,mid,k,ul,ur,tree[p].lson);//安排左子节点区间并修改。
		if(ur>mid)update(mid+1,r,k,ul,ur,tree[p].rson);//安排右子节点区间并修改。
		push_up(p);
	}
	inline int query(int l,int r,int ux,int p){
		if(!p)return 0;//没开过这个点,也就是从来没更新过,直接返回0。
		if(l>=ux&&r<=ux)return tree[p].val;
		spread(p,l,r);
		int mid=l+r>>1;
		int ans=0;
		if(ux<=mid)return query(l,mid,ux,tree[p].lson);
		else return query(mid+1,r,ux,tree[p].rson);//同上操作。
	}
}ST;

例题

将询问按照右端点排序。

这个人在区间上只会采出现了至少两次的花。换句话说,点 \(a_i\) 与其颜色 \(col_i\) 对答案有贡献,当且仅当 \(col_i\) 上次出现的位置 \(last_{col_i}\) 满足 \(l<=last_{col_i}<a_i<=r\)。预处理一下即可。

\(loc_i\) 表示 \(col_i\) 上次出现的位置,\(last_i\)\(col_i\) 本次出现的位置,则区间 \(last_i\sim col_i\) 上的答案增加 1 贡献,没有 last 就更新一下,有 last 就直接增加答案,用线段树维护区间。

然后就被了。

题解里用的是树状数组,也就是说我们以很细小的冗余时间T了本题,那么就可以用动态开点优化,因为没开过的点根本就不用看。

#include<bits/stdc++.h>
#define MAXN 2000005
using namespace std;
int n,c,m;
int col[MAXN],loc[MAXN],last[MAXN];
struct node{
	int l,r,id;
}q[MAXN];
int ans[MAXN];
bool cmp(node a,node b){
	return a.r<b.r;
}
struct Segment_Tree{//上面的板子就是从这粘来的,不解释了。
	int tot=1,root=1;
	struct TREE{
		int lson,rson;
		int val,tag;
	}tree[MAXN<<1];
	inline void push_up(int p){
		tree[p].val=tree[tree[p].lson].val+tree[tree[p].rson].val;
		return;
	}
	inline void spread(int p,int l,int r){
		if(!tree[p].lson)tree[p].lson=++tot;
		if(!tree[p].rson)tree[p].rson=++tot;
		int mid=l+r>>1;
		tree[tree[p].lson].val+=tree[p].tag*(mid-l+1);
		tree[tree[p].rson].val+=tree[p].tag*(r-mid);
		tree[tree[p].lson].tag+=tree[p].tag;
		tree[tree[p].rson].tag+=tree[p].tag;
		tree[p].tag=0;
	}
	inline void update(int l,int r,int k,int ul,int ur,int &p){
		if(!p)p=++tot;
		if(l>=ul&&r<=ur){
			tree[p].val+=k*(r-l+1);
			tree[p].tag+=k;
			return;
		}
		spread(p,l,r);
		int mid=l+r>>1;
		if(ul<=mid)update(l,mid,k,ul,ur,tree[p].lson);
		if(ur>mid)update(mid+1,r,k,ul,ur,tree[p].rson);
		push_up(p);
	}
	inline int query(int l,int r,int ux,int p){
		if(!p)return 0;
		if(l>=ux&&r<=ux)return tree[p].val;
		spread(p,l,r);
		int mid=l+r>>1;
		int ans=0;
		if(ux<=mid)return query(l,mid,ux,tree[p].lson);
		else return query(mid+1,r,ux,tree[p].rson);
	}
}ST;
signed main(){
	scanf("%d%d%d",&n,&c,&m);
	for(int i=1;i<=n;i++){
		scanf("%lld",&col[i]);
		loc[i]=last[col[i]];
		last[col[i]]=i;//花的顺序不会变,就地预处理。
	}
	for(int i=1,l,r;i<=m;i++){
		scanf("%d%d",&l,&r);
		q[i].l=l,q[i].r=r;q[i].id=i;
	}
	sort(q+1,q+1+m,cmp);
	int tmp=1;
	for(int i=1;i<=n;i++){
		if(loc[i])ST.update(1,n,1,loc[loc[i]]+1,loc[i],ST.root);//能更新就更新
		while(q[tmp].r==i&&tmp<=m){
			ans[q[tmp].id]=ST.query(1,n,q[tmp].l,ST.root);
			++tmp;//排好序后线性处理询问,我们只需要知道l点能采多少即可。 
		}
		if(tmp>m)break;
	}
	for(int i=1;i<=m;i++)
		printf("%d\n",ans[i]);
	return 0;
}

线段树合并

顾名思义,就是把两棵线段树合并一棵线段树,保留或合并两棵树已有的数据(如果有可加性的话),这颗线段树的节点信息,编号等都由先前两棵树得来。

这个东西的全名叫动态开点权值线段树合并。也就是缝合了前面提到的两者的一种操作。

对于两棵待合并的线段树 \(A,B\),合并操作实现如下:

  • 处理节点问题。由于都是动态开点线段树,所以可能出现 \(A\) 树有节点 \(p\)\(B\) 树没有的情况。如果有一棵树没有该节点,就将另一棵树的节点编号赋值给新树。
  • 处理信息问题。在当前两棵树完成节点合并后使用 push_up 操作统一信息。
  • 操作是递归实现的,完成合并后返回合并好的节点编号供上一级节点使用。
	int merge(int x,int y){
		if(!x)return y;
		if(!y)return x;
		ls(x)=merge(ls(x),ls(y));
		rs(x)=merge(rs(x),rs(y));//合并节点编号
		push_up(x);//合并节点信息
		return x;//返回合并好的节点编号,与无注释两行同作用。
	}

例题

不妨让权值线段树维护:当前节点的子树中有多少节点权值比它大。叶子节点存的是当前元素有多少个,query时直接扫数量即可。

然而影响当前节点答案的是它的子树。我们要先对树结构进行 dfs ,退出当前节点时合并他自己以及他的所有子节点,就地统计答案后方能得到“自己的子树中有多少大于它权值的点”。

void dfs(int u,int fa){
	for(int i=0;i<edge[u].size();i++){
		int v=edge[u][i];
		if(v==fa)continue;
		dfs(v,u);
		rt[u]=ST.merge(rt[u],rt[v]);
	}
	ans[u]=ST.query(1,n,val[u]+1,rt[u]);//query为查询有多少点权大于等于当前点。这是权值线段树,直接看节点val[u]右侧节点值之和即可。
    //由上文策略,退出当前节点时统计一下线段树中有多少点大于自己。
    //之后本节点会作为子节点合并到他的父节点,统计父节点时再用。
}

但是这个数据太大了得先离散化。

int n;
struct node{
	int v,id;
}vaL[MAXN];
bool cmp(node a,node b){
	return a.v<b.v;
}
int val[MAXN],rt[MAXN];
map<int,int>nti;
int itn[MAXN],tmp;

...

int main(){
	scanf("%lld",&n);
	for(int i=1;i<=n;i++)scanf("%lld",&vaL[i].v),val[i]=vaL[i].v,vaL[i].id=i;
	sort(vaL+1,vaL+1+n,cmp);
	for(int i=1;i<=n;i++){
		if(!nti[vaL[i].v])nti[vaL[i].v]=++tmp,itn[tmp]=vaL[i].v;
		val[vaL[i].id]=nti[vaL[i].v];
	}
//网上好多是lowerbound和unique离散化,看不懂就先不这么用了。

由于线段树合并要求的是动态开点的权值线段树,故上传操作如下:

	void update(int l,int r,int k,int &p){
		if(!p)p=++tot;//没有当前节点或者儿子时先开点。
		if(l==r){//到达叶子节点后维护当前元素有多少个。
			++tree[p].val;
			return;
		}
		int mid=l+r>>1;//没有到达叶子节点时维护当前区间内有多少元素
		if(k<=mid)update(l,mid,k,ls(p));//出于&p的设定,赋值儿子节点编号是顺带的。
		else update(mid+1,r,k,rs(p));
		push_up(p);
	}
                                 ...
signed main(){
...
 	for(int i=2,fa;i<=n;i++){
		scanf("%lld",&fa);
		edge[fa].push_back(i);
		edge[i].push_back(fa);//建图,给dfs跑。
	}
	for(int i=1;i<=n;i++)ST.update(1,n,val[i],rt[i]);//出于&p的设定,赋值当前点在线段树中的实际点编号也是顺带的。
    ...
                                        
	return 0;                                 
}

然后是查询与合并操作。

	int query(int l,int r,int x,int p){//元素值为(l,r)的区间中有多少是大于等于x的?
		if(!p)return 0;//要查询的节点是开过的点,没开过的就返回0
		if(l>=x)return tree[p].val;//当前节点区间左端点值已经大于x了。
        //权值线段树中节点编号直接反应权值大小。直接加上整个节点的数目。
		int res=0;
		int mid=l+r>>1;
		if(mid>=x)res+=query(l,mid,x,ls(p));//x卡在半区间左侧,说明(l,mid)内也有可能有大于等于x的元素存在。
		res+=query(mid+1,r,x,rs(p));//找右半区间有多少大于等于x的节点。
        
        //因而查询区间被分割为:完全大于x的区间,可能大于x的区间。
        //可能大于x的区间最终会因为二分值mid而被完全分割成完全大于x的小段。
		return res;
	}
	int merge(int x,int y){//线段树合并操作,上文就是从这里复制的。
		if(!x)return y;
		if(!y)return x;
		ls(x)=merge(ls(x),ls(y));
		rs(x)=merge(rs(x),rs(y));
		push_up(x);
		return x;
	}

大概就是这样了。放一下总代码水字数。

#include<bits/stdc++.h>
#define int long long
#define MAXN 100005
using namespace std;
int n;
struct node{
	int v,id;
}vaL[MAXN];
bool cmp(node a,node b){
	return a.v<b.v;
}
int val[MAXN],rt[MAXN];
map<int,int>nti;
int itn[MAXN],tmp;
vector<int>edge[MAXN];
struct Segment_Tree{
	#define ls(p) tree[p].lson
	#define rs(p) tree[p].rson
	#define push_up(p) tree[p].val=tree[ls(p)].val+tree[rs(p)].val
	int tot;
	struct TREE{
		int val;
		int lson,rson;
	}tree[MAXN*20];
	void update(int l,int r,int k,int &p){
		if(!p)p=++tot;
		if(l==r){
			++tree[p].val;
			return;
		}
		int mid=l+r>>1;
		if(k<=mid)update(l,mid,k,ls(p));
		else update(mid+1,r,k,rs(p));
		push_up(p);
	}
	int query(int l,int r,int x,int p){
		if(!p)return 0;
		if(l>=x)return tree[p].val;
		int res=0;
		int mid=l+r>>1;
		if(mid>=x)res+=query(l,mid,x,ls(p));
		res+=query(mid+1,r,x,rs(p));
		return res;
	}
	int merge(int x,int y){
		if(!x)return y;
		if(!y)return x;
		ls(x)=merge(ls(x),ls(y));
		rs(x)=merge(rs(x),rs(y));
		push_up(x);
		return x;
	}
}ST;
int ans[MAXN];
void dfs(int u,int fa){
	for(int i=0;i<edge[u].size();i++){
		int v=edge[u][i];
		if(v==fa)continue;
		dfs(v,u);
		rt[u]=ST.merge(rt[u],rt[v]);
	}
	ans[u]=ST.query(1,n,val[u]+1,rt[u]);
}
signed main(){
	scanf("%lld",&n);
	for(int i=1;i<=n;i++)scanf("%lld",&vaL[i].v),val[i]=vaL[i].v,vaL[i].id=i;
	sort(vaL+1,vaL+1+n,cmp);
	for(int i=1;i<=n;i++){
		if(!nti[vaL[i].v])nti[vaL[i].v]=++tmp,itn[tmp]=vaL[i].v;
		val[vaL[i].id]=nti[vaL[i].v];
	}
	for(int i=2,fa;i<=n;i++){
		scanf("%lld",&fa);
		edge[fa].push_back(i);
		edge[i].push_back(fa);
	}
	for(int i=1;i<=n;i++)ST.update(1,n,val[i],rt[i]);
	dfs(1,0);
	for(int i=1;i<=n;i++)printf("%lld\n",ans[i]);
	return 0;
}

最后只会留下一颗线段树。然这棵线段树集成了先前小树的各种节点。先前的小树也可以被理解为是“残破的”,因为只负责维护部分信息。也就是说整个 dfs 与合并过程都是建树过程

本题中的查询操作对“线段树的残破性”是必须的。当前处理的节点对“残破性”的要求是:线段树只能维护他自己以及他的子节点。自始至终只来得及开一颗线段树,在合并过程中就满足了部分点的答案,进行更新即可。

为什么不用板题讲?

因为我用的网上的树剖+树上差分过的。

所以在这讲一下树上差分。

树上差分

最早的想法是靠板题学线段树合并。然而

刚学完树剖。树上路径修改状操作反正我是只会用树剖写的。

然后发现不会写,查看题解,确实有树剖写法,用到了树上差分。

树上差分就是序列上差分的扩展。

差分树上,节点 \(i\) 的值 \(a_i\) 反应的是点 \(i\) 的实际权值 \(val_i\) 与其父亲节点权值 \(val_{fa}\) 的权值差,因此,进行路径 \((1,i)\) 的遍历并汇总即可还原节点 \(i\)

进行路径遍历并汇总即可还原节点。这句话很重要,看不懂代码先想想明不明白这句话的意思。

完成剖分后,对于一条链的底端点的下一个点 \(x\) 与顶点 \(top\)。在当前路径上增加救济粮 \(k\) 可以理解为:

val[x]-=k;
val[top]+=k;
namespace HPD{
	void modify(int x,int y,int k){
		while(top[x]!=top[y]){
			if(dep[top[x]]<dep[top[y]])swap(x,y);
			tags[dfn[top[x]]].push_back(k);//对链上的点打tag
			tags[dfn[x]+1].push_back(-k);//更新到点x,实际要更新dfn[x]+1这个点的差分值。
			x=father[top[x]]; 
		}
		if(dep[x]>dep[y])swap(x,y);
		tags[dfn[x]].push_back(k),tags[dfn[y]+1].push_back(-k);
	}
}

至于为什么要打tag,向量tag的具体意义:

题目中给出的操作是:给路径上放类型 \(z\) 的救济粮,然我们现在实现的是给路径上每个点权值加上 \(z\)

但是我们忽略掉了剖分时的线段树的类型。

如果这是一棵权值线段树,“给节点权值加 \(z\)”这样的操作实际为:将权值 \(z\) 的个数 \(+1\)

线段树叶子节点 \(p\) 是一个桶。对于叶子节点,\(tree[p].val\) 为第 \(p\) 种救济粮的个数。对于非叶子节点,\(tree[p].val\)\(p\) 所管辖的第 \((l,r)\) 类救济粮中最多个数的那个的个数,\(tree[p].res\) 则为最多个数救济粮的种类。由此写出线段树。

struct Segment_Tree{
	#define leftson(p) p<<1
	#define rightson(p) p<<1|1
	struct TREE{
		int l,r;
		int val,tag;
		int res;
	}tree[MAXN<<2];
	void push_up(int p){
		if(tree[leftson(p)].val>=tree[rightson(p)].val)tree[p].res=tree[leftson(p)].res,tree[p].val=tree[leftson(p)].val;//选举个数最多救济粮的信息并存储到父亲节点。
		else tree[p].res=tree[rightson(p)].res,tree[p].val=tree[rightson(p)].val;
	}
	void build(int l,int r,int p){
		tree[p].l=l,tree[p].r=r,tree[p].val=tree[p].tag=0;
		if(l==r){tree[p].res=l;return;}//初始化编号
		int mid=l+r>>1;
		build(l,mid,leftson(p));
		build(mid+1,r,rightson(p));
		push_up(p);
	}	
	void update(int x,int k,int p){//将第x种救济粮个数增加k
		if(tree[p].l==tree[p].r){
			tree[p].val+=k;
			tree[p].tag+=k;
			return;
		}
		int mid=tree[p].l+tree[p].r>>1;
		if(x<=mid)update(x,k,leftson(p));
		else update(x,k,rightson(p));
		push_up(p);
	}
}ST;

关于答案统计。

重链剖分是按照 dfn 建树的,因此还原节点时也用 dfn 统计。

	for(int i=1;i<=n;i++){
		for(int id=0;id<tags[i].size();id++){
			int num=tags[i][id];
			if(num>0)ST.update(num,1,1);
			else ST.update(-num,-1,1);
		}
		ans[rk[i]]=ST.tree[1].val?ST.tree[1].res:0;
	}

\(tag[i][j]\) 表示对第 \(i\) 种救济粮的增减量为 \(tag[i][j]\)

在树的 \(dfs\) 序上扫描点,扫描到一个点时用权值线段树更新点信息。

将一条链拆解为 \((l_1,r_1)...(l_m,r_m)\) 后,实际为在 \(l_i\) 处放置 \(z\) 品种救济粮,在 \(r_i+1\) 处减去这个救济粮。

根据树上差分的思想还原节点,统计答案即可。

#include<bits/stdc++.h>
#define MAXN 100005
using namespace std;
int n,q;
vector<int> edge[MAXN];
int father[MAXN],son[MAXN],siz[MAXN],dep[MAXN];
void dfs1(int u,int fa){
	dep[u]=dep[fa]+1;
	father[u]=fa;
	siz[u]=1;
	int maxson=-1;
	for(int i=0;i<edge[u].size();i++){
		int v=edge[u][i];
		if(v==fa)continue;
		dfs1(v,u);
		siz[u]+=siz[v];
		if(siz[v]>maxson)son[u]=v,maxson=siz[v];
	}
}
int top[MAXN],dfn[MAXN],rk[MAXN],tim;
void dfs2(int u,int topf){
	top[u]=topf;
	dfn[u]=++tim;
	rk[tim]=u;
	if(!son[u])return;
	dfs2(son[u],topf);
	for(int i=0;i<edge[u].size();i++){
		int v=edge[u][i];
		if(v==father[u]||v==son[u])continue;
		dfs2(v,v);
	}
}
struct Segment_Tree{
	#define leftson(p) p<<1
	#define rightson(p) p<<1|1
	struct TREE{
		int l,r;
		int val,tag;
		int res;
	}tree[MAXN<<2];
	void push_up(int p){
		if(tree[leftson(p)].val>=tree[rightson(p)].val)tree[p].res=tree[leftson(p)].res,tree[p].val=tree[leftson(p)].val;
		else tree[p].res=tree[rightson(p)].res,tree[p].val=tree[rightson(p)].val;
	}
	void build(int l,int r,int p){
		tree[p].l=l,tree[p].r=r,tree[p].val=tree[p].tag=0;
		if(l==r){tree[p].res=l;return;}
		int mid=l+r>>1;
		build(l,mid,leftson(p));
		build(mid+1,r,rightson(p));
		push_up(p);
	}	
	void update(int x,int k,int p){
		if(tree[p].l==tree[p].r){
			tree[p].val+=k;
			tree[p].tag+=k;
			return;
		}
		int mid=tree[p].l+tree[p].r>>1;
		if(x<=mid)update(x,k,leftson(p));
		else update(x,k,rightson(p));
		push_up(p);
	}
}ST;
int range;
vector<int>tags[MAXN];
namespace HPD{
	void modify(int x,int y,int k){
		while(top[x]!=top[y]){
			if(dep[top[x]]<dep[top[y]])swap(x,y);
			tags[dfn[top[x]]].push_back(k);
			tags[dfn[x]+1].push_back(-k);
			x=father[top[x]]; 
		}
		if(dep[x]>dep[y])swap(x,y);
		tags[dfn[x]].push_back(k),tags[dfn[y]+1].push_back(-k);
	}
}
int ans[MAXN];
int main(){
	scanf("%d%d",&n,&q);
	for(int i=1,u,v;i<n;i++){
		scanf("%d%d",&u,&v);
		edge[u].push_back(v);
		edge[v].push_back(u);
	}
	dfs1(1,0);
	dfs2(1,1);
	for(int i=1,x,y,val;i<=q;i++){
		scanf("%d%d%d",&x,&y,&val);
		range=max(range,val);
		HPD::modify(x,y,val);
	}
	ST.build(1,range,1);
	for(int i=1;i<=n;i++){
		for(int id=0;id<tags[i].size();id++){
			int num=tags[i][id];
			if(num>0)ST.update(num,1,1);
			else ST.update(-num,-1,1);
		}
		ans[rk[i]]=ST.tree[1].val?ST.tree[1].res:0;
	}
	for(int i=1;i<=n;i++)printf("%d\n",ans[i]);
	return 0;
}

例题思路与代码

永无乡

给定岛屿排名求第 \(k\) 小的数,套权值线段树板子即可,注意查询返回的是点编号不是点权值,额外维护一个 \(tree_{id}\) 即可。

考虑合并操作,显然就是线段树合并。但是涉及到联通问题,所以还需要一个并查集,每次获得当前联通块所在根节点。记得合并的是联通块的首领节点,调过样例就切了。比蓝还好做。

#include<bits/stdc++.h>
#define MAXN 300005
using namespace std;
int n,m,q;
int rt[MAXN];
int father[MAXN];
int getf(int x){
	if(father[x]==x)return father[x];
	else return father[x]=getf(father[x]);
}
struct Segment_Tree{
	#define ls(p) tree[p].lson
	#define rs(p) tree[p].rson
	#define push_up(p) tree[p].val=tree[ls(p)].val+tree[rs(p)].val
	int tot=1;
	struct TREE{
		int lson,rson;
		int val,id;
	}tree[MAXN*20];
	inline void update(int l,int r,int k,int &p,int idx){
		if(!p)p=++tot;
		if(l==r){
			++tree[p].val;
			tree[p].id=idx;
		//	printf("tree %d .idx=%d\n",p,idx);
			return;
		}
		int mid=l+r>>1;
		if(k<=mid)update(l,mid,k,ls(p),idx);
		else update(mid+1,r,k,rs(p),idx);
		push_up(p);
	}
	inline int query(int l,int r,int k,int p){
		if(!p)return 0;
		if(l==r)return tree[p].id;
		int s1=tree[ls(p)].val,s2=tree[rs(p)].val;
		int mid=l+r>>1;
		if(k<=s1)return query(l,mid,k,ls(p));
		else return query(mid+1,r,k-s1,rs(p)); 
	}
	inline int merge(int x,int y){
		if(!x)return y;
		if(!y)return x;
		ls(x)=merge(ls(x),ls(y));
		rs(x)=merge(rs(x),rs(y));
		push_up(x);
		return x;
	}
}ST;//线段树合并板子,唯一的区别在查询函数返回值为点编号,最早更新的时候注意一下。
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1,val;i<=n;i++){
		scanf("%d",&val);
		ST.update(1,n,val,rt[i],i);//更新权值。
	}	
	for(int i=1;i<=n;i++)father[i]=i;
	for(int i=1,u,v;i<=m;i++){
		scanf("%d%d",&u,&v);
		int fu=getf(u),fv=getf(v);
		if(fu!=fv){//已经联通的节点就不要再合并了免得出问题。
			father[fv]=fu;
			rt[fu]=ST.merge(rt[fu],rt[fv]);
		}
	}
	scanf("%d",&q);
	for(int i=1,x,y,k;i<=q;i++){
		char opt;
		cin>>opt;
		if(opt=='B'){
			scanf("%d%d",&x,&y);
			int fu=getf(x),fv=getf(y);
			if(fu!=fv){
				father[fv]=fu;	
				rt[fu]=ST.merge(rt[fu],rt[fv]);//线段树合并。
			}
		}
		if(opt=='Q'){
			scanf("%d%d",&x,&k);
			x=getf(x);
			int res=ST.query(1,n,k,rt[x]);
			printf("%d\n",res?res:-1);//无解输出-1注意即可。
		}
	}
	return 0;
}

魔法少女LJJ

永无乡 plus。

啊这个是真的恶心。

1,2,5 操作就是永无乡,讨论 3,4,6,7 操作。

  • 对于操作 3,4,可以按照题意硬搞:把所有权值小于或者大于等于 \(x\) 的清零,统计个数 \(val\),给节点 \(x\) 加上 \(val\)。于是出现了懒标记问题,合并操作里是不能出现 push_up 状物的不然一直交一直 wa,我也不知道为什么。
  • 关于懒标记:只需要标记是否清零就可以了。
  • 对于操作 6,如果硬要维护乘积的话得高精,那我就不会了。不过他让比对大小,于是对乘积取对数,维护乘积变成了维护对数和。(这谁能想到啊)
  • 对于操作 7,输出 \(tree[root_a].val\)

操作 8,9 是不出现的,要不然我也不会做。

调起来细节很多,第一天晚上都没调出来,第二天重构了一遍就过了,懒得看之前错哪了。

#include<bits/stdc++.h>
#define MAXN 400005
using namespace std;
int T,n;
int rt[MAXN];
const int inf=1e9;
int father[MAXN];
int getf(int x){
	if(father[x]==x)return x;
	return father[x]=getf(father[x]);
}
struct Segment_Tree{
	#define ls(p) tree[p].lson
	#define rs(p) tree[p].rson
	int tot;
	struct TREE{
		int lson,rson;
		int tag,val;
		double gval;
	}tree[MAXN*20];
	inline void push_up(int p){
		tree[p].val=tree[ls(p)].val+tree[rs(p)].val;
		tree[p].gval=tree[ls(p)].gval+tree[rs(p)].gval;
		return;
	}	
	inline void spread(int p){
		if(tree[p].tag){
			tree[ls(p)].val=tree[rs(p)].val=0;
			tree[ls(p)].gval=tree[rs(p)].gval=0;
			tree[ls(p)].tag=tree[rs(p)].tag=1;
			tree[p].tag=0;
			return;
		}
	}
	inline void update(int l,int r,int x,double logval,int k,int &p){
		if(!p)p=++tot;
		if(l==r){
			tree[p].val+=k;
		//	printf("p=%d nowval:%d\n",p,tree[p].val);
			tree[p].gval+=(1.0*k*logval);
			tree[p].tag=0;
			return;
		}
		spread(p);
	//	printf("nowp:%d\n",p);
		int mid=l+r>>1;
		if(x<=mid)update(l,mid,x,logval,k,ls(p));
		else update(mid+1,r,x,logval,k,rs(p));
		push_up(p);
	}
	inline int query(int l,int r,int ul,int ur,int p){
		if(!p)return 0;
		if(l>=ul&&r<=ur)return tree[p].val;
		//printf("nowp:%d,ul,ur=%d %d \n",p,ul,ur);
		spread(p);
		int mid=l+r>>1;
		int res=0;
		if(ul<=mid)res+=query(l,mid,ul,ur,ls(p));//,printf("led to:%d %d node:%d\n",l,mid,ls(p));
		if(ur>mid)res+=query(mid+1,r,ul,ur,rs(p));//,printf("res\n");
		return res;
	}
	inline void del(int l,int r,int ul,int ur,int p){
		if(!p)return;
		if(l>=ul&&r<=ur){
			tree[p].gval=0;
			tree[p].val=0;
			tree[p].tag=1;
			return;
		}
		spread(p);
		int mid=l+r>>1;
		if(ul<=mid)del(l,mid,ul,ur,ls(p));
		if(ur>mid)del(mid+1,r,ul,ur,rs(p));
		push_up(p);	
	}
	int kth(int l,int r,int k,int p){
		if(l==r)return l;
		spread(p);
		int mid=l+r>>1;
		if(k<=tree[ls(p)].val)return kth(l,mid,k,ls(p));
		else return kth(mid+1,r,k-tree[ls(p)].val,rs(p)); 
	}
	inline int merge(int x,int y){
		if(!y)return x;
		if(!x)return y;
		spread(x),spread(y);
		tree[x].val+=tree[y].val;
		tree[x].gval+=tree[y].gval;
		ls(x)=merge(ls(x),ls(y));
		rs(x)=merge(rs(x),rs(y));
		return x;
	}
	double getlog(int p){
		return tree[p].gval;
	}
	int getsum(int p){
		return tree[p].val;
	}
}ST;
int main(){
	scanf("%d",&T);
	while(T--){
		int opt,x,y,a,b,k;
		scanf("%d",&opt);
		if(opt==1){
			scanf("%d",&x);
			ST.update(1,inf,x,log(x),1,rt[++n]);
			father[n]=n;
			//printf("nowdot:%d updatedval:%d\n",n,ST.query(1,inf,x,x,rt[n]));
		}
		if(opt==2){
			scanf("%d%d",&x,&y);
			x=getf(x),y=getf(y);
			if(x!=y){
				father[y]=x;
				rt[x]=ST.merge(rt[x],rt[y]);
			//	printf("eff merged:%d & %d,newroot:%d\n",x,y,rt[x]);
			}
		}
		if(opt==3){
			scanf("%d%d",&a,&x);
			a=getf(a);
			int val=ST.query(1,inf,1,x,rt[a]);
			ST.del(1,inf,1,x,rt[a]);
			ST.update(1,inf,x,log(x),val,rt[a]);
		}
		if(opt==4){
			scanf("%d%d",&a,&x);
			a=getf(a);
			int val=ST.query(1,inf,x,inf,rt[a]);
			ST.del(1,inf,x,inf,rt[a]);
			ST.update(1,inf,x,log(x),val,rt[a]);
		}
		if(opt==5){
			scanf("%d%d",&a,&k);
			a=getf(a);
		//	printf("father=%d,root:%d\n",a,rt[a]);
			printf("%d\n",ST.kth(1,inf,k,rt[a]));
		}
		if(opt==6){
			scanf("%d%d",&a,&b);
			a=getf(a),b=getf(b);
			printf("%d\n",ST.getlog(rt[a])>ST.getlog(rt[b]));
		}
		if(opt==7){
			scanf("%d",&a);
			a=getf(a);
			printf("%d\n",ST.getsum(rt[a]));
		}
	}	
	return 0;
}

一棵线段树合并中其实同时存在了好多线段树。

大融合

之前跟同学玩ygo白龙卡组必塞一张大融合。

但是这道题跟ygo没有关系,令人忍俊不禁。

单次查询 \((x,y)\) 输出的答案是断开改边后两点所属联通块大小之积,边是肯定不能真断的,考虑用子树大小硬凑。

子树大小难以在线更新,所以题解提出:先离线操作,建好最终的树(森林)后,用线段树合并与并查集充当连边操作。然对于查询操作,考虑对最终的树跑 \(dfs\) 序,对边 \((u,v)\) 负载的运算有公式:

\(\forall dfn_u<dfn_v,ans=(ST.query(1,dfn[v]-1,rt_{x})+ST.query(dfn[v]+siz[v],n,rt_{x}))*(ST.query(dfn[v],dfn[v]+siz[v]-1,rt_x))\)

坑点:没事不要用父亲节点 \(u\) 表示公式,\(u\) 不止一个儿子因此其所属子树节点编号最高能到 \(dfn[v]-1\),进而其子树大小 \(siz[u] \neq siz[v]+1\)

因为线段树还没有合并,所以 \(1\sim dfn[v]\) 等的遍历不会获得最终的节点个数。很有意思。

#include<bits/stdc++.h>
#define MAXN 100005
#define int long long
using namespace std;
int n,q;
vector<int>edge[MAXN];
struct node{
	int type;
	int x,y;
}wzw[MAXN];
int father[MAXN];
int getf(int x){
	if(father[x]==x)return x;
	else return father[x]=getf(father[x]);
}
int dfn[MAXN],siz[MAXN],tim,efa[MAXN];
void dfs(int u,int fa){
	dfn[u]=++tim;
	efa[u]=fa;
	siz[u]=1;
	for(int i=0;i<edge[u].size();i++){
		int v=edge[u][i];
		if(v==fa)continue;
		dfs(v,u);
		siz[u]+=siz[v];
	}	
}
int rt[MAXN];
struct Segment_Tree{
	#define ls(p) tree[p].lson
	#define rs(p) tree[p].rson
	#define push_up(p) tree[p].val=tree[ls(p)].val+tree[rs(p)].val
	int tot;
	struct TREE{
		int lson,rson;
		int val;
	}tree[MAXN*20];
	inline void update(int l,int r,int x,int &p){
		if(!p)p=++tot;
		if(l==r){
			++tree[p].val;
			return;
		}
		int mid=l+r>>1;
		if(x<=mid)update(l,mid,x,ls(p));
		else update(mid+1,r,x,rs(p));
		push_up(p);
	}
	inline int query(int l,int r,int ul,int ur,int p){
		if(!p)return 0;
		if(l>=ul&&r<=ur)return tree[p].val;
		int mid=l+r>>1;
		int res=0;
		if(ul<=mid)res+=query(l,mid,ul,ur,ls(p));
		if(ur>mid)res+=query(mid+1,r,ul,ur,rs(p));
		return res;
	}
	inline int merge(int x,int y){
		if(!x)return y;
		if(!y)return x;
		ls(x)=merge(ls(x),ls(y));
		rs(x)=merge(rs(x),rs(y));
		push_up(x);
		return x;
	}
}ST;
signed main(){
	scanf("%lld%lld",&n,&q);
	for(int i=1;i<=n;i++)father[i]=i;
	for(int i=1,u,v;i<=q;i++){
		char opt;
		cin>>opt;
		if(opt=='A'){
			scanf("%lld%lld",&u,&v);
			edge[u].push_back(v);
			edge[v].push_back(u);
			wzw[i].type=1;
			wzw[i].x=u,wzw[i].y=v;	
		}
		else{
			scanf("%lld%lld",&u,&v);
			wzw[i].x=u,wzw[i].y=v;
		}
	}
	for(int i=1;i<=n;i++)if(!dfn[i])dfs(i,0);
	for(int i=1;i<=n;i++)ST.update(1,n,dfn[i],rt[i]);
	for(int i=1,opt,u,v;i<=q;i++){
		u=wzw[i].x,v=wzw[i].y,opt=wzw[i].type;
		if(opt){
			u=getf(u),v=getf(v);
			father[v]=u;
			rt[u]=ST.merge(rt[u],rt[v]);
		}
		else{
			if(v==efa[u])swap(u,v);
			int fx=getf(u),fy=getf(v);
			int val1=ST.query(1,n,dfn[v],dfn[v]+siz[v]-1,rt[fx]);
			int val2=ST.query(1,n,1,dfn[v]-1,rt[fx])+ST.query(1,n,dfn[v]+siz[v],n,rt[fx]);
		//	printf("%lld %lld\n",val1,val2);
			printf("%lld\n",val1*val2);
		}
	}
	return 0;
}

实力足够后会添加:线段树合并 维护 dp

posted @ 2024-02-20 19:33  RVm1eL_o6II  阅读(3)  评论(0编辑  收藏  举报