点分治与动态点分治学习笔记

点分治

点分治是处理树上路径的一类有力算法。

分治算法我们都经常用到,平时我们在序列上可以直接分治。

但是如果在树上怎么办呢?

我们可以指定一个根递归下去处理子树。

将原来的数分成许多子树,对每个子树分别处理

如果我们随缘指定一个根,递归层数可能是\(n\)一级别的,复杂度显然会退化。

借鉴我们在分治序列时我们每次取\(mid\)的思想。

于是我们在分治树时我们每次取树的重心。

树的重心\(x\)定义为树上的所有点中,删除此节点后最大子树\(siz\)最小的一个点

计算方法就是简单的\(dfs\)

//total为总大小
void GetRoot(int p,int fa)
{
    hson[p]=0,siz[p]=1;
    for(int i=h[p],v=e[i].v;~i;i=e[i].next,v=e[i].v)
        if((fa!=v)&&!vis[v])
            GetRoot(v,p),siz[p]+=siz[v],hson[p]=max(hson[p],siz[v]);
    hson[p]=max(hson[p],total-siz[p]);
    if(hson[p]<hson[Hroot])Hroot=p;
}

于是我们在点分治过程中每次选取子树重心为子树的根,

这样就保证了递归层数不超过\(logn\)层。

大致算法流程

void solve(int x)
{
	vis[x]=1;
	work();
	for(遍历x的出边x->v)
        if(!vis[v])
        {
           	GetRoot(v);
            solve(Hroot);
        }
}

这个算法的难点主要在\(work()\)上,其他的主要是框架。

接下来介绍几道例题作为入门。

题目介绍

\(1\).洛谷P3806【模板】点分治

题目描述

给定一棵有\(n\)个点的树

询问树上距离为\(k\)的点对是否存在。

思想分析

考虑每个路径分为两类,经过根的与不经过根的。

\(dis_i\)\(i\)到当前根的距离。

所有经过根的路径\(u,v\)的长度都可以表示为\(dis_u+dis_v\)

对于不经过根的路径,它一定在根的某个子树中,我们对子树处理一次就可以了。

于是算法的分治思想已经呼之欲出了。

我们对于每个节点统计经过根的路径,然后对子树分治

如何统计经过根的路径?

我们对于每个点开一个\(map\),记录子树中的点到根的\(dis\),扫描子树时,对于子树中的每个\(dis_i\),看看有没有\(k-dis_i\)就可以辣。

\(set\)也可以,似乎快一点的样子。

手写哈希没人拦你

复杂度\(O(nlog^2n)\)可以稳稳通过本题\((n\le10000)\)

使用\(hash\)应该可以降到\(O(nlogn)\)

update:emm好像直接开也开的下来着

代码实现

/*
@Date    : 2019-08-29 21:10:53
@Author  : Adscn (adscn@qq.com)
@Link    : https://www.cnblogs.com/LLCSBlog
*/
#include<bits/stdc++.h>
using namespace std;
#define IL inline
#define RG register
#define gi getint()
#define gc getchar()
#define File(a) freopen(a".in","r",stdin);freopen(a".out","w",stdout)
IL int getint()
{
	RG int xi=0;
	RG char ch=gc;
	bool f=0;
	while(ch<'0'||ch>'9')ch=='-'?f=1:f,ch=gc;
	while(ch>='0'&&ch<='9')xi=(xi<<1)+(xi<<3)+ch-48,ch=gc;
	return f?-xi:xi;
}
template<typename T>
IL void pi(T k,char ch=0)
{
	if(k<0)k=-k,putchar('-');
	if(k>=10)pi(k/10,0);
	putchar(k%10+'0');
	if(ch)putchar(ch);
}
const int MAXN=10007;
struct Edge{
    int v,next,w;
}e[MAXN<<1];
int h[MAXN],cnt,n,m,siz[MAXN],hson[MAXN],Hroot;
char vis[MAXN],ans[107];
int stk[MAXN],top,K[107],dis[MAXN],total;
set<int>f;
IL void add(int u,int v,int w){e[++cnt]=(Edge){v,h[u],w};h[u]=cnt;}
IL void GetRoot(int p,int fa)
{
    hson[p]=0,siz[p]=1;
    for(int i=h[p],v=e[i].v;~i;i=e[i].next,v=e[i].v)
        if((fa^v)&&!vis[v])
            GetRoot(v,p),siz[p]+=siz[v],hson[p]=max(hson[p],siz[v]);
    hson[p]=max(hson[p],total-siz[p]);
    if(hson[p]<hson[Hroot])Hroot=p;
}
IL void dfs(int p,int fa)
{
    stk[++top]=dis[p];
    for(int i=h[p],v=e[i].v;~i;i=e[i].next,v=e[i].v)
        if((fa^v)&&!vis[v])
            dis[v]=dis[p]+e[i].w,dfs(v,p);
}
IL void Query(int x){for(int i=m;i;i--)ans[i]|=*f.lower_bound(K[i]-x)==K[i]-x;}
IL void solve(int p)
{
    vis[p]=1;
    f.clear();
    f.insert(0);
    for(int i=h[p],v=e[i].v;~i;i=e[i].next,v=e[i].v)
        if(!vis[v]){
            top=0,dis[v]=e[i].w;
            dfs(v,p);
            for(int i=top;i;--i)Query(stk[i]);
            for(int i=top;i;--i)f.insert(stk[i]);
        }
    for(int i=h[p],v=e[i].v;~i;i=e[i].next,v=e[i].v)
        if(!vis[v])
        {
            Hroot=0;
            total=siz[v];
            GetRoot(v,0);
            solve(Hroot);
        }
}
int main(void)
{
    fill(h,h+MAXN+1,-1);
    n=gi,m=gi;
    for(int i=1,a,b,c;i<n;++i)a=gi,b=gi,c=gi,add(a,b,c),add(b,a,c);
    for(int i=m;i;--i)K[i]=gi;
    hson[Hroot=0]=total=n,GetRoot(1,0),solve(Hroot);
    for(int i=m;i;--i)puts(ans[i]?"AYE":"NAY");
    return 0;
}

