推荐博客:
这一个讲的比较详细,很适合初学者看。
https://blog.csdn.net/a_forever_dream/article/details/81778649
这一个写的也很好,有时间复杂度的证明。
https://www.cnblogs.com/bztMinamoto/p/9489473.html
点分治:
本文以洛谷P3806作为例题来讲述一下点分治的思想。同时对推荐的第一篇博客中的重点内容进行详细阐述和优化。
点分治常常用来处理树上路径的问题,顾名思义,我们肯定要找到一些点,把树分成几个部分,分别处理。
显然,这个点的选取是非常影响时间复杂度的,点分治选取的点就是树的重心,因而时间复杂度较为优秀。我们先来看一下树的重心的定义:
显然,我们可以在
O
(
n
)
O(n)
O(n)的复杂度内求出树的重心,下面给出代码,看注释应该就懂了。(本文存边方式是前向星 没学过的建议先看一下这个)
为了方便大家理解,先给出加边的代码:
inline void addedge(int u,int v,int d)// u->v 权为d
{
edge[++tot].to=v,edge[tot].nxt=head[u],edge[tot].dis=d,head[u]=tot;
}
下面给出求以 x x x为根的树的重心的代码:
int n,rt,num; //节点个数 树的重心 num用来辅助计算树的重心
void dfs(int u,int fa)
{
siz[u]=1;//以u为重心的子树的节点个数
int MAX=0;
for(int i=head[u];i;i=edge[i].nxt)
{
int v=edge[i].to;
if(v==fa||vis[v])//vis 用来标记某个点是否被处理过了 可以先不管这个
continue;
dfs(v,u); //dfs子树
siz[u]+=siz[v]; //修改以u为根的树的节点个数
MAX=max(MAX,siz[v]);//取子树节点最大值
}
MAX=max(MAX,n-siz[u]);//删去节点u后 树分成几个连通块
if(MAX<num) //n-siz[u]表示除去u的子树外的那个连通块的节点个数
rt=u,num=MAX;
}
int getrt(int x)//得到以x为根的树的重心
{
rt=0,num=INF;
dfs(x,0);
return rt;
}
MAX=max(MAX,n-siz[u]);
这行代码不是很理解的可以看下图:
树的重心问题(分)解决了,现在考虑一下怎么计算路径权值(治),很容易想到用一个
d
i
s
dis
dis数组来记录某个节点到根节点路径的值,然后将其两两组合就可以得到任意两个节点之间的路径的值,然后就可以统计答案辣!不过还有一个小问题,我们看一下下图:
假设
1
1
1是重心,每条边的权值都为
1
1
1,那么有
d
i
s
[
1
]
=
0
,
d
i
s
[
2
]
=
1
,
d
i
s
[
3
]
=
2
,
dis[1]=0,dis[2]=1,dis[3]=2,
dis[1]=0,dis[2]=1,dis[3]=2,
d
i
s
[
4
]
=
2
,
d
i
s
[
5
]
=
1
dis[4]=2,dis[5]=1
dis[4]=2,dis[5]=1。我们将其两两组合计算距离,不难发现,
1
、
2
、
4
1、2、4
1、2、4到
5
5
5的距离是正确的,但是
3
3
3到
4
4
4的距离是错误的,因为
1
−
2
1-2
1−2这条边被被算了两次,这个问题怎么解决呢?容斥原理(个人感觉是点分治中比较重要的一个地方)。我们在以
1
1
1为根的树的计算过程中,统计了所有的情况,但是某些情况是不合理的,比如上例中假设
k
=
4
k=4
k=4,那么实际答案应该是
0
0
0,但是由于
d
i
s
[
3
]
+
d
i
s
[
4
]
=
4
dis[3]+dis[4]=4
dis[3]+dis[4]=4被错误的统计了,最终得到的答案是
1
1
1,为了让答案正确,我们需要消除这些不合理的情况,即合理情况=所有情况-不合理情况。
容斥原理大概指的是这个意思:
(
t
i
p
s
:
tips:
tips:虽然该题求的是
=
k
=k
=k的数量,下面这个求的是
<
=
k
<=k
<=k的数量,但是这种容斥的思想是一致的)
下面给出相关代码。
首先是求
d
i
s
dis
dis数组的代码:
void getdis(int u,int fa)//求dis数组 非常简单 不多解释了
{
b[++b[0]]=dis[u];//这里要存储路径的权 计算贡献要用到
for(int i=head[u];i;i=edge[i].nxt)
{
int v=edge[i].to;
if(v==fa||vis[v])
continue;
dis[v]=dis[u]+edge[i].dis;
getdis(v,u);
}
}
下面是治过程的代码:
函数
c
a
l
cal
cal不理解没关系,后面会解释。
void work(int u)//统计以u为根的子树的结果
{
vis[u]=1; //标记 u 已经计算过了
re+=cal(u,0); //统计答案
for(int i=head[u];i;i=edge[i].nxt)
{
int v=edge[i].to;
if(vis[v])
continue;
re-=cal(v,edge[i].dis); //减去多余的贡献
tree.n=siz[v]; //修改树的节点数目
work(tree.getrt(v)); //得到子树v的重心 然后分治 v
}
}
ll cal(int u,int val)//计算u的子树的贡献 dis[u]=val
{
dis[u]=val,b[0]=0;
getdis(u,0); //得到dis数组
sort(b+1,b+1+b[0]);//从小到大排序
len=1,a[1].val=b[1],a[1].num=1;
for(int i=2;i<=b[0];i++) //去重 统计个数
{
if(b[i]==a[len].val)
++a[len].num;
else
a[++len].val=b[i],a[len].num=1;
}
int l=1,r=len;
ll ans=0;
while(l<=r) // two-pointer
{
if(a[l].val+a[r].val==k)
{
if(l==r)
{
ans+=(ll)a[l].num*(a[l].num-1)/2;
return ans;
}
ans+=(ll)a[l].num*a[r].num;
++l,--r;
}
else if(a[l].val+a[r].val>k)
--r;
else
++l;
}
return ans;
}
我们来思考一下,为什么通过下面这行代码就可以减去多余的贡献:
re-=cal(v,edge[i].dis);
大家不妨回到刚才的例子中看一下:
其实路径计算错误的节点都是
2
2
2的子树中的节点,而错误的原因就是因为
1
−
2
1-2
1−2的权值被算了进去,更一般的,在统计以
u
u
u为根的子树的答案时,对于其子节点
v
v
v的子树的节点,我们错误的计算了贡献,因为
u
−
v
u-v
u−v的权值被算了进去,在
w
o
r
k
work
work函数中,统计子树
u
u
u的贡献时,调用了
c
a
l
(
u
,
0
)
cal(u,0)
cal(u,0),因而
d
i
s
[
u
]
=
0
dis[u]=0
dis[u]=0,减去子树
v
v
v的贡献时,调用了
c
a
l
(
v
,
e
d
g
e
[
i
]
.
d
i
s
)
cal(v,edge[i].dis)
cal(v,edge[i].dis),因而
d
i
s
[
v
]
=
e
d
g
e
[
i
]
.
d
i
s
dis[v]=edge[i].dis
dis[v]=edge[i].dis,这个
e
d
g
e
[
i
]
.
d
i
s
edge[i].dis
edge[i].dis其实就是
u
−
v
u-v
u−v的边权,所以这个调用返回的结果实际上是满足
d
i
s
[
x
]
+
d
i
s
[
y
]
=
k
+
2
∗
e
d
g
e
[
i
]
.
d
i
s
dis[x]+dis[y]=k+2*edge[i].dis
dis[x]+dis[y]=k+2∗edge[i].dis的节点对
(
x
,
y
)
(x,y)
(x,y)的数目(其中
x
、
y
x、y
x、y均是
v
v
v的子节点),而这一部分恰恰就是统计子树
u
u
u的答案时多计算的那一部分!所以我们得到的结果是正确的~下面给出源代码,
t
w
o
−
p
o
i
n
t
e
r
two-pointer
two−pointer就不多赘述了,很简单的一个小技巧。
#include<bits/stdc++.h>
#define INF 0x3f3f3f3f
using namespace std;
typedef long long ll;
const int maxn=1e4+5;
struct Edge
{
int to,nxt,dis;
}edge[maxn<<1];
struct node
{
int val,num;
}a[maxn];
ll re; //记录答案
int n,m,k,tot,len;
int head[maxn],dis[maxn],siz[maxn],vis[maxn],b[maxn];
inline void addedge(int u,int v,int d)
{
edge[++tot].to=v,edge[tot].nxt=head[u],edge[tot].dis=d,head[u]=tot;
}
struct Tree
{
int n,rt,num; //节点个数 树的重心 num用来辅助计算树的重心
void dfs(int u,int fa)
{
siz[u]=1;//以u为重心的子树的节点个数
int MAX=0;
for(int i=head[u];i;i=edge[i].nxt)
{
int v=edge[i].to;
if(v==fa||vis[v])
continue;
dfs(v,u);
siz[u]+=siz[v];
MAX=max(MAX,siz[v]);
}
MAX=max(MAX,n-siz[u]);
if(MAX<num)
rt=u,num=MAX;
}
int getrt(int x)//得到以x为根的树的重心
{
rt=0,num=INF;
dfs(x,0);
return rt;
}
}tree;
void getdis(int u,int fa)//求dis数组
{
b[++b[0]]=dis[u];
for(int i=head[u];i;i=edge[i].nxt)
{
int v=edge[i].to;
if(v==fa||vis[v])
continue;
dis[v]=dis[u]+edge[i].dis;
getdis(v,u);
}
}
ll cal(int u,int val)
{
dis[u]=val,b[0]=0;
getdis(u,0);
sort(b+1,b+1+b[0]);
len=1,a[1].val=b[1],a[1].num=1;
for(int i=2;i<=b[0];i++)
{
if(b[i]==a[len].val)
++a[len].num;
else
a[++len].val=b[i],a[len].num=1;
}
int l=1,r=len;
ll ans=0;
while(l<=r) // two-pointer
{
if(a[l].val+a[r].val==k)
{
if(l==r)
{
ans+=(ll)a[l].num*(a[l].num-1)/2;
return ans;
}
ans+=(ll)a[l].num*a[r].num;
++l,--r;
}
else if(a[l].val+a[r].val>k)
--r;
else
++l;
}
return ans;
}
void work(int u)
{
vis[u]=1; //标记 u 已经计算过了
re+=cal(u,0);
for(int i=head[u];i;i=edge[i].nxt)
{
int v=edge[i].to;
if(vis[v])
continue;
re-=cal(v,edge[i].dis); //减去多余的贡献
tree.n=siz[v];
work(tree.getrt(v)); //分治 v
}
}
int main()
{
while(~scanf("%d %d",&n,&m))
{
tot=0;
memset(head,0,sizeof(head));
int u,v,w;
for(int i=1;i<n;i++)
{
scanf("%d %d %d",&u,&v,&w);
addedge(u,v,w),addedge(v,u,w);
}
for(int i=0;i<m;i++)
{
scanf("%d",&k);
memset(vis,0,sizeof(vis));
re=0,tree.n=n;
work(tree.getrt(1));
if(re)
printf("AYE\n");
else
printf("NAY\n");
}
}
return 0;
}