树上点分治题目以及收获和代码

树点分治

定义

V u V_u Vu表示以 u u u为根的子树中节点的集合。

S u S_u Su表示节点 u u u的直接子节点集。

d i s ( i , j ) dis(i,j) dis(i,j)为节点 i , j i,j i,j之间的距离。

经典例题一

洛谷P3806 【模板】点分治1

题意:给定一棵有 n 个点的树,询问树上距离为 k的点对是否存在。

题目链接:https://www.luogu.com.cn/problem/P3806

问题分析

考虑分治,计算所有以 u u u为根的子树中满足条件的点对数,可以发现所有路径可以分为经过 u u u的和不经过 u u u的,不经过 u u u的路径一定在以 u u u的子节点为根的子树中,可以递归处理;对于经过 u u u的路径,直接统计所有 i , j ∈ V u , d i s ( u , i ) + d i s ( u , j ) = k i,j\in V_u,dis(u,i)+dis(u,j)= k i,jVu,dis(u,i)+dis(u,j)=k ∄ v ∈ S u , i , j ∈ V v \not\exist v\in S_u,i,j\in V_v vSu,i,jVv,无法避免枚举子树中的节点,所以采取容斥的方法来统计,即不考虑 ∄ v ∈ S u , i , j ∈ V v \not\exist v\in S_u,i,j\in V_v vSu,i,jVv的限制,最后再减去处于同一子树中的不合法情况。对于统计点对,我们采取先排序再统计的方式,对于排好序的 d i s dis dis集可以在线性复杂度内完成统计,而每层递归排序的复杂度为 n log ⁡ n n\log n nlogn.所以最后整个分治的复杂度取决于分治的层数。所以对根的选取对于时间复杂度至关重要。采取每次以子树的重心作为根,我们可以证明最后递归的层数不会超过 log ⁡ 2 n \log_2 n log2n,所以整体的时间复杂度为 n log ⁡ 2 n n\log^{2}n nlog2n.

代码如下

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cstdlib>
#include<queue>
#include<set>
#define ll long long
using namespace std;
const int N=1e4+11;
int n,m,cnt,maxn;
int k[101],f[N],sz[N],dis[N],ans[N],head[N],wson[N];
bool use[N];
struct Edge{
	int pre,val,to;
}edge[N<<1];
void addedge(int fr,int to,int val){
	edge[++cnt].pre=head[fr];edge[head[fr]=cnt].to=to;edge[cnt].val=val;
}
int read(){
	int num=0,flag=1;char s=getchar();
	for(;(s>'9'||s<'0')&&s!='-';s=getchar());
	if(s=='-') flag=-1,s=getchar();
	for(;s>='0'&&s<='9';s=getchar()) num=num*10+s-'0';
	return num*flag;
}
void getroot(int s,int pre,int& rt,int siz){
	sz[s]=1;wson[s]=0;f[s]=pre;
	for(int i=head[s];i;i=edge[i].pre){
		int to=edge[i].to;
		if(use[to]||to==pre) continue;
		getroot(to,s,rt,siz);
		sz[s]+=sz[to];
		wson[s]=max(wson[s],sz[to]);
	}
	wson[s]=max(wson[s],siz-sz[s]);
	if(!wson[rt]||wson[s]<wson[rt]) rt=s;
}
void getdis(int s,int pre,int w,int& num){
	if(w>maxn) return;
	dis[++num]=w;
	for(int i=head[s];i;i=edge[i].pre){
		int to=edge[i].to;
		if(use[to]||to==pre) continue;
		getdis(to,s,w+edge[i].val,num);
	}
}
void countPair(int s,int op,int w){
	int num=0;
	getdis(s,0,w,num);
	sort(dis+1,dis+num+1);
	for(int i=1;i<=m;++i){
		int l=1,r=num;
		while(l<r){
			while(l<r&&dis[l]+dis[r]>k[i]) --r;
			while(l<r&&dis[l]+dis[r]==k[i]) ans[i]+=op,--r;
			++l;
		}
	}
}
void solve(int now,int siz){
	int mid=now;now=0;
	getroot(mid,0,now,siz);
	use[now]=1;
	countPair(now,1,0);
	for(int i=head[now];i;i=edge[i].pre){
		int to=edge[i].to;
		if(use[to]) continue;
		countPair(to,-1,edge[i].val);
		solve(to,to==f[now]?siz-sz[now]:sz[to]);
	}
}
int main(){
//  freopen(".in","r",stdin);
//  freopen(".out","w",stdout);
	n=read();m=read();
	for(int i=1;i<n;++i){
		int u=read(),v=read(),w=read();
		addedge(u,v,w);addedge(v,u,w); 
	}
	for(int i=1;i<=m;++i) {
		k[i]=read();
		maxn=max(k[i],maxn);
	}
	solve(1,n);
	for(int i=1;i<=m;++i)
		if(ans[i]) printf("AYE\n");
		else printf("NAY\n");
	return 0;
}