\(2\).洛谷P2634 [国家集训队]聪聪可可

这题其实有简单好写复杂度又低的树形dp做法

题目大意

求长度被\(3\)整除的路径条数

\((a,b)\)\((b,a)\)视为两条

思想分析

根据套路

我们要统计经过根\(rt\)的长度为\(3\)的倍数的路径。

记录路径长度模\(3\)\(0,1,2\)的个数。

\(pre[3]\)为访问\(rt\)的子树\(v\)之前的记录,\(now[3]\)为当前子树\(v\)的记录。

贡献为\(2(pre[0]*now[0]+pre[1]*now[2]+pre[2]*now[1]+now[0])\)

然后访问\(v\)后把\(now\)累加到\(pre\)中即可。

最后的\(ans\)要记得\(+n\)

因为每个点也算一条路径。

代码实现

/*
@Date    : 2019-08-29 21:39:48
@Author  : Adscn (adscn@qq.com)
@Link    : https://www.cnblogs.com/LLCSBlog
*/
#include<bits/stdc++.h>
using namespace std;
#define IL inline
#define RG register
#define gi getint()
#define gc getchar()
#define File(a) freopen(a".in","r",stdin);freopen(a".out","w",stdout)
template<typename T>IL bool chkmax(T &x,const T &y){return x<y?x=y,1:0;}
template<typename T>IL bool chkmin(T &x,const T &y){return x>y?x=y,1:0;}
IL int getint()
{
	RG int xi=0;
	RG char ch=gc;
	bool f=0;
	while(!isdigit(ch))ch=='-'?f=1:f,ch=gc;
	while(isdigit(ch))xi=(xi<<1)+(xi<<3)+ch-48,ch=gc;
	return f?-xi:xi;
}
template<typename T>
IL void pi(T k,char ch=0)
{
	if(k<0)k=-k,putchar('-');
	if(k>=10)pi(k/10,0);
	putchar(k%10+'0');
	if(ch)putchar(ch);
}
#define For_tree(x) for(register int i=head[x],v=e[i].v;~i;i=e[i].nxt,v=e[i].v)
const int N=2e4+7;
struct edge{
	int v,nxt,w;
}e[N<<1];
int head[N],cnt;
inline void add(int u,int v,int w){e[++cnt]=(edge){v,head[u],w},head[u]=cnt;}
inline void init(){memset(head,cnt=-1,sizeof head);}
int n;
bool vis[N];
int siz[N],hson[N],dis[N];
int pre[3],now[3];
int total,Hroot;
int ans;
inline void groot(int x,int fa){
	siz[x]=1,hson[x]=0;
	For_tree(x)
		if(v^fa&&!vis[v])
			groot(v,x),siz[x]+=siz[v],chkmax(hson[x],siz[v]);
	chkmax(hson[x],total-siz[x]);
	if(hson[x]<hson[Hroot])Hroot=x;
}
inline void gdis(int x,int fa){
	++now[dis[x]%3];
	For_tree(x)
		if(v^fa&&!vis[v])
			dis[v]=dis[x]+e[i].w,gdis(v,x);
}
inline void work(int x){
	For_tree(x)
		if(!vis[v])
		{
			now[0]=now[1]=now[2]=0;
			dis[v]=e[i].w,gdis(v,x);
			ans+=2*(pre[0]*now[0]+pre[1]*now[2]+pre[2]*now[1]+now[0]);
			pre[0]+=now[0],pre[1]+=now[1],pre[2]+=now[2];
		}
}
inline void solve(int x){
	vis[x]=1;
	work(x);
	pre[0]=pre[1]=pre[2]=0;
	For_tree(x)
		if(!vis[v]){
			total=siz[v],Hroot=0;
			groot(v,x),solve(Hroot);
		}
}
inline int gcd(int a,int b){return b?gcd(b,a%b):a;}
int main(void)
{
	init();
	n=gi;
	for(int i=1,u,v,w;i<n;++i)u=gi,v=gi,w=gi,add(u,v,w),add(v,u,w);
	hson[Hroot=0]=total=n;
	groot(1,0),solve(Hroot);
	ans+=n;
	int sum=n*n;
	int Gcd=gcd(ans,sum);
	cout<<ans/Gcd<<"/"<<sum/Gcd;
	return 0;
}

思想分析2

其实我们可以每个节点\(x\)求出其子树内的\(dis%3\)的余数记录数组\(m\),经过该点的路径数即为\(2*m[1]*m[2]+m[0]^2\)

但是这样计算\(x\)时,对于子树\(v\),我们计算\(v\)中的答案时重复计算了既经过\(x\)这个点,又经过\(v\)这一个点的路径,于是我们要容斥减去它。

这个时候就不要\(+n\)\(qwq\)

详见代码

代码实现2

