基本概念
点分治,分动静两类,我所说的是静态的,他又叫树分治。
静态点分治,所谓分治,就是“分而治之”,把大的问题转化为若干个小的问题去解决。
他是一种能高效计算树上路径信息的算法。
静态点分治只查询不修改;动态点分治能修改加查询。
它是 树分治 ,之所以这样说,是因为他以重心将树分成相对平均的两个子树,使得不管是“链状”树,“菊花”树等特殊的树,他们的时间复杂度都相对平均。一般来说,分治可以将暴力的复杂度 O ( n 2 ) O(n^2) O(n2) 降到 O ( n l o g n ) O(n logn) O(nlogn) 。
我知道你很急着学会点分治,但你先别急,我们要一步一步来。
子树大小
下面有这样一个问题:
给出一个无根树,假如以
i
i
i 结点为根,结点
j
j
j 是
i
i
i 的儿子结点,且j子树的结点数量是最多的,那么称结点
j
j
j 是结点
i
i
i 的"大儿子"。求每个节点的“大儿子”子树的节点数量。
定义
f
i
f_i
fi 表示:结点
i
i
i 的"大儿子"子树的结点数量。
由上图得:
f
1
=
4
f_1=4
f1=4 ,
f
2
=
5
f_2=5
f2=5 ,
f
3
=
3
f_3=3
f3=3 ,
f
4
=
5
f_4=5
f4=5 ,
f
5
=
3
f_5=3
f5=3 ,
f
6
=
5
f_6=5
f6=5 .
注意:
f
i
f_i
fi 是以
i
i
i 为根,不要被图迷惑,并不是以
1
1
1 为根。
以下是计算子树的代码:
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+5;
int n,idx,head[maxn],size[maxn],f[maxn];
struct NODE{ int v,next; }e[maxn*2];
void Insert(int u,int v){
e[++idx].v=v;
e[idx].next=head[u];
head[u]=idx;
}
void Dfs(int now,int fa,int nodeCnt){
size[now]=1;//nodeCnt在这里虽然没用,但后面点分治时会用到
f[now]=0;
for(int i=head[now];i;i=e[i].next){
int son=e[i].v;
if(son!=fa){
Dfs(son,now,nodeCnt);
size[now]+=size[son];
f[now]=max(f[now],size[son]);
}
}
f[now]=max(f[now],nodeCnt-size[now]);
}
int main(){
cin>>n;
for(int i=1;i<n;i++){
int x,y;
cin>>x>>y;
Insert(x,y);
Insert(y,x);
}
Dfs(1,0,n);
for(int i=1;i<=n;i++)
cout<<f[i]<<" ";
return 0;
}
到这里,我们已经回了如何计算子树。那接着需要在树上进行分治,以什么来分? 那当然是 树的重心 啦!
树的重心
树的重心 也叫 树的质心 ,它用于无根树(所以说点分治也是用于无根树)。
树的重心 u u u 是这样的一个节点 :以树上任意节点为根计算他子树的节点数,如果节点 u u u 的最大子树的节点数最少,那么 u u u 就是树的重心。——《算法竞赛》
若有一个问题:让你求一棵
n
n
n 个节点
n
−
1
n-1
n−1 条边的无根树的重心以及树上各个节点到重心的距离。(给定边权)
则代码如下:
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+5;
int n,idx,head[maxn],hson_s,root,size[maxn],dis[maxn];
struct TREE{ int v,w,next; }e[maxn*2];
void Insert(int u,int v,int w){
e[++idx].v=v;
e[idx].w=w;
e[idx].next=head[u];
head[u]=idx;
}
void Find_root(int now,int fa,int nodeCnt){
size[now]=1;
int s=0; //s记录以now为根的子树节点数量的最大值
for(int i=head[now];i;i=e[i].next){
int son=e[i].v;
if(son!=fa){
Find_root(son,now,nodeCnt);
size[now]+=size[son];
s=max(s,size[son]);
}
}
s=max(s,nodeCnt-size[now]);
if(s<hson_s) hson_s=s,root=now;
}
void Get_dis(int now,int fa,int d){
dis[now]=d;
for(int i=head[now];i;i=e[i].next){
int son=e[i].v;
if(son!=fa) Get_dis(son,now,d+e[i].w);
}
}
void Solve(int now,int fa,int nodeCnt){
hson_s=nodeCnt-1,root=now;//hson_s为重心的重儿子的节点数量, root为重心
Find_root(now,fa,nodeCnt);
dis[root]=0;
for(int i=head[root];i;i=e[i].next){
int son=e[i].v;
Get_dis(son,root,e[i].w);
}
}
int main(){
cin>>n;
for(int i=1;i<n;i++){
int x,y,z;
cin>>x>>y>>z;
Insert(x,y,z);
Insert(y,x,z);
}
Solve(1,0,n);
cout<<root<<"\n";
for(int i=1;i<=n;i++)
cout<<dis[i]<<" ";
return 0;
}
!!!树的重心的性质: !!!
s
i
z
e
[
i
]
size[i]
size[i] 为记录子树i的节点数量。
假设以树的重心
r
o
o
t
root
root 为根的所有儿子中,儿子
i
i
i 为根的子树的节点数最多,记为
s
i
z
e
[
i
]
size[i]
size[i] ,那么
s
i
z
e
[
i
]
≤
s
i
z
e
[
r
o
o
t
]
/
2
size[i] \le size[root]/2
size[i]≤size[root]/2 。
证明:可使用 反证法 。若 s i z e [ i ] > s i z e [ r o o t ] / 2 size[i] > size[root]/2 size[i]>size[root]/2 则不满足 r o o t root root 为树的重心,此时重心就不为 r o o t root root 了,所以得证。
求经过重心的合法路径数量
下面给出一个问题:
给出一棵
n
n
n 个节点
n
−
1
n-1
n−1 条边的无根树,边权都是
1
1
1 ,求出重心
r
o
o
t
root
root 之后(如果有多个
r
o
o
t
root
root ,只要结点编号最小的那个
r
o
o
t
root
root ),求有多少条简单路径满足如下条件:
(1)简单路径经过重心
r
o
o
t
root
root 。
(2)简单路径的起点和终点不能在
r
o
o
t
root
root 的同一颗子树内。
(3)简单路径的长度等于
k
k
k ,路径长度是指经过的边的权值之和。
1
≤
n
≤
50000
1 \le n \le 50000
1≤n≤50000 ,
1
≤
k
≤
500
1 \le k \le 500
1≤k≤500 .
对于这个问题,一般我们有
2
2
2 种做法。
两种做法的最大区别在于:
- 一种是直接计算答案
- 一种是所有答案 − - − 不合法的答案(适由于求方案数)
当然,两种做法各有所长,要因题而异。
F1:
这种做法是 直接计算答案 。
题目说两个端节点不能在同一棵子树内,那我们就分别计算以重心为根的每一棵子树对答案的贡献。
#include<bits/stdc++.h>
using namespace std;
const int maxn=5e4+5;
int n,k,idx,head[maxn],size[maxn],dis[maxn],ans,hson_s,root,p,cnt[maxn];
bool vis[maxn];
struct TREE{ int v,next; }e[maxn*2];
void Insert(int u,int v){
e[++idx].v=v;
e[idx].next=head[u];
head[u]=idx;
}
void Find_root(int now,int fa,int nodeCnt){
size[now]=1;
int s=0;
for(int i=head[now];i;i=e[i].next){
int son=e[i].v;
if(son!=fa&&!vis[son]){
Find_root(son,now,nodeCnt);
size[now]+=size[son];
s=max(s,size[son]);
}
}
s=max(s,nodeCnt-size[now]);
if(s<hson_s) hson_s=s,root=now;
}
void Get_dis(int now,int fa,int d){
size[now]=1;
dis[++p]=d;//dis数组保存各个子树的节点到root的长度的数量,dis[i]=j表示有j个节点到root节点的长度为i
for(int i=head[now];i;i=e[i].next){
int son=e[i].v;
if(son!=fa&&!vis[son]){
Get_dis(son,now,d+1);
size[now]+=size[son];
}
}
}
void Solve(int now,int fa,int nodeCnt){
hson_s=nodeCnt-1,root=now;
Find_root(now,fa,nodeCnt);//找当前now所在的树的重心
vis[root]=true;//相当于删掉重心节点
p=0;
int l=0;
cnt[0]=1;//cnt[i]=j表示有j个节点到root节点的长度为i
for(int i=head[root];i;i=e[i].next){//!!统计答案的时候,要确保路径的两个端点来自于root的不同子树
int son=e[i].v;
if(son!=fa&&!vis[son]){
Get_dis(son,root,1);
//至此,son子树内所有节点到root的长度,保存在dis[l+1...p]
//接下来先统计答案,在合并子树的信息
for(int j=l+1;j<=p;j++)
if(k>=dis[j]) ans+=cnt[k-dis[j]];
for(int j=l+1;j<=p;j++)
cnt[dis[j]]++;//更新cnt数组,这样就保证了答案都来自于不同子树
l=p;
}
}
for(int i=1;i<=p;i++)//还原cnt数组,因为后面会再次调用Solve函数
cnt[dis[i]]--;
}
int main(){
cin>>n>>k;
for(int i=1;i<n;i++){
int x,y;
cin>>x>>y;
Insert(x,y);
Insert(y,x);
}
Solve(1,0,n);
cout<<ans;
return 0;
}
F2:
这种做法是 所有答案
−
-
− 不合法的答案 (适由于求方案数)。
以这道题来说就是 (两个端点在不同的子树
+
+
+ 两个端点在同一个子树)
−
-
− 两个端点在同一个子树;
这种方法看似没用,但他计算答案的时候序列是具有单调性的(序列是有序的),这样就可以用到二分。我这样说或许不太清晰,那就直接看程序吧。
#include<bits/stdc++.h>
using namespace std;
const int maxn=5e4+5;
int n,k,idx,head[maxn],size[maxn],dis[maxn],ans,hson_s,root,p;
bool vis[maxn];
struct TREE{ int v,next; }e[maxn*2];
void Insert(int u,int v){
e[++idx].v=v;
e[idx].next=head[u];
head[u]=idx;
}
void Find_root(int now,int fa,int nodeCnt){
size[now]=1;
int s=0;
for(int i=head[now];i;i=e[i].next){
int son=e[i].v;
if(son!=fa&&!vis[son]){
Find_root(son,now,nodeCnt);
size[now]+=size[son];
s=max(s,size[son]);
}
}
s=max(s,nodeCnt-size[now]);
if(s<hson_s) hson_s=s,root=now;
}
void Get_dis(int now,int fa,int d){
size[now]=1;
dis[++p]=d;//dis数组保存各个子树的节点到root的长度的数量,dis[i]=j表示有j个节点到root节点的长度为i
for(int i=head[now];i;i=e[i].next){
int son=e[i].v;
if(son!=fa&&!vis[son]){
Get_dis(son,now,d+1);
size[now]+=size[son];
}
}
}
void Calc(int flag){
sort(dis+1,dis+p+1);//排序
for(int i=1;i<=p;i++){//二分
int x=upper_bound(dis+1,dis+i,k-dis[i])-(dis+1);
int y=upper_bound(dis+1,dis+i,k-dis[i]-1)-(dis+1);
ans+=flag*(x-y);
}
//也可用利用双指针,单调处理
}
void Solve(int now,int fa,int nodeCnt){
hson_s=nodeCnt-1,root=now;
Find_root(now,fa,nodeCnt);//找当前now所在的树的重心
vis[root]=true;//相当于删掉重心节点
p=0;
Get_dis(root,0,0);//计算各个节点到root的长度,保存在dis[1...p];并重新计算size数组
Calc(1);//统计经过重心root的点对(i,j)且dis[i]+dis[j]=k的数量,1表示是加,-1表示减
for(int i=head[root];i;i=e[i].next){
int son=e[i].v;
if(son!=fa&&!vis[son]){
p=0;//记得每次都要重置为0
Get_dis(son,root,1);//计算以son为根的子树内所有节点到root的距离,保存在dis[1...p]
Calc(-1);//相当于减去两个端点都在同一颗子树内的方案数
}
}
}
int main(){
cin>>n>>k;
for(int i=1;i<n;i++){
int x,y;
cin>>x>>y;
Insert(x,y);
Insert(y,x);
}
Solve(1,0,n);
cout<<ans;
return 0;
}
求路径长度等于k的数量
(这道题应该就是一道正宗的点分治题了吧)纯属个人猜测
这道题是这样的:
给出一棵无根树,边权都是
1
1
1 ,求有多少条简单路径的长度等于
k
k
k ,路径长度是指经过的边的权值之和。
1
≤
n
≤
50000
1 \le n \le 50000
1≤n≤50000 ,
1
≤
k
≤
500
1 \le k \le 500
1≤k≤500 .
这道题与上一题相比,无非就是这条路径上的起点和终点没有限制,就他们可以在同一子树内,也可以在不同子树内。
那根据题目,
答案
=
路径上起点和终点不在同一子树内的数量
+
路径上起点和终点在同一子树内的数量
答案=路径上起点和终点不在同一子树内的数量+路径上起点和终点在同一子树内的数量
答案=路径上起点和终点不在同一子树内的数量+路径上起点和终点在同一子树内的数量 ; 那前半段我们在上一题就解决了,后半段可以这样想:把重心
r
o
o
t
root
root 删除后,这棵树就分成了他的几棵子树,问题就变成了——计算各棵子树有多少条简单路径的长度等于
k
k
k(这不就又回到了我们刚开始的问题了吗)。所以我们可以用 递归 多次调用函数,不断的删除树的重心。
我们直接在上题程序中加上删除
r
o
o
t
root
root 在递归子问题就好了。(我只写了
F
1
F1
F1 的程序)
这个的时间复杂度应该是
O
(
n
l
o
g
n
)
O(n logn)
O(nlogn) 吧! 谁能告诉我正确的复杂度啊~
#include<bits/stdc++.h>
using namespace std;
const int maxn=5e4+5;
int n,k,idx,head[maxn],size[maxn],dis[maxn],ans,hson_s,root,p,cnt[maxn];
bool vis[maxn];
struct TREE{ int v,next; }e[maxn*2];
void Insert(int u,int v){
e[++idx].v=v;
e[idx].next=head[u];
head[u]=idx;
}
void Find_root(int now,int fa,int nodeCnt){
size[now]=1;
int s=0;
for(int i=head[now];i;i=e[i].next){
int son=e[i].v;
if(son!=fa&&!vis[son]){
Find_root(son,now,nodeCnt);
size[now]+=size[son];
s=max(s,size[son]);
}
}
s=max(s,nodeCnt-size[now]);
if(s<hson_s) hson_s=s,root=now;
}
void Get_dis(int now,int fa,int d){
size[now]=1;
dis[++p]=d;//dis数组保存各个子树的节点到root的长度的数量,dis[i]=j表示有j个节点到root节点的长度为i
for(int i=head[now];i;i=e[i].next){
int son=e[i].v;
if(son!=fa&&!vis[son]){
Get_dis(son,now,d+1);
size[now]+=size[son];
}
}
}
void Solve(int now,int fa,int nodeCnt){
hson_s=nodeCnt-1,root=now;
Find_root(now,fa,nodeCnt);//找当前now所在的树的重心
vis[root]=true;//相当于删掉重心节点
p=0;
int l=0;
cnt[0]=1;//cnt[i]=j表示有j个节点到root节点的长度为i
for(int i=head[root];i;i=e[i].next){//!!统计答案的时候,要确保路径的两个端点来自于root的不同子树
int son=e[i].v;
if(son!=fa&&!vis[son]){
Get_dis(son,root,1);
//至此,son子树内所有节点到root的长度,保存在dis[l+1...p]
//接下来先统计答案,在合并子树的信息
for(int j=l+1;j<=p;j++)
if(k>=dis[j]) ans+=cnt[k-dis[j]];
for(int j=l+1;j<=p;j++)
cnt[dis[j]]++;//更新cnt数组,这样就保证了答案都来自于不同子树
l=p;
}
}
for(int i=1;i<=p;i++)//还原cnt数组,因为后面会再次调用Solve函数
cnt[dis[i]]--;
for(int i=head[root];i;i=e[i].next){//分治,处理子问题
int son=e[i].v;
if(son!=fa&&!vis[son]) Solve(son,root,size[son]);
//!!!下面是我调了很久的错误:!!!
//上面son的父亲是root而不是now;
//子树的变化导致整棵树的节点数量也在变,不再是n,而是size[son]。
}
}
int main(){
cin>>n>>k;
for(int i=1;i<n;i++){
int x,y;
cin>>x>>y;
Insert(x,y);
Insert(y,x);
}
Solve(1,0,n);
cout<<ans;
return 0;
}
注意:上面程序中有本人调了很久才调出来的细节错误,请注意。
到此,我们终于大体明白了点分治。但要掌握,还需要跨过点分治的练习题海。
练习题
1. Tree
很特别,我做的第一道点分治题居然不是洛谷上的模板题!
但我还是要好好纪念一下,毕竟这题我足足调了3个小时;当然,最后这题不是我调出来的,是sst大佬,%%%感谢感谢。
言归正传,这题我用了上述的
F
2
F2
F2 。
计算答案的时候直接算当前子树每个点对答案的贡献,就是在已经算好的子树的结点中找有多少个结点到重心
r
o
o
t
root
root 的距离
≤
(
k
−
当前结点到重心
r
o
o
t
的距离
)
\le (k-当前结点到重心root的距离)
≤(k−当前结点到重心root的距离) 。
我们可以用个数组记录已经算好的子树的各个结点到重心
r
o
o
t
root
root 的距离,并保证数组有序,查询时就直接二分就好啦!
∴
\therefore
∴ 这种方法的时间复杂度大致为:
O
(
n
l
o
g
2
n
)
O(n log^2 n)
O(nlog2n) 。
#include<bits/stdc++.h>
using namespace std;
const int maxn=4e4+5;
int n,k,idx,head[maxn],size[maxn],dis[maxn],ans,hson_s,root,p;
bool vis[maxn];
struct TREE{ int v,w,next; }e[maxn*2];
void Insert(int u,int v,int w){
e[++idx].v=v;
e[idx].w=w;
e[idx].next=head[u];
head[u]=idx;
}
void Find_root(int now,int fa,int nodeCnt){
size[now]=1;
int s=0;
for(int i=head[now];i;i=e[i].next){
int son=e[i].v;
if(son!=fa&&!vis[son]){
Find_root(son,now,nodeCnt);
size[now]+=size[son];
s=max(s,size[son]);
}
}
s=max(s,nodeCnt-size[now]);
if(s<hson_s) hson_s=s,root=now;
}
void Get_dis(int now,int fa,int d){
size[now]=1;
dis[++p]=d;
for(int i=head[now];i;i=e[i].next){
int son=e[i].v;
if(son!=fa&&!vis[son]){
Get_dis(son,now,d+e[i].w);
size[now]+=size[son];
}
}
}
void Calc(int flag){
sort(dis+1,dis+p+1);
for(int i=1;i<=p;i++){
int x=upper_bound(dis+1,dis+i,k-dis[i])-(dis+1);
ans+=flag*x;
}
}
void Solve(int now,int fa,int nodeCnt){ //1
hson_s=nodeCnt-1,root=now;
Find_root(now,fa,nodeCnt);
vis[root]=true;
p=0;
Get_dis(root,0,0);
Calc(1);
for(int i=head[root];i;i=e[i].next){
int son=e[i].v;
if(son!=fa&&!vis[son]){
p=0;
Get_dis(son,root,e[i].w);
Calc(-1);
}
}
for(int i=head[root];i;i=e[i].next){
int son=e[i].v;
if(son!=fa&&!vis[son]) Solve(son,root,size[son]); //2
}
}
int main(){
cin>>n;
for(int i=1;i<n;i++){
int x,y,z;
cin>>x>>y>>z;
Insert(x,y,z);
Insert(y,x,z);
}
cin>>k;
Solve(1,0,n);
cout<<ans;
return 0;
}
注意两个小细节就好了:
1.第
50
50
50 行的
n
o
d
e
C
n
t
nodeCnt
nodeCnt ,记得每次调用的
n
o
d
e
C
n
t
nodeCnt
nodeCnt 都不同,不是一成不变的
n
n
n ;
2.第
67
67
67 行的
S
o
l
v
e
Solve
Solve 函数中,第二个参数是
r
o
o
t
root
root ,不要误写成
n
o
w
now
now ,
s
o
n
son
son 的父亲不是
n
o
w
now
now 。
2. [IOI2011]Race
这道题不是求 方案数 ,所以我们不能继续沿用
F
2
F2
F2 ,只好用
F
1
F1
F1 。
这题我们在
G
e
t
Get
Get_
d
i
s
dis
dis 中求出每个点到重心
r
o
o
t
root
root 的边权和所经过的边的数量,计算答案时把求和改为最小值就好了。
有一点需要注意:在算答案的时候,我们需要以每个结点到
r
o
o
t
root
root 的边权和作为下标,此时下标可能会很大,超出可定范围;但好在对答案可能有贡献的边权和一定是
≤
k
\le k
≤k 的,只有边权和符合条件情况下的我们才会加入
c
n
t
cnt
cnt 数组,所以在操作或更新
c
n
t
cnt
cnt 数组时先判断下标
≤
k
\le k
≤k 的再执行。
#include<bits/stdc++.h>
using namespace std;
const int maxn=2e5+5,INF=0x3f3f3f3f,maxk=1e6+5;
int n,k,idx,head[maxn],size[maxn],dis[maxn],ans,hson_s,root,p,dis2[maxn],cnt[maxk];
bool vis[maxn];
struct TREE{ int v,w,next; }e[maxn*2];
void Insert(int u,int v,int w){
e[++idx].v=v;
e[idx].w=w;
e[idx].next=head[u];
head[u]=idx;
}
void Find_root(int now,int fa,int nodeCnt){
size[now]=1;
int s=0;
for(int i=head[now];i;i=e[i].next){
int son=e[i].v;
if(son!=fa&&!vis[son]){
Find_root(son,now,nodeCnt);
size[now]+=size[son];
s=max(s,size[son]);
}
}
s=max(s,nodeCnt-size[now]);
if(s<hson_s) hson_s=s,root=now;
}
void Get_dis(int now,int fa,int d,int d2){
size[now]=1;
dis[++p]=d;
dis2[p]=d2;
for(int i=head[now];i;i=e[i].next){
int son=e[i].v;
if(son!=fa&&!vis[son]){
Get_dis(son,now,d+e[i].w,d2+1);
size[now]+=size[son];
}
}
}
void Solve(int now,int fa,int nodeCnt){
hson_s=nodeCnt-1,root=now;
Find_root(now,fa,nodeCnt);
vis[root]=true;
p=0;
int l=0;
cnt[0]=0;
for(int i=head[root];i;i=e[i].next){
int son=e[i].v;
if(son!=fa&&!vis[son]){
Get_dis(son,root,e[i].w,1);
for(int j=l+1;j<=p;j++)
if(k>=dis[j]) ans=min(ans,cnt[k-dis[j]]+dis2[j]);//1 如上文,就这里(1)和下面(2)加个判断
for(int j=l+1;j<=p;j++)
if(dis[j]<=maxk) cnt[dis[j]]=min(cnt[dis[j]],dis2[j]);//2
l=p;
}
}
for(int i=1;i<=p;i++)
if(dis[i]<=maxk) cnt[dis[i]]=INF;
for(int i=head[root];i;i=e[i].next){
int son=e[i].v;
if(son!=fa&&!vis[son]) Solve(son,root,size[son]);
}
}
int main(){
cin>>n>>k;
for(int i=1;i<n;i++){
int x,y,z;
cin>>x>>y>>z;
x++,y++;
Insert(x,y,z);
Insert(y,x,z);
}
for(int i=1;i<maxk;i++)
cnt[i]=INF;
ans=INF;
Solve(1,0,n);
if(ans==INF) cout<<-1;
else cout<<ans;
return 0;
}
3. 树上黑点路径
这道题也是用
F
1
F1
F1 ,算完每条路径上的黑点个数后,就直接查询在已经算完的结点中黑点个数
≤
(
k
−
当前这条路径的黑点个数的所有节点之中的最大边权值
)
\le (k - 当前这条路径的黑点个数的所有节点之中的最大边权值)
≤(k−当前这条路径的黑点个数的所有节点之中的最大边权值)。
相较于上一题,就把单点查询改为区间查询,可以用树状数组或线段树进行存储。(我用了前者,因为码量相对来说小些)
注意:计算重心
r
o
o
t
root
root 的不同子树间的两条路径的黑点个数的时候还要算上
r
o
o
t
root
root 是否为黑点,是的话黑点个数还要加一!
#include<bits/stdc++.h>
using namespace std;
const int maxn=2e5+5;
int n,k,m,idx,head[maxn],size[maxn],dis[maxn],ans,hson_s,root,p,dis2[maxn],f[maxn];
bool vis[maxn],hd[maxn];
struct TREE{ int v,w,next; }e[maxn*2];
void Insert(int u,int v,int w){
e[++idx].v=v;
e[idx].w=w;
e[idx].next=head[u];
head[u]=idx;
}
void Find_root(int now,int fa,int nodeCnt){
size[now]=1;
int s=0;
for(int i=head[now];i;i=e[i].next){
int son=e[i].v;
if(son!=fa&&!vis[son]){
Find_root(son,now,nodeCnt);
size[now]+=size[son];
s=max(s,size[son]);
}
}
s=max(s,nodeCnt-size[now]);
if(s<hson_s) hson_s=s,root=now;
}
void Get_dis(int now,int fa,int d,int d2){
size[now]=1;
dis[++p]=d;
dis2[p]=d2;
for(int i=head[now];i;i=e[i].next){
int son=e[i].v;
if(son!=fa&&!vis[son]){
Get_dis(son,now,d+hd[son],d2+e[i].w);
size[now]+=size[son];
}
}
}
void Add(int x,int y){
if(!x) return;
while(x<=k){
f[x]=max(f[x],y);
x+=x&(-x);
}
}
int Ask(int x){
int sum=0;
if(!x) sum=f[x];
while(x>0){
sum+=f[x];
x-=x&(-x);
}
return sum;
}
void Del(int x){
if(!x){
f[x]=0;
return;
}
while(x<=k){
f[x]=0;
x+=x&(-x);
}
}
void Solve(int now,int fa,int nodeCnt){
hson_s=nodeCnt-1,root=now;
Find_root(now,fa,nodeCnt);
vis[root]=true;
p=0;
int l=0;
for(int i=head[root];i;i=e[i].next){
int son=e[i].v;
if(son!=fa&&!vis[son]){
Get_dis(son,root,hd[son],e[i].w);
for(int j=l+1;j<=p;j++)
if(k>=dis[j]+hd[root]) ans=max(ans,Ask(k-dis[j]-hd[root])+dis2[j]);//这里就是上文所说的注意地方
for(int j=l+1;j<=p;j++)
Add(dis[j],dis2[j]);
l=p;
}
}
for(int i=1;i<=p;i++)
Del(dis[i]);
for(int i=head[root];i;i=e[i].next){
int son=e[i].v;
if(son!=fa&&!vis[son]) Solve(son,root,size[son]);
}
}
int main(){
cin>>n>>k>>m;
for(int i=1;i<=m;i++){
int x;
cin>>x;
hd[x]=1;
}
for(int i=1;i<n;i++){
int x,y,z;
cin>>x>>y>>z;
Insert(x,y,z);
Insert(y,x,z);
}
Solve(1,0,n);
cout<<ans;
return 0;
}
4. [BJOI2017] 树的难题
这题的细节很多,我还没做。
5. Ruri Loves Maschera
这题我用的是
F
2
F2
F2 ,虽然说是求和,但我觉得
F
2
F2
F2 好写些。
此题也是区间查询,运用了二分+树状数组。
!!写的时候要注意下标,思路在脑子里尽量保持清晰。
!!记得
a
n
s
ans
ans 要开
l
o
n
g
l
o
n
g
long long
longlong ,不开long long见祖宗
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+5;
int n,L,R,idx,head[maxn],size[maxn],hson_s,root,p,f[maxn];
long long ans; //注意点,要开long long
bool vis[maxn];
struct TREE{ int v,next; long long w; }e[maxn*2];
struct NODE{ int d; long long d2; }dis[maxn];
void Insert(int u,int v,long long w){
e[++idx].v=v;
e[idx].w=w;
e[idx].next=head[u];
head[u]=idx;
}
void Find_root(int now,int fa,int nodeCnt){
size[now]=1;
int s=0;
for(int i=head[now];i;i=e[i].next){
int son=e[i].v;
if(son!=fa&&!vis[son]){
Find_root(son,now,nodeCnt);
size[now]+=size[son];
s=max(s,size[son]);
}
}
s=max(s,nodeCnt-size[now]);
if(s<hson_s) hson_s=s,root=now;
}
void Get_dis(int now,int fa,int d,long long d2){
size[now]=1;
dis[++p]={d,d2};
for(int i=head[now];i;i=e[i].next){
int son=e[i].v;
if(son!=fa&&!vis[son]){
Get_dis(son,now,d+1,max(d2,e[i].w));
size[now]+=size[son];
}
}
}
bool cmp(NODE a,NODE b){
return a.d2<b.d2;
}
void Add(int x,int y){
x++;
while(x<=n){
f[x]+=y;
x+=x&(-x);
}
}
int Ask(int x){
x++;
int s=0;
while(x>0){
s+=f[x];
x-=x&(-x);
}
return s;
}
void Calc(int flag){
sort(dis+1,dis+p+1,cmp);
for(int i=1;i<=p;i++){
ans+=flag*((Ask(R-dis[i].d)-Ask(L-dis[i].d-1))*dis[i].d2);
Add(dis[i].d,1);
}
for(int i=1;i<=p;i++)
Add(dis[i].d,-1);
}
void Solve(int now,int fa,int nodeCnt){
hson_s=nodeCnt-1,root=now;
Find_root(now,fa,nodeCnt);
vis[root]=true;
p=0;
Get_dis(root,0,0,0);
Calc(1);
for(int i=head[root];i;i=e[i].next){
int son=e[i].v;
if(son!=fa&&!vis[son]){
p=0;
Get_dis(son,root,1,e[i].w);
Calc(-1);
}
}
for(int i=head[root];i;i=e[i].next){
int son=e[i].v;
if(son!=fa&&!vis[son]) Solve(son,root,size[son]);
}
}
int main(){
cin>>n>>L>>R;
for(int i=1;i<n;i++){
int x,y,z;
cin>>x>>y>>z;
Insert(x,y,z);
Insert(y,x,z);
}
Solve(1,0,n);
cout<<ans*2; //答案记得*2,因为路径(x,y)与(y,x)是不同的两条路径
return 0;
}
6. Close Vertices
这道题和第
5
5
5 题大体一样,还是用
F
2
F2
F2 ;
就查询答案的时候不能用二分,用双指针,单调处理。
!!!
C
a
l
c
Calc
Calc 函数中的双指针统计答案需要注意下。
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+5;
int n,L,W,idx,head[maxn],size[maxn],hson_s,root,p,f[maxn];
long long ans;
bool vis[maxn];
struct TREE{ int v,w,next; }e[maxn*2];
struct NODE{ int d,d2; }dis[maxn];
void Insert(int u,int v,int w){
e[++idx].v=v;
e[idx].w=w;
e[idx].next=head[u];
head[u]=idx;
}
void Find_root(int now,int fa,int nodeCnt){
size[now]=1;
int s=0;
for(int i=head[now];i;i=e[i].next){
int son=e[i].v;
if(son!=fa&&!vis[son]){
Find_root(son,now,nodeCnt);
size[now]+=size[son];
s=max(s,size[son]);
}
}
s=max(s,nodeCnt-size[now]);
if(s<hson_s) hson_s=s,root=now;
}
void Get_dis(int now,int fa,int d,int d2){
size[now]=1;
dis[++p]={d,d2};
for(int i=head[now];i;i=e[i].next){
int son=e[i].v;
if(son!=fa&&!vis[son]){
Get_dis(son,now,d+1,d2+e[i].w);
size[now]+=size[son];
}
}
}
bool cmp(NODE a,NODE b){
return a.d2<b.d2;
}
void Add(int x,int y){
x++;
while(x<=n+1){
f[x]+=y;
x+=x&(-x);
}
}
int Ask(int x){
x++;
int s=0;
while(x>0){
s+=f[x];
x-=x&(-x);
}
return s;
}
void Calc(int flag){ //这个函数注意一下就好了
sort(dis+1,dis+p+1,cmp);
for(int i=1;i<=p;i++)
Add(dis[i].d,1);
int r=p;
for(int l=1;l<=p;l++){
Add(dis[l].d,-1);
while(dis[l].d2+dis[r].d2>W&&l<r){
Add(dis[r].d,-1);
r--;
}
if(l==r) break;
ans+=flag*Ask(L-dis[l].d);
}
}
void Solve(int now,int fa,int nodeCnt){
hson_s=nodeCnt-1,root=now;
Find_root(now,fa,nodeCnt);
vis[root]=true;
p=0;
Get_dis(root,0,0,0);
Calc(1);
for(int i=head[root];i;i=e[i].next){
int son=e[i].v;
if(son!=fa&&!vis[son]){
p=0;
Get_dis(son,root,1,e[i].w);
Calc(-1);
}
}
for(int i=head[root];i;i=e[i].next){
int son=e[i].v;
if(son!=fa&&!vis[son]) Solve(son,root,size[son]);
}
}
int main(){
cin>>n>>L>>W;
for(int i=2;i<=n;i++){
int y,z;
cin>>y>>z;
Insert(i,y,z);
Insert(y,i,z);
}
Solve(1,0,n);
cout<<ans;
return 0;
}
7. GCD Counting
还未做,但是这题我已知的有3种解法,可以上洛谷see see。
总结
静态点分治是一个比较难的算法,需要多次练习。
所谓:“温故而知新,可以成才为师矣”!