【XSY4746】会议选址(三度化,边分治,点分治)

这种关键点的重心问题,一般有两种思路。

一种是合并:对于两个不交的点集 S , T S,T S,T S ∪ T S\cup T ST 的重心必在 S , T S,T S,T 重心间的路径上,二分即可,需要数据结构支持 dfn 区间内 S ∪ T S\cup T ST 内的点的个数。

使用该做法能将本题做到 O ( n log ⁡ 3 n + q log ⁡ n ) O(n\log^3n+q\log n) O(nlog3n+qlogn)

另一种想法是,对于单次询问点集 S S S,先从任意一个点开始,考虑往当前最大的子树走,直到最大的子树大小小于等于 S / 2 S/2 S/2

如果用树剖+二分暴躁加速这个过程的话,单次询问还是 O ( log ⁡ 3 n ) O(\log^3n) O(log3n) 的。

优秀的做法是使用三度化和边分治。我们先一一初步介绍。

三度化的具体作用是:将带权树,在不改变任意两点间距离、任意两点间祖先后代关系的情况下,转化成每个点度数至多为 3 3 3 的点数至多翻倍的树.

但三度化会改变两点间的直接父子关系,以及新增一些原树没有的虚点,这些都是需要注意的。

三度化有两种方法,如下图:

在这里插入图片描述

称原树中的点为旧点,新增的点为虚点,三度化后的树为新树。

其中使用第一种方法,能让新树的深度不超过 O ( d log ⁡ n d ) O(d\log \frac{n}{d}) O(dlogdn),其中 d , n d,n d,n 是原树的深度和大小。但这个性质和本题没有关系。

边分治是如下的过程:

假设当前树大小为 n n n,每个点的度数不超过 D D D。找到一条边使得分出来两棵树的大小相差最小,设找到的边是 ( u , v ) (u,v) (u,v),且 u u u 对应的那棵子树的大小更大,设为 S S S

u u u 的各个儿子的子树大小为 s 1 , ⋯   , s D − 1 s_1,\cdots,s_{D-1} s1,,sD1(不足 D − 1 D-1 D1 个儿子用 0 0 0 补齐),那么显然对于任意 i i i 满足 s i ≤ n − s i s_i\leq n-s_i sinsi,进一步地,应满足 ( n − s i ) − s i ≥ S − ( n − S ) (n-s_i)-s_i\geq S-(n-S) (nsi)siS(nS),得到 S + s i ≤ n S+s_i\leq n S+sin

将这 D − 1 D-1 D1 条不等式加起来,得到 ( D − 1 ) S + ∑ s i ≤ ( D − 1 ) n (D-1)S+\sum s_i\leq (D-1)n (D1)S+si(D1)n,即 D S − 1 ≤ ( D − 1 ) n DS-1\leq (D-1)n DS1(D1)n,那么 S ≤ D − 1 D n + 1 S\leq\frac{D-1}{D}n+1 SDD1n+1

从而,在将树三度化的基础上,按上述方法找到的边,能使得分成的两棵子树大小都不超过 2 3 n + 1 \frac{2}{3}n+1 32n+1

于是边分树的深度是 O ( log ⁡ 3 2 n ) = O ( log ⁡ n log ⁡ 3 2 ) = O ( log ⁡ n ) O(\log_{\frac{3}{2}}n)=O\left(\dfrac{\log n}{\log\frac{3}{2}}\right)=O(\log n) O(log23n)=O(log23logn)=O(logn) 级别的。

具体回到这题。我们考虑用如下的方式找原树上若干关键点的重心:对于每一条边 ( a , b ) (a,b) (a,b),若 a a a 子树内的关键点个数小于 b b b 子树内的关键点个数,那么令这条边的方向是 a → b a\to b ab;否则,若 b b b 子树内的关键点个数小于 a a a 子树内的关键点个数,那么令这条边的方向是 b → a b\to a ba。若 a , b a,b a,b 子树内的关键点个数相同,那么令这条边无向。

那么可以证明,最后树上肯定形如:存在一个无向边的连通块,然后其余所有边都指向这个连通块。此时连通块内的任意一个点都可以是这些关键点的重心。