/*
@Date    : 2019-08-30 07:55:55
@Author  : Adscn (adscn@qq.com)
@Link    : https://www.cnblogs.com/LLCSBlog
*/
#include<bits/stdc++.h>
using namespace std;
#define IL inline
#define RG register
#define gi getint()
#define gc getchar()
#define File(a) freopen(a".in","r",stdin);freopen(a".out","w",stdout)
template<typename T>IL bool chkmax(T &x,const T &y){return x<y?x=y,1:0;}
template<typename T>IL bool chkmin(T &x,const T &y){return x>y?x=y,1:0;}
IL int getint()
{
	RG int xi=0;
	RG char ch=gc;
	bool f=0;
	while(!isdigit(ch))ch=='-'?f=1:f,ch=gc;
	while(isdigit(ch))xi=(xi<<1)+(xi<<3)+ch-48,ch=gc;
	return f?-xi:xi;
}
template<typename T>
IL void pi(T k,char ch=0)
{
	if(k<0)k=-k,putchar('-');
	if(k>=10)pi(k/10,0);
	putchar(k%10+'0');
	if(ch)putchar(ch);
}
#define For_tree(x) for(register int i=head[x],v=e[i].v;~i;i=e[i].nxt,v=e[i].v)
const int N=2e4+7;
struct edge{
	int v,nxt,w;
}e[N<<1];
int head[N],cnt;
inline void add(int u,int v,int w){e[++cnt]=(edge){v,head[u],w},head[u]=cnt;}
inline void init(){memset(head,cnt=-1,sizeof head);}
int n;
bool vis[N];
int siz[N],hson[N],dis[N];
int m[3];
int total,Hroot;
int ans;
inline void groot(int x,int fa){
	siz[x]=1,hson[x]=0;
	For_tree(x)
		if(v^fa&&!vis[v])
			groot(v,x),siz[x]+=siz[v],chkmax(hson[x],siz[v]);
	chkmax(hson[x],total-siz[x]);
	if(hson[x]<hson[Hroot])Hroot=x;
}
inline void gdis(int x,int fa){
	++m[dis[x]%3];
	For_tree(x)
		if(v^fa&&!vis[v])
			dis[v]=dis[x]+e[i].w,gdis(v,x);
}
inline int work(int x,int distan){
	dis[x]=distan;
	m[0]=m[1]=m[2]=0;
	gdis(x,0);
	return m[0]*m[0]+2*m[1]*m[2];
}
inline void solve(int x){
	ans+=work(x,0);
	vis[x]=1;
	For_tree(x)
		if(!vis[v]){
			ans-=work(v,e[i].w);
			total=siz[v],Hroot=0;
			groot(v,x),solve(Hroot);
		}
}
inline int gcd(int a,int b){return b?gcd(b,a%b):a;}
int main(void)
{
	init();
	n=gi;
	for(int i=1,u,v,w;i<n;++i)u=gi,v=gi,w=gi,add(u,v,w),add(v,u,w);
	hson[Hroot=0]=total=n;
	groot(1,0),solve(Hroot);
//	ans+=n;
	int sum=n*n;
	int Gcd=gcd(ans,sum);
	cout<<ans/Gcd<<"/"<<sum/Gcd;
	return 0;
}

如果你非常顺利的做完了上面的题目,恭喜你,你已经成功点分治入门辣!

接下来我们开始进阶部分\(QwQ\)


\(3.\)洛谷P2664 树上游戏

题目大意

给定树上每个点的颜色。

定义\(s(i,j)\)\(i\)\(j\)的颜色数量。

\(sum_i=\sum\limits_{j=1}^ns(i,j)\)

求出所有的\(sum_i\)

思想分析

想不到吧,这题也有简单的\(O(n)\)做法

根据套路,

我们要在\(O(n)\)的时间统计经过根的路径的点对的答案。

我们首先要想到对于树上的一条路径,每种颜色只有出现的第一个颜色有贡献。

我们考虑每个点到根的路径

对于树上的一个点\(v\)

如果\(v\)的颜色是\(v\)到根\(x\)的路径上第一次出现,

\(v\)的颜色会对\(x\)的其它子树中的点\(u\)贡献\(siz_v\)

但是如果\(u\)\(x\)的路径上也有这个颜色,我们就重复计算了。

于是我们就把它扫一遍,减掉。

\(Another=siz[x]-siz[v]\)

统计时\(ans+=Sum+num*Another\)

\(Sum\)是上面写的贡献总和,\(num\)\(j\)到根上路径的颜色数,不包括根。

\(Another\)是其他子树大小。

处理子树时要把当前的子树贡献从总体中去掉,之后再加回来。

具体细节见代码。

代码实现

略长。

