首页 > 其他分享 >点分治入门

点分治入门

时间:2023-09-06 13:58:42浏览次数:33  
标签:子树 入门 int 复杂度 分治 solve now 节点

点分治,顾名思义,对树上的节点进行分治。其实就是把一棵树拆成几棵分别处理。

要把一棵树分成几棵怎么做?显然选一个节点做根节点,分出它的子树就行。不难发现,这个点的选取对时间复杂度影响很大,去一个极端例子:一条链
image

如果选取1,5号节点,时间复杂度是O(n),而如果选取3号节点,时间复杂度是O(log n)影响整体时间复杂度的是分出的最大的子树,所以目标是使最大子树最小。

最大子树最小,不就是找重心吗?树的重心的定义就是其所有的子树中最大的子树节点数最少。怎么求?直接dfs,下面是代码:

void get_rt(int now,int fa){//now为当前根节点
	siz[now]=1;//子树大小
	son[now]=0;//最大子树的大小
	for(int i=head[now];i;i=nxt[i]){
		int to=nbr[i];
		if(to==fa||vis[to]) continue;//vis下面会用到,访问过了就不用再访问
		get_rt(to,now);
		siz[now]+=siz[to];
		son[now]=max(son[now],siz[to]);
	}
	son[now]=max(son[now],size-siz[now]);//当前节点若作为根节点,那么其父节点也是它的儿子
	if(son[now]<maxn){
		maxn=son[now];
		root=now;//更改重心
	}
}

下面是点分治的正式部分,先放代码:

void divide(int now){
	vis[now]=true;
	solve(now,1,0);//当前节点的答案 
	for(int i=head[now];i;i=nxt[i]){
		int to=nbr[i];
		if(vis[to]) continue;//访问过 
		solve(to,-1,edge[i]);//去重,既经过了now也经过v[now][i].to 
		maxn=0x3f3f3f3f,root=0,size=siz[to];//更新
		get_rt(to,0);//重新找重心 
		divide(root);
	}
}

先解释一下solve里的1,-1是为了方便处理第二个solve里的去重,计算答案的时候加-1。具体的solve因题而异。

计算now的贡献不是只要处理now就可以了,给一棵树你就明白为什么要去重了:
image
这个图里计算1的贡献时肯定会有一条1 2 5 6的路径,也有一条1 2 5的路径。最后算答案的时候肯定要拿两条链合并的,不然像2 1 3这种贡献就没法计算。那么1 2 5 6可以和1 2 5合并吗?显然不能,毕竟都在根节点的一棵子树里。至于solve的第三个参数。你考虑现在要在1的答案里减去子树2的答案。其实从12已经有了一个边权,长度从1->2的长度开始计算,也就是代码里的edge[i]

现在你已经会了点分治的基本操作,来看一道板题:
P3806 【模板】点分治1
题意:给定一棵有n个点的树,询问树上距离为k的点对是否存在。

先看数据加强前的代码,主要就是solve函数不同,我们找一个桶存当前点所有的链的长度,双层循环匹配任意两条链。

void query(int now,int fa,int use){//dfs找出所有链的长度
	 stk[++top]=use;
	for(int i=head[now];i;i=nxt[i]){
		int to=nbr[i];
		if(vis[to] || to==fa) continue;
		query(to,now,use+edge[i]);
	}
}

void solve(int now,int f,int use){
	top=0;
	query(now,0,use);
	for(int i=1;i<top;i++){
		for(int j=i+1;j<=top;j++){
			ans[stk[i]+stk[j]]+=f;//ans数组不能用bool,还有删除
		}
	}
}

下面是完整的代码:

$Talk$ $is$ $cheap,$ $show$ $me$ $your$ $code.$(点我看代码)
#include<bits/stdc++.h>
using namespace std;

const int N=10005;
int ans[10000005];
int nbr[N<<1],head[N],nxt[N<<1],edge[N<<1];
int siz[N],son[N],stk[N];bool vis[N];
int n,m,size,maxn,root,tot,top;

inline int read()
{
char ch=getchar();bool f=0;int x=0;
for(;!isdigit(ch);ch=getchar())if(ch=='-')f=1;
for(;isdigit(ch);ch=getchar())x=(x<<1)+(x<<3)+(ch^48);
if(f==1)x=-x;return x;
}

inline void add(int from,int to,int val){
	nbr[++tot]=to,nxt[tot]=head[from],head[from]=tot,edge[tot]=val;
	nbr[++tot]=from,nxt[tot]=head[to],head[to]=tot,edge[tot]=val;
}

void get_rt(int now,int fa){
	//cout<<now<<" "<<fa<<endl;
	siz[now]=1;//子树大小
	son[now]=0;//最大子树的大小
	for(int i=head[now];i;i=nxt[i]){
		int to=nbr[i];
		if(to==fa||vis[to]) continue;
		get_rt(to,now);
		siz[now]+=siz[to];
		son[now]=max(son[now],siz[to]);
	}
	son[now]=max(son[now],size-siz[now]);
	if(son[now]<maxn){
		maxn=son[now];
		root=now;//更改重心
	}
}

