「算法笔记」点分治 & 点分树

2021 年写的(已折叠,入门可参考)
一、基本思想

点分治是用来解决树上路径问题的一种方法。简单地写一下 QwQ。

首先,给这棵树钦定一个根(不妨设为 \(x\)),再将这棵树上的所有简单路径分为两个部分:

  • 第一部分:经过 \(x\) 的简单路径(设路径的两端为 \((u,v)\),下同。\(u,v\) 在根 \(x\) 的不同子树内)。
  • 第二部分:不经过 \(x\) 的简单路径(\(u,v\) 都在根 \(x\) 的某棵子树内)。

根据 分治 的思想,发现对于第二部分的路径可以作为一个子问题递归到子树内的点计算,于是我们对于 \(x\) 只考虑第一部分的路径即可。

复杂度的瓶颈在递归次数上,每个分治子树的根节点不能随便选择。

若我们每次选择子树的 重心 作为根节点,则最大的子树大小不会超过原树的一半(每次递归子树大小至少缩小一半),可以保证递归层数最少,时间复杂度为 \(\mathcal{O}(n\log n)\)

二、代码实现

Luogu P3806 【模板】点分治 1

给定一棵有 \(n\) 个点的树,\(m\) 次询问,每次询问树上距离为 \(k\) 的点对是否存在。

\(1\leq n\leq 10^4,1\leq m\leq 100,1\leq k\leq 10^7\)

找重心:这里 提到过。重心就是 最大子树大小 最小 的节点。

若一个点已经被当做根过,那我们就不去管它。最后每个节点都会被作为重心递归一次(请自行思考),所以不会漏算。具体见代码。

//Luogu P3806
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e4+5,K=1e8+5;
int n,m,x,y,z,q[N],cnt,hd[N],to[N<<1],nxt[N<<1],val[N<<1],sz[N],g[N],tot,root,dis[N],top,s[N],t[N];
bool vis[N],f[K],ans[N];
void add(int x,int y,int z){
    to[++cnt]=y,nxt[cnt]=hd[x],hd[x]=cnt,val[cnt]=z;
}
void getRoot(int x,int fa){    //找根 
    sz[x]=1,g[x]=0;    //g[x]: 以 x 为根时的最大子树大小 
    for(int i=hd[x];i;i=nxt[i]){
        int y=to[i];
        if(y==fa||vis[y]) continue;    //vis[x]: x 是否被当做根过 
        getRoot(y,x),sz[x]+=sz[y],g[x]=max(g[x],sz[y]);
    }
    g[x]=max(g[x],tot-sz[x]);    //tot: 当前递归的这棵子树的大小 
    if(g[x]<g[root]) root=x;    //root: 当前找到的根 
}
void getDis(int x,int fa,int d){ 
    s[++top]=dis[x]=d;
    for(int i=hd[x];i;i=nxt[i]){
        int y=to[i],z=val[i];
        if(y==fa||vis[y]) continue;
        getDis(y,x,d+z);
    }
}
void calc(int x){    //对于 x,考虑经过 x 的路径
    int num=0;
    for(int i=hd[x];i;i=nxt[i]){
        int y=to[i],z=val[i];
        if(vis[y]) continue;
        top=0,getDis(y,x,z);    //计算以 y 为根的子树中的点与 x 之间的距离 
        for(int j=1;j<=top;j++)
            for(int k=1;k<=m;k++)
                if(q[k]>=s[j]) ans[k]|=f[q[k]-s[j]];    //对于第 k 次询问 q[k],s[j] 是以 y 为根的子树中的某个点与 x 之间的距离,然后看之前是否有和 x 距离为 q[k]-s[j] 的点(两点在 x 的两个不同子树中) 
        for(int j=1;j<=top;j++)
            f[t[++num]=s[j]]=1;    //标记。记录 t 数组方便清空(这样就可以不用 memset,以保证复杂度)
    }
    for(int i=1;i<=num;i++) f[t[i]]=0;
}
void solve(int x){
    vis[x]=f[0]=1,calc(x);
    for(int i=hd[x];i;i=nxt[i]){
        int y=to[i];
        if(vis[y]) continue;
        tot=sz[y],root=0,getRoot(y,0),solve(root);    //递归 
    }
}
signed main(){
    scanf("%lld%lld",&n,&m);
    for(int i=1;i<n;i++){
        scanf("%lld%lld%lld",&x,&y,&z);
        add(x,y,z),add(y,x,z);
    }
    for(int i=1;i<=m;i++)
        scanf("%lld",&q[i]);
    g[root=0]=1e18,tot=n,getRoot(1,0),solve(root);
    for(int i=1;i<=m;i++)
        puts(ans[i]?"AYE":"NAY");
    return 0;
}

后面可能还要写点什么,先占个坑 QAQ。

一、点分治

解决路径统计问题。方法:

  1. 对于 \(x\) 的子树 \(y_{1\sim k}\),先统计 \(y_i\)\(y_{1\sim i-1}\) 的答案,再加入 \(y_i\) 的信息。

    需要考虑怎么合并两段路径,使用桶/数据结构。

  2. 若统计的信息满足可减性:比如算有多少长度为 \(k\) 的路径经过 \(x\),先求任选两个 \(d\) 加起来是 \(k\) 的方案数,容斥减掉这两个 \(d\) 来自同个 \(y_i\) 的方案数。计数/点分树常用这种方法。

技巧:\(x,y\) 在分治重心的同一子树时,\(d_x+d_y\) 只会把 \(dis(x,y)\) 算大,适用于与 \(dis\) 相关的最小化问题。

单一重心移动模型:check 合法点在哪个方向,点分治优化移动过程,复杂度 \(\log n\cdot \mathcal O(\text{check})\)

点分治优化建图:点分治,每次解决跨越分治中心 \(rt\) 的连边需求,结合建立虚点减少边数。

结合完全图 MST:完全图边权与原树的路径有关。只要考虑了原树的每条路径就考虑了完全图的每条边,树上路径问题考虑点分治。MST 还有一种重要思路是保留有用的边,每次选出一个边集求 MST,没有被选中的边一定不会在最终的 MST 中,因此只要选出边集的并等于原图,并将所有边集的 MST 的并再求一次 MST 即可。用点分治来选出这些边集。

结合树上连通块:直接树形 DP 需要背包合并,复杂度高达容量平方,但单点插入复杂度不高,于是考虑使用插入代替合并,任选一个必选点作为根,从根节点出发,用“父亲传给儿子,在子树里绕一圈再传给父亲”的另一种依赖背包实现方式。如果要对每个 \(i\) 求包含 \(i\) 的连通块答案,可以 dfs 序上 DP + 前后缀合并。我们不关心连通块在原树中的最高点,只关心连通块过哪个分治中心,每次假设分治中心作为必选的根即可。

多组数据时记得清空 vis

1. P3806 【模板】点分治1

给定一棵有 \(n\) 个点的树,\(m\) 次询问,每次询问树上距离为 \(k\) 的点对是否存在。

\(1\leq n\leq 10^4\)\(1\leq m\leq 100\)\(1\leq k\leq 10^7\)

注意 mx[x]=0!vis[y]

#include<bits/stdc++.h>
using namespace std;
const int N=1e4+5,K=1e7+5;
int n,m,x,y,z,q[N],rt,vis[N],sz[N],mx[N],d[N],ans[N],f[K];
vector<int>tmp,s;
vector<pair<int,int> >v[N];
void getrt(int x,int fa,int tot){
	sz[x]=1,mx[x]=0;
	for(auto p:v[x]){
		int y=p.first;
		if(y!=fa&&!vis[y])
			getrt(y,x,tot),sz[x]+=sz[y],mx[x]=max(mx[x],sz[y]); 
	}
	if((mx[x]=max(mx[x],tot-sz[x]))<mx[rt]) rt=x;
} 
void getd(int x,int fa){
	if(d[x]>1e7) return ;
	s.push_back(d[x]);
	for(auto p:v[x]){
		int y=p.first;
		if(y!=fa&&!vis[y]) d[y]=d[x]+p.second,getd(y,x);
	}
}
void div(int x){
	vis[x]=1,f[0]=1,tmp.clear();
	for(auto p:v[x]){
		int y=p.first;
		if(!vis[y]){
			s.clear(),d[y]=p.second,getd(y,x);
			for(int i=1;i<=m;i++)
				for(int j:s) ans[i]|=q[i]>=j&&f[q[i]-j];
			for(int i:s) tmp.push_back(i),f[i]=1;
		}
	}
	for(int i:tmp) f[i]=0;
	for(auto p:v[x]){
		int y=p.first;
		if(!vis[y]) rt=0,getrt(y,x,sz[y]),div(rt);
	}
}
signed main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<n;i++){
		scanf("%d%d%d",&x,&y,&z);
		v[x].push_back({y,z}),v[y].push_back({x,z});
	}
	for(int i=1;i<=m;i++) scanf("%d",&q[i]);
	mx[0]=1e9,getrt(1,0,n),div(rt);
	for(int i=1;i<=m;i++) puts(ans[i]?"AYE":"NAY");
	return 0;
}

另一种写法:

#include<bits/stdc++.h>
using namespace std;
const int N=1e4+5,K=1e7+5;
int n,m,x,y,z,q[N],rt,vis[N],sz[N],mx[N],d[N],ans[N],buc[K];
vector<int>s;
vector<pair<int,int> >v[N];
void getrt(int x,int fa,int tot){
	sz[x]=1,mx[x]=0;
	for(auto p:v[x]){
		int y=p.first;
		if(y!=fa&&!vis[y])
			getrt(y,x,tot),sz[x]+=sz[y],mx[x]=max(mx[x],sz[y]); 
	}
	if((mx[x]=max(mx[x],tot-sz[x]))<mx[rt]) rt=x;
} 
void dfs(int x,int fa){
	if(d[x]>1e7) return ;
	s.push_back(x);
	for(auto p:v[x]){
		int y=p.first;
		if(y!=fa&&!vis[y]) d[y]=d[x]+p.second,dfs(y,x);
	}
}
void calc(int v){
	for(int x:s) buc[d[x]]++;
	for(int i=1;i<=m;i++)
		for(int x:s)
			if(q[i]>=d[x]) ans[i]+=buc[q[i]-d[x]]*v;
	for(int x:s) buc[d[x]]--;
	s.clear();
}
void div(int x){
	vis[x]=1,d[x]=0,dfs(x,0),calc(1);
	for(auto p:v[x]){
		int y=p.first;
		if(!vis[y]) dfs(y,x),calc(-1);
	}
	for(auto p:v[x]){
		int y=p.first;
		if(!vis[y]) rt=0,getrt(y,x,sz[y]),div(rt);
	}
}
signed main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<n;i++){
		scanf("%d%d%d",&x,&y,&z);
		v[x].push_back({y,z}),v[y].push_back({x,z});
	}
	for(int i=1;i<=m;i++) scanf("%d",&q[i]);
	mx[0]=1e9,getrt(1,0,n),div(rt);
	for(int i=1;i<=m;i++) puts(ans[i]?"AYE":"NAY");
	return 0;
}

2. P6626 [省选联考 2020 B 卷] 消息传递

2022.12.23

给出一棵 \(n\) 个节点的树,\(m\) 次询问与 \(x\) 距离为 \(k\) 的点的个数。

\(1\leq T\leq 5\)\(1\leq n,m\leq 10^5\)

//O2
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int t,n,m,x,y,k,rt,sz[N],mx[N],vis[N],d[N],buc[N],ans[N];
vector<int>v[N],s;
vector<pair<int,int> >q[N];
void getrt(int x,int fa,int tot){
	sz[x]=1,mx[x]=0;
	for(int y:v[x]) if(y!=fa&&!vis[y])
		getrt(y,x,tot),sz[x]+=sz[y],mx[x]=max(mx[x],sz[y]); 
	if((mx[x]=max(mx[x],tot-sz[x]))<mx[rt]) rt=x;
} 
void dfs(int x,int fa){
	s.push_back(x);
	for(int y:v[x])
		if(y!=fa&&!vis[y]) d[y]=d[x]+1,dfs(y,x);
}
void calc(int v){
	for(int x:s) buc[d[x]]++;
	for(int x:s)
		for(auto p:q[x]){
			int k=p.first;
			if(k>=d[x]) ans[p.second]+=buc[k-d[x]]*v;
		}
	for(int x:s) buc[d[x]]--;
	s.clear();
}
void div(int x){
	vis[x]=1,d[x]=0,dfs(x,0),calc(1);
	for(int y:v[x])
		if(!vis[y]) dfs(y,x),calc(-1);
	for(int y:v[x])
		if(!vis[y]) rt=0,getrt(y,x,sz[y]),div(rt);
}
signed main(){
	scanf("%d",&t);
	while(t--){
		scanf("%d%d",&n,&m);
		for(int i=1;i<=n;i++)
			v[i].clear(),q[i].clear(),vis[i]=0;
		for(int i=1;i<n;i++)
			scanf("%d%d",&x,&y),
			v[x].push_back(y),v[y].push_back(x);
		for(int i=1;i<=m;i++)
			scanf("%d%d",&x,&k),q[x].push_back({k,i}),ans[i]=0;
		mx[rt=0]=1e9,getrt(1,0,n),div(rt);
		for(int i=1;i<=m;i++) printf("%d\n",ans[i]);
	}
	return 0;
}

3. P4075 [SDOI2016]模式字符串

2022.12.23

给出一棵 \(n\) 个节点的树,节点 \(i\) 上有一个字符 \(a_i\)。给出一个长度为 \(m\) 的字符串 \(b\),问有多少路径形成的字符串是 \(b\) 重复整数倍的结果。

\(1\leq T\leq 10\)\(3\leq \sum n\leq 10^6\)\(3\leq \sum m\leq 10^6\)

对于一个分治中心,哈希判断每个点能否作为它到分治中心的一段前缀/后缀,桶统计。