考虑将这个方法作用在新树上:那么原树上一条边 ( f a a , a ) (fa_a,a) (faa,a) 的指向,和新树上 ( f a a ′ , a ) (fa_a',a) (faa,a) 的指向相同。

不妨假设新树上那些关键点的重心在 r t rt rt,注意 r t rt rt 可能是虚点。设 r t rt rt 在旧点 u u u 在新树上的子树内,但不在 u u u 任意一个旧儿子在新树上的子树内。考虑 ( f a u , u ) (fa_u,u) (fau,u) 的指向,由于 f a u ′ → u fa_u'\to u fauu,那么 f a u → u fa_u\to u fauu。对于 u u u 的任意旧儿子 v v v,考虑 ( u , v ) (u,v) (u,v) 的指向,由于 v → f a v ′ v\to fa_v' vfav,那么 v → u v\to u vu。从而,我们证明了 u u u 是原树上关键点的重心。

于是我们找出新树上的重心即可。我们从边分树的根开始往下走。每次考虑当前边 e e e 的指向。考虑直接数出 e e e 两边的关键点个数。首先要数 A , B A,B A,B 子树内编号在 [ l , r ] [l,r] [l,r] 内的点数,可以通过 dfn 序+可持久化做到 O ( n log ⁡ n ) − O ( log ⁡ n ) O(n\log n)-O(\log n) O(nlogn)O(logn)。而对于 A , B A,B A,B 外的那些点,它们在边分树上对应不超过 O ( log ⁡ n ) O(\log n) O(logn) 棵子树,且这些子树内的关键点个数我们也可以在走下来的过程中顺便记录,于是我们只需要知道当前这些子树每一棵属于 A A A 还是 B B B 即可:这其实很简单,因为这些子树肯定整体都在 e e e 的一边,所以随便选取一个代表点,利用 dfn 序判断它在 e e e 的哪一边即可。

于是,我们单次可以 O ( log ⁡ 2 n ) O(\log^2n) O(log2n) 找到一个区间的关键点的重心。

再离线下来用点分治求出在原树上对应的重心到所有点的距离,可以做到 O ( ( n + q ) log ⁡ n ) O((n+q)\log n) O((n+q)logn)

从而,我们在 O ( n log ⁡ n + q log ⁡ 2 n ) O(n\log n+q\log^2n) O(nlogn+qlog2n) 的时间内解决这个问题。

#include<bits/stdc++.h>

#define N 100010
#define ll long long

using namespace std;

inline int read()
{
	int x=0,f=1;
	char ch=getchar();
	while(ch<'0'||ch>'9')
	{
		if(ch=='-') f=-1;
		ch=getchar();
	}
	while(ch>='0'&&ch<='9')
	{
		x=(x<<1)+(x<<3)+(ch^'0');
		ch=getchar();
	}
	return x*f;
}

int n,Q;

vector<int> e[N];

namespace P2
{
	struct query{int u,id,coef;};
	vector<query> q[N];
}

namespace P1
{
	#define lc(u) ch[u][0]
	#define rc(u) ch[u][1]
	const int N1=N<<1,N2=N1<<1;
	int n1,cnt=1,head[N1],to[N1<<1],nxt[N1<<1];
	int dfx,fa[N1],in[N1],out[N1],belong[N1];
	void adde(int u,int v)
	{
		to[++cnt]=v,nxt[cnt]=head[u],head[u]=cnt;
		to[++cnt]=u,nxt[cnt]=head[v],head[v]=cnt;
	}
	void D3(int u,int fa)
	{
		vector<int> son;
		for(int v:e[u]) if(v!=fa) son.push_back(v);
		function<int(int,int)> dfs=[&](int l,int r)
		{
			if(l==r) return son[l];
			int mid=(l+r)>>1;
			int x=(l==0&&r==(int)son.size()-1?u:++n1);
			belong[x]=u;
			adde(x,dfs(l,mid)),adde(x,dfs(mid+1,r));
			return x;
		};
		belong[u]=u;
		if(!son.empty())
		{
			if(son.size()==1) adde(u,son[0]);
			else dfs(0,son.size()-1);
		}
		for(int v:son) D3(v,u);
	}
	void dfs(int u)
	{
		in[u]=++dfx;
		for(int i=head[u];i;i=nxt[i])
		{
			int v=to[i];
			if(v==fa[u]) continue;
			fa[v]=u,dfs(v);
		}
		out[u]=dfx;
	}
	int n2,Rt,ch[N2][2],eid[N2];
	int idx,id[N1],L[N2],R[N2];
	int nrt,maxn,nn,size[N1];
	bool vis[N1<<1];
	void getsize(int u,int fa)
	{
		size[u]=1;
		for(int i=head[u];i;i=nxt[i])
		{
			int v=to[i];
			if(v==fa||vis[i]) continue;
			getsize(v,u),size[u]+=size[v];
		}
	}
	void getroot(int u,int fa)
	{
		for(int i=head[u];i;i=nxt[i])
		{
			int v=to[i];
			if(v==fa||vis[i]) continue;
			int x=abs(nn-size[v]-size[v]);
			if(x<maxn) maxn=x,nrt=i;
			getroot(v,u);
		}
	}
	int build(int u)
	{
		getsize(u,0);
		if(size[u]==1)
		{
			id[u]=L[u]=R[u]=++idx;
			return u;
		}
		int x=++n2;
		nn=size[u],maxn=INT_MAX,getroot(u,0);
		eid[x]=nrt,vis[nrt]=vis[nrt^1]=1;
		int a=to[nrt],b=to[nrt^1];
		lc(x)=build(a),rc(x)=build(b);
		L[x]=L[lc(x)],R[x]=R[rc(x)];
		return x;
	}
	int rt[N];
	namespace Seg
	{
		const int NN=10000000;
		int node,ch[NN][2],sum[NN];
		void update(int &u,int lst,int l,int r,int x)
		{
			u=++node,lc(u)=lc(lst),rc(u)=rc(lst),sum[u]=sum[lst]+1;
			if(l==r) return;
			int mid=(l+r)>>1;
			if(x<=mid) update(lc(u),lc(lst),l,mid,x);
			else update(rc(u),rc(lst),mid+1,r,x);
		}
		int query(int u,int l,int r,int ql,int qr)
		{
			if(!u) return 0;
			if(ql<=l&&r<=qr) return sum[u];
			int mid=(l+r)>>1,ans=0;
			if(ql<=mid) ans+=query(lc(u),l,mid,ql,qr);
			if(qr>mid) ans+=query(rc(u),mid+1,r,ql,qr);
			return ans;
		}
		int query(int pl,int pr,int ql,int qr)
		{
			return query(rt[pr],1,n1,ql,qr)-query(rt[pl-1],1,n1,ql,qr);
		}
	}
	struct data{int kp,s;};
	bool judge(int p,int a,int b)
	{
		if(fa[a]==b) return in[a]<=in[p]&&in[p]<=out[a];
		return !(in[b]<=in[p]&&in[p]<=out[b]);
	}
	int query(int u,int pl,int pr,vector<data> c)
	{
		if(u<=n1) return u;
		int a=to[eid[u]],b=to[eid[u]^1];
		int sa=Seg::query(pl,pr,L[lc(u)],R[lc(u)]),ta=0;
		int sb=Seg::query(pl,pr,L[rc(u)],R[rc(u)]),tb=0;
		for(auto now:c) (judge(now.kp,a,b)?ta:tb)+=now.s;
		if(sa+ta>sb+tb)
		{
			c.push_back(data{b,sb});
			return query(lc(u),pl,pr,c);
		}
		else
		{
			c.push_back(data{a,sa});
			return query(rc(u),pl,pr,c);
		}
	}
	void main()
	{
		n1=n,D3(1,0); dfs(1);
		n2=n1,Rt=build(1);
		for(int i=1;i<=n;i++)
			Seg::update(rt[i],rt[i-1],1,n1,id[i]);
		for(int i=1;i<=Q;i++)
		{
			int l=read(),r=read();
			int u=belong[query(Rt,l,r,vector<data>())];
			P2::q[r].push_back(P2::query{u,i,1});
			P2::q[l-1].push_back(P2::query{u,i,-1});
		}
	}
	#undef lc
	#undef rc
}

namespace P2
{
	int fa[N];
	int rt,maxn,nn,size[N];
	bool vis[N];
	vector<int> dis[N];
	void getsize(int u,int fa)
	{
		size[u]=1;
		for(int v:e[u]) if(v!=fa&&!vis[v])
			getsize(v,u),size[u]+=size[v];
	}
	void getroot(int u,int fa)
	{
		int nmax=nn-size[u];
		for(int v:e[u]) if(v!=fa&&!vis[v])
			getroot(v,u),nmax=max(nmax,size[v]);
		if(nmax<maxn) maxn=nmax,rt=u;
	}
	void getdis(int u,int fa,int dis,vector<int> *D)
	{
		D[u].push_back(dis);
		for(int v:e[u]) if(v!=fa&&!vis[v])
			getdis(v,u,dis+1,D);
	}
	void build(int u)
	{
		vis[u]=1;
		getdis(u,0,0,dis);
		for(int v:e[u])
		{
			if(vis[v]) continue;
			getsize(v,0);
			nn=size[v],maxn=INT_MAX,getroot(v,0);
			fa[rt]=u,build(rt);
		}
	}
	struct data
	{
		ll sd; int sz;
		data(){}
		data(ll a,int b){sd=a,sz=b;}
		void operator += (data b){sd+=b.sd,sz+=b.sz;}
		data operator - (data b){return data(sd-b.sd,sz-b.sz);}
		ll calc(int d){return 1ll*d*sz+sd;}
	}s1[N],s2[N];
	ll ans[N];
	void add(int x)
	{
		int u=x;
		s1[u]+=data(0,1);
		for(int i=(int)dis[x].size()-2;i>=0;i--,u=fa[u])
			s1[fa[u]]+=data(dis[x][i],1),s2[u]+=data(dis[x][i],1);
	}
	ll query(int x)
	{
		int u=x;
		ll ans=s1[u].sd;
		for(int i=(int)dis[x].size()-2;i>=0;i--,u=fa[u])
			ans+=(s1[fa[u]]-s2[u]).calc(dis[x][i]);
		return ans;
	}
	void main()
	{
		getsize(1,0);
		nn=size[1],maxn=INT_MAX,getroot(1,0);
		build(rt);
		for(int i=1;i<=n;i++)
		{
			add(i);
			for(auto now:q[i]) ans[now.id]+=now.coef*query(now.u);
		}
		for(int i=1;i<=Q;i++)
			printf("%lld\n",ans[i]);
	}
}

int main()
{
	n=read(),Q=read();
	for(int i=1;i<n;i++)
	{
		int u=read(),v=read();
		e[u].push_back(v),e[v].push_back(u);
	}
	P1::main();
	P2::main();
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值