void query(int now,int fa,int use){
	 stk[++top]=use;
	for(int i=head[now];i;i=nxt[i]){
		int to=nbr[i];
		if(vis[to] || to==fa) continue;
		query(to,now,use+edge[i]);
	}
}

void solve(int now,int f,int use){
	top=0;
	query(now,0,use);
	for(int i=1;i<top;i++){
		for(int j=i+1;j<=top;j++){
			ans[stk[i]+stk[j]]+=f;
		}
	}
}

void divide(int now){
	vis[now]=true;
	solve(now,1,0);//当前节点的答案 
	for(int i=head[now];i;i=nxt[i]){
		int to=nbr[i];
		if(vis[to]) continue;//访问过 
		solve(to,-1,edge[i]);//去重,既经过了now也经过v[now][i].to 
		maxn=0x3f3f3f3f,root=0,size=siz[to];//更新
		get_rt(to,0);//重新找重心 
		divide(root);
	}
}

int main(){
	freopen("test.in","r",stdin);
	//freopen("test.out","w",stdout);
	n=read();
	m=read();
	for(int i=1;i<n;i++){
		int u=read(),v=read(),w=read();
		add(u,v,w);
	}
	root=0;
	maxn=0x3f3f3f3f;
	size=n;
	get_rt(1,0);
	divide(root);
	//cout<<"UES"<<endl;
	while(m--){
		int k;
		k=read();
		puts(ans[k]?"AYE":"NAY");
	}
	return 0;
}

有一个细节就是树的所有边权权值总和达到了\(1e8\),所以\(ans\)数组要开到\(1e8\)

然后你惊讶的发现:

image

这是必然的,本来solve就是\(n^2\)级别的,还带一个log的常数,肯定跑不了1e4

你考虑这样一件事,就是说:时间复杂度到底为什么会多,不就是两条链长度之和大量的重复,这是完全不必要的,因为题目只在乎有没有长度为k的点对,只要解决去重的问题就好办了。

考虑如果没有去重,可以根据到当前节点的距离排序,可以类似双指针解决。当前两条链的长度之和短了左端点右移,长了右端点左移,把询问离线下来复杂度一次log n总复杂度O(nlogn)

只剩最后一个问题,怎么去重?只要多维护一个数组维护每一个在当前节点子树内的节点在当前节点的哪一个子树内就可以了。

点击查看代码
#include<bits/stdc++.h>
using namespace std;

const int N=10005;
const int M=105;
int ans[10000005];
int nbr[N<<1],head[N],nxt[N<<1],edge[N<<1];
int siz[N],son[N],stk[N];bool vis[N];
int n,m,size,root,tot,top;
int a[N],d[N],b[N];
bool ok[N];
int que[M];

inline int read()
{
char ch=getchar();bool f=0;int x=0;
for(;!isdigit(ch);ch=getchar())if(ch=='-')f=1;
for(;isdigit(ch);ch=getchar())x=(x<<1)+(x<<3)+(ch^48);
if(f==1)x=-x;return x;
}

inline void add(int from,int to,int val){
	nbr[++tot]=to,nxt[tot]=head[from],head[from]=tot,edge[tot]=val;
	nbr[++tot]=from,nxt[tot]=head[to],head[to]=tot,edge[tot]=val;
}

bool cmp(int x,int y){
	return d[x]<d[y];
}

void get_rt(int now,int fa){
	//cout<<now<<" "<<fa<<endl;
	siz[now]=1;//子树大小
	son[now]=0;//最大子树的大小
	for(int i=head[now];i;i=nxt[i]){
		int to=nbr[i];
		if(to==fa||vis[to]) continue;
		get_rt(to,now);
		siz[now]+=siz[to];
		son[now]=max(son[now],siz[to]);
	}
	son[now]=max(son[now],size-siz[now]);
	if(!root || son[now]<son[root]){
		root=now;//更改重心
	}
}

void query(int now,int fa,int use,int from){
	a[++top]=now;//在当前节点子树内的节点 
	d[now]=use;//距离 
	b[now]=from;//所在子树 
	for(int i=head[now];i;i=nxt[i]){
		int to=nbr[i];
		if(vis[to] || to==fa) continue;
		query(to,now,use+edge[i],from);
	}
}

void solve(int now){
	top=0;
	a[++top]=now;//别忘了加自己 
	d[now]=0;
	b[now]=now;
	for(int i=head[now];i;i=nxt[i]){
		int to=nbr[i];
		if(vis[to]) continue;
		query(to,now,edge[i],to);
	}
	sort(a+1,a+1+top,cmp);//排序 
	for(int i=1;i<=m;i++){
		int l=1,r=top;
		if(ok[i]) continue;
		while(l<r){
			if(d[a[l]]+d[a[r]]>que[i]) r--;//右端点左移 
			else if(d[a[l]]+d[a[r]]<que[i]) l++;//左端点右移 
			else if(b[a[l]]==b[a[r]]){//在同一棵子树里 
				if(d[a[r]]==d[a[r-1]]) r--;
				else l++;
			}
			else{
				ok[i]=true;//第i个询问结果为真 
				break;
			}
		}
	}
}

