点分治与动态点分治学习笔记
点分治
点分治是处理树上路径的一类有力算法。
分治算法我们都经常用到,平时我们在序列上可以直接分治。
但是如果在树上怎么办呢?
我们可以指定一个根递归下去处理子树。
即将原来的数分成许多子树,对每个子树分别处理。
如果我们随缘指定一个根,递归层数可能是\(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\),满足某些条件。
动态点分治的套路是用数据结构维护题目所给定的条件,有时候需要容斥。