//O2
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+5,c=131;
int t,n,m,x,y,rt,vis[N],sz[N],mx[N],d[N],buc[N];
long long pw[N],pre[N],suf[N],h[N],ans;
char a[N],b[N];
vector<int>v[N],s,rs;
void getrt(int x,int fa,int tot){
	sz[x]=1,mx[x]=0;
	for(int y:v[x]) if(y!=fa&&!vis[y])
		getrt(y,x,tot),sz[x]+=sz[y],mx[x]=max(mx[x],sz[y]); 
	if((mx[x]=max(mx[x],tot-sz[x]))<mx[rt]) rt=x;
} 
void dfs(int x,int fa){
	d[x]=d[fa]+1,h[x]=h[fa]+a[x]*pw[d[x]-1];
	if(pre[d[x]]==h[x]) s.push_back(d[x]);
	if(suf[d[x]]==h[x]) rs.push_back(d[x]);
	for(int y:v[x])
		if(y!=fa&&!vis[y]) dfs(y,x);
}
void calc(int v){
	for(int i:s) buc[(i-1)%m+1]++;
	for(int i:rs) ans+=v*buc[m-((i-1)%m+1)+1];
	for(int i:s) buc[(i-1)%m+1]--;
	s.clear(),rs.clear();
}
void div(int x){
	vis[x]=1,dfs(x,0),calc(1);
	for(int y:v[x])
		if(!vis[y]) dfs(y,x),calc(-1);
	for(int y:v[x])
		if(!vis[y]) rt=0,getrt(y,x,sz[y]),div(rt);
}
signed main(){
	scanf("%d",&t),pw[0]=1;
	for(int i=1;i<N;i++) pw[i]=pw[i-1]*c;
	while(t--){
		scanf("%d%d%s",&n,&m,a+1),ans=0;
		for(int i=1;i<=n;i++) v[i].clear(),vis[i]=0;
		for(int i=1;i<n;i++)
			scanf("%d%d",&x,&y),
			v[x].push_back(y),v[y].push_back(x);
		scanf("%s",b+1);
		for(int i=1;i<=n;i++)
			pre[i]=pre[i-1]*c+b[(i-1)%m+1],
			suf[i]=suf[i-1]*c+b[m-((i-1)%m+1)+1];
		mx[rt=0]=1e9,getrt(1,0,n),div(rt);
		printf("%lld\n",ans); 
	}
	return 0;
}

4. P5306 [COCI2018-2019#5] Transport

2022.12.23

给出一棵 \(n\) 个节点的树,求有多少有序对 \((x,y)\),使得:从 \(x\) 出发走到 \(y\)(简单路径),初始 \(cnt=0\),每经过一个点 \(cnt\) 加上点权,经过一条边 \(cnt\) 减去边权,满足 \(cnt\) 始终 \(\geq 0\)

\(1\leq n\leq 10^5\)\(1\leq a_i,w_i\leq 10^9\)

求出 \(dv_i,de_i\) 分别表示 \(i\) 到分治中心 \(rt\) 的点权和、边权和。

要选出来自不同子树的 \(x,y\) 使得:

  1. \(\min_{i\in \text{Path}(x,rt)}\{(dv_x-dv_i)-(de_x-de_i)\}\geq 0\),移项得 \((dv_x-de_x)-dv_i+de_i\)
  2. \((dv_x-a_{rt})-de_x+\min_{i\in\text{Path}(rt,y)}\{dv_{fa_i}-de_i\}\geq 0\)

维护 \(mn1_x=\min_{i\in\text{Path}(x,rt)}\{-dv_i+de_i\}\)\(mn2_y=\min_{i\in\text{Path}(rt,y)}\{dv_{fa_i}-de_i\}\),那么 \(dv_x-de_x+mn1_x\geq 0\)\(dv_x-a_{rt}-de_x+mn2_y\geq 0\)

排序 + 双指针即可,容斥减掉来自同个子树的情况。

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int n,a[N],x,y,z,sz[N],mx[N],rt,vis[N],tp1,tp2;
long long dv[N],de[N],mn1[N],mn2[N],s1[N],s2[N],ans;
vector<pair<int,int> >v[N];
void getrt(int x,int fa,int tot){
	sz[x]=1,mx[x]=0;
	for(auto p:v[x]){
		int y=p.first;
		if(y!=fa&&!vis[y])
			getrt(y,x,tot),sz[x]+=sz[y],mx[x]=max(mx[x],sz[y]); 
	}
	if((mx[x]=max(mx[x],tot-sz[x]))<mx[rt]) rt=x;
} 
void getd(int x,int fa){
	dv[x]=dv[fa]+a[x];
	mn1[x]=min(mn1[fa],-dv[x]+de[x]);
	mn2[x]=min(mn2[fa],dv[fa]-de[x]);
	for(auto p:v[x]){
		int y=p.first;
		if(y!=fa&&!vis[y]) de[y]=de[x]+p.second,getd(y,x);
	}
}
void dfs(int x,int fa,int rt){
	if(dv[x]-de[x]+mn1[x]>=0) s1[++tp1]=dv[x]-a[rt]-de[x];
	s2[++tp2]=mn2[x];
	for(auto p:v[x]){
		int y=p.first;
		if(y!=fa&&!vis[y]) dfs(y,x,rt);
	}
}
void calc(int v){
	sort(s1+1,s1+1+tp1),sort(s2+1,s2+1+tp2);
	for(int i=1,j=tp2+1;i<=tp1;i++){
		while(j>1&&s1[i]+s2[j-1]>=0) j--;
		ans+=v*(tp2-j+1);
	}
	tp1=tp2=0;
}
void div(int x){
	vis[x]=1,de[x]=0,getd(x,0),dfs(x,0,x),calc(1);
	for(auto p:v[x]){
		int y=p.first;
		if(!vis[y]) dfs(y,x,x),calc(-1);
	}
	for(auto p:v[x]){
		int y=p.first;
		if(!vis[y]) rt=0,getrt(y,x,sz[y]),div(rt);
	}
}
signed main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++) scanf("%d",&a[i]);
	for(int i=1;i<n;i++){
		scanf("%d%d%d",&x,&y,&z);
		v[x].push_back({y,z}),v[y].push_back({x,z});
	}
	mx[0]=mn1[0]=mn2[0]=1e9,getrt(1,0,n),div(rt);
	printf("%lld\n",ans-n);
	return 0;
}

5. P2664 树上游戏

2023.1.10

给出一棵 \(n\) 个节点的树,每个点有颜色 \(a_i\)。对每个 \(x\)\(\sum_{y=1}^n (x\to y\ 路径上的颜色数)\)

\(1\leq n,a_i\leq 10^5\)

点分治,计算跨过 \(rt\) 的路径对端点的贡献。

先考虑一端为 \(rt\)。对颜色处理,从 \(rt\) 开始 dfs,设 dfs 到 \(x\),若 \(rt\leadsto x\) 的路径上 \(a_x\) 第一次出现,当前连通块中颜色 \(a_x\)\(ans_{rt}\) 的贡献 \(val_{a_x}\) 会增加 \(sz_x\),当前连通块对 \(ans_{rt}\) 的总贡献 \(sum\) 也增加了 \(sz_x\)

然后考虑路径两端在 \(rt\) 不同子树内的情况,枚举 \(rt\) 的子树,需要统计其他子树对当前子树的贡献。

  1. 去掉该子树对 \(val_i,sum\) 的贡献。

  2. 从该子树的根开始 dfs,设 dfs 到 \(x\)。若 \(rt\leadsto x\) 的路径上颜色 \(i\) 出现过,则对 \(ans_x\) 的贡献是其他子树并上 \(rt\) 的总点数 \(num\);否则对 \(ans_x\) 的贡献是 \(val_i\)

    实现时,先默认所有颜色 \(i\) 都没有出现,贡献为 \(sum=\sum val_i\),然后当 \(rt\leadsto x\) 的路径上颜色 \(a_x\) 第一次出现时,令 \(sum\gets sum-val_{a_x}+num\)

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int n,a[N],x,y,mx[N],sz[N],vis[N],rt,buc[N],num;
long long val[N],sum,ans[N];
vector<int>v[N];
void getrt(int x,int fa,int tot){
	sz[x]=1,mx[x]=0;
	for(int y:v[x]) if(y!=fa&&!vis[y])
		getrt(y,x,tot),sz[x]+=sz[y],mx[x]=max(mx[x],sz[y]);
	if((mx[x]=max(mx[x],tot-sz[x]))<mx[rt]) rt=x;
}
void getsz(int x,int fa){
	sz[x]=1;
	for(int y:v[x])
		if(y!=fa&&!vis[y]) getsz(y,x),sz[x]+=sz[y];
}
void dfs(int x,int fa,int op){
	num+=op;
	if(!buc[a[x]]++) val[a[x]]+=op*sz[x],sum+=op*sz[x];
	for(int y:v[x])
		if(y!=fa&&!vis[y]) dfs(y,x,op);
	buc[a[x]]--;
}
void calc(int x,int fa){
	if(!buc[a[x]]++) sum-=val[a[x]],sum+=num;
	ans[x]+=sum;
	for(int y:v[x])
		if(y!=fa&&!vis[y]) calc(y,x);
	if(!--buc[a[x]]) sum+=val[a[x]],sum-=num;
}
void div(int x){
	vis[x]=1;
	getsz(x,0),dfs(x,0,1),ans[x]+=sum;
	for(int y:v[x]) if(!vis[y]){
		val[a[x]]-=sz[y],sum-=sz[y];	//!!! a[x] 出现过,贡献是其他子树并上 x 的总点数 sz[x]-sz[y],而不是 sz[x]
		buc[a[x]]++,dfs(y,x,-1),buc[a[x]]--;
		calc(y,x);
		val[a[x]]+=sz[y],sum+=sz[y];
		buc[a[x]]++,dfs(y,x,1),buc[a[x]]--;
	}
	dfs(x,0,-1);
	for(int y:v[x])
		if(!vis[y]) rt=0,getrt(y,x,sz[y]),div(rt);
}
signed main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++) scanf("%d",&a[i]);
	for(int i=1;i<n;i++)
		scanf("%d%d",&x,&y),
		v[x].push_back(y),v[y].push_back(x);
	mx[rt]=1e9,getrt(1,0,n),div(rt);
	for(int i=1;i<=n;i++) printf("%lld\n",ans[i]);
	return 0;
} 

6. P3714 [BJOI2017]树的难题

2023.1.10

给出一棵 \(n\) 个节点的树,边有颜色。共有 \(m\) 种颜色,颜色有权值 \(a_i\)

一条路径 \(x\to y\) 的权值为每个颜色段的颜色权值之和,求边数 \(\in[l,r]\) 的所有路径中路径权值的最大值。

\(1\leq n,m\leq 2\times 10^5\)\(|a_i|\leq 10^4\)

点分治,要从 \(rt\) 的不同子树中选出 \(x,y\),路径权值为 \(val_{rt\leadsto x}+val_{rt\leadsto y}-[top_x=top_y]a_{top_x}\),要求 \(len_{rt\leadsto x}+len_{rt\leadsto y}\in[l,r]\),其中 \(top_x\) 表示 \(rt\leadsto x\) 路径上第一条边的颜色。

将子树按 \(top\) 排序,\(top\) 相同的子树一起处理,开两棵线段树分别维护 \(top\) 不同和 \(top\) 相同的情况,线段树下标为 \(len\),值为 \(val\)。每次加入一棵子树的所有 \(rt\leadsto x\) 的链,统计与之前子树拼的最大值,再在线段树中修改。

具体见代码。时间复杂度 \(\mathcal O(n\log^2 n)\)

//O2
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+5;
int n,m,l,r,a[N],x,y,z,sz[N],mx[N],rt,vis[N],d[N],val[N],ans=-2e9;
vector<pair<int,int> >v[N];
vector<int>h[N],col,s[N];
struct Seg{
	int mx[N<<2];
	vector<int>tmp;
	void modify(int p,int l,int r,int pos,int v){
		tmp.push_back(p);
		if(l==r){mx[p]=max(mx[p],v);return ;}
		int mid=(l+r)/2;
		if(pos<=mid) modify(p<<1,l,mid,pos,v);
		else modify(p<<1|1,mid+1,r,pos,v);
		mx[p]=max(mx[p<<1],mx[p<<1|1]);
	}
	int query(int p,int l,int r,int lx,int rx){
		if(lx>rx) return -2e9;
		if(l>=lx&&r<=rx) return mx[p];
		int mid=(l+r)/2,ans=-2e9;
		if(lx<=mid) ans=query(p<<1,l,mid,lx,rx);
		if(rx>mid) ans=max(ans,query(p<<1|1,mid+1,r,lx,rx));
		return ans;
	}
	void clear(){
		for(int i:tmp) mx[i]=-2e9; tmp.clear();
	}
}t1,t2;
void getrt(int x,int fa,int tot){
	sz[x]=1,mx[x]=0;
	for(auto p:v[x]){
		int y=p.first;
		if(y!=fa&&!vis[y])
			getrt(y,x,tot),sz[x]+=sz[y],mx[x]=max(mx[x],sz[y]); 
	}
	if((mx[x]=max(mx[x],tot-sz[x]))<mx[rt]) rt=x;
} 
void dfs(int x,int fa,int pre,int rt){
	d[x]=d[fa]+1,h[rt].push_back(x);
	for(auto p:v[x]){
		int y=p.first,z=p.second;
		if(y!=fa&&!vis[y]) val[y]=val[x]+(z!=pre)*a[z],dfs(y,x,z,rt);
	}
}
void div(int x){
	vis[x]=1,d[x]=0;
	for(auto p:v[x]){
		int y=p.first,z=p.second;
		if(!vis[y])
			h[y].clear(),val[y]=a[z],dfs(y,x,z,y),
			col.push_back(z),s[z].push_back(y);
	}
	sort(col.begin(),col.end());
	col.erase(unique(col.begin(),col.end()),col.end());
	t1.clear(),t1.modify(1,0,n-1,0,0);
	for(int c:col){
		t2.clear();
		for(int y:s[c]){
			for(int p:h[y])
				ans=max({ans,val[p]+t1.query(1,0,n-1,max(l-d[p],0),r-d[p]),
				val[p]+t2.query(1,0,n-1,max(l-d[p],0),r-d[p])-a[c]});	//!!! max(,0)
			for(int p:h[y]) t2.modify(1,0,n-1,d[p],val[p]);
		}
		for(int y:s[c])
			for(int p:h[y]) t1.modify(1,0,n-1,d[p],val[p]);
	}
	for(int c:col) s[c].clear(); col.clear();
	for(auto p:v[x]){
		int y=p.first;
		if(!vis[y]) rt=0,getrt(y,x,sz[y]),div(rt);
	}
}
signed main(){
	scanf("%d%d%d%d",&n,&m,&l,&r);
	for(int i=1;i<=m;i++) scanf("%d",&a[i]);
	for(int i=1;i<n;i++)
		scanf("%d%d%d",&x,&y,&z),
		v[x].push_back({y,z}),v[y].push_back({x,z});
	for(int i=1;i<=(n<<2);i++) t1.mx[i]=t2.mx[i]=-2e9;
	mx[0]=1e9,getrt(1,0,n),div(rt),printf("%d\n",ans);
	return 0;
} 