/*
@Date    : 2019-08-30 08:28:48
@Author  : Adscn (adscn@qq.com)
@Link    : https://www.cnblogs.com/LLCSBlog
*/
#include<bits/stdc++.h>
using namespace std;
#define IL inline
#define RG register
#define gi getint()
#define gc getchar()
#define File(a) freopen(a".in","r",stdin);freopen(a".out","w",stdout)
template<typename T>IL bool chkmax(T &x,const T &y){return x<y?x=y,1:0;}
template<typename T>IL bool chkmin(T &x,const T &y){return x>y?x=y,1:0;}
IL int getint()
{
	RG int xi=0;
	RG char ch=gc;
	bool f=0;
	while(!isdigit(ch))ch=='-'?f=1:f,ch=gc;
	while(isdigit(ch))xi=(xi<<1)+(xi<<3)+ch-48,ch=gc;
	return f?-xi:xi;
}
template<typename T>
IL void pi(T k,char ch=0)
{
	if(k<0)k=-k,putchar('-');
	if(k>=10)pi(k/10,0);
	putchar(k%10+'0');
	if(ch)putchar(ch);
}
#define For_tree(x) for(register int i=head[x],v=e[i].v;~i;i=e[i].nxt,v=e[i].v)
const int N=1e5+7;
typedef long long ll;
struct edge{
	int v,nxt;
}e[N<<1];
int head[N],edge_cnt;
inline void init(){memset(head,edge_cnt=-1,sizeof head);}
inline void add(int u,int v){e[++edge_cnt]=(edge){v,head[u]},head[u]=edge_cnt;}
bool vis[N];
int hson[N],siz[N],Hroot,total,col[N],color[N],cnt[N],Sum,n,Another,num;
ll ans[N];
inline void groot(int x,int fa){
	siz[x]=1,hson[x]=0;
	For_tree(x)
		if(v^fa&&!vis[v])
			groot(v,x),siz[x]+=siz[v],chkmax(hson[x],siz[v]);
	chkmax(hson[x],total-siz[x]);
	if(hson[x]<hson[Hroot])Hroot=x;
}
inline void modify(int x,int fa,int val){
	++cnt[col[x]],siz[x]=1;
	For_tree(x)if(v^fa&&!vis[v])modify(v,x,val),siz[x]+=siz[v];
	if(cnt[col[x]]==1)Sum+=siz[x]*val,color[col[x]]+=siz[x]*val;
	--cnt[col[x]];
}
inline void clear(int x,int fa){
	cnt[col[x]]=color[col[x]]=0;
	For_tree(x)if(v^fa&&!vis[v])clear(v,x);
}
inline void count(int x,int fa)
{
	++cnt[col[x]];
	if(cnt[col[x]]==1)Sum-=color[col[x]],++num;
	ans[x]+=Sum+1ll*num*Another;
	For_tree(x)if(v^fa&&!vis[v])count(v,x);
	if(cnt[col[x]]==1)Sum+=color[col[x]],--num;
	--cnt[col[x]];
}
inline void work(int x){
	modify(x,0,1);
	ans[x]+=Sum-color[col[x]]+siz[x];
	++cnt[col[x]];
	For_tree(x)
		if(!vis[v])
		{
			Sum-=siz[v],color[col[x]]-=siz[v],modify(v,x,-1);
			Another=siz[x]-siz[v];
			count(v,x);
			Sum+=siz[v],color[col[x]]+=siz[v],modify(v,x,1);	
		}
	--cnt[col[x]],Sum=num=0;
	clear(x,0);
}
inline void solve(int x){
	vis[x]=1;
	work(x);
	For_tree(x)
		if(!vis[v])
		{
			Hroot=0,total=siz[v];
			groot(v,x);
			solve(Hroot);
		}
}
int main(void)
{
	init();
	n=gi;
	for(int i=1;i<=n;++i)col[i]=gi;
	for(int i=1,u,v;i<n;++i)u=gi,v=gi,add(u,v),add(v,u);
	hson[Hroot=0]=total=n;
	groot(1,0);
	solve(Hroot);
	for(int i=1;i<=n;++i)pi(ans[i],'\n');
	return 0;
}

动态点分治

你可以选择先看一下周书予dalao的博客。

简单地说就是通过重心重建一颗点分树来支持修改。

显然点分树的深度是\(logn\)级别的。

修改的时候就跳点分树修改。

点分树满足一些性质,这也是动态点分治(点分树)处理问题的基础

性质1:点分树上两点的lca​一定在原树两点的路径上

性质2:点分治上的一个子树总是原树上的一个连通块

这两个性质画画图很容易理解,但是一定要牢记,不然写题目的时候可能发现自己的方法是伪的

先看几道题目吧。

因为一些原因,难度是降序的(因为这就是我的开题顺序)

题目介绍

\(1\).幻想乡战略游戏

题目大意

给你一颗树,边上有距离。

每个点上有点权\(D_v\),初始为0。

你要选择一个点\(u\),最小化\(\sum\limits_{v\in tree} D_v*dis(u,v)\)

每次会修改点权,询问这个最小值。

思想分析

假设当前补给站是\(x\),并以\(x\)为根,\(v\)\(x\)的一个子节点,设\(sumdis_i\)\(i\)子树中到\(x\)的距离和 。

那么显然\(v\)\(x\)更优当且仅当\(2*sumdis_v>sumdis_x\)

因为假如补给站移动到\(v\),增长,总距离会缩短\(sumdis_v*e(x,v)\)然后增加\((sumdis_x-sumdis_v)*e(x,v)\)

移项就可以了。

如果不存在更优的\(v\),\(x\)就是最优位置。

我们维护三个数组

\(sumv[x]\)表示点分树上以x为根的子树点权总和

\(dis1[x]\)表示点分树上以x为根的子树到达x的代价和

\(dis2[x]\)表示点分树上以x为根的子树到达\(fa_x\)的代价和

当我们在点分树上移动时。

假设我们从一个点\(x\)移动到\(fa_x\)

我们要计算\(fa_x\)其他的子树的贡献,这个改变了\(dis(x,fa[x])*(sumv[fa[x]]-sumv[x])\)

考虑\(x\)子树中的贡献,于是还要减掉原来的子树的\(dis2\),加上新的\(dis1\)

这个比较好理解。

代码比较好敲\(qwq\)

代码实现

