\qquad
对于一棵树中的一个节点,我们将这一节点以及与其相连的边删除,会将这棵树分为若干个连通块。现在记这若干个连通块中包含点数最多的连通块内包含的点数为
s
z
e
m
a
x
[
i
]
sze_{max}[i]
szemax[i] ,那么树的重心即为使
s
z
e
m
a
x
[
i
]
sze_{max}[i]
szemax[i] 最小的点
i
i
i。
\qquad
现在,我们来思考一下树的重心有什么性质。不难发现,树的重心的
s
z
e
m
a
x
sze_{max}
szemax 不会超过整棵树点数的一半。这点我们可以用反证法来思考。
\qquad
那么如何求树的重心呢?我们只需求出
s
z
e
[
x
]
sze[x]
sze[x] ,即以
x
x
x 为根的子树的大小,然后对于每个节点,它的
s
z
e
m
a
x
[
x
]
=
max
(
s
z
e
[
y
]
(
y
∈
s
o
n
[
x
]
)
,
n
−
s
z
e
[
x
]
)
sze_{max}[x] = \max(sze[y](y\in son[x]), n - sze[x])
szemax[x]=max(sze[y](y∈son[x]),n−sze[x])。要与
n
−
s
z
e
[
x
]
n -sze[x]
n−sze[x] 取
max
\max
max 是因为把
x
x
x 删去后,
x
x
x 子树外的店会构成一个连通块。
\qquad
现在,我们想,若我们任意选取根节点来进行点分治算法,那么时间复杂度最坏将退化到
O
(
n
2
)
O(n^2)
O(n2) ,即链的情况。这是我们不能接受的,因为它跟暴力一个层级……然而,我们思考,如果我们每次以树的重心作为根,那么点分治的时间复杂度是不是就平均成了
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn) 呢?现在考虑证明。我们已经知道,树的重心的
s
z
e
m
a
x
sze_{max}
szemax 一定小于等于整棵树的节点数的一半,那么我们每次选取树的重心为根,将根删除后,即使是链,被分开的两个连通块的大小都为
n
/
2
n / 2
n/2(
n
n
n 即为整棵树的节点数)。一直递归下去,最多递归
l
o
g
n
logn
logn 次,就可以将整棵树处理完。所以以树的重心为根是使时间复杂度最小的方案。
\qquad
时间复杂度搞定后,我们来思考:如何才能求出所有符合要求的过根路径呢?不难想到,一条过根路径一定是由一段左子树上的链与一段右子树上的链拼成的,所以我们可以每次遍历
x
x
x 的所有子树,当前子树产生的贡献由前几个子树计算完记录的信息来更新。
\qquad
一道很显然的点分治题,想要满足边权和为
k
k
k,同时还要边数最小,我们便可以开一个
m
i
n
e
mine
mine 数组,
m
i
n
e
[
k
]
mine[k]
mine[k] 表示距离当前根节点距离为
k
k
k 的边数最小值,然后每次在
c
a
l
c
calc
calc 函数内统计答案,在
c
h
a
n
g
e
change
change 函数内维护
m
i
n
e
mine
mine 数组即可。
C
o
d
e
:
Code:
Code:
#include<bits/stdc++.h>usingnamespace std;constint maxn =2e5+5;int n, k;struct pic {int lst, to, val;}edge[maxn <<1];int head[maxn], tot =0;int sze[maxn], maxx, root, now;int mine[maxn *5], ans;//mine[i]:到根的距离为i的边数最小值int dis[maxn], dep[maxn];bool vis[maxn];inlineint read(){int x =0, f =1;char ch = getchar();while(!isdigit(ch)){if(ch =='-') f =-1; ch = getchar();}while(isdigit(ch)){ x =(x <<1)+(x <<3)+(ch ^48); ch = getchar();}return x * f;}inlinevoid add(int x,int y,int z){ edge[++ tot].lst = head[x]; edge[tot].to = y; edge[tot].val = z; head[x]= tot;}void get_root(int x,int fa){ sze[x]=1;intMaxx=0;for(int i = head[x]; i !=-1; i = edge[i].lst){intTo= edge[i].to,Val= edge[i].val;if(To== fa || vis[To])continue; get_root(To, x); sze[x]+= sze[To];Maxx= max(Maxx, sze[To]);}Maxx= max(Maxx, now - sze[x]);if(Maxx< maxx){ maxx =Maxx; root = x;}}void calc(int x,int fa){if(dis[x]> k)return; ans = min(ans, dep[x]+ mine[k - dis[x]]);//一共距离为k,当前距离为dis[x],边数为dep[x],找前面子树中与当前边权合起来刚好为k(即k-dis[x])的答案for(int i = head[x]; i !=-1; i = edge[i].lst){intTo= edge[i].to,Val= edge[i].val;if(To== fa || vis[To])continue; dis[To]= dis[x]+Val; dep[To]= dep[x]+1; calc(To, x);}}void change(int x,int fa,int opt){if(dis[x]> k)return;if(opt) mine[dis[x]]= min(mine[dis[x]], dep[x]);//维护mineelse mine[dis[x]]= n;for(int i = head[x]; i !=-1; i = edge[i].lst){intTo= edge[i].to;if(To== fa || vis[To])continue; change(To, x, opt);}}void solve(int x){ mine[0]=0; vis[x]=1;for(int i = head[x]; i !=-1; i = edge[i].lst){intTo= edge[i].to,Val= edge[i].val;if(vis[To])continue; dep[To]=1;//深度(边数) dis[To]=Val; calc(To,0); change(To,0,1);//更新桶数组}for(int i = head[x]; i !=-1; i = edge[i].lst){intTo= edge[i].to;if(vis[To])continue; change(To,0,0);//回溯,还原桶数组}for(int i = head[x]; i !=-1; i = edge[i].lst){intTo= edge[i].to;if(vis[To])continue; now = sze[To], root =0, maxx =1e9; get_root(To,0); get_root(root,0); solve(root);}}int main(){ n = read(), k = read(); memset(head,-1,sizeof head);for(int i =1, x, y, z; i < n; i ++){ x = read()+1, y = read()+1, z = read(); add(x, y, z), add(y, x, z);}for(int i =1; i <= k; i ++) mine[i]= n; now = ans = n; maxx =1e9, root =0; get_root(1,0); get_root(root,0); solve(root);if(ans == n) puts("-1");else printf("%d\n", ans);return0;}
\qquad
首先,题面中的“路径条数”让我们一眼点分治。然而,我们如何去查找一段平衡的路径呢?现在,我们想,如果把“阳边”的边权看做
1
1
1,把“阴边”的边权看做
−
1
-1
−1,那么是不是只需找一段边权和为
0
0
0 的路径就行了呢?找“平衡路径”的问题解决了,那么如何解决休息点的问题呢?不难发现,若
(
x
,
y
)
(x, y)
(x,y) 这条路径上有一个休息点
k
k
k,那么一定满足
d
i
s
(
x
−
>
k
)
=
d
i
s
(
k
−
>
y
)
dis(x->k) = dis(k->y)
dis(x−>k)=dis(k−>y) 。所以我们只需记录一个
d
i
s
[
x
]
dis[x]
dis[x] 表示
x
x
x 距离当前根节点的路径权值,那么找到一个
d
i
s
[
y
]
dis[y]
dis[y] 与之前记录的
d
i
s
[
x
]
dis[x]
dis[x] 相同即可。又因为要统计路径条数,所以我们要开数组另外记录
d
i
s
[
x
]
dis[x]
dis[x] 出现的次数。不难发现,我们需要开两个数组分别记录子树外的
d
i
s
[
x
]
dis[x]
dis[x] 条数与子树内的
d
i
s
[
x
]
dis[x]
dis[x] 条数,前后拼凑才能保证一条过根路径满足条件。然而,我们又双叒叕发现,我们如何判断拼出的这条路径中间有没有休息点呢?这时,我们想:若我们在递归到点
x
x
x 之前,就已经有长度为
d
i
s
[
x
]
dis[x]
dis[x] 的路径了,那是不是一定可以拼出一条合法路径呢?我们只需要以上一个长度为
d
i
s
[
x
]
dis[x]
dis[x] 的点作为休息点即可。但如果没出现过,我们就不确定是否能拼出来。所以我们记录条数的数组还应再多加一维为
0
/
1
0/1
0/1 ,来记录这一距离以前是否出现过,若出现过则累加到
1
1
1 的那一维,否则加到
0
0
0 的那一维。总体框架就是这样,还有更多细节体现在代码里。
C
o
d
e
:
Code:
Code:
#include<bits/stdc++.h>usingnamespace std;constint maxn =1e5+5;typedeflonglong LL;int n;LL ans =0;struct pic {int lst, to, val;}edge[maxn <<1];int head[maxn], tot =0;int sze[maxn], root, maxx, now;int dis[maxn], max_deep;int t[maxn <<1];//标记这一边权以前是否出现过LL g[maxn <<1][2], f[maxn <<1][2];//g:子树外的长度为i的路径,有/无休息点的条数 f:子树内bool vis[maxn];inlineint read(){int x =0, f =1;char ch = getchar();while(!isdigit(ch)){if(ch =='-') f =-1; ch = getchar();}while(isdigit(ch)){ x =(x <<1)+(x <<3)+(ch ^48); ch = getchar();}return x * f;}inlinevoid add(int x,int y,int z){ edge[++ tot].lst = head[x]; edge[tot].to = y; edge[tot].val = z; head[x]= tot;}void get_root(int x,int fa){ sze[x]=1;intMaxx=0;for(int i = head[x]; i !=-1; i = edge[i].lst){intTo= edge[i].to;if(To== fa || vis[To])continue; get_root(To, x); sze[x]+= sze[To];Maxx= max(Maxx, sze[To]);}Maxx= max(Maxx, now - sze[x]);if(Maxx< maxx){ maxx =Maxx; root = x;}}void dfs(int x,int fa){ max_deep = max(max_deep, abs(dis[x]- n));//纪录最大边长,便于更新桶数组if(t[dis[x]])++ f[dis[x]][1];//若出现过,则有休息点else++ f[dis[x]][0];//否则没有++ t[dis[x]];//只有端点为子树内的点的路径才能用当前点做休息点,所以t[]标记在进入子树前加上,出子树减去for(int i = head[x]; i !=-1; i = edge[i].lst){intTo= edge[i].to,Val= edge[i].val;if(To== fa || vis[To])continue; dis[To]= dis[x]+Val; dfs(To, x);}-- t[dis[x]];//出子树减去}void solve(int x){ vis[x]=1, g[n][0]=1;//g[n][0]=1是根节点作为休息点的情况int mx_deep =0;//减小清零时间复杂度的,只用把用过的清零即可for(int i = head[x]; i !=-1; i = edge[i].lst){intTo= edge[i].to,Val= edge[i].val;if(vis[To])continue; dis[To]= n +Val;//+n是因为有负边权,让边权整体偏移n max_deep =1; dfs(To,0); mx_deep = max(mx_deep, max_deep); ans +=(g[n][0]-1)* f[n][0];//根节点不能作为端点,所以是(g[n][0]-1)for(int j =-max_deep; j <= max_deep; j ++){ ans +=(g[n - j][1]* f[n + j][1])+(g[n - j][0]* f[n + j][1])+(g[n - j][1]* f[n + j][0]);//至少保证有一个休息点,即有一个维度为1的}for(int j =-max_deep; j <= max_deep; j ++){ g[n - j][1]+= f[n - j][1]; g[n - j][0]+= f[n - j][0]; f[n - j][1]= f[n - j][0]=0;//更新桶数组}}for(int i =-mx_deep; i <= mx_deep; i ++){ g[n - i][0]= g[n - i][1]=0;}for(int i = head[x]; i !=-1; i = edge[i].lst){intTo= edge[i].to;if(vis[To])continue; now = sze[To]; root =0; maxx =1e9; get_root(To,0); get_root(root,0); solve(root);}}int main(){ n = read(); memset(head,-1,sizeof head);for(int i =1, x, y, z; i < n; i ++){ x = read(), y = read(), z = read();if(!z) z =-1;//把“阴边”的边权变成-1 add(x, y, z); add(y, x, z);} maxx =1e9; now = n; get_root(1,0); get_root(root,0);//基本操作 solve(root); printf("%lld\n", ans);return0;}
#include<bits/stdc++.h>usingnamespace std;constint maxn =2e4+5;int n;struct pic {int lst, to, val;}edge[maxn <<1];int head[maxn], tot =0;int sze[maxn], root, maxx, now;int f[3], g[3];//子树内、子树外bool vis[maxn];int dis[maxn];int ans1 =0, ans2 =0;inlineint read(){int x =0, f =1;char ch = getchar();while(!isdigit(ch)){if(ch =='-') f =-1; ch = getchar();}while(isdigit(ch)){ x =(x <<1)+(x <<3)+(ch ^48); ch = getchar();}return x * f;}inlinevoid add(int x,int y,int z){ edge[++ tot].lst = head[x]; edge[tot].to = y; edge[tot].val = z; head[x]= tot;}void get_root(int x,int fa){ sze[x]=1;intMaxx=0;for(int i = head[x]; i !=-1; i = edge[i].lst){intTo= edge[i].to;if(To== fa || vis[To])continue; get_root(To, x); sze[x]+= sze[To];Maxx= max(Maxx, sze[To]);}Maxx= max(Maxx, now - sze[x]);if(Maxx< maxx){ maxx =Maxx; root = x;}}void dfs(int x,int fa){ f[dis[x]%3]++;//不用判断以前是否出现过,直接累加答案即可for(int i = head[x]; i !=-1; i = edge[i].lst){intTo= edge[i].to,Val= edge[i].val;if(To== fa || vis[To])continue; dis[To]= dis[x]+Val; dfs(To, x);}}void solve(int x){ vis[x]=1;for(int i = head[x]; i !=-1; i = edge[i].lst){intTo= edge[i].to,Val= edge[i].val;if(vis[To])continue; dis[To]=Val; dfs(To, x); ans1 += g[0]* f[0]+ g[1]* f[2]+ g[2]* f[1]+ f[0];//+f[0]:可能当前节点到根的距离也合法 g[0]+= f[0], g[1]+= f[1], g[2]+= f[2]; f[0]= f[1]= f[2]=0;} g[0]= g[1]= g[2]=0;for(int i = head[x]; i !=-1; i = edge[i].lst){intTo= edge[i].to;if(vis[To])continue; root =0, maxx =1e9, now = sze[To]; get_root(To,0); get_root(root,0); solve(root);}}int gcd(int x,int y){if(x < y) swap(x, y);while(y){int temp = x % y; x = y; y = temp;}return x;}int main(){ n = read(); memset(head,-1,sizeof head);for(int i =1, x, y, z; i < n; i ++){ x = read(), y = read(), z = read(); z %=3; add(x, y, z); add(y, x, z);} now = n, root =0, maxx =1e9; get_root(1,0); get_root(root,0); solve(root); ans1 <<=1; ans1 += n; ans2 = n * n;intGcd= gcd(ans1, ans2); printf("%d/%d\n", ans1 /Gcd, ans2 /Gcd);//约成最简分数return0;}
#4 [2018/7 D班集训]路径规划
\qquad
依然是一道一眼点分治的题。然而本题与前两题不同,前两题是让我们求合法的路径条数,但是本题是让求最大值。怎么办呢?把记录条数改为记录最大值不就行啦! 但是,我们以前查询是只查询固定值的答案(例如
R
a
c
e
Race
Race 中的
m
i
n
e
mine
mine 数组是固定路径长为
k
k
k 的最小边数),而这道题我们需要查询 区间(或后缀)的答案。什么意思呢?假定我们以当前从根开始的路径的最小值为整条路径的最小值,去找前面子树中的路径拼处整条路径,那么我们要查询的就是值域在 [当前从根开始的路径的最小值,极大值] 中的最大边权和。那么该怎么存储区间最值呢?不难想到线段树。但是本蒟蒻亲测线段树会 T。怎么办呢?观察值域,我们发现我们要查询的是一个后缀最大值,那么就很自然的想到用常熟更小的树状数组来搞。另外本题还有一个小细节:我们只用当前路径作为最小值与前面路径拼了,并没有和后面子树拼,这就会导致少考虑情况。这时候我们仅需将边按照反的顺序再建一边,然后再跑一遍即可。
C
o
d
e
:
Code:
Code:
#include<bits/stdc++.h>usingnamespace std;typedeflonglong LL;constint maxn =3e5+5;constint maxv =1e6+5;int n;LL INF;struct pic {int val, to, lst;}edge[maxn <<1];LL c[maxv];int head[maxn], tot =0;int sze[maxn], root, now, maxx;int x[maxn], y[maxn], z[maxn];LL dis[maxn], minn[maxv];//minn[x]:最小边权为x时最大边权和bool vis[maxn];LL ans;inlineint read(){int x =0, f =1;char ch = getchar();while(!isdigit(ch)){if(ch =='-') f =-1; ch = getchar();}while(isdigit(ch)){ x =(x <<1)+(x <<3)+(ch ^48); ch = getchar();}return x * f;}inlineint lowbit(int x){return x &(-x);}//树状数组前缀操作改后缀:将循环的顺序换一下即可,原理可以感性理解一下void modify(int x, LL y){while(x){//前缀:x<=INF c[x]= max(c[x], y); x -= lowbit(x);}}voidClear(int x){while(x){ c[x]=0; x -= lowbit(x);}}LL ask(int x){ LL cnt =0;while(x <= INF){//前缀:x cnt = max(cnt, c[x]); x += lowbit(x);}return cnt;}inlinevoid add(int x,int y,int z){ edge[++ tot].lst = head[x]; edge[tot].to = y; edge[tot].val = z; head[x]= tot;}void get_root(int x,int fa){ sze[x]=1;intMaxx=0;for(int i = head[x]; i !=-1; i = edge[i].lst){intTo= edge[i].to;if(To== fa || vis[To])continue; get_root(To, x); sze[x]+= sze[To];Maxx= max(Maxx, sze[To]);}Maxx= max(Maxx, now - sze[x]);if(Maxx< maxx){ maxx =Maxx; root = x;}}void calc(int x,int fa, LL Minn){for(int i = head[x]; i !=-1; i = edge[i].lst){intTo= edge[i].to,Val= edge[i].val;if(To== fa || vis[To])continue; dis[To]= dis[x]+Val; LL MINN = min(Minn,1LL*Val);//本条路径上的最小值 ans = max(ans, MINN *(ask(MINN)+ dis[To]));//更新答案 calc(To, x, MINN);}}void change(int x,int fa,int opt, LL Minn){for(int i = head[x]; i !=-1; i = edge[i].lst){intTo= edge[i].to,Val= edge[i].val;if(To== fa || vis[To])continue; LL MINN = min(Minn,1LL*Val);if(opt){ minn[MINN]= max(minn[MINN], dis[To]); modify(MINN, minn[MINN]);//更新桶数组}else{ minn[MINN]=0;Clear(MINN);} change(To, x, opt, MINN);}}void solve(int x){ vis[x]=1;for(int i = head[x]; i !=-1; i = edge[i].lst){intTo= edge[i].to,Val= edge[i].val;if(vis[To])continue; dis[To]=Val; ans = max(ans,1LL*Val*(ask(Val)+ dis[To]));//可以仅由一条边构成 calc(To, x,Val); minn[Val]= max(minn[Val], dis[To]); modify(Val, minn[Val]); change(To, x,1,Val);} change(x,0,0,1e9);for(int i = head[x]; i !=-1; i = edge[i].lst){intTo= edge[i].to;if(vis[To])continue; now = sze[To], root =0, maxx =1e9; get_root(To,0); get_root(root,0); solve(root);}}int main(){ n = read(); memset(head,-1,sizeof head);for(int i =1; i < n; i ++){ x[i]= read(), y[i]= read(), z[i]= read(); add(x[i], y[i], z[i]); add(y[i], x[i], z[i]); INF = max(INF,1LL* z[i]);} INF ++; now = n, root =0, maxx =1e9; get_root(1,0); get_root(root,0); solve(root); memset(head,-1,sizeof head); memset(vis,0,sizeof vis); memset(c,0,sizeof c); tot =0;for(int i = n -1; i >=1; i --){//反顺序建边 add(x[i], y[i], z[i]); add(y[i], x[i], z[i]);} memset(minn,0,sizeof minn); now = n, root =0, maxx =1e9; get_root(1,0); get_root(root,0); solve(root); printf("%lld\n", ans);return0;}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· .NET10 - 预览版1新功能体验(一)