经典例题二

问题描述

有一棵 n n n个节点的树,树上有每个节点有一点权 w i w_i wi,求树上有多少条路径满足路径上的点权异或和在给定的区间 [ l , r ] [l,r] [l,r]上。

问题分析

典型的树上点分治题,对于点分治的做法核心是设计出通过某个节点的路径条数的计数方法,在本例中我们需要找到异或和在给定区间中的路径条数,根据这个特性我们需要对异或和进行二进制拆分,我们便需要引入tri树来方便我们计算不超过某个界的路径异或和条数。

代码如下

#include<iostream>
#include<malloc.h> 
#include<cstdio>
#include<cstring>
#define ll long long
using namespace std;
const int N = 1e5 + 11;
int n, l, r;
int cnt, id;
int head[N],wson[N], fa[N], dis[N], siz[N], val[N];
bool use[N];
struct Edge {
	int pre, to;
}edge[N << 1];
struct Tri {
	Tri* to[2];
	int siz;
};
Tri* newnode() {
	Tri* op = (Tri*)malloc(sizeof(Tri));
	op->siz = 0; op->to[1] = op->to[0] = NULL;
	return op;
}
int read() {
	int num = 0; char s = getchar();
	for (; s > '9' || s < '0'; s = getchar());
	for (; s <= '9' && s >= '0'; s = getchar()) num = num * 10 + s - '0';
	return num;
}
void addedge(int fr, int to) {
	edge[++cnt].pre = head[fr]; edge[head[fr] = cnt].to = to;
}
void findctr(int s, int pre, int num, int& ctr) {
	siz[s] = 1; fa[s] = pre; wson[s] = 0;
	for (int i = head[s]; i; i = edge[i].pre) {
		int to = edge[i].to;
		if (to == pre || use[to]) continue;
		findctr(to, s, num, ctr);
		siz[s] += siz[to];
		wson[s] = max(siz[to], wson[s]);
	}
	wson[s] = max(wson[s], num - siz[s]);
	if (!wson[ctr] || wson[s] < wson[ctr]) ctr = s;
}
void getdis(int s, int sum,int pre, int& num) {
	dis[++num] = val[s] ^ sum;
	for (int i = head[s]; i; i = edge[i].pre) {
		int to = edge[i].to;
		if (!use[to] && to != pre)
			getdis(to, val[s]^sum, s,num);//不要偷懒最好按照定义来 
	}
}
int getans(Tri* now, int num, int bound) {
	int ans = 0;
	for (int i = 31; i >= 0; --i) {
		bool op1 = num & (1 << i);
		bool op2 = bound & (1 << i);
		if (op2&&now->to[op1]!=NULL) ans += now->to[op1]->siz;
		now = now->to[op1 ^ op2];
		if (now == NULL) return ans;
	}
	return ans+now->siz;
}
void insert(Tri* now, int num) {
	for (int i = 31; i >= 0; --i) {
		now->siz++;
		bool op = num & (1 << i);
		if (now->to[op] == NULL)
			now->to[op] = newnode();
		now = now->to[op];
	}
	now->siz++;
}
ll solve(int s, int num, int bound) {
	int ctr = 0;ll ans = 0;
	Tri* rot = newnode();//Tri树
	findctr(s, 0, num, ctr);//找到树的重心节点编号
	use[ctr] = 1;//标记结点以作为重心
	for (int i = head[ctr]; i; i = edge[i].pre) {
		int to = edge[i].to;
		if (use[to]) continue;
		cnt = 0;
		getdis(to, val[ctr], 0,cnt);//计算出子树中每个结点到当前根的异或和
		for (int i = 1; i <= cnt; ++i) {
			ans += getans(rot, dis[i] ^ val[ctr], bound);//统计经过根的路径
			if (dis[i] <= bound)
				++ans;//统计端点为根的路径
		}
		for (int i = 1; i <= cnt; ++i)
			insert(rot, dis[i]);//将异或和插入
	}
	for (int i = head[ctr]; i; i = edge[i].pre) {
		int to = edge[i].to;
		if (use[to]) continue;
		if (to == fa[ctr])
			ans += solve(to, num - siz[ctr], bound);
		else
			ans += solve(to, siz[to], bound);
	}
	return ans;
}
int main() {
	freopen("data.in","r",stdin);
//	freopen("z.out","w",stdout);
	n = read(); l = read(); r = read();
	for (int i = 1; i <= n; ++i)
		val[i] = read();
	for (int i = 1; i < n; ++i) {
		int a = read(), b = read();
		addedge(a, b); addedge(b, a);
	}
	if (l == 0)
		printf("%lld", solve(1, n, r));
	else {
		ll a = solve(1, n, r);
		memset(use, 0, sizeof use);
		printf("%lld", a - solve(1, n, l - 1));//拆分的思路,即当求解某个区间较难时,可以考虑是否可以将问题转化为两个端点的前缀和来表示
	}
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值