/*
@Date    : 2019-08-30 17:51:05
@Author  : Adscn (adscn@qq.com)
@Link    : https://www.cnblogs.com/LLCSBlog
*/
#include<bits/stdc++.h>
using namespace std;
#define IL inline
#define RG register
#define gi getint()
#define gc getchar()
#define File(a) freopen(a".in","r",stdin);freopen(a".out","w",stdout)
template<typename T>IL bool chkmax(T &x,const T &y){return x<y?x=y,1:0;}
template<typename T>IL bool chkmin(T &x,const T &y){return x>y?x=y,1:0;}
IL int getint()
{
	RG int xi=0;
	RG char ch=gc;
	bool f=0;
	while(!isdigit(ch))ch=='-'?f=1:f,ch=gc;
	while(isdigit(ch))xi=(xi<<1)+(xi<<3)+ch-48,ch=gc;
	return f?-xi:xi;
}
template<typename T>
IL void pi(T k,char ch=0)
{
	if(k<0)k=-k,putchar('-');
	if(k>=10)pi(k/10,0);
	putchar(k%10+'0');
	if(ch)putchar(ch);
}
const int N=1e5+7;
#define For_tree(x) for(int i=head[x],v=e[i].v;~i;i=e[i].nxt,v=e[i].v)
#define Fortree(x) for(int i=Head[x],v=E[i].v;~i;i=E[i].nxt,v=E[i].v)
typedef long long ll;
struct Edge{
	int v,nxt,w;
}e[N<<1],E[N<<1];
int head[N],Head[N],cnt,cnt_;
int first[N<<1],ver[N<<1],depth_ver[N<<1],dfn;
int f[21][N<<1],n,m,s;
bool vis[N];
int siz[N],hson[N],dis[N],lg2[N<<2],total,Hrt;
int par[N];
ll dis1[N],dis2[N],sumv[N];
inline void init(){
	memset(head,cnt=-1,sizeof head);
	memset(Head,cnt_=-1,sizeof Head);
	lg2[0]=-1;
	for(int i=1;i<(N<<2);++i)lg2[i]=lg2[i>>1]+1;
}
IL void add(int u,int v,int w)
{
	e[++cnt]=(Edge){v,head[u],w};
	head[u]=cnt;
}
IL void Add(int u,int v,int w)
{
	E[++cnt_]=(Edge){v,Head[u],w};
	Head[u]=cnt_;
}
IL void dfs(int x,int dep)
{
	vis[x]=1,first[x]=++dfn,ver[dfn]=x,depth_ver[dfn]=dep;
	For_tree(x)
		if(!vis[v])
		{
			dis[v]=dis[x]+e[i].w;
			dfs(v,dep+1);
			ver[++dfn]=x,depth_ver[dfn]=dep;
		}
}
IL void STinit(int k)
{
	int len=lg2[k];
	for(RG int i=1;i<=k;i++)f[0][i]=i;
	for(RG int i=1;i<=len;i++)
		for(RG int j=1;j+(1<<i)-1<=k;j++)
			f[i][j]=(depth_ver[f[i-1][j]]<=depth_ver[f[i-1][j+(1<<(i-1))]])?f[i-1][j]:f[i-1][j+(1<<(i-1))];
}
IL int STQuery(int l,int r)
{
	int k=lg2[r-l+1];
	return depth_ver[f[k][l]]<=depth_ver[f[k][r-(1<<k)+1]]?f[k][l]:f[k][r-(1<<k)+1];
}
IL int LCA(int u,int v)
{
	if(first[u]>first[v])swap(u,v);
	return ver[STQuery(first[u],first[v])];
}
IL int Dis(int u,int v){
	return dis[u]+dis[v]-2*dis[LCA(u,v)];
}
inline void grt(int x,int fa){
	siz[x]=1,hson[x]=0;
	For_tree(x)
		if(v^fa&&!vis[v])
			grt(v,x),siz[x]+=siz[v],chkmax(hson[x],siz[v]);
	chkmax(hson[x],total-siz[x]);
	if(hson[x]<hson[Hrt])Hrt=x;
}
inline void presolve(int x,int fa){
	vis[x]=1,par[x]=fa;
	For_tree(x)
		if(!vis[v]){
			Hrt=0,total=siz[v];
			grt(v,0),Add(x,Hrt,v);
			presolve(Hrt,x);
		}
}
inline void modify(int x,int val){
	sumv[x]+=val;
	for(int i=x;par[i];i=par[i]){
		ll dis=Dis(x,par[i]);
		dis1[par[i]]+=dis*val;
		dis2[i]+=dis*val;
		sumv[par[i]]+=val;
	}
}
inline ll work(int x){
	ll ans=dis1[x];
	for(int i=x;par[i];i=par[i]){
		int dis=Dis(x,par[i]);
		ans+=dis1[par[i]]-dis2[i];
		ans+=dis*(sumv[par[i]]-sumv[i]);
	}
	return ans;
}
inline ll query(int x){
	ll ans=work(x);
	Fortree(x)if(work(E[i].w)<ans)return query(v);
	return ans;
}
int main(void)
{
	init();
	int n=gi,m=gi;
	for(int i=1,u,v,w;i<n;++i)u=gi,v=gi,w=gi,add(u,v,w),add(v,u,w);
	dfs(1,0);
	memset(vis,0,sizeof vis);
	STinit(dfn);
	hson[Hrt=0]=total=n;
	grt(1,0);int tmp=Hrt;
	presolve(Hrt,0);
	Hrt=tmp;
	while(m--){
		int x=gi,y=gi;
		modify(x,y);
		pi(query(Hrt),'\n');
	}
	return 0;
}

\(2.\)BZOJ3730震波

题目大意

给你一颗树,边长均为\(1\),点有点权。

两个操作

\(1.\)询问离\(x\)不超过\(k\)的点的点权之和。

\(2.\)单点修改点权

强制在线。

思想分析

ps:以下均指点分树。

我们对每个节点开一颗动态开点线段树,维护子树中的点权和。

具体的,以距离为下标,值为点权和。

修改时跳点分树,直接改。

查询的时候跳到父亲,查询一下[0,k-dis]的和。

但是我们跳点分树的时候会重复计算原子树的贡献。

再开个线段树记录一下子树对父亲的贡献,每次减掉就可以了。

这题就不用ST表求LCA了,复杂度是一样的,但是这题卡时间

\(O(nlog^2n)\)

此题卡时间,丧心病狂。

线段树常数大没法做,把线段树改成BIT就可以,但我懒得写了\(qwq\)

码量仍然不大

代码实现