void divide(int now){
	vis[now]=true;
	solve(now);//当前节点的答案 
	for(int i=head[now];i;i=nxt[i]){
		int to=nbr[i];
		if(vis[to]) continue;//访问过 
		root=0,size=siz[to];//更新
		get_rt(to,0);//重新找重心 
		divide(root);
	}
}

int main(){
	//freopen("test.in","r",stdin);
	//freopen("test.out","w",stdout);
	n=read();
	m=read();
	for(int i=1;i<n;i++){
		int u=read(),v=read(),w=read();
		add(u,v,w);
	}
	root=0;
	size=n;
	//cout<<"UES"<<endl;
	for(int i=1;i<=m;i++){
		que[i]=read();
		if(!que[i]) ok[i]=true;
	}
	get_rt(1,0);
	divide(root);
	for(int i=1;i<=m;i++){
		if(ok[i]) cout<<"AYE"<<endl;
		else cout<<"NAY"<<endl;
	}
	return 0;
}

标签:子树,入门,int,复杂度,分治,solve,now,节点
From: https://www.cnblogs.com/wangwenhan/p/17681996.html

相关文章

  • k8s 入门到实战--部署应用到 k8s
    背景最近这这段时间更新了一些k8s相关的博客和视频,也收到了一些反馈;大概分为这几类:公司已经经历过服务化改造了,但还未接触过云原生。公司部分应用进行了云原生改造,但大部分工作是由基础架构和运维部门推动的,自己只是作为开发并不了解其中的细节,甚至k8s也接触不到。还处......
  • vuejs3.0 从入门到精通——项目创建
    项目创建 完成VueCLI脚手架的安装后,即可快速构建一个全新Vue.js项目,包括可初始化项目的整体结构、依赖包、插件等相关工作。一、命令构建项目1.1、创建项目:[root@JumperServer:project_vue]#vuecreatevue3-element-plus-adminVueCLIv5.0.8?Pleasepickapr......
  • 1分钟带你入门RequireJs并了解FastAdmin中JS是如何调用的
    1分钟带你入门RequireJs并了解FastAdmin中JS是如何调用的发布于2018-08-2522:22:57使用fastadmin,前端方面第一个难点就是requirejs,这是一个强大却鲜为人知(对于后端开发人员来说)的框架。在fastadmin交流群混了一段时间,发现不少小白总在问一些很基础的问题,本人实在看不下去了,......
  • vuejs3.0 从入门到精通——脚手架安装
    脚手架安装 VueCLI是基于Vue.js进行快速开发的完整系统,支持搭建交互式项目、快速开始零配置原型开发、丰富的官方插件集合,以及完全图形化地创建和管理Vue.js项目的用户界面。 VueCLI致力于将Vue.js生态中的工具基础标准化,它确保各种构件工具基于智能的默认配置即......
  • C#入门之基础知识
    基本结构一个C#程序主要包括以下部分:命名空间声明(Namespacedeclaration)一个classClass方法Class属性一个Main方法语句(Statements)&表达式(Expressions)注释对比于java语言,c#可以说非常相似java的package相似于c#的命名空间java的类和c#的类一样,并且对于一个c#......
  • AI 入门课程
    PracticalAIforTeachersandStudentsPracticalAIforTeachersandStudents面向教师和学生的实用AI不太深入的一个简单入门介绍;可以了解一些;欢迎关注公-众-号【TaonyDaily】、留言、评论,一起学习。Don’treinventthewheel,librarycodeistheretohelp.文......
  • 《python从入门到实践》第七章习题记录
    点击查看代码#7-1汽车租赁:编写一个程序,询问用户要租赁什么样的汽车,并打印一条消息,如“LetmeseeifIcanfindyouaSubaru”。car=input("whichcardoyoulike?>")print(f"LetmeseeifIcanfindyoua{car}")#7-2餐馆订位:编写一个程序,询问用户有多少人用......
  • OpenTK 入门 Vsync 垂直同步对刷新率的影响
    本文将和大家介绍Vsync垂直同步的开启对OpenTK应用的刷新率的影响在上一篇博客OpenTK入门初始化窗口告诉了大家如何初始化OpenTK承载OpenGL的窗口的应用,在上一篇博客基础上,咱尝试修改创建MainWindow的参数,从而测试Vsync垂直同步对刷新率的影响回顾上一篇博客提......
  • 【C++STL基础入门】队列的基础使用
    提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档@TOC前言C++标准模板库(STL)提供了一系列强大的容器和算法,方便我们在编程中处理数据和实现各种功能。其中,queue(队列)是STL中的一个重要容器,用于按照先进先出(FIFO)的顺序处理元素。本文将介绍queue的基础使用方法,帮助读者初......
  • 代码审计入门之XHCMS
    啥是xhcms熊海CMS是由熊海开发的一款可广泛应用于个人博客,个人网站,企业网站的一套网站综合管理系统,采用了前后端整套,只需要环境Apapche+Mysql+PHP5即可开箱即用。现在好像停止维护了工具准备seay源代码审计系统环境安装环境下载:https://www.lanzoux.com/izeFjfxbxah......