7. CF150E Freezing with Style(*3000)

2023.1.10 单调队列按秩合并

给出一棵 \(n\) 个节点的树,边有边权。求一条边数 \(\in[l,r]\) 的路径,使得路径上边权中位数最大(\(a_{1\sim k}\) 的中位数是 \(a_{\lfloor\frac{k+1}{2}\rfloor}\)),输出路径端点。

\(1\leq n\leq 10^5\)\(0\leq w_i\leq 10^9\)

二分答案,\(\geq mid\) 的看作 \(1\)\(<mid\) 的看作 \(-1\),要找边数 \(\in[l,r]\) 且边权和 \(\geq 0\) 的路径。

\(g_i\) 表示之前所有子树中深度为 \(i\) 的路径边权和最大值,\(f_i\) 表示当前子树的。要判 \(f_i+\max\limits_{l-i\leq j\leq r-i} g_j\geq 0\),发现随着 \(i\) 增大,可选的 \(j\) 区间向左滑动,于是按子树最大深度从小到大排序后单调队列即可。

排序:因为加入子树 \(y\) 时,单调队列移动覆盖区域 \([l-mxd_y,r]\) 较广。由于实际覆盖区域大小不超过队列长度(之前的 \(mxd\)),所以才排序来保证复杂度,使总覆盖区域为 \(\mathcal O(\sum mxd)=\mathcal O(\sum sz)=\mathcal O(n\log n)\)

时间复杂度 \(\mathcal O(n\log n\log V)\)

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int n,L,R,x,y,z,w[N],cnt,lim,sz[N],mx[N],mxd[N],d[N],val[N],vis[N],rt,len,q[N],a1,a2;
pair<int,int>f[N],g[N];
vector<pair<int,int> >v[N];
void getrt(int x,int fa,int tot){
	sz[x]=1,mx[x]=0;
	for(auto p:v[x]){
		int y=p.first;
		if(y!=fa&&!vis[y])
			getrt(y,x,tot),sz[x]+=sz[y],mx[x]=max(mx[x],sz[y]); 
	}
	if((mx[x]=max(mx[x],tot-sz[x]))<mx[rt]) rt=x;
} 
void dfs(int x,int fa){
	d[x]=d[fa]+1,mxd[x]=1;
	for(auto p:v[x]){
		int y=p.first;
		if(y!=fa&&!vis[y])
			val[y]=val[x]+(p.second>=lim?1:-1),dfs(y,x),mxd[x]=max(mxd[x],mxd[y]+1);
	}
}
void dfs2(int x,int fa){
	f[d[x]]=max(f[d[x]],{val[x],x});
	for(auto p:v[x]){
		int y=p.first;
		if(y!=fa&&!vis[y]) dfs2(y,x);
	}
}
void div(int x){
	vis[x]=1,val[x]=len=0,dfs(x,0);
	sort(v[x].begin(),v[x].end(),[](pair<int,int>x,pair<int,int>y){return mxd[x.first]<mxd[y.first];});
	fill(g+1,g+1+mxd[x],make_pair(-1e9,0)),g[0]={0,x};	//!!! {,x}
	for(auto p:v[x]){
		int y=p.first,l=1,r=0,j=min(R,len);
		if(vis[y]) continue;
		fill(f,f+1+mxd[y],make_pair(-1e9,0)),dfs2(y,x);
		for(int i=1;i<=mxd[y];i++){
			while(j>=max(L-i,0)){
				while(l<=r&&g[q[r]]<=g[j]) r--;
				q[++r]=j--;
			}
			while(l<=r&&q[l]>R-i) l++;
			if(l<=r&&f[i].first+g[q[l]].first>=0) a1=f[i].second,a2=g[q[l]].second;
		}
		for(int i=1;i<=mxd[y];i++) g[i]=max(g[i],f[i]);
		len=mxd[y];
	}
	for(auto p:v[x]){
		int y=p.first;
		if(!vis[y]) rt=0,getrt(y,x,sz[y]),div(rt);
	}
}
bool ok(int mid){
	lim=mid,fill(vis+1,vis+1+n,0),a1=a2=-1;
	mx[rt=0]=1e9,getrt(1,0,n),div(rt);
	return ~a1;
}
signed main(){
	scanf("%d%d%d",&n,&L,&R),d[0]=-1;
	for(int i=1;i<n;i++)
		scanf("%d%d%d",&x,&y,&z),w[++cnt]=z,
		v[x].push_back({y,z}),v[y].push_back({x,z});
	sort(w+1,w+1+cnt),cnt=unique(w+1,w+1+cnt)-w-1;	//卡常
	int l=1,r=cnt,ans=0;
	while(l<=r){
		int mid=(l+r)/2;
		if(ok(w[mid])) ans=mid,l=mid+1;
		else r=mid-1;
	}
	ok(w[ans]),printf("%d %d\n",a1,a2);
	return 0;
}

8. P4886 快递员

2023.1.10 点分治的重心移动套路

给出一棵 \(n\) 个节点的树,边有边权。

给出 \(m\) 个点对 \((a_i,b_i)\),求使得 \(\max\limits_{1\leq i\leq m}\{dis(a_i,p)+dis(b_i,p)\}\) 最小的点 \(p\)

\(1\leq n,m\leq 10^5\)\(1\leq w_i\leq 10^3\)

调整法,当前的 \(p\) 怎么移动能使答案最小。设 \(dis(a_i,p)+dis(b_i,p)\) 最大的 \(i\) 们是 \(s_{1\sim k}\)

  • 若存在 \(i\) 使得 \((a_{s_i},b_{s_i})\)\(p\) 的不同子树,那么没有更优解了:

    因为 \(p\)\(a_{s_i}\leadsto b_{s_i}\) 的路径上,如果移到路径外 \(dis(a_{s_i},p)+dis(b_{s_i},p)\) 会变大,如果移到路径上的其他点可能会产生其他更大的 \(dis(a_j,p)+dis(b_j,p)\)\(p\) 一定不劣。

  • 反之,\((a_{s_i},b_{s_i})\)\(p\) 同个子树,往这个子树方向移动可能更优:

    “可能”而非“一定”是因为可能会产生其他更大的 \(dis(a_j,p)+dis(b_j,p)\)

那么如果 \(p\) 要移动,每个 \(i\)\((a_{s_i},b_{s_i})\) 都在 \(p\) 的同个子树。往哪个子树移可能有更优解?

  • 发现若 \((a_{s_i},b_{s_i}),(a_{s_j},b_{s_j})\)\(p\) 的不同子树,也没有更优解了,因为无论 \(p\) 往哪边移动,两个点对中总有一个到 \(p\) 的距离和会增大。

所以只可能往一个子树移动。

来条链就能把每次暴力移动一步叉了。所以使用点分治来优化,将每次移动一步改为跳到目标子树的重心。

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int n,m,x,y,z,a[N],b[N],bl[N],sz[N],mx[N],rt,s[N],vis[N],d[N],ans=1e9;
vector<pair<int,int> >v[N];
void getrt(int x,int fa,int tot){
	sz[x]=1,mx[x]=0;
	for(auto p:v[x]){
		int y=p.first;
		if(y!=fa&&!vis[y])
			getrt(y,x,tot),sz[x]+=sz[y],mx[x]=max(mx[x],sz[y]);
	}
	if((mx[x]=max(mx[x],tot-sz[x]))<mx[rt]) rt=x;
}
void dfs(int x,int fa,int rt){
	bl[x]=rt;
	for(auto p:v[x]){
		int y=p.first;
		if(y!=fa) d[y]=d[x]+p.second,dfs(y,x,!rt?y:rt);
	}
}
void div(int x){
	vis[x]=1,d[x]=0,dfs(x,0,0);
	int mx=0,top=0,val,ok=1,y;
	for(int i=1;i<=m;i++){
		if((val=d[a[i]]+d[b[i]])>mx) mx=val,s[top=1]=i;
		else if(val==mx) s[++top]=i;
	}
	ans=min(ans,mx),y=bl[a[s[1]]];
	for(int i=1;i<=top;i++)
		ok&=bl[a[s[i]]]==bl[b[s[i]]]&&bl[a[s[i]]]==y;
	if(ok&&!vis[y]) rt=0,getrt(y,x,sz[y]),div(rt);
}
signed main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<n;i++)
		scanf("%d%d%d",&x,&y,&z),
		v[x].push_back({y,z}),v[y].push_back({x,z});
	for(int i=1;i<=m;i++)
		scanf("%d%d",&a[i],&b[i]);
	mx[0]=1e9,getrt(1,0,n),div(rt);
	printf("%d\n",ans);
	return 0;
}

9. CF566C Logistical Questions(*3000)

2023.1.11

给出一棵 \(n\) 个节点的树,点有点权 \(a_i\),边有边权,求使得 \(\sum_{i=1}^n a_i\times dis(x,i)^{\frac 3 2}\) 最小的 \(x\)(带权重心)。

\(1\leq n\leq 2\times 10^5\)\(0\leq a_i\leq 10^8\)\(1\leq w_i\leq 10^3\)

虽然是魔改的树上重心,但仍然有这样的性质:可以证明一个点靠近重心,带权距离和减小,否则增加。且如果从一个点往某子树方向调整,至多只有一个子树满足调整后可能更优。找出这个子树可以求导,套上点分治来优化。

具体地,设当前站在点 \(x\),让 \(x\) 沿着 \((x,y)\) 走距离 \(d\),带权距离和为:

\[f_y(d)=\sum_{i\not\in subtree_y}a_i(dis(x,i)+d)^{\frac 3 2}+\sum_{i\in subtree_y}a_i(dis(x,i)-d)^{\frac 3 2} \]

求导后(求导时 \(a,dis\) 视为常数,\(d\) 为变量,注意算 \((dis\pm d)^{\frac 3 2}\) 时要按复合函数求导)为:

\[f'_y(d)=\sum_{i\not\in subtree_y}\frac 3 2a_i(dis(x,i)+d)^{\frac 1 2}-\sum_{i\in subtree_y}\frac 3 2 a_i(dis(x,i)-d)^{\frac 1 2} \]

\(f'_y(0)<0\) 就往子树 \(y\) 走,即往 \(\sum_{i=1}^n a_i\sqrt{dis(x,i)}-2\sum_{i\in subtree_y}a_i\sqrt{dis(x,i)}<0\) 的子树 \(y\) 走,显然这样的子树 \(y\) 最多只有一个。

#include<bits/stdc++.h>
using namespace std;
const int N=2e5+5;
int n,a[N],x,y,z,sz[N],mx[N],rt,vis[N],d[N];
double f[N],sum;
pair<double,int>ans={1e30,0};	//!!! 8e18 不够,见 ZR#266
vector<pair<int,int> >v[N];
void getrt(int x,int fa,int tot){
	sz[x]=1,mx[x]=0;
	for(auto p:v[x]){
		int y=p.first;
		if(y!=fa&&!vis[y])
			getrt(y,x,tot),sz[x]+=sz[y],mx[x]=max(mx[x],sz[y]);
	}
	if((mx[x]=max(mx[x],tot-sz[x]))<mx[rt]) rt=x;
}
void dfs(int x,int fa){
	sum+=a[x]*pow(d[x],1.5),f[x]=a[x]*sqrt(d[x]);
	for(auto p:v[x]){
		int y=p.first;
		if(y!=fa) d[y]=d[x]+p.second,dfs(y,x),f[x]+=f[y];
	}
}
void div(int x){
	vis[x]=1,d[x]=sum=0,dfs(x,0),ans=min(ans,{sum,x});
	for(auto p:v[x]){
		int y=p.first;
		if(!vis[y]&&f[x]-2*f[y]<0) return rt=0,getrt(y,x,sz[y]),div(rt);
	}
}
signed main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++) scanf("%d",&a[i]);
	for(int i=1;i<n;i++)
		scanf("%d%d%d",&x,&y,&z),
		v[x].push_back({y,z}),v[y].push_back({x,z});
	mx[0]=1e9,getrt(1,0,n),div(rt);
	printf("%d %.10lf\n",ans.second,ans.first);
	return 0;
}

10. ZR#2267. [22 C班 Day1] 狗子旅游

2023.1.12 点分治优化建图

给出一棵 \(n\) 个节点的树,每个点有值 \(a_i\)。新建一张新图 \(G\)\((x\to y)\in E\) 当且仅当树上 \(dis(x,y)=a_x\),你可以从 \(G\) 上任选一个点出发,可以多次经过一个点,问最多能访问到多少个不同的点。

\(1\leq n\leq 10^5\)

\(\mathcal O(n^2)\) 做法就是暴力连边,SCC 上一个点到达了就能全部到,于是 SCC 缩点然后在 DAG 上 DP 求最长链即可。

考虑减少边数。点分治,每次处理跨越分治中心 \(rt\) 的连边需求,连 \(x\) 的边时,一定是距离 \(rt\)\(a_x-dis(x,rt)\) 的一个前缀和后缀。对每一层的点分开用前缀优化建图即可。

时间复杂度 \(\mathcal O(n\log n)\)