/*
@Date    : 2019-09-01 09:02:02
@Author  : Adscn (adscn@qq.com)
@Link    : https://www.cnblogs.com/LLCSBlog
*/
#include<bits/stdc++.h>
using namespace std;
#define IL inline
#define RG register
#define gi getint()
#define gc getchar()
#define File(a) freopen(a".in","r",stdin);freopen(a".out","w",stdout)
template<typename T>IL bool chkmax(T &x,const T &y){return x<y?x=y,1:0;}
template<typename T>IL bool chkmin(T &x,const T &y){return x>y?x=y,1:0;}
IL int getint()
{
	RG int xi=0;
	RG char ch=gc;
	bool f=0;
	while(!isdigit(ch))ch=='-'?f=1:f,ch=gc;
	while(isdigit(ch))xi=(xi<<1)+(xi<<3)+ch-48,ch=gc;
	return f?-xi:xi;
}
template<typename T>
IL void pi(T k,char ch=0)
{
	if(k<0)k=-k,putchar('-');
	if(k>=10)pi(k/10,0);
	putchar(k%10+'0');
	if(ch)putchar(ch);
}
const int N=1e5+7;
namespace segtree{
	struct sgt{int ls,rs,val;}tree[N*150];
	int root[N<<1],node_cnt;
	#define mid ((l+r)>>1)
	inline void modify(int &rt,int l,int r,const int &pos,const int &val){
		if(!rt)rt=++node_cnt;
		tree[rt].val+=val;
		if(l==r)return;
		if(pos<=mid)modify(tree[rt].ls,l,mid,pos,val);
		else modify(tree[rt].rs,mid+1,r,pos,val);
	}
	inline int query(int rt,int l,int r,const int &L,const int &R){
		if(!rt)return 0;
		if(L<=l&&r<=R)return tree[rt].val;
		int ret=0;
		if(L<=mid)ret+=query(tree[rt].ls,l,mid,L,R);
		if(R>mid)ret+=query(tree[rt].rs,mid+1,r,L,R);
		return ret;
	}
	#undef mid
}
using segtree::root;
#define For_tree(x) for(register int i=head[x],v=e[i].v;~i;i=e[i].nxt,v=e[i].v)
struct edge{int v,nxt;}e[N<<1];
int head[N],cnt;
int lg2[N<<1];
int f[18][N<<1],vis[N],first[N],depth[N],dfn,siz[N],hson[N],Hrt,total,par[N],val[N],n,m;
inline void init(){
	memset(head,cnt=-1,4*n+4);
	lg2[0]=-1;
	for(int i=1;i<=(n<<1);++i)lg2[i]=lg2[i>>1]+1;
}
inline void add(int u,int v){e[++cnt]=(edge){v,head[u]},head[u]=cnt;}
inline void dfs(int x,int dep){
	vis[x]=1,first[x]=++dfn,f[0][dfn]=dep;
	depth[x]=dep;
	For_tree(x)
		if(!vis[v])
			dfs(v,dep+1),f[0][++dfn]=dep;
}
inline void STinit(int k)
{
	int len=lg2[k];
	for(int i=1;i<=len;++i)
		for(int j=1;j+(1<<i)-1<=k;++j)
			f[i][j]=min(f[i-1][j],f[i-1][j+(1<<(i-1))]);
}
inline int STDisQuery(int l,int r){
	if(l>r)swap(l,r);
	return min(f[lg2[r-l+1]][l],f[lg2[r-l+1]][r-(1<<lg2[r-l+1])+1]);
}
inline int Dis(int u,int v){return depth[u]+depth[v]-2*STDisQuery(first[u],first[v]);}
void grt(int x,int fa=0){
	siz[x]=1,hson[x]=0;
	For_tree(x)
		if(v^fa&&!vis[v])
			grt(v,x),siz[x]+=siz[v],chkmax(hson[x],siz[v]);
	chkmax(hson[x],total-siz[x]);
	if(hson[x]<hson[Hrt])Hrt=x;
}
void presolve(int x,int fa=0)
{
	vis[x]=1,par[x]=fa;
	For_tree(x)
		if(!vis[v]){
			Hrt=0,total=siz[v];
			grt(v),presolve(Hrt,x);
		}
}
inline void modify(int x,int val)
{
	segtree::modify(root[x],0,n-1,0,val);
	for(int i=x;par[i];i=par[i])
	{
		int dis=Dis(x,par[i]);
		segtree::modify(root[par[i]],0,n-1,dis,val);
		segtree::modify(root[i+n],0,n-1,dis,val);
	}
}
inline int query(int x,int k){
	int ret=segtree::query(root[x],0,n-1,0,k);
	for(int i=x;par[i];i=par[i])
	{
		int dis=Dis(x,par[i]);
		if(dis<=k)ret+=segtree::query(root[par[i]],0,n-1,0,k-dis)-segtree::query(root[i+n],0,n-1,0,k-dis);
	}
	return ret;
}
int main(void)
{
	n=gi,m=gi;
	init();
	for(int i=1;i<=n;++i)val[i]=gi;
	for(int i=1,u,v;i<n;++i)u=gi,v=gi,add(u,v),add(v,u);
	dfs(1,0),STinit(dfn);
	memset(vis,0,4*n+4);
	hson[Hrt=0]=total=n;
	grt(1),presolve(Hrt);
	for(int i=1;i<=n;++i)modify(i,val[i]);
	int lastans=0;
	while(m--){
		int opt=gi,x=gi^lastans,y=gi^lastans;
		if(!opt)pi(lastans=query(x,y),'\n');
		else modify(x,y-val[x]),val[x]=y;
	}
	return 0;
}

3.BZOJP4372烁烁的游戏

题目大意

给你一颗树,边长均为\(1\),点有点权。

两个操作

\(1.\)增加离\(x\)不超过\(d\)的点的点权一个值\(w\)

\(2.\)询问点\(x\)的点权

思想分析

这个就是上面的题目改一下,

线段树存离\(d\)的修改量就可以了。

变成了区间修改,单点查询。

代码实现

