【XSY3971】不难题(点分治)

树分治 同时被 2 个专栏收录
4 篇文章 0 订阅
2 篇文章 0 订阅

题面

不难题

题解

百年未有之写点分……

好久没写了,也当复习了一遍吧。

对于树上的一个扫描半径为 d d d 的在 u u u 节点的雷达,我们将其所能覆盖到的点的集合称作 “圆 ( u , d ) (u,d) (u,d)”。

那么题目就是询问有多少个点至少被 k k k 个给定的圆中的 k − 1 k-1 k1 个圆的交集包含。

显然,对于两个圆 ( A , d a ) (A,d_a) (A,da) ( B , d b ) (B,d_b) (B,db),我们容易得到它们的交:

  • 若这两圆相离,那么它们的交为空集。

  • 若这两圆中其中一个包含另一个,那么它们的交为被包含的那个圆。

  • 若这两圆相交,那么它们的交仍为一个圆,只不过这个圆的圆心可能并不在点上而已:

    在这里插入图片描述

    所以我们考虑拆点:在每一条边上都新增一个虚点。这样就能保证题目给的两个圆的交集的圆心在一个点上了。但这样能保证大于等于 2 2 2 个圆的交集的圆心也一定在某个点上吗?

    假设当前已经合并了若干个圆得到了圆 ( A , d a ) (A,d_a) (A,da) 和圆 ( B , d b ) (B,d_b) (B,db),满足 A A A B B B 都在点上。

    我们将两圆合并,设两圆的交集为 ( M , d m ) (M,d_m) (M,dm)。设 A A A M M M 的距离为 d i s a dis_a disa B B B M M M 的距离为 d i s b dis_b disb A A A B B B 的距离为 d i s dis dis,其中 d a , d b , d i s d_a,d_b,dis da,db,dis 都是已知的。那么我们要解的其实是这么一个方程:
    { d i s a + d i s b = d i s d a − d i s a = d b − d i s b \begin{cases} dis_a+dis_b=dis\\ d_a-dis_a=d_b-dis_b \end{cases} {disa+disb=disdadisa=dbdisb
    得到:
    { d i s a = d i s + d a − d b 2 d i s b = d i s + d b − d a 2 \begin{cases} dis_a=\dfrac{dis+d_a-d_b}{2}\\ dis_b=\dfrac{dis+d_b-d_a}{2} \end{cases} disa=2dis+dadbdisb=2dis+dbda
    那么 M M M 在点上等价于 d i s a dis_a disa 是整数,即 d i s + d a − d b dis+d_a-d_b dis+dadb 为偶数。

    我们不妨设置一个函数 onvir ⁡ ( u ) \operatorname{onvir}(u) onvir(u) 表示 u u u 是否在我们拆边建出来的虚点上,那么显然 d i s dis dis 的奇偶性和 onvir ⁡ ( A ) + onvir ⁡ ( B ) \operatorname{onvir}(A)+\operatorname{onvir}(B) onvir(A)+onvir(B) 的奇偶性相同,那么 d i s + d a − d b dis+d_a-d_b dis+dadb 的奇偶性和 ( onvir ⁡ ( A ) + d a ) + ( onvir ⁡ ( B ) − d b ) (\operatorname{onvir}(A)+d_a)+(\operatorname{onvir}(B)-d_b) (onvir(A)+da)+(onvir(B)db) 的奇偶性相同。

    考虑归纳证明对于合并出来的圆 O O O onvir ⁡ ( O ) \operatorname{onvir}(O) onvir(O) d o d_o do 奇偶性始终相同:

    • 对于初始状态,圆 O O O 是题目给出的圆, onvir ⁡ ( O ) = 0 \operatorname{onvir}(O)=0 onvir(O)=0 d o d_o do 为偶数。

    • 对于某两个圆 A A A B B B,如果它们都满足条件,那么对于 A , B A,B A,B 合并出来的新圆 M M M 来说:
      { onvir ⁡ ( M ) ≡ onvir ⁡ ( A ) + d i s a ( m o d 2 ) d m = d a − d i s a \begin{cases} \operatorname{onvir}(M)\equiv \operatorname{onvir}(A)+dis_a\pmod 2\\ d_m=d_a-dis_a \end{cases} {onvir(M)onvir(A)+disa(mod2)dm=dadisa
      (这里借用了上面推导的式子)

      易知 onvir ⁡ ( M ) \operatorname{onvir}(M) onvir(M) d m d_m dm 奇偶性相同当且仅当 onvir ⁡ ( A ) \operatorname{onvir}(A) onvir(A) d a d_a da 奇偶性相同。

      证毕。

    所以 ( onvir ⁡ ( A ) + d a ) + ( onvir ⁡ ( B ) − d b ) (\operatorname{onvir}(A)+d_a)+(\operatorname{onvir}(B)-d_b) (onvir(A)+da)+(onvir(B)db) 始终为偶数,那么 d i s a dis_a disa 始终为整数,那么合并出来的圆的圆心始终在点上。

现在我们可以在 O ( log ⁡ n ) O(\log n) O(logn) 甚至 O ( n log ⁡ n ) − O ( 1 ) O(n\log n)-O(1) O(nlogn)O(1) 的时间内在线维护两个圆的交了。

然后 “至少被 k − 1 k-1 k1 个圆包含” 可以容斥:设 a n s i ans_i ansi 表示被除了第 i i i 个圆以外的圆都包含的点数, a n s ans ans 表示被所有圆都包含的点数,那么答案即为 ∑ i = 1 k a n s i − k ⋅ a n s \sum\limits_{i=1}^kans_i-k\cdot ans i=1kansikans。现在考虑如何维护。

首先 “除了第 i i i 个圆以外的圆的交集” 可以用前后缀合并处理出来,接下来的问题就是求树上一个圆所包含的点数,直接点分治即可。

大概就是先预处理出当前子树的答案,再往上跳处理父亲的答案,注意除去这个子树的答案的重复贡献,然后再类似地往上跳。

可以结合线段树类似地分析时间复杂度(一层 O ( n ) O(n) O(n),一共 log ⁡ n \log n logn 层)。

代码如下:

#include<bits/stdc++.h>

#define LN 18
#define N 200010
#define K 300010
#define INF 0x7fffffff

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;
int cnt,head[N],nxt[N<<1],to[N<<1];

void adde(int u,int v)
{
	to[++cnt]=v;
	nxt[cnt]=head[u];
	head[u]=cnt;
}

namespace LCA
{
	int pow2[LN];
	int d[N],fa[N][LN];
	void dfs(int u)
	{
		for(int i=1;i<=17;i++)
			fa[u][i]=fa[fa[u][i-1]][i-1];
		for(int i=head[u];i;i=nxt[i])
		{
			int v=to[i];
			if(v==fa[u][0]) continue;
			fa[v][0]=u;
			d[v]=d[u]+1;
			dfs(v);
		}
	}
	int getlca(int a,int b)
	{
		if(d[a]<d[b]) swap(a,b);
		for(int i=17;i>=0;i--)
			if(d[fa[a][i]]>=d[b])
				a=fa[a][i];
		if(a==b) return a;
		for(int i=17;i>=0;i--)
			if(fa[a][i]!=fa[b][i])
				a=fa[a][i],b=fa[b][i];
		return fa[a][0];
	}
	int getdis(int a,int b,int lca=-1)
	{
		if(lca==-1) lca=getlca(a,b);
		return d[a]+d[b]-2*d[lca];
	}
	int jump(int u,int d)
	{
		for(int i=17;i>=0;i--)
		{
			if(d>=pow2[i])
			{
				u=fa[u][i];
				d-=pow2[i];
			}
		}
		return u;
	}
	void init()
	{
		pow2[0]=1;
		for(int i=1;i<=17;i++)
			pow2[i]=pow2[i-1]<<1;
		d[1]=1;
		dfs(1);
	}
}

namespace Tree
{
	int nn,rt,size[N],maxn[N];
	int nt,num[N],tim[N];
	bool vis[N];
	int fa[N];
	vector<int>v1[N],v2[N];
	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[v]) continue;
			getsize(v,u);
			size[u]+=size[v];
		}
	}
	void getroot(int u,int fa)
	{
		size[u]=1,maxn[u]=0;
		for(int i=head[u];i;i=nxt[i])
		{
			int v=to[i];
			if(v==fa||vis[v]) continue;
			getroot(v,u);
			size[u]+=size[v];
			maxn[u]=max(maxn[u],size[v]);
		}
		maxn[u]=max(maxn[u],nn-size[u]);
		if(maxn[u]<maxn[rt]) rt=u;
	}
	void getdis(int u,int fa,int dis)
	{
		if(tim[dis]!=nt)
		{
			tim[dis]=nt;
			num[dis]=0;
		}
		if(u<=n) num[dis]++;
		for(int i=head[u];i;i=nxt[i])
		{
			int v=to[i];
			if(v==fa||vis[v]) continue;
			getdis(v,u,dis+1);
		}
	}
	void turn(vector<int> &v)
	{
		int now=0;
		for(int i=0;tim[i]==nt;i++)
		{
			now+=num[i];
			v.push_back(now);
		}
	}
	void build(int u)
	{
		nt++,getdis(u,0,0);
		turn(v1[u]);
		getsize(u,0);
		vis[u]=1;
		for(int i=head[u];i;i=nxt[i])
		{
			int v=to[i];
			if(vis[v]) continue;
			rt=0,nn=size[v];
			getroot(v,0);
			fa[rt]=u;
			nt++,getdis(v,0,0);
			turn(v2[rt]);
			build(rt);
		}
	}
	void init()
	{
		maxn[0]=INF;
		rt=0,nn=(n<<1);
		getroot(1,0);
		build(rt);
	}
	int get(vector<int> &v,int d)
	{
		if(d<0||!v.size()) return 0;
		return v[min(d,(int)v.size()-1)];
	}
	int query(int u,int d)
	{
		if(d<0) return 0;
		int st=u,dd=d,ans=0;
		while(u)
		{
			ans+=get(v1[u],d);
			d=dd-LCA::getdis(fa[u],st);
			ans-=get(v2[u],d-1);
			u=fa[u];
		}
		return ans;
	}
}

struct data
{
	int u,d;
	data(){};
	data(int a,int b){u=a,d=b;}
}p[K],pre[K],suf[K];

data operator + (data a,data b)
{
	if(a.d<0||b.d<0) return data(114514,-1);
	int lca=LCA::getlca(a.u,b.u);
	int dis=LCA::getdis(a.u,b.u,lca);
	if(a.d>dis+b.d) return b;
	if(b.d>dis+a.d) return a;
	int disa=(dis+a.d-b.d)/2,disb=(dis+b.d-a.d)/2;
	int mid;
	if(disa<=LCA::getdis(a.u,lca,lca)) mid=LCA::jump(a.u,disa);
	else mid=LCA::jump(b.u,disb);
	return data(mid,a.d-disa);
}

int query(data a)
{
	return Tree::query(a.u,a.d);
}

int main()
{
	n=read();
	int nn=(n<<1)-1;
	for(int i=1;i<n;i++)
	{
		int u=read(),v=read();
		adde(u,n+i),adde(n+i,u);
		adde(v,n+i),adde(n+i,v);
	}
	LCA::init();
	Tree::init();
	q=read();
	while(q--)
	{
		int k=read();
		for(int i=1;i<=k;i++)
			p[i].u=read(),p[i].d=(read()<<1);
		pre[0]=suf[k+1]=data(1,nn);
		for(int i=1;i<=k;i++)
			pre[i]=pre[i-1]+p[i];
		for(int i=k;i>=1;i--)
			suf[i]=suf[i+1]+p[i];
		int ans=0;
		for(int i=1;i<=k;i++)
			ans+=query(pre[i-1]+suf[i+1]);
		ans-=(k-1)*query(pre[k]);
		printf("%d\n",ans);
	}
	return 0;
}
/*
10
1 3
6 4
9 8
1 8
3 4
2 8
10 3
4 5
8 7
2
3
8 1
3 1
3 2
2
7 3
6 0
*/
/*
5
1 2
1 3
3 4
3 5
114514
2
1 1
5 1
*/
  • 1
    点赞
  • 2
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 1024 设计师:白松林 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值