#include<bits/stdc++.h>
using namespace std;
const int N=2e6+5;
int n,x,y,cnt,a[N],sz[N],mx[N],rt,vis[N],d[N],mxd[N],pos[N],top,s[N],tim,dfn[N],low[N],tmp,tot,c[N],in[N],num[N],f[N],ans;
vector<int>v[N],g[N],g2[N];
queue<int>q;
void getrt(int x,int fa,int tot){
	sz[x]=1,mx[x]=0;
	for(int y:v[x]) if(y!=fa&&!vis[y])
		getrt(y,x,tot),sz[x]+=sz[y],mx[x]=max(mx[x],sz[y]);
	if((mx[x]=max(mx[x],tot-sz[x]))<mx[rt]) rt=x;
}
void dfs(int x,int fa){
	d[x]=d[fa]+1,mxd[x]=1;
	if(d[x]==a[x]) g[x].push_back(rt);
	if(d[x]==a[rt]) g[rt].push_back(x);
	for(int y:v[x])
		if(y!=fa&&!vis[y]) dfs(y,x),mxd[x]=max(mxd[x],mxd[y]+1);
}
void dfs1(int x,int fa){
	if(a[x]>d[x]&&pos[a[x]-d[x]]) g[x].push_back(pos[a[x]-d[x]]);
	for(int y:v[x])
		if(y!=fa&&!vis[y]) dfs1(y,x);
}
void dfs2(int x,int fa){
	g[pos[d[x]]].push_back(x);
	for(int y:v[x])
		if(y!=fa&&!vis[y]) dfs2(y,x);
}
void div(int x){
	vis[x]=1,dfs(x,0);
	for(int o=0;o<2;o++){
		for(int y:v[x]) if(!vis[y]){
			dfs1(y,x);
			for(int i=1;i<=mxd[y];i++){
				cnt++;
				if(pos[i]) g[cnt].push_back(pos[i]);
				pos[i]=cnt;
			}
			dfs2(y,x);
		}
		fill(pos,pos+1+mxd[x],0);	//!!! 注意必须放后面清,保证所有 pos 均为 0,如果放前面 pos[a[x]-d[x]] 可能是之前的
		reverse(v[x].begin(),v[x].end());
	}
	for(int y:v[x])
		if(!vis[y]) rt=0,getrt(y,x,sz[y]),div(rt);
}
void tarjan(int x){
	dfn[x]=low[x]=++tim,s[++top]=x;
	for(int y:g[x]){
		if(!dfn[y]) tarjan(y),low[x]=min(low[x],low[y]);
		else if(!c[y]) low[x]=min(low[x],dfn[y]);
	}
	if(dfn[x]==low[x]){
		tot++;
		do{c[tmp=s[top--]]=tot,num[tot]+=tmp<=n;}while(tmp!=x);
	}
}
signed main(){
	scanf("%d",&n),cnt=n,d[0]=-1;
	for(int i=1;i<n;i++)
		scanf("%d%d",&x,&y),
		v[x].push_back(y),v[y].push_back(x);
	for(int i=1;i<=n;i++) scanf("%d",&a[i]);
	mx[0]=1e9,getrt(1,0,n),div(rt);
	for(int i=1;i<=cnt;i++)
		if(!dfn[i]) tarjan(i);
	for(int x=1;x<=cnt;x++)
		for(int y:g[x])
			if(c[x]!=c[y]) g2[c[x]].push_back(c[y]),in[c[y]]++;
	for(int i=1;i<=tot;i++)
		if(!in[i]) q.push(i),f[i]=num[i];
	while(q.size()){
		int x=q.front();q.pop();
		ans=max(ans,f[x]);
		for(int y:g2[x]){
			f[y]=max(f[y],f[x]+num[y]);
			if(!--in[y]) q.push(y);
		}
	}
	printf("%d\n",ans);
	return 0;
}

11. AT_cf17_final_j Tree MST

2023.1.12

给出一棵 \(n\) 个节点的树,点有点权 \(a_i\),边有边权。建立一张完全图,边 \((x,y)\) 的边权为 \(a_x+a_y+dis(x,y)\)。求这张完全图的 MST。

\(2\leq n\leq 2\times 10^5\)\(1\leq a_i,w_i\leq 10^9\)

点分治,对于每个分治中心 \(rt\),求出对应连通块中每个点到 \(rt\) 的距离 \(dis_i\),找到其中 \(a_i+dis_i\) 最小的点与每个点配对即可,不需要考虑在同个子树的情况,因为这种边一定不优,会在不同子树时考虑。

这样得到了 \(\mathcal O(n\log n)\) 条有用的边,跑 Kruskal 复杂度 \(\mathcal O(n\log^2 n)\)

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N=2e5+5;
int n,a[N],x,y,z,sz[N],mx[N],rt,vis[N],f[N];
ll d[N],ans;
pair<ll,int>mn;
vector<int>s;
vector<pair<int,int> >v[N];
struct E{int x,y; ll z;};
vector<E>e;
void getrt(int x,int fa,int tot){
	sz[x]=1,mx[x]=0;
	for(auto p:v[x]){
		int y=p.first;
		if(y!=fa&&!vis[y])
			getrt(y,x,tot),sz[x]+=sz[y],mx[x]=max(mx[x],sz[y]);
	}
	if((mx[x]=max(mx[x],tot-sz[x]))<mx[rt]) rt=x;
}
void dfs(int x,int fa){
	s.push_back(x);
	mn=min(mn,{a[x]+d[x],x});
	for(auto p:v[x]){
		int y=p.first;
		if(y!=fa&&!vis[y]) d[y]=d[x]+p.second,dfs(y,x);
	}
}
void div(int x){
	vis[x]=1,d[x]=0,s.clear(),mn={1e18,0},dfs(x,0);
	for(int i:s)
		e.push_back({i,mn.second,mn.first+a[i]+d[i]});
	for(auto p:v[x]){
		int y=p.first;
		if(!vis[y]) rt=0,getrt(y,x,sz[y]),div(rt);
	}
}
int find(int x){return x==f[x]?x:f[x]=find(f[x]);}
signed main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
		scanf("%d",&a[i]),f[i]=i;
	for(int i=1;i<n;i++)
		scanf("%d%d%d",&x,&y,&z),
		v[x].push_back({y,z}),v[y].push_back({x,z});
	mx[0]=1e9,getrt(1,0,n),div(rt);
	sort(e.begin(),e.end(),[](E x,E y){return x.z<y.z;});
	for(auto i:e){
		int x=find(i.x),y=find(i.y);
		if(x!=y) f[y]=x,ans+=i.z;
	}
	printf("%lld\n",ans);
	return 0;
} 

12. HDU 6643 Ridiculous Netizens

2023.1.12

给出一棵 \(n\) 个节点的无根树,点有点权 \(a_i\)。求有多少连通块的点权之积 \(\leq m\),答案对 \(10^9+7\) 取模。

\(1\leq T\leq 10\)\(1\leq n\leq 2000\)\(1\leq a_i\leq m\leq 10^6\)

枚举连通块的根,选一个点必须选它的父亲,做树上依赖背包。可以在 dfs 序上 dp,不过有个更简单的方法:设 \(f_{x,i}\) 表示 \(x\) 已经加入的子树,乘积为 \(i\) 的方案数,dfs 访问到儿子时用父亲更新它,回溯时再用它更新父亲即可。

第二维较大,按套路把 \(i\) 换成 \(\lfloor\frac m i \rfloor\),这样第二维状态数 \(\mathcal O(\sqrt m)\)

枚举每个点作为根复杂度较高。用点分治优化,我们不关心连通块在原树中的最高点,只关心连通块过哪个分治中心。

#include<bits/stdc++.h>
using namespace std;
const int N=2e3+5,M=1e6+5,mod=1e9+7;
int t,n,m,x,y,a[N],val[N],id[M],tot,vis[N],sz[N],mx[N],f[N][N],rt,ans;
vector<int>v[N];
void getrt(int x,int fa,int tot){
	sz[x]=1,mx[x]=0;
	for(int y:v[x]) if(y!=fa&&!vis[y])
		getrt(y,x,tot),sz[x]+=sz[y],mx[x]=max(mx[x],sz[y]);
	if((mx[x]=max(mx[x],tot-sz[x]))<mx[rt]) rt=x;
}
void dfs(int x,int fa){
	fill(f[x],f[x]+1+tot,0);
	for(int i=1;i<=tot;i++)
		(f[x][id[val[i]/a[x]]]+=f[fa][i])%=mod;
	for(int y:v[x])
		if(y!=fa&&!vis[y]) dfs(y,x);
	if(fa) for(int i=1;i<=tot;i++) (f[fa][i]+=f[x][i])%=mod;
}
void div(int x){
	vis[x]=1,f[0][id[m/1]]=1,dfs(x,0);
	for(int i=1;i<=tot;i++) (ans+=f[x][i])%=mod;
	for(int y:v[x])
		if(!vis[y]) rt=0,getrt(y,x,sz[y]),div(rt);
}
signed main(){
	scanf("%d",&t);
	while(t--){
		scanf("%d%d",&n,&m),tot=ans=0;
		for(int i=1,lst=0;i<=m;i++){
			if((x=m/i)!=lst) val[id[x]=++tot]=x;
			lst=x;
		}
		for(int i=1;i<=n;i++)
			scanf("%d",&a[i]),v[i].clear(),vis[i]=0;
		for(int i=1;i<n;i++)
			scanf("%d%d",&x,&y),
			v[x].push_back(y),v[y].push_back(x);
		mx[0]=1e9,rt=0,getrt(1,0,n),div(rt);
		printf("%d\n",ans),f[0][id[m/1]]=0;
	}
	return 0;
}

13. QOJ#4815. Flower's Land

2023.1.14

给出一棵 \(n\) 个节点的树,点有点权。

对每个点,求包含它的大小恰为 \(k\) 的所有子连通块中,最大的点权和。

\(1\leq n\leq 40000\)\(1\leq k\leq \min(n,3000)\)\(1\leq a_i\leq 5\times 10^5\)

类似上一题。

点分治,每次处理包含分治中心的所有子连通块,dfs 序上 DP,处理出前后缀信息合并即可。时间复杂度 \(\mathcal O(nk\log n)\)。具体地:

  • \(f_{i,j}\) 表示考虑了 dfs 序上第 \([1,i]\) 个点,已经选了 \(j\) 个点的最大点权和。

    \(f_{i-1,j-1}+a_{id_i}\to f_{i,j}\)(选 \(i\)),\(f_{i-1,j}\to f_{i+sz_{id_i}-1,j}\)(不选 \(i\)\([i,i+sz_{id_i}-1]\) 都不能选,都考虑完了。注意不是转移到 \(f_{i+sz_{id_i},j}\),因为 \(i+sz_{id_i}\) 并没有考虑过是否选择)。

  • \(g_{i,j}\) 表示考虑了 \([i,cnt]\)

  • \(ans_{id_i}\gets \max f_{i-1,j}+g_{i+1,k-j-1}+a_{id_i}\)如果 \(i\) 的祖先不选就直接跳过 \(i-1\) 了,无法转移到 \(f_{i-1,j}\),所以已经体现了 \(i\) 的祖先都选

若当前分治块大小 \(<k\) 直接返回,复杂度 \(\mathcal O(nk\log\frac n k)\)(每分治一层 sz/=2\(<k\)return 了)。

#include<bits/stdc++.h>
using namespace std;
const int N=4e4+5,M=3e3+5;
int n,k,a[N],x,y,sz[N],mx[N],rt,vis[N],id[N],tim;
long long pre[N][M],suf[N][M],ans[N];
vector<int>v[N];
void getrt(int x,int fa,int tot){
	sz[x]=1,mx[x]=0;
	for(int y:v[x]) if(y!=fa&&!vis[y])
		getrt(y,x,tot),sz[x]+=sz[y],mx[x]=max(mx[x],sz[y]);
	if((mx[x]=max(mx[x],tot-sz[x]))<mx[rt]) rt=x;
}
void dfs(int x,int fa){
	sz[x]=1,id[++tim]=x;
	for(int y:v[x])
		if(y!=fa&&!vis[y]) dfs(y,x),sz[x]+=sz[y];
}
void div(int x){
	vis[x]=1,tim=0,dfs(x,0);
	if(tim<k) return ;
	for(int i=0;i<=tim+1;i++)
		for(int j=0;j<k;j++) pre[i][j]=suf[i][j]=-1e18;
	pre[0][0]=suf[tim+1][0]=0;
	for(int i=1;i<=tim;i++)
		for(int j=0;j<k;j++){
			if(j) pre[i][j]=max(pre[i][j],pre[i-1][j-1]+a[id[i]]);
			pre[i+sz[id[i]]-1][j]=max(pre[i+sz[id[i]]-1][j],pre[i-1][j]);
		}
	for(int i=tim;i>=1;i--)
		for(int j=0;j<k;j++){
			if(j) suf[i][j]=suf[i+1][j-1]+a[id[i]];
			suf[i][j]=max(suf[i][j],suf[i+sz[id[i]]][j]);
		}
	for(int i=1;i<=tim;i++)
		for(int j=0;j<k;j++)
			ans[id[i]]=max(ans[id[i]],pre[i-1][j]+suf[i+1][k-j-1]+a[id[i]]);
	for(int y:v[x])
		if(!vis[y]) rt=0,getrt(y,x,sz[y]),div(rt);
}
signed main(){
	scanf("%d%d",&n,&k);
	for(int i=1;i<=n;i++) scanf("%d",&a[i]);
	for(int i=1;i<n;i++)
		scanf("%d%d",&x,&y),
		v[x].push_back(y),v[y].push_back(x);
	mx[0]=1e9,getrt(1,0,n),div(rt);
	for(int i=1;i<=n;i++) printf("%lld ",ans[i]);
	return 0;
}

15. Gym101002K YATP

2023.1.31

给出一棵 \(n\) 个节点的树,边有边权 \(w_i\),点有点权 \(a_i\),对每个 \(x\in[1,n]\)\(\min_y\{dis(x,y)+a_x\cdot a_y\}\)

\(1\leq n\leq 2\times 10^5\)\(1\leq a_i,w_i\leq 10^6\)

点分治,\(x,y\) 在分治中心的同一子树时 \(d_x+d_y\) 只会把 \(dis(x,y)\) 算大。要对分治中心的每个 \(x\)\(d_x+\max_y\{d_y+a_x\cdot a_y\}\)。考虑 \(d_y+a_x\cdot a_y\) 就是用斜率为 \(a_x\) 的直线去截点 \((a_y,d_y)\) 的截距,类似斜率优化,维护 \((a_y,d_y)\) 的下凸壳,在凸壳上二分找最优的 \(y\)。当然也可以排序后 two-pointers。

16. P6670 [清华集训2016] 汽水

2023.2.1

给出一棵 \(n\) 个节点的树,边有边权,求其中一条路径使得 \(|\)边权平均值 \(-k|\) 最小,输出这个最小值。

\(1\leq n\leq 5\times 10^4\)\(0\leq w_i,k\leq 10^{13}\)

二分答案 \(mid\),check 是否存在一条路径边权平均值 \(\in[k-mid,k+mid]\)。即将每条边权值减 \((k-mid)\) 后,该路径边权和 \(\geq 0\),且将每条边边权减 \((k+mid)\) 后,该路径边权和 \(\leq 0\)

点分治,每次求出所有边权减 \((k-mid)\) 时每个点到分治中心的边权和 \(d_1\),以及减 \((k+mid)\) 同理得到 \(d_2\)