/*
@Date    : 2019-09-02 06:25:31
@Author  : Adscn (adscn@qq.com)
@Link    : https://www.cnblogs.com/LLCSBlog
*/
#include<bits/stdc++.h>
using namespace std;
#define IL inline
#define RG register
#define gi getint()
#define gc getchar()
#define File(a) freopen(a".in","r",stdin);freopen(a".out","w",stdout)
template<typename T>IL bool chkmax(T &x,const T &y){return x<y?x=y,1:0;}
template<typename T>IL bool chkmin(T &x,const T &y){return x>y?x=y,1:0;}
IL int getint()
{
	RG int xi=0;
	RG char ch=gc;
	bool f=0;
	while(!isdigit(ch))ch=='-'?f=1:f,ch=gc;
	while(isdigit(ch))xi=(xi<<1)+(xi<<3)+ch-48,ch=gc;
	return f?-xi:xi;
}
template<typename T>
IL void pi(T k,char ch=0)
{
	if(k<0)k=-k,putchar('-');
	if(k>=10)pi(k/10,0);
	putchar(k%10+'0');
	if(ch)putchar(ch);
}
const int N=1e5+7;
namespace segtree{
	struct sgt{int ls,rs,val;}tree[N*150];
	int root[N<<1],node_cnt;
	#define mid ((l+r)>>1)
	inline void modify(int &rt,int l,int r,const int &L,const int &R,const int &val){
		if(!rt)rt=++node_cnt;
		if(L<=l&&r<=R){tree[rt].val+=val;return;}
		if(L<=mid)modify(tree[rt].ls,l,mid,L,R,val);
		if(R>mid) modify(tree[rt].rs,mid+1,r,L,R,val);
	}
	inline int query(int rt,int l,int r,const int &pos){
		if(!rt)return 0;
		if(l==r)return tree[rt].val;
		int ret=tree[rt].val;
		if(pos<=mid)ret+=query(tree[rt].ls,l,mid,pos);
		else ret+=query(tree[rt].rs,mid+1,r,pos);
		return ret;
	}
	#undef mid
}
using segtree::root;
#define For_tree(x) for(register int i=head[x],v=e[i].v;~i;i=e[i].nxt,v=e[i].v)
struct edge{int v,nxt;}e[N<<1];
int head[N],cnt;
int lg2[N<<1];
int f[18][N<<1],vis[N],first[N],depth[N],dfn,siz[N],hson[N],Hrt,total,par[N],val[N],n,m;
inline void init(){
	memset(head,cnt=-1,4*n+4);
	lg2[0]=-1;
	for(int i=1;i<=(n<<1);++i)lg2[i]=lg2[i>>1]+1;
}
inline void add(int u,int v){e[++cnt]=(edge){v,head[u]},head[u]=cnt;}
inline void dfs(int x,int dep){
	vis[x]=1,first[x]=++dfn,f[0][dfn]=dep;
	depth[x]=dep;
	For_tree(x)
		if(!vis[v])
			dfs(v,dep+1),f[0][++dfn]=dep;
}
inline void STinit(int k)
{
	int len=lg2[k];
	for(int i=1;i<=len;++i)
		for(int j=1;j+(1<<i)-1<=k;++j)
			f[i][j]=min(f[i-1][j],f[i-1][j+(1<<(i-1))]);
}
inline int STDisQuery(int l,int r){
	if(l>r)swap(l,r);
	return min(f[lg2[r-l+1]][l],f[lg2[r-l+1]][r-(1<<lg2[r-l+1])+1]);
}
inline int Dis(int u,int v){return depth[u]+depth[v]-2*STDisQuery(first[u],first[v]);}
void grt(int x,int fa=0){
	siz[x]=1,hson[x]=0;
	For_tree(x)
		if(v^fa&&!vis[v])
			grt(v,x),siz[x]+=siz[v],chkmax(hson[x],siz[v]);
	chkmax(hson[x],total-siz[x]);
	if(hson[x]<hson[Hrt])Hrt=x;
}
void presolve(int x,int fa=0)
{
	vis[x]=1,par[x]=fa;
	For_tree(x)
		if(!vis[v]){
			Hrt=0,total=siz[v];
			grt(v),presolve(Hrt,x);
		}
}
inline void modify(int x,int d,int val)
{
	segtree::modify(root[x],0,n-1,0,d,val);
	for(int i=x;par[i];i=par[i])
	{
		int dis=Dis(x,par[i]);
		if(dis>d)continue;
		segtree::modify(root[par[i]],0,n-1,0,d-dis,val);
		segtree::modify(root[i+n],0,n-1,0,d-dis,val);
	}
}
inline int query(int x){
	int ret=segtree::query(root[x],0,n-1,0);
	for(int i=x;par[i];i=par[i])
	{
		int dis=Dis(x,par[i]);
		ret+=segtree::query(root[par[i]],0,n-1,dis)-segtree::query(root[i+n],0,n-1,dis);
	}
	return ret;
}
int main(void)
{
	n=gi,m=gi;
	init();
	for(int i=1,u,v;i<n;++i)u=gi,v=gi,add(u,v),add(v,u);
	dfs(1,0),STinit(dfn);
	memset(vis,0,4*n+4);
	hson[Hrt=0]=total=n;
	grt(1),presolve(Hrt);
	int lastans=0;
	while(m--){
		char opt=gc;
		int x=gi;
		if(opt=='Q')pi(query(x),'\n');
		else
		{
			int d=min(gi,n),w=gi;
			modify(x,d,w);
		}
	}
	return 0;
}

4.QTREE5

\(luogu\)\(remote\ judge\)似乎炸了,所以上面放的是\(vjudge\)

这题还有\(lct\)做法,具体看我的另一篇博客

题目大意

