算重学(2) 函数式编程的发扬光大&点分治&边分治

算重学(2) 函数式编程的发扬光大&点分治&边分治

引入

首先,一个朴素的想法,如何统计树上点对信息?

定义 solve(x) 表示解决以 \(x\) 为根的树的问题。

显然它的答案为 solve(son_x)+儿子间相互的统计

接下来,你考虑断掉 \(x\) 和它的儿子间的边,你去 solve(son_x) 时就显然指的是解决以 \(son_x\) 为根的树的答案了。

点分治

如何优化?

一个朴素的想法,我第一次调用 solve 的时候可以调用其重心!那么复杂度似乎有一点优化!那么,之后调用 solve 的时候为啥不调用重心呢?

为什么我第一次可以调用重心,因为我只需要统计一棵无根树的答案!

那么在你断边之后,显然形成若干棵无根树,答案独立,因此你统计的和上面是一样的!因此你调用其重心开始也显然是正确的!因为你都断边了,各个问题独立了。

性质:所有的点最后都会被叉。即每个点都会被 solve 到。假设没有,则与该点有边的点都不会被 solve 到,于是所有的点都不会被 solve 到。

关于为什么所有点对都能被统计到。

考虑反证。一次 solve(x) 显然使 x 独立了。根据反证,显然 \((x,y)\) 始终在一个连通块。那么它至少是个路径吧!那么显然这些点都还没被叉,假如还没被叉的话,不满足所有的点最后都会被叉的性质。

#include <cstdio>
#include <algorithm>
#include <iostream>
#include <cstring>

#define N 10010
#define BN 10001000

using namespace std;

struct edge {
	int nex,to,w;
}e[N<<1];

int n,m,tmp[BN],judge[BN];
int sz[N],vis[N];
int head[N],q[N];
int size,f[N];
int cnt,rt,dis[N];
int t[BN],ok[N],tot;

int re() {
    int w=1,s=0;char c;
    while(!isdigit(c=getchar())) if(c=='-') w=-1;
    while(isdigit(c)) s=(s<<1)+(s<<3)+(c&15),c=getchar();
    return s*w;
}

void add(int x,int y,int z) {
	e[++cnt].nex=head[x];
	e[cnt].to=y;
	e[cnt].w=z;
	head[x]=cnt;
}

void get_rt(int x,int fa) {
	sz[x]=1; f[x]=0;
	for(int i=head[x];i;i=e[i].nex) {
		int y=e[i].to; if(fa==y||vis[y]) continue;
		get_rt(y,x);
		sz[x]+=sz[y];
		f[x]=max(f[x],sz[y]);
	}
	f[x]=max(f[x],tot-sz[x]);
	if(f[x]<f[rt]) rt=x;
}

void getdis(int x,int fa) {
	tmp[++tmp[0]]=dis[x];
	for(int i=head[x];i;i=e[i].nex) {
		int y=e[i].to;
		if(vis[y]||y==fa) continue;
		dis[y]=dis[x]+e[i].w;
		getdis(y,x);
	}
}

void cal(int x) {
	int p=0;
	for(int i=head[x];i;i=e[i].nex) {
		int y=e[i].to;
		if(vis[y]) continue;
		tmp[0]=0; dis[y]=e[i].w; //y-rt x
		getdis(y,x);
		for(int k=tmp[0];k;k--) {
			for(int l=1;l<=m;l++) {
				if(q[l]>=tmp[k]) ok[l]|=judge[q[l]-tmp[k]];
			}
		}
		for(int k=tmp[0];k;k--) t[++p]=tmp[k],judge[tmp[k]]=1;  //现在的距离就可以搞出来 
	}
	for(int i=p;i;i--) judge[t[i]]=0; //搞回去 
}

void solve(int x) {
	vis[x]=judge[0]=1; cal(x); //计算贡献
	for(int i=head[x];i;i=e[i].nex) {
		int y=e[i].to;
		if(vis[y]) continue;
		tot=sz[y];
		f[rt=0]=0x7fffffff;
		get_rt(y,0);
		solve(rt); //分治求解 
	} 
}

int main() {
	n=re(); m=re();
	int x,y,z;
	for(int i=1;i<n;i++) {
		x=re(); y=re(); z=re();
		add(x,y,z); add(y,x,z);
	}
	for(int i=1;i<=m;i++) {
		q[i]=re();
	}
	
	f[rt=0]=n;
	tot=n;
	get_rt(1,0);
	solve(rt);
	
	for(int i=1;i<=m;i++) if(ok[i]) printf("AYE\n"); else printf("NAY\n");
	return 0;
}

边分治

边分治相对于点分治的优势就是每次你只要考虑两个连通块之间的贡献即可,然后之后便断开。

因此,我 solve 的定义我觉得仍然是一样的,即解决某棵树的问题,然后找边,然后解决两部分相互间的问题,然后断开边,形成 2 棵新的独立树。如是解决下去!

好处就是,点分每次可能多个连通块的答案互相算很麻烦,边分一定只有 2 个!

else

包括像记录每次分治中心(点/边)上树后支持动态修改的,点分树之类的。

posted @ 2022-12-16 12:24  FxorG  阅读(37)  评论(0)    收藏  举报