问题转化为找在不同子树的 \(i,j\) 使得 \(d_1(i)+d_1(j)\geq 0,d_2(i)+d_2(j)\leq 0\)。依次遍历所有子树,枚举 \(j\),要判前面的子树中是否存在 \(i\) 满足该条件。由于固定了 \(d_1(j),d_2(j)\),放到平面上剩下的问题是 2-side 的,树状数组维护每个 \(x\) 坐标对应 \(y\) 坐标的最小值即可。

17. Gym104076L Tree Distance

2023.2.13 点分治 + 保留有用点对 + 扫描线,同 P9058 [Ynoi2004] rpmtdq

给出一棵 \(n\) 个节点的树,边有边权 \(w_i\)\(q\) 次询问,每次给出 \(l,r\),求 \(\min\limits_{l\leq i<j\leq r}dis(i,j)\)。不存在 \(1\leq i<j\leq r\) 输出 \(-1\)

\(1\leq n\leq 2\times 10^5\)\(1\leq q\leq 10^6\)\(1\leq w_i\leq 10^9\)

点分治,对于分治中心 \(rt\),求出对应连通块的每个点 \(x\)\(rt\) 的距离 \(d_x\),查询 \([l,r]\) 就是取出连通块中 \(x\in[l,r]\)\(d_x\) 最小值 + \(d_x\) 次小值 来更新答案(两点在同一子树不会更优)。但每次都要考虑 \(\mathcal O(n)\) 个询问。

将对应连通块的所有点按编号从小到大排序,依次枚举某个点 \(x\)\(d_x\) 是次小值,可能成为最小值的点一定是 \(x\) 左右边分别第一个 \(<d_x\) 的,原因是若左边有两个点 \(d_i<d_x,d_j<d_x,j<i<x\),那么选 \((j,i)\) 一定比选 \((j,x)\) 优。这样大小为 \(sz\) 的连通块提出了 \(\mathcal O(2\cdot sz)\)有用点对,一共能得到 \(\mathcal O(n\log n)\) 个有用点对。

对有用点对和查询的 \(r\) 扫描线即可,树状数组维护。

时间复杂度 \(\mathcal O(n\log^2 n+q\log n)\)

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N=2e5+5,M=1e6+5;
int n,m,x,y,z,sz[N],mx[N],vis[N],rt,cnt,id[N],top,s[N];
ll d[N],c[N],ans[M];
vector<pair<int,int> >v[N],q[N];
vector<pair<int,ll> >p[N];
void getrt(int x,int fa,int tot){
	sz[x]=1,mx[x]=0;
	for(auto p:v[x]){
		int y=p.first;
		if(y!=fa&&!vis[y])
			getrt(y,x,tot),sz[x]+=sz[y],mx[x]=max(mx[x],sz[y]); 
	}
	if((mx[x]=max(mx[x],tot-sz[x]))<mx[rt]) rt=x;
}
void dfs(int x,int fa){
	id[++cnt]=x;
	for(auto p:v[x]){
		int y=p.first;
		if(y!=fa&&!vis[y]) d[y]=d[x]+p.second,dfs(y,x);
	}
}
void div(int x){
	vis[x]=1,d[x]=cnt=0,dfs(x,0);
	sort(id+1,id+1+cnt),top=0;
	for(int i=1;i<=cnt;i++){
		while(top&&d[id[i]]<d[s[top]])
			p[id[i]].push_back({s[top],d[id[i]]+d[s[top]]}),top--;
		s[++top]=id[i];
	}
	top=0;
	for(int i=cnt;i>=1;i--){
		while(top&&d[id[i]]<d[s[top]])
			p[s[top]].push_back({id[i],d[id[i]]+d[s[top]]}),top--;
		s[++top]=id[i];
	}
	for(auto p:v[x]){
		int y=p.first;
		if(!vis[y]) rt=0,getrt(y,x,sz[y]),div(rt);
	}
}
void upd(int x,ll y){
	for(int i=x;i;i-=i&(-i)) c[i]=min(c[i],y);
}
ll query(int x){
	ll ans=1e18;
	for(int i=x;i<=n;i+=i&(-i)) ans=min(ans,c[i]);
	return ans;
}
signed main(){
	scanf("%d",&n);
	for(int i=1;i<n;i++)
		scanf("%d%d%d",&x,&y,&z),
		v[x].push_back({y,z}),v[y].push_back({x,z});
	scanf("%d",&m);
	for(int i=1;i<=m;i++)
		scanf("%d%d",&x,&y),q[y].push_back({x,i});
	mx[0]=1e9,getrt(1,0,n),div(rt);
	fill(c+1,c+1+n,1e18);
	for(int i=1;i<=n;i++){
		for(auto j:p[i]) upd(j.first,j.second);
		for(auto j:q[i]) ans[j.second]=query(j.first);
	}
	for(int i=1;i<=m;i++) printf("%lld\n",ans[i]<1e18?ans[i]:-1);
	return 0;
} 

18. 小练习

2021.11.17

给出一棵 \(n\) 个节点的树,以及 \(k,L_{1\sim n}\)。求:

\[k!\sum_{S\subseteq\{1,2,3,\cdots,n\},|S|=k}\sum_{x=1}^n[\forall y\in S,dis(x,y)\leq L_x]\pmod{998244353} \]

\(1\leq k\leq n\leq 2\times 10^5\)\(1\leq w,L_i\leq 10^9\)

枚举 \(x\),记 \(f_x=\sum_{y=1}^n [dis(x,y)\leq L_x]\),则 \(ans=k!\sum_{x=1}^n\binom{f_x}{k}\)

考虑点分治,每次处理跨越分治中心 \(rt\)\((x,y)\)。对 \(f_x\) 有贡献的 \(y\),就是 \(dis(y,rt)\leq L_x-dis(x,rt)\) 的里面扣掉与 \(x\) 同子树的。处理出 \(dis(x,rt)\) sort 一下,每次二分出合法前缀长度,容斥掉同个子树的即可。

时间复杂度 \(\mathcal O(n\log^2 n)\)

#include<bits/stdc++.h>
using namespace std;
const int N=2e5+5,mod=998244353;
int n,k,l[N],rt,mx[N],sz[N],vis[N],t,f[N],fac[N],inv[N],ans;
long long d[N],a[N];
vector<pair<int,int> >v[N];
vector<int>s;
void getrt(int x,int fa,int tot){
	sz[x]=1,mx[x]=0;
	for(auto p:v[x]){
		int y=p.first;
		if(y!=fa&&!vis[y])
			getrt(y,x,tot),sz[x]+=sz[y],mx[x]=max(mx[x],sz[y]);
	}
	if((mx[x]=max(mx[x],tot-sz[x]))<mx[rt]) rt=x;
}
void dfs(int x,int fa){
	s.push_back(x);
	for(auto p:v[x]){
		int y=p.first;
		if(y!=fa&&!vis[y]) d[y]=d[x]+p.second,dfs(y,x);
	}
}
void calc(int v){
	for(int x:s) a[++t]=d[x];
	sort(a+1,a+1+t);
	for(int x:s)
		f[x]+=v*(upper_bound(a+1,a+1+t,l[x]-d[x])-a-1);
	s.clear(),t=0;
}
void div(int x){
	vis[x]=1,d[x]=0,dfs(x,0),calc(1);
	for(auto p:v[x]){
		int y=p.first;
		if(!vis[y]) dfs(y,x),calc(-1);
	}
	for(auto p:v[x]){
		int y=p.first;
		if(!vis[y]) rt=0,getrt(y,x,sz[y]),div(rt);
	}
}
int A(int n,int m){
	return n<m?0:1ll*fac[n]*inv[n-m]%mod;
}
signed main(){
	scanf("%d%d",&n,&k);
	fac[0]=inv[0]=inv[1]=1;
	for(int i=2;i<=n;i++) inv[i]=1ll*inv[mod%i]*(mod-mod/i)%mod;
	for(int i=1;i<=n;i++)
		scanf("%d",&l[i]),
		fac[i]=1ll*fac[i-1]*i%mod,inv[i]=1ll*inv[i-1]*inv[i]%mod;
	for(int i=1,x,y,z;i<n;i++)
		scanf("%d%d%d",&x,&y,&z),
		v[x].push_back({y,z}),v[y].push_back({x,z});
	mx[0]=1e9,getrt(1,0,n),div(rt);
	for(int i=1;i<=n;i++) ans=(ans+A(f[i],k))%mod;
	printf("%d\n",ans);
	return 0;
}

19. P6326 Shopping

2024.4.20 二进制拆分优化多重背包

经典的“父亲传给儿子,在子树里绕一圈再传回父亲”的套路。

#include<bits/stdc++.h>
using namespace std;
const int N=510,M=4e3+5;
int t,n,m,w[N],c[N],d[N],x,y,rt,sz[N],mx[N],vis[N],f[N][M],ans;
vector<int>v[N];
void getrt(int x,int fa,int tot){
	sz[x]=1,mx[x]=0;
	for(int y:v[x]) if(y!=fa&&!vis[y])
		getrt(y,x,tot),sz[x]+=sz[y],mx[x]=max(mx[x],sz[y]);
	if((mx[x]=max(mx[x],tot-sz[x]))<mx[rt]) rt=x;
}
void ins(int *f,int w,int v,int c){
	auto add=[&](int w,int v){
		for(int i=m;i>=w;i--) f[i]=max(f[i],f[i-w]+v);
	};
	int k=0;
	while(c>=(1<<k))
		add(w*(1<<k),v*(1<<k)),c-=1<<k,k++;
	if(c) add(w*c,v*c);
}
void dfs(int x,int fa){
	for(int i=0;i<=m;i++)
		f[x][i]=i>=c[x]?f[fa][i-c[x]]+w[x]:-2e9;
	ins(f[x],c[x],w[x],d[x]-1);
	for(int y:v[x])
		if(y!=fa&&!vis[y]) dfs(y,x);
	if(fa)
		for(int i=0;i<=m;i++) f[fa][i]=max(f[fa][i],f[x][i]);
}
void div(int x){
	vis[x]=1,dfs(x,0);
	for(int i=0;i<=m;i++) ans=max(ans,f[x][i]);
	for(int y:v[x])
		if(!vis[y]) rt=0,getrt(y,x,sz[y]),div(rt);
}
signed main(){
	scanf("%d",&t);
	while(t--){
		scanf("%d%d",&n,&m),ans=0;
		memset(f,-0x3f,sizeof(f)),f[0][0]=0;
		for(int i=1;i<=n;i++) v[i].clear(),vis[i]=0;
		for(int i=1;i<=n;i++) scanf("%d",&w[i]);
		for(int i=1;i<=n;i++) scanf("%d",&c[i]);
		for(int i=1;i<=n;i++) scanf("%d",&d[i]);
		for(int i=1;i<n;i++)
			scanf("%d%d",&x,&y),
			v[x].push_back(y),v[y].push_back(x);
		fill(f[0]+1,f[0]+1+m,-2e9);
		mx[rt=0]=1e9,getrt(1,0,n),div(rt);
		printf("%d\n",ans);
	}
	return 0;
}

二、点分树

把点分治的过程抽象成树。

点分树:点分治中每层分治重心和上一层重心连边。

不难发现:

  1. 点分治树高 \(\mathcal O(\log n)\)

    这样不仅可以暴力跳父亲,还可以得到点分树上 \(\sum sz_i\)\(\mathcal O(n\log n)\) 的(一个点可以贡献其共 \(\mathcal O(\log n)\) 个祖先的 \(sz\))。

  2. 点分树上两点的 \(\text{lca}\) 一定在原树两点的路径上。

    处理一些路径权值类问题:\(dis(x,y)=dis(x,\text{lca}(x,y))+dis(y,\text{lca}(x,y))\),注意这里的 \(dis\) 是原树的。

    \(x\) 的总贡献:在点分树上 \(x,y\)\(\text{lca}\) 统计 \(y\)\(x\) 的贡献。\(x\) 开始在点分树上暴力跳父亲来枚举 \(\text{lca}\),容斥去掉 \(x,y\)\(\text{lca}\) 的同个子树的情况。数据结构维护(有时不需要线段树,vector + 二分即可完成)。

  3. 点分树上 \(x\) 的子树就是点分治时以 \(x\) 为分治中心的连通块。

    对于原树上任意一个连通块,存在一个连通块的点,使得整个连通块都在这个点的子树中:可以想象这个连通块在点分治过程中“散架”的过程,初始是完整的,设 \(rt\) 第一个在连通块中的分治中心,那么连通块中其余点一定在 \(rt\) 的子树中。实际上,这个 \(rt\) 就是该连通块在点分树上最浅的点。

    有时处理连通块可以把问题转化点分树上去。比如要找 \(x\) 在某种条件下的连通块,可以先暴力找点分树上最浅的和 \(x\) 在该条件下连通的点 \(rt\)\(rt\) 就是这个连通块中的第一个分治中心,那么连通块中任一点都在点分树上 \(rt\) 的子树中,且与 \(x\) 连通等价于与 \(rt\) 连通。

  4. 点分树上 \(x\) 的儿子数 $\leq $ 原树上 \(x\) 的度数。

点分树和原树基本可以看作没有联系。

技巧:

  1. 由于点分树上两点的 \(\text{lca}\) 一定在原树两点的路径上,考虑在点分树上的 \(\text{lca}(x,y)=l\) 考虑 \(y\)(修改)对 \(ans_x\)(询问)的影响。

    由于点分树上 \(l\) 的子树就是点分治时以 \(l\) 为分治中心的连通块,所以 \(x,y\) 都在以 \(l\) 为分治中心的连通块中,可以在初始点分治的时候预处理一些信息。

1. P6329 【模板】点分树 | 震波

2022.7.28

给出一棵树,点有点权 \(a_i\)\(m\) 次询问:

  • 0 x k:求与 \(x\) 距离 \(\leq k\) 的所有点的点权和。
  • 1 x y\(a_x\gets y\)

强制在线。\(1\leq m\leq 10^5\)\(1\leq a_i,y\leq 10^5\)

对于 \(dis_{x,y}\leq k\),在点分树上 \(x,y\)\(\text{lca}\) 统计 \(y\)\(x\) 的贡献。枚举 \(x\) 的祖先 \(w\)\(fa_w=l\)\(l\) 作为 \(\text{lca}(x,y)\)

\[ans=\sum_{dis(x,l)+dis(y,l)\leq k\land \text{lca}(x,y)=l}a_y=\sum_{dis(y,l)\leq k-dis(x,l)\land y\in subtree(l)}a_y-\sum_{dis(y,l)\leq k-dis(x,l)\land y\in subtree(w)}a_y \]