你被给定一棵n个点的树,点从1到n编号。每个点可能有两种颜色:黑或白。我们定义dist(a,b)为点a至点b路径上的边个数。

一开始所有的点都是黑色的。

要求作以下操作:

0 i 将点i的颜色反转(黑变白,白变黑)

1 v 询问dist(u,v)的最小值。u点必须为白色(u与v可以相同),显然如果v是白点,查询得到的值一定是0。

特别地,如果作'1'操作时树上没有白点,输出-1.

思想分析

对于每个点我们开一个\(multiset\)存一下它离点分树中白点的距离,以及编号

然后跳点分树找就可以了

用堆的话,查询复杂度可以做到\(nlogn\),因为查询堆顶是\(O(1)\)

但是我懒

代码实现

/*
@Date    : 2019-09-12 10:45:46
@Author  : Adscn (adscn@qq.com)
@Link    : https://www.cnblogs.com/LLCSBlog
*/
#include<bits/stdc++.h>
using namespace std;
#define IL inline
#define RG register
#define gi getint()
#define gc getchar()
#define File(a) freopen(a".in","r",stdin);freopen(a".out","w",stdout)
template<typename T>IL bool chkmax(T &x,const T &y){return x<y?x=y,1:0;}
template<typename T>IL bool chkmin(T &x,const T &y){return x>y?x=y,1:0;}
IL int getint()
{
	RG int xi=0;
	RG char ch=gc;
	bool f=0;
	while(!isdigit(ch))ch=='-'?f=1:f,ch=gc;
	while(isdigit(ch))xi=xi*10+ch-48,ch=gc;
	return f?-xi:xi;
}
template<typename T>
IL void pi(T k,char ch=0)
{
	if(k<0)k=-k,putchar('-');
	if(k>=10)pi(k/10,0);
	putchar(k%10+'0');
	if(ch)putchar(ch);
}
#define Fortree(x) for(int i=head[x],v=e[i].v;~i;i=e[i].nxt,v=e[i].v)
#define fi first
#define se second
#define mp make_pair
typedef pair<int,int> pii;
typedef multiset<pii> Mi;
const int N=2e5+7;
struct edge{
	int v,nxt;
}e[N<<1];
int head[N],cnt;
int siz[N],lg2[N],dfn,f[20][N<<1],depth[N],first[N],vis[N],hson[N],total,Hrt,par[N],col[N],tot;
Mi h[N];
inline void init(){
	memset(head,cnt=-1,sizeof head);
	lg2[0]=-1;
	for(int i=1;i<N;++i)lg2[i]=lg2[i>>1]+1;
}
inline void add(int u,int v){e[++cnt]=(edge){v,head[u]},head[u]=cnt;}
inline void dfs(int x,int dep){
	vis[x]=1,first[x]=++dfn,f[0][dfn]=depth[x]=dep;
	Fortree(x)if(!vis[v])dfs(v,dep+1),f[0][++dfn]=dep;
	vis[x]=0;
}
inline void STinit(int k){
	for(int i=1;i<=lg2[k];++i)
		for(int j=1;j+(1<<i)-1<=k;++j)
			f[i][j]=min(f[i-1][j],f[i-1][j+(1<<(i-1))]);
}
inline int STQuery(int l,int r)
{
	if(l>r)swap(l,r);
	int k=lg2[r-l+1];
	return min(f[k][l],f[k][r-(1<<k)+1]);
}
inline int Dis(int u,int v){return depth[u]+depth[v]-(STQuery(first[u],first[v])<<1);}
inline void grt(int x,int fa=0){
	siz[x]=1,hson[x]=0;
	Fortree(x)
		if(v^fa&&!vis[v])
			grt(v,x),siz[x]+=siz[v],chkmax(hson[x],siz[v]);
	chkmax(hson[x],total-siz[x]);
	if(hson[x]<hson[Hrt])Hrt=x;
}
inline void presolve(int x,int fa=0)
{
	vis[x]=1,par[x]=fa;
	Fortree(x)
		if(v^fa&&!vis[v])
			Hrt=0,total=siz[v],grt(v),presolve(Hrt,x);
}
inline void Modify(int x){
	(col[x]^=1)?++tot:--tot;
	if(col[x])for(int i=x;i;i=par[i])h[i].insert(mp(Dis(i,x),x));
	else for(int i=x;i;i=par[i])h[i].erase(mp(Dis(i,x),x));
}
inline int Query(int x){
	if(!tot)return -1;
	int ret=INT_MAX;
	for(int i=x;i;i=par[i])if(!h[i].empty())chkmin(ret,Dis(x,h[i].begin()->se));
	return ret;
}
int main(void)
{
	int n=gi;
	init();
	for(int i=1,u,v;i<n;++i)u=gi,v=gi,add(u,v),add(v,u);
	dfs(1,0),STinit(dfn);
	hson[Hrt=0]=total=n,grt(1),presolve(Hrt);
	for(int i=1,m=gi;i<=m;++i){
		int opt=gi,x=gi;
		if(opt^1)Modify(x);
		else pi(Query(x),'\n');
	}
	return 0;
}

总结

点分治的经典应用是询问树上某些满足条件的点对。

套路是容斥,

但如果你沿着子树逐个处理,其实就不用容斥。

但是有时候逐个合并的复杂度过高,你就不能去一个个处理,只能容斥(但我还真没做过这样的题目)。

动态点分治经典的应用是询问一个点到其他很多特定点的信息。(并支持修改)

比如

  • 询问一个点到所有黑点的距离/距离不超过\(k\)的点权和

  • 询问对于一个点\(u\),有多少\(v\),满足某些条件。

动态点分治的套路是用数据结构维护题目所给定的条件,有时候需要容斥。

posted @ 2019-08-29 22:12  Adscn  阅读(312)  评论(1编辑  收藏  举报