\(l\) 开一棵线段树,\(seg_i=\sum_{dis(y,l)=i\land y\in subtree(l)} a_y\),预处理 + 查区间和。

但两点在点分树上的距离与在原树上的距离没有任何联系,那么 \(dis(y,l)\)\(dis(y,w)\) 也没有联系,所以还需要对 \(w\) 开另一棵线段树,\(seg_i=\sum_{dis(y,fa_w=l)=i\land y\in subtree(w)}a_y\)

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5,M=N<<6;
int n,m,op,x,y,a[N],rt,sz[N],mx[N],dep[N],f[N][20],vis[N],fa[N],ans;
vector<int>v[N];
struct T{
	int tot,rt[N],lc[M],rc[M],sum[M];
	void modify(int &p,int l,int r,int pos,int v){
		if(!p) p=++tot;
		sum[p]+=v;
		if(l==r) return ;
		int mid=(l+r)/2;
		if(pos<=mid) modify(lc[p],l,mid,pos,v);
		else modify(rc[p],mid+1,r,pos,v);
	}
	int query(int p,int l,int r,int lx,int rx){
		if(!p) return 0;
		if(l>=lx&&r<=rx) return sum[p];
		int mid=(l+r)/2,ans=0;
		if(lx<=mid) ans=query(lc[p],l,mid,lx,rx);
		if(rx>mid) ans+=query(rc[p],mid+1,r,lx,rx);
		return ans;
	}
}t1,t2;
void dfs(int x,int fa){
	for(int i=0;i<=16;i++) f[x][i+1]=f[f[x][i]][i];
	for(int y:v[x])
		if(y!=fa) dep[y]=dep[x]+1,f[y][0]=x,dfs(y,x);
}
int lca(int x,int y){
	if(dep[x]<dep[y]) swap(x,y);
	for(int i=17;i>=0;i--) if(dep[f[x][i]]>=dep[y]) x=f[x][i];	//开 20 会 TLE
	if(x==y) return x;
	for(int i=17;i>=0;i--)
		if(f[x][i]!=f[y][i]) x=f[x][i],y=f[y][i];
	return f[x][0];
}
int dis(int x,int y){return dep[x]+dep[y]-2*dep[lca(x,y)];}
void getrt(int x,int fa,int tot){
	sz[x]=1,mx[x]=0;
	for(int y:v[x]) if(!vis[y]&&y!=fa)
		getrt(y,x,tot),sz[x]+=sz[y],mx[x]=max(mx[x],sz[y]);
	if((mx[x]=max(mx[x],tot-sz[x]))<mx[rt]) rt=x;
}
void div(int x){
	vis[x]=1;
	for(int y:v[x])
		if(!vis[y]) rt=0,getrt(y,x,sz[y]),fa[rt]=x,div(rt);
}
void upd(int x,int v){
	for(int i=x;i;i=fa[i]){
		t1.modify(t1.rt[i],0,n,dis(x,i),v);
		if(fa[i]) t2.modify(t2.rt[i],0,n,dis(x,fa[i]),v);
	}
}
int qry(int x,int k){
	int ans=0;
	for(int i=x;i;i=fa[i]){
		ans+=t1.query(t1.rt[i],0,n,0,k-dis(x,i));
		if(fa[i]) ans-=t2.query(t2.rt[i],0,n,0,k-dis(x,fa[i]));
	}
	return ans;
}
signed main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++) scanf("%d",&a[i]);
	for(int i=1;i<n;i++)
		scanf("%d%d",&x,&y),v[x].push_back(y),v[y].push_back(x);
	mx[0]=1e9,dfs(1,0),getrt(1,0,n),div(rt);
	for(int i=1;i<=n;i++) upd(i,a[i]);
	while(m--){
		scanf("%d%d%d",&op,&x,&y),x^=ans,y^=ans;
		if(!op) printf("%d\n",ans=qry(x,y));
		else upd(x,y-a[x]),a[x]=y;
	}
	return 0;
}

更快:

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int n,m,op,x,y,a[N],rt,sz[N],mx[N],tim,dfn[N],dep[N],vis[N],fa[N],tmp[N<<6],cur,*id=tmp,ans;
vector<int>v[N];
pair<int,int>f[N][25];
struct BIT{
	int n,*c;
	void init(int sz){c=id,id+=(n=sz+1);}
	void add(int x,int y){
		for(int i=x+1;i<=n;i+=i&(-i)) c[i]+=y;
	}
	int query(int x){
		int ans=0;
		for(int i=min(n,x+1);i>=1;i-=i&(-i)) ans+=c[i];
		return ans;
	}
}t1[N],t2[N];
void dfs(int x,int fa){
	f[dfn[x]=++tim][0]={dfn[fa],fa};
	for(int y:v[x])
		if(y!=fa) dep[y]=dep[x]+1,dfs(y,x);
}
int lca(int x,int y){
	if(x==y) return x;
	if((x=dfn[x])>(y=dfn[y])) swap(x,y);
	int k=__lg(y-(++x)+1);
	return min(f[x][k],f[y-(1<<k)+1][k]).second;
}
int dis(int x,int y){return dep[x]+dep[y]-2*dep[lca(x,y)];}
void getrt(int x,int fa,int tot){
	sz[x]=1,mx[x]=0;
	for(int y:v[x]) if(!vis[y]&&y!=fa)
		getrt(y,x,tot),sz[x]+=sz[y],mx[x]=max(mx[x],sz[y]);
	if((mx[x]=max(mx[x],tot-sz[x]))<mx[rt]) rt=x;
}
void div(int x){
	vis[x]=1;
	t1[x].init(cur),t2[x].init(cur);
	for(int y:v[x])
		if(!vis[y]) rt=0,getrt(y,x,cur=sz[y]),fa[rt]=x,div(rt);
}
void upd(int x,int v){
	for(int i=x;i;i=fa[i]){
		t1[i].add(dis(x,i),v);
		if(fa[i]) t2[i].add(dis(x,fa[i]),v);
	}
}
int qry(int x,int k){
	int ans=0;
	for(int i=x;i;i=fa[i]){
		ans+=t1[i].query(k-dis(x,i));
		if(fa[i]) ans-=t2[i].query(k-dis(x,fa[i]));
	}
	return ans;
}
signed main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++) scanf("%d",&a[i]);
	for(int i=1;i<n;i++)
		scanf("%d%d",&x,&y),
		v[x].push_back(y),v[y].push_back(x);
	dfs(1,0);
	for(int j=1;(1<<j)<=n;j++)
		for(int i=1;i+(1<<j)-1<=n;i++)
			f[i][j]=min(f[i][j-1],f[i+(1<<(j-1))][j-1]);
	mx[0]=1e9,getrt(1,0,cur=n),div(rt);
	for(int i=1;i<=n;i++) upd(i,a[i]);
	while(m--){
		scanf("%d%d%d",&op,&x,&y),x^=ans,y^=ans;
		if(!op) printf("%d\n",ans=qry(x,y));
		else upd(x,y-a[x]),a[x]=y;
	}
	return 0;
}

2. BZOJ 2117 [2010国家集训队] Crash 的旅游计划

2022.8.30

给出一棵 \(n\) 个节点的树,边有边权,对于每个点,求它到其他点距离中的第 \(k\) 小值。

\(n\leq 5\times 10^4\)\(k<n\)\(w_i\leq 10^4\)

二分答案 \(mid\),转化为询问与 \(x\) 距离 \(\leq mid\) 的点数。

不需要线段树,直接对每个点 \(i\)vector 存:1. \(i\) 点分树子树内的点,与 \(i\) 的原树距离。2. \(i\) 点分树子树内的点,与点分树上 \(i\) 的父亲的原树距离。然后查询时直接 upper_bound

时间复杂度 \(\mathcal O(n\log^3 n)\)

#include<bits/stdc++.h>
#define dis(x,y) (dis[x]+dis[y]-2*dis[lca(x,y)])
using namespace std;
const int N=5e4+5;
int n,k,x,y,z,rt,sz[N],mx[N],dep[N],dis[N],vis[N],fa[N],f[N][17],ans;
vector<pair<int,int> >v[N];
vector<int>d[N],d2[N];
void dfs(int x,int fa){
	for(int i=0;i<=15;i++) f[x][i+1]=f[f[x][i]][i];
	for(auto p:v[x]){
		int y=p.first;
		if(y!=fa) dep[y]=dep[f[y][0]=x]+1,dis[y]=dis[x]+p.second,dfs(y,x);
	}
}
int lca(int x,int y){
	if(dep[x]<dep[y]) swap(x,y);
	for(int i=16;i>=0;i--) if(dep[f[x][i]]>=dep[y]) x=f[x][i];
	if(x==y) return x;
	for(int i=16;i>=0;i--)
		if(f[x][i]!=f[y][i]) x=f[x][i],y=f[y][i];
	return f[x][0];
}
void getrt(int x,int fa,int tot){
	sz[x]=1,mx[x]=0;
	for(auto p:v[x]){
		int y=p.first;
		if(!vis[y]&&y!=fa) getrt(y,x,tot),sz[x]+=sz[y],mx[x]=max(mx[x],sz[y]);
	}
	if((mx[x]=max(mx[x],tot-sz[x]))<mx[rt]) rt=x;
}
void div(int x){
	vis[x]=1;
	for(auto p:v[x]){
		int y=p.first;
		if(!vis[y]) rt=0,getrt(y,x,sz[y]),fa[rt]=x,div(rt);
	}
}
int qry(int x,int k){
	int ans=0;
	for(int i=x;i;i=fa[i]){
		ans+=upper_bound(d[i].begin(),d[i].end(),k-dis(x,i))-d[i].begin();
		if(fa[i]) ans-=upper_bound(d2[i].begin(),d2[i].end(),k-dis(x,fa[i]))-d2[i].begin();
	}
	return ans;
}
signed main(){
	scanf("%d%d",&n,&k),k++;
	for(int i=1;i<n;i++){
		scanf("%d%d%d",&x,&y,&z);
		v[x].push_back({y,z}),v[y].push_back({x,z});
	}
	mx[0]=1e9,dfs(1,0),getrt(1,0,n),div(rt);
	for(int x=1;x<=n;x++)
		for(int i=x;i;i=fa[i]){
			d[i].push_back(dis(x,i)); 
			if(fa[i]) d2[i].push_back(dis(x,fa[i]));
		}
	for(int i=1;i<=n;i++)
		sort(d[i].begin(),d[i].end()),sort(d2[i].begin(),d2[i].end());
	for(int i=1;i<=n;i++){
		int l=0,r=1e9,ans=0;
		while(l<=r){	//注意不能二分最大的 qry(i,mid)<=k 的,因为可能 <= 第 k 小的距离的有 >=k 个(距离可以有很多相同)
			int mid=(l+r)/2;
			if(qry(i,mid)>=k) ans=mid,r=mid-1;
			else l=mid+1;
		}
		printf("%d\n",ans);
	}
	return 0;
}

3. Gym102391K Wind of Change

2022.10.11

给出两棵 \(n\) 个节点的树 \(T_1,T_2\),边有边权。对每个 \(i\)\(\min_{i\neq j}dis(T_1,i,j)+dis(T_2,i,j)\)

\(2\leq n\leq 10^5\)\(1\leq w\leq 10^9\)

对两棵树分别求点分树。记 \(l_1,l_2\) 分别为 \(i,j\)\(T_1,T_2\) 点分树上的 \(\text{lca}\),则 \(dis(T_1,i,j)+dis(T_2,i,j)=(dis(T_1,l_1,i)+dis(T_2,l_2,i))+(dis(T_1,l_1,j)+dis(T_2,l_2,j))\),这样做的好处是拆开了 \(i,j\),且可以利用点分树树高 \(\mathcal O(\log n)\) 的性质暴力。

固定 \(l_1\),枚举 \(T_1\) 点分树上 \(l_1\) 的子孙 \(i\),枚举 \(T_2\) 点分树上 \(i\) 的祖先 \(l_2\),对 \(l_2\) 维护 \(dis(T_1,l_1,i)+dis(T_2,l_2,i)\) 的最小值和次小值即可,也可以只维护最小值再反过来做一次,具体见代码。

(为什么要这么枚举呢?如果依次枚举 \(i,L_1,L_2\),需要 map<pair<int,int>,ll> 之类的存 \(mn_{L_1,L_2}\),而依次枚举 \(L_1,i,L_2\) 就可以将 \(mn_{L_1,L_2}\) 改为 \(mn_{L_2}\)

正确性:若 \(i,j\) 的 LCA 不是 \(l_1/l_2\) 只会把答案算大,最小的答案一定会被算到。

时间复杂度 \(\mathcal O(n\log^2 n)\)

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N=1e5+5;
int n,x,y,z;
ll d[N],mn[N],ans[N];
vector<int>tmp;
struct tree{
	int o,sz[N],mx[N],rt,vis[N],fa[N];
	vector<pair<int,int> >v[N];
	vector<pair<int,ll> >q[N];
	void getrt(int x,int fa,int tot){
		sz[x]=1,mx[x]=0;
		for(auto p:v[x]){
			int y=p.first;
			if(!vis[y]&&y!=fa) getrt(y,x,tot),sz[x]+=sz[y],mx[x]=max(mx[x],sz[y]);
		}
		if((mx[x]=max(mx[x],tot-sz[x]))<mx[rt]) rt=x;
	}
	void getd(int x,int fa,int rt){
		if(o) q[rt].push_back({x,d[x]});
		else q[x].push_back({rt,d[x]});
		for(auto p:v[x]){
			int y=p.first;
			if(!vis[y]&&y!=fa) d[y]=d[x]+p.second,getd(y,x,rt);
		}
	}
	void div(int x){
		vis[x]=1,d[x]=0,getd(x,0,x);
		for(auto p:v[x]){
			int y=p.first;
			if(!vis[y]) rt=0,getrt(y,x,sz[y]),fa[rt]=x,div(rt);
		}
	}
	void build(){
		for(int i=1;i<n;i++)
			scanf("%d%d%d",&x,&y,&z),
			v[x].push_back({y,z}),v[y].push_back({x,z}); 
		mx[0]=1e9,getrt(1,0,n),div(rt);
	}
}t1,t2;
void calc(int l1){
	tmp.clear();
	for(auto i:t1.q[l1]){
		int x=i.first;
		for(auto j:t2.q[x]){
			int l2=j.first; ll d=i.second+j.second;
			if(~mn[l2]) ans[x]=min(ans[x],mn[l2]+d),mn[l2]=min(mn[l2],d);
			else mn[l2]=d,tmp.push_back(l2);
		}
	}
	for(int i:tmp) mn[i]=-1;
}
signed main(){
	scanf("%d",&n),t1.o=1,t1.build(),t2.build();
	for(int i=1;i<=n;i++) ans[i]=1e18,mn[i]=-1;
	for(int i=1;i<=n;i++)
		calc(i),reverse(t1.q[i].begin(),t1.q[i].end()),calc(i);
	for(int i=1;i<=n;i++) printf("%lld\n",ans[i]);
	return 0;
}

4. 弹跳声呐

2022.10.27

给出一棵 \(n\) 个节点的树,\(m\) 次操作:

  • b t x:在时刻 \(t\),一个声呐出现在节点 \(x\)。它会对时刻 \(t+i\,(i\geq 0)\) 的所有与 \(x\) 最短距离恰好为 \(i\) 的节点贡献 \(1\) 的声呐强度。
  • q t x:询问在时刻 \(t\),节点 \(x\) 的声呐强度。

\(1\leq n,m\leq 10^5\)\(0\leq t\leq 10^9\),保证 \(t\) 单调不降。

时刻 \(t\) 放在 \(y\) 的声呐,会在时刻 \(t+dis(y,x)\)\(x\) 产生影响。

在点分树上 \(x,y\)\(\text{lca}\) 统计放在 \(y\) 的声呐对 \(x\) 的贡献。

对于 \(l=\text{lca}(x,y)\)\(t_y+dis(x,l)+dis(y,l)=t_x\Leftrightarrow t_y+dis(y,l)=t_x-dis(x,l)\)

\[\sum_{t_y+dis(y,l)=t_x-dis(x,l)\land \text{lca}(x,y)=l}1=\sum_{t_y+dis(y,l)=t_x-dis(x,l)\land y\in subtree(l)}1-\sum_{t_y+dis(y,l)=t_x-dis(x,l)\land y\in subtree(w)}1 \]

\(l\) 处开一个桶,\(buc_i=\sum_{t_y+dis(y,l)=i\land y\in subtree(l)} 1\),预处理 + 查单点 \(buc_{t_x-dis(x,l)}\)

再对 \(w\) 开一个,\(buc_i=\sum_{t_y+dis(y,fa_w=l)=i\land y\in subtree(w)}1\)

#include<bits/stdc++.h>
#define dis(x,y) (dep[x]+dep[y]-2*dep[lca(x,y)])
using namespace std;
const int N=1e5+5;
int n,m,x,y,t,dep[N],f[N][25],sz[N],mx[N],fa[N],vis[N],rt,ans;
char s[5];
vector<int>v[N];
unordered_map<int,int>mp1[N],mp2[N];
void dfs(int x,int fa){
	for(int i=0;i<=16;i++) f[x][i+1]=f[f[x][i]][i];
	for(int y:v[x])
		if(y!=fa) dep[y]=dep[x]+1,f[y][0]=x,dfs(y,x);
}
int lca(int x,int y){
	if(dep[x]<dep[y]) swap(x,y);
	for(int i=17;i>=0;i--) if(dep[f[x][i]]>=dep[y]) x=f[x][i];
	if(x==y) return x;
	for(int i=17;i>=0;i--)
		if(f[x][i]!=f[y][i]) x=f[x][i],y=f[y][i];
	return f[x][0];
}
void getrt(int x,int fa,int tot){
	sz[x]=1,mx[x]=0;
	for(int y:v[x]) if(y!=fa&&!vis[y])
		getrt(y,x,tot),sz[x]+=sz[y],mx[x]=max(mx[x],sz[y]);
	if((mx[x]=max(mx[x],tot-sz[x]))<mx[rt]) rt=x;
}
void div(int x){
	vis[x]=1;
	for(int y:v[x])
		if(!vis[y]) rt=0,getrt(y,x,sz[y]),fa[rt]=x,div(rt);
}
signed main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<n;i++)
		scanf("%d%d",&x,&y),v[x].push_back(y),v[y].push_back(x);
	mx[0]=1e9,dfs(1,0),getrt(1,0,n),div(rt);
	while(m--){
		scanf("%s%d%d",s+1,&t,&x);
		if(s[1]=='b'){
			for(int i=x;i;i=fa[i]){
				mp1[i][t+dis(x,i)]++;
				if(fa[i]) mp2[i][t+dis(x,fa[i])]++;
			}
		}
		else{
			ans=0;
			for(int i=x;i;i=fa[i]){
				ans+=mp1[i][t-dis(x,i)];
				if(fa[i]) ans-=mp2[i][t-dis(x,fa[i])];
			}
			printf("%d\n",ans);
		}
	}
	return 0;
}

5. P2056 [ZJOI2007] 捉迷藏

2022.12.27 同 P4115 Qtree4、SP2666 QTREE4 - Query on a tree IV,SPOJ 被卡常了没过

给出一棵 \(n\) 个节点的树,初始所有点为白色,\(m\) 次操作:

  • C x:翻转 \(x\) 的颜色。
  • G:询问树上最远的两个白点的距离。

\(n\leq 10^5\)\(m\leq 5\times 10^5\)

对于每个 \(x\),要找两个来自 \(x\) 不同子树的白点使得它们到 \(x\) 的距离之和最大:对于 \(x\) 的儿子 \(y_{1\sim k}\),设它们子树内到 \(x\) 距离最远的白点的距离是 \(s_{1\sim k}\),取出 \(s_{1\sim k}\) 中的最大值和次大值拼即可。

实现时,维护三个大根堆 \(D_{1\sim n},s_{1\sim n},ans\)\(D_y\) 维护 \(y\) 子树内所有白点到 \(x\)(即 \(fa_y\))的距离,\(s_x\) 维护所有 \(D_y\) 的堆顶,\(ans\) 维护所有 \(s_x\) 的堆顶 + 次堆顶(即维护全局答案)。

\(D,s\) 就是为了防止最大值和次大值来自 \(x\) 的同一子树。

放在点分树上,这样只需对 \(\mathcal O(\log n)\) 个祖先改。

//O2
#include<bits/stdc++.h>
#define dis(x,y) dep[x]+dep[y]-dep[lca(x,y)]*2
using namespace std;
const int N=1e5+5;
int n,m,x,y,dep[N],f[N],sz[N],son[N],top[N],fa[N],mx[N],rt,vis[N],num,col[N];
char op;
vector<int>v[N],tmp;
struct H{	//可删堆。multiset 太慢了
	priority_queue<int>q,d;
	int size(){return q.size()-d.size();}
	void push(int x){q.push(x);}
	void del(int x){d.push(x);}
	void upd(){
		while(d.size()&&q.top()==d.top()) q.pop(),d.pop();
	}
	int top(){
		return upd(),q.size()?q.top():0;
	}
	int top2(){
		if(size()<2) return 0;
		int x,y;
		return x=top(),q.pop(),y=top(),q.push(x),x+y;
	}
}D[N],s[N],ans;
void dfs(int x,int fa){
	dep[x]=dep[fa]+1,f[x]=fa,sz[x]=1;
	for(int y:v[x]) if(y!=fa){
		dfs(y,x),sz[x]+=sz[y];
		if(sz[y]>sz[son[x]]) son[x]=y;
	}
}
void dfs2(int x,int tp){
	top[x]=tp;
	if(son[x]) dfs2(son[x],tp);
	for(int y:v[x])
		if(y!=f[x]&&y!=son[x]) dfs2(y,y);
}
int lca(int x,int y){
	while(top[x]^top[y]){
		if(dep[top[x]]<dep[top[y]]) swap(x,y);
		x=f[top[x]];
	}
	return dep[x]<dep[y]?x:y;
}
void getrt(int x,int fa,int tot){
	sz[x]=1,mx[x]=0,tmp.push_back(x);
	for(int y:v[x]) if(y!=fa&&!vis[y])
		getrt(y,x,tot),sz[x]+=sz[y],mx[x]=max(mx[x],sz[y]);
	if((mx[x]=max(mx[x],tot-sz[x]))<mx[rt]) rt=x;
}
void div(int x){
	vis[x]=1,s[x].push(0);
	for(int y:v[x]) if(!vis[y]){
		rt=0,tmp.clear(),getrt(y,x,sz[y]),fa[rt]=x;
		for(int i:tmp) D[rt].push(dis(fa[rt],i));
		s[x].push(D[rt].top()),div(rt);	//某 sb 把 rt 写成 y 了
	}
	ans.push(s[x].top2());
}
signed main(){
	scanf("%d",&n);
	for(int i=1;i<n;i++)
		scanf("%d%d",&x,&y),
		v[x].push_back(y),v[y].push_back(x);
	scanf("%d",&m),dfs(1,0),dfs2(1,1);
	mx[0]=1e9,getrt(1,0,num=n),div(rt);
	while(m--){
		scanf(" %c",&op);
		if(op=='C'){
			scanf("%d",&x),num-=!col[x],col[x]^=1,num+=!col[x];
			ans.del(s[x].top2());
			!col[x]?s[x].push(0):s[x].del(0);
			ans.push(s[x].top2());
			for(int i=x;fa[i];i=fa[i]){
				ans.del(s[fa[i]].top2());
				if(D[i].size()) s[fa[i]].del(D[i].top());
				!col[x]?D[i].push(dis(x,fa[i])):D[i].del(dis(x,fa[i]));
				if(D[i].size()) s[fa[i]].push(D[i].top());	//某 sb 没写 if(D[i].size())
				ans.push(s[fa[i]].top2());
			}
		}
		else{
			if(!num) puts("-1");
			else if(num==1) puts("0");
			else printf("%d\n",ans.top());
		}
	}
	return 0;
} 

6. P3345 [ZJOI2015]幻想乡战略游戏

2023.1.11

给出一棵 \(n\) 个节点的树,有边权 \(w_i\) 点权 \(a_i\)

\(m\) 次操作,每次给出 \(x,v\),令 \(a_x\gets a_x+v\),然后查询 \(\min\limits_{1\leq x\leq n}\{\sum_{i=1}^n a_i\times dis(x,i)\}\)(带权重心)。

\(1\leq n,m\leq 10^5\)\(1\leq w_i\leq 10^3\)\(|v|\leq 10^3\),保证每个点的度数 \(\leq 20\)

套路地考虑重心从 \(x\) 移动到儿子 \(y\),带权距离和 \(\Delta=w_{x,y}((\sum a_i-s_y)-s_y)=w_{x,y}(\sum a_i-2s_y)\),其中 \(s_i\) 表示子树 \(i\) 的点权和,\(\Delta<0\) 时移动到 \(y\) 更优,这样的 \(y\) 至多一个。点分治优化。

现在带修,需要点分树。注意,在点分树上移动一步,相当于在原树上移动了很多步,不能根据在点分树上走一步带权距离和变小判断能否往该子树移动。 正确的判法是,点分树上 \(x\) 到邻点 \(y\),设 \(y\) 在以 \(x\) 为分治中心时所在子树的根是 \(p\),应判 \(p\) 是否比 \(x\) 优。

并且判断条件中 \(s_y\) 是对于原树而言的,每次操作完不太好对于原树直接维护,对点分树 换根也比较烦,更简单的方法是直接套路点分树算出带权距离和进行比较。

具体地,枚举 \(\text{lca}(x,i)\)\(a_i\times dis(x,i)=a_i\times (dis(x,\text{lca})+dis(\text{lca},i))\),对每个 \(\text{lca}\) 维护点分树上子树的 \(\sum a_i\)\(\sum a_i\times dis(\text{lca},i)\) 即可,再维护子树的 \(\sum a_i\times dis(fa_{\text{lca}},i)\) 容斥掉 \(x,i\)\(\text{lca}\) 同一子树的情况。

#include<bits/stdc++.h>
#define ll long long
#define dis(x,y) (dis[x]+dis[y]-2*dis[lca(x,y)])
using namespace std;
const int N=1e5+5;
int n,m,x,y,z,sz[N],dep[N],dis[N],son[N],f[N],top[N],mx[N],rt,vis[N],fa[N],pos[N],tmp;
ll s[N],s1[N],s2[N],ans;
vector<pair<int,int> >v[N];
vector<int>g[N];
void dfs(int x,int fa){
	dep[x]=dep[fa]+1,sz[x]=1;
	for(auto p:v[x]){
		int y=p.first;
		if(y!=fa){
			dis[y]=dis[f[y]=x]+p.second,dfs(y,x),sz[x]+=sz[y];
			if(sz[y]>sz[son[x]]) son[x]=y;
		}
	}
}
void dfs2(int x,int tp){
	top[x]=tp;
	if(son[x]) dfs2(son[x],tp);
	for(auto p:v[x]){
		int y=p.first;
		if(y!=f[x]&&y!=son[x]) dfs2(y,y);
	}
}
int lca(int x,int y){
	while(top[x]!=top[y]){
		if(dep[top[x]]<dep[top[y]]) swap(x,y);
		x=f[top[x]];
	}
	return dep[x]<dep[y]?x:y;
}
void getrt(int x,int fa,int tot){
	sz[x]=1,mx[x]=0;
	for(auto p:v[x]){
		int y=p.first;
		if(y!=fa&&!vis[y])
			getrt(y,x,tot),sz[x]+=sz[y],mx[x]=max(mx[x],sz[y]);
	}
	if((mx[x]=max(mx[x],tot-sz[x]))<mx[rt]) rt=x;
}
void div(int x){
	vis[x]=1;
	for(auto p:v[x]){
		int y=p.first;
		if(!vis[y])
			rt=0,getrt(y,x,sz[y]),g[fa[rt]=x].push_back(rt),pos[rt]=y,div(rt);
	}
}
void upd(int x,int v){
	for(int i=x;i;i=fa[i]){
		s[i]+=v,s1[i]+=1ll*dis(x,i)*v;
		if(fa[i]) s2[i]+=1ll*dis(x,fa[i])*v;
	}
}
ll calc(int x){
	ll ans=0;
	for(int i=x;i;i=fa[i]){
		ans+=s[i]*dis(x,i)+s1[i];
		if(fa[i]) ans-=s[i]*dis(x,fa[i])+s2[i];
	}
	return ans;
}
void dfs3(int x){
	ll tmp=calc(x);
	ans=min(ans,tmp);
	for(int y:g[x])
		if(calc(pos[y])<tmp) return dfs3(y);
}
signed main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<n;i++)
		scanf("%d%d%d",&x,&y,&z),
		v[x].push_back({y,z}),v[y].push_back({x,z});
	dfs(1,0),dfs2(1,1);
	mx[0]=1e9,getrt(1,0,n),div(tmp=rt);	//点分树的根 tmp
	while(m--){
		scanf("%d%d",&x,&y),upd(x,y);
		ans=1e18,dfs3(tmp),printf("%lld\n",ans);
	}
	return 0;
}

7. P3676 小清新数据结构题

2023.1.11

给出一棵 \(n\) 个节点的树,点有点权 \(a_i\)\(m\) 次操作:

  • 1 x v\(a_x\gets v\)
  • 2 rt:查询以 \(rt\) 为根时,每棵子树点权和的平方之和。

\(1\leq n,m\leq 2\times 10^5\)\(-10\leq a_i,y\leq 10\)

\(sum\) 为所有点点权和,\(s_i\) 为子树 \(i\) 点权和。\(sum\) 随便维护。

利用“\(\sum_{i=1}^n s_i\cdot (sum-s_i)\) 与根节点无关”降次,\(\sum_{i=1}^n s_i^2=-\sum_{i=1}^n s_i(sum-s_i)+\sum_{i=1}^n s_isum\)

然后拆成每个点的贡献,具体地:

  1. 这部分与根节点无关:

    \[\sum_{i=1}^n s_i(sum-s_i)=\sum_{i=1}^n\sum_{j=1}^n a_i\cdot a_j\cdot dis(i,j) \]

    操作后其增量为 \(v\cdot \sum_{j=1}^n a_j\cdot dis(x,j)\),带权距离和按 P3345 点分树计算即可。

  2. 这部分与根节点有关:

    \[\sum_{i=1}^n s_i=\sum_{i=1}^n a_i(dis(i,rt)+1)=\sum_{i=1}^n a_i\cdot dis(i,rt)+sum \]

    \(\sum_{i=1}^n a_i\cdot dis(i,rt)\) 点分树算。

#include<bits/stdc++.h>
#define ll long long
#define dis(x,y) (dep[x]+dep[y]-2*dep[lca(x,y)])
using namespace std;
const int N=2e5+5;
int n,m,op,x,y,a[N],dep[N],sz[N],son[N],top[N],f[N],mx[N],rt,vis[N],fa[N],sum,s[N];
ll cur,s1[N],s2[N];
vector<int>v[N]; 
void dfs(int x,int fa){
	dep[x]=dep[fa]+1,sz[x]=1;
	for(int y:v[x]) if(y!=fa){
		f[y]=x,dfs(y,x),sz[x]+=sz[y];
		if(sz[y]>sz[son[x]]) son[x]=y;
	}
}
void dfs2(int x,int tp){
	top[x]=tp;
	if(son[x]) dfs2(son[x],tp);
	for(int y:v[x])
		if(y!=f[x]&&y!=son[x]) dfs2(y,y);
}
int lca(int x,int y){
	while(top[x]!=top[y]){
		if(dep[top[x]]<dep[top[y]]) swap(x,y);
		x=f[top[x]];
	}
	return dep[x]<dep[y]?x:y;
}
void getrt(int x,int fa,int tot){
	sz[x]=1,mx[x]=0;
	for(int y:v[x]) if(y!=fa&&!vis[y])
		getrt(y,x,tot),sz[x]+=sz[y],mx[x]=max(mx[x],sz[y]);
	if((mx[x]=max(mx[x],tot-sz[x]))<mx[rt]) rt=x;
}
void div(int x){
	vis[x]=1;
	for(int y:v[x])
		if(!vis[y]) rt=0,getrt(y,x,sz[y]),fa[rt]=x,div(rt);
}
ll calc(int x){
	ll ans=0;
	for(int i=x;i;i=fa[i]){
		ans+=s[i]*dis(x,i)+s1[i];
		if(fa[i]) ans-=s[i]*dis(x,fa[i])+s2[i];
	}
	return ans;
}
void upd(int x,int v){
	sum+=v,cur+=v*calc(x);
	for(int i=x;i;i=fa[i]){
		s[i]+=v,s1[i]+=dis(x,i)*v;
		if(fa[i]) s2[i]+=dis(x,fa[i])*v;
	}
}
signed main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<n;i++)
		scanf("%d%d",&x,&y),
		v[x].push_back(y),v[y].push_back(x);
	dfs(1,0),dfs2(1,1);
	mx[0]=1e9,getrt(1,0,n),div(rt);
	for(int i=1;i<=n;i++)
		scanf("%d",&a[i]),upd(i,a[i]);
	while(m--){
		scanf("%d%d",&op,&x);
		if(op==1) scanf("%d",&y),upd(x,y-a[x]),a[x]=y;
		else printf("%lld\n",-cur+(calc(x)+sum)*sum);
	}
	return 0;
}

8. P3241 [HNOI2015] 开店

2023.1.11

给出一棵 \(n\) 个节点的树,点有点权 \(a_i\),边有边权 \(w_i\)

\(m\) 次询问,每次给出 \(x,l,r\),求 \(\sum_{i=1}^n[l\leq a_i\leq r]dis(x,i)\)

\(n\leq 1.5\times 10^5\)\(m\leq 2\times 10^5\)\(a_i\leq 10^9\)\(w_i\leq 10^3\),强制在线。

按套路枚举点分树上 \(\text{lca}(x,i)\)\(dis(x,i)=dis(x,\text{lca})+dis(\text{lca},i)\),需要维护 \(\text{lca}\) 子树内点权 \(\in[l,r]\) 的点的个数、这些点与 \(\text{lca}\) 的距离之和,还有这些点与 \(\text{lca}\) 父亲的距离之和来容斥。

(upd on 2023.7.31:另一个可能的思路是,离线再用 P4211 [LNOI2014] LCA 的做法)

可以对每个 \(p\) 维护子树内所有点 (点权,与 \(p\) 距离,与 \(fa_p\) 距离) 组成的 vector,按点权排序后求距离的前缀和,询问时对 \(l,r\) lower_bound/upper_bound

//O2
#include<bits/stdc++.h>
#define ll long long
#define dis(x,y) (dis[x]+dis[y]-2*dis[lca(x,y)])
using namespace std;
const int N=1.5e5+5;
int n,m,lim,a[N],x,y,z,l,r,dep[N],sz[N],dis[N],son[N],top[N],f[N],mx[N],rt,vis[N],fa[N];
ll ans;
struct P{
	int a;
	ll d1,d2;
	bool operator<(P x)const{return a<x.a;}
};
vector<P>h[N];
vector<pair<int,int> >v[N];
void dfs(int x,int fa){
	dep[x]=dep[fa]+1,sz[x]=1;
	for(auto p:v[x]){
		int y=p.first;
		if(y!=fa){
			dis[y]=dis[f[y]=x]+p.second,dfs(y,x),sz[x]+=sz[y];
			if(sz[y]>sz[son[x]]) son[x]=y;
		}
	}
}
void dfs2(int x,int tp){
	top[x]=tp;
	if(son[x]) dfs2(son[x],tp);
	for(auto p:v[x]){
		int y=p.first;
		if(y!=f[x]&&y!=son[x]) dfs2(y,y);
	}
}
int lca(int x,int y){
	while(top[x]!=top[y]){
		if(dep[top[x]]<dep[top[y]]) swap(x,y);
		x=f[top[x]];
	}
	return dep[x]<dep[y]?x:y;
}
void getrt(int x,int fa,int tot){
	sz[x]=1,mx[x]=0;
	for(auto p:v[x]){
		int y=p.first;
		if(y!=fa&&!vis[y])
			getrt(y,x,tot),sz[x]+=sz[y],mx[x]=max(mx[x],sz[y]);
	}
	if((mx[x]=max(mx[x],tot-sz[x]))<mx[rt]) rt=x;
}
void div(int x){
	vis[x]=1;
	for(auto p:v[x]){
		int y=p.first;
		if(!vis[y]) rt=0,getrt(y,x,sz[y]),fa[rt]=x,div(rt);
	}
}
signed main(){
	scanf("%d%d%d",&n,&m,&lim);
	for(int i=1;i<=n;i++) scanf("%d",&a[i]);
	for(int i=1;i<n;i++)
		scanf("%d%d%d",&x,&y,&z),
		v[x].push_back({y,z}),v[y].push_back({x,z});
	dfs(1,0),dfs2(1,1);
	mx[0]=1e9,getrt(1,0,n),div(rt);
	for(int x=1;x<=n;x++)
		for(int i=x;i;i=fa[i])
			h[i].push_back({a[x],dis(x,i),dis(x,fa[i])});
	for(int x=1;x<=n;x++){
		sort(h[x].begin(),h[x].end());
		for(int i=1;i^h[x].size();i++)
			h[x][i].d1+=h[x][i-1].d1,h[x][i].d2+=h[x][i-1].d2;
	}
	while(m--){
		scanf("%d%d%d",&x,&l,&r);
		l=(l+ans)%lim,r=(r+ans)%lim,ans=0;
		if(l>r) swap(l,r);
		auto qry=[&](int p,int op){
			int x=lower_bound(h[p].begin(),h[p].end(),(P){l,0,0})-h[p].begin();
			int y=upper_bound(h[p].begin(),h[p].end(),(P){r,0,0})-h[p].begin()-1;
			return !op?y-x+1:(op==1?(~y?h[p][y].d1:0)-(x?h[p][x-1].d1:0):(~y?h[p][y].d2:0)-(x?h[p][x-1].d2:0));
		};
		for(int i=x;i;i=fa[i]){
			int c=qry(i,0);
			ans+=1ll*dis(x,i)*c+qry(i,1);	//!!! 1ll
			if(fa[i]) ans-=1ll*dis(x,fa[i])*c+qry(i,2);
		}
		printf("%lld\n",ans);
	}
	return 0;
}

9. P5311 [Ynoi2011] 成都七中

2023.1.11

给出一棵 \(n\) 个节点的树,点有颜色 \(a_i\)

\(m\) 次操作,每次给出 \(l,r,x\),询问只保留编号 \(\in[l,r]\) 的点,\(x\) 所在连通块的颜色种类数。

\(n,m,a_i\leq 10^5\)

对于询问 \((l,r,x)\),暴力找到(点分树上)最浅的与 \(x\) 连通的点 \(p\),那么这个询问可以等效成 \((l,r,p)\),并且对询问有影响的点都在 \(p\) 的子树内。

将询问挂在 \(p\) 上,对每个 \(p\) 分别处理。考虑 \(p\) 的子树中,每个点 \(x\) 可以写作三元组 \((mn,mx,col)\),表示原树 \(x\leadsto p\) 路径上的最小编号/最大编号以及 \(a_x\)。转化为求 \(l\leq mn\leq mx\leq r\) 的颜色数。

\((mn,mx)\) 看作二维平面上一个点,就是询问 \((l,r)\) 右下角的颜色数,对横坐标从右往左扫描线,记录每种颜色出现的最小纵坐标,树状数组维护。

时间复杂度 \(\mathcal O(n\log^2 n)\)

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int n,m,a[N],x,y,l,r,sz[N],mx[N],rt,vis[N],c[N],pos[N],ans[N];
vector<int>v[N];
struct P{int x,y,z,op;};
vector<P>anc[N],s[N];
void getrt(int x,int fa,int tot){
	sz[x]=1,mx[x]=0;
	for(int y:v[x]) if(y!=fa&&!vis[y])
		getrt(y,x,tot),sz[x]+=sz[y],mx[x]=max(mx[x],sz[y]);
	if((mx[x]=max(mx[x],tot-sz[x]))<mx[rt]) rt=x;
}
void dfs(int x,int fa,int mn,int mx){
	mn=min(mn,x),mx=max(mx,x);
	s[rt].push_back({mn,mx,a[x],0});
	anc[x].push_back({mn,mx,rt,0});
	for(int y:v[x])
		if(y!=fa&&!vis[y]) dfs(y,x,mn,mx);
}
void div(int x){
	vis[x]=1,dfs(x,0,1e9,0);
	for(int y:v[x])
		if(!vis[y]) rt=0,getrt(y,x,sz[y]),div(rt);
}
void add(int x,int y){
	for(int i=x;i<=n;i+=i&(-i)) c[i]+=y;
}
int query(int x){
	int ans=0;
	for(int i=x;i;i-=i&(-i)) ans+=c[i];
	return ans;
}
signed main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++) scanf("%d",&a[i]);
	for(int i=1;i<n;i++)
		scanf("%d%d",&x,&y),
		v[x].push_back(y),v[y].push_back(x);
	mx[0]=1e9,getrt(1,0,n),div(rt);
	for(int i=1;i<=m;i++){
		scanf("%d%d%d",&l,&r,&x);
		for(auto p:anc[x])
			if(l<=p.x&&p.y<=r){s[p.z].push_back({l,r,i,1});break;}
	}
	memset(pos,0x3f,sizeof(pos));
	for(int i=1;i<=n;i++){
		sort(s[i].begin(),s[i].end(),[](P x,P y){return x.x^y.x?x.x>y.x:x.op<y.op;});
		for(auto p:s[i]){
			if(p.op) ans[p.z]=query(p.y);
			else if(p.y<pos[p.z]) add(pos[p.z],-1),add(pos[p.z]=p.y,1);
		}
		for(auto p:s[i])
			if(!p.op) add(pos[p.z],-1),pos[p.z]=1e9;
	}
	for(int i=1;i<=m;i++) printf("%d\n",ans[i]);
	return 0;
} 

10. CodeChef BTREE Union on Tree

给出一棵 \(n\) 个节点的树,\(q\) 次询问,每次给出一个关键点集 \(S\) 以及点集中每个点的 \(r\),一个关键点可以覆盖距离它 \(\leq r\) 的点,求共覆盖了所有点。

\(1\leq n,q\leq 5\times 10^5\)\(\sum|S|\leq 5\times 10^5\)

\(S\) 建立虚树,求出虚树上每个点能向外覆盖的距离。

一个 tricky 的计数:一个被覆盖的点一定是被虚树上的一个连通块覆盖到,根据 点数 - 边数 = 1,转化成统计虚树上每个点覆盖到的点数之和 - 每条边两端同时覆盖到的点数。

前者用点分树求距离一个点 \(\leq d\) 的点数。后者可以转化为求距离这条边的中点 \(\leq \min(d_x,d_y)-\frac{len}{2}\) 的点数(证明:必要性显然,充分性反证)。

posted @ 2021-02-23 16:41  maoyiting  阅读(163)  评论(0)    收藏  举报