题意:
给你一棵
n
n
n个点的树,边有边权,求树上长度为
k
k
k的路径至少要经过几条边,如果没有符合条件的路径,输出-1。
n
<
=
2
e
5
,
k
<
=
1
e
6
n<=2e5,k<=1e6
n<=2e5,k<=1e6
题解:
首先感谢DT_Kang的帮助!DT_Kang大佬帮我调了这个题,并且给我指出了我之前的点分治的复杂度都有问题,写的都是假的。原因是,之前在往子树递归的时候,需要去找子树的重心,这里会传入一个size函数,但是你直接传参数的话,原来dfs时的根是上一层的重心,不是这一层的重心,所以size是上一层时的size,而我们会发现,在当前这一层的所有子树内会有之多一个点是在上一层的时候位于当前这个点的父节点,而它在这时的正确size应该是这一层的总size减去x的size,不然的话再去找重心时用一个不对的size可能会找到的不是重心。
这个东西我也不知道只是常数上的优化还是可以被卡得复杂度不对,感觉上如果找重心时传进去的总size大了的话,找到的点应该会离这个子树的根比较近,所以应该是不优的。如果有哪位大佬知道这个东西到底是复杂度不对还是只是常数大,欢迎与我交流。
接下来说一下这个题的做法。
题目本身不是特别难,被许多大佬称为点分入门题、模板题。首先一个常见的点分治的问题是,如何避免来自同一个子树的路径在当前这一层产生的影响。我的写法是一个子树一个子树的做,先把这个子树的路径与之前已经考虑过的子树的路径合并来更新答案,之后再把这个子树内的路径加进去,这样就不会出现两条来自同一子树的路径在这一层更新了答案的情况了。这个题我们就是记录一下从子树到当前分治重心长度为k的路径最少经过几条路径,可以开一个大小为k的桶,注意每次更换分治重心的时候要把桶清空。但是清空的时候需要注意不能直接memset,因为桶的大小是1e6级别的,直接memset会超时,所以我们用与更新答案时相同的方法,dfs每一棵子树,然后把每一条路径的长度对于的最少边数改为inf。更新答案时的合并就是记录一个当前点离分治重心的距离和经过的边数,让用这个和已经有的桶里的答案来更新答案就好了。没有合法的就输出-1。
思路就是这个样子,具体实现可以看看代码。
代码:
#include <bits/stdc++.h>
using namespace std;
int n,k,t[1000010],hed[200010],cnt,ans=1e9,vis[200010];
int sz[200010],mx[200010],rt;
struct node
{
int to,next,dis;
}a[400010];
inline int read()
{
int x=0;
char s=getchar();
while(s>'9'||s<'0')
s=getchar();
while(s>='0'&&s<='9')
{
x=x*10+s-'0';
s=getchar();
}
return x;
}
inline void add(int from,int to,int dis)
{
a[++cnt].to=to;
a[cnt].dis=dis;
a[cnt].next=hed[from];
hed[from]=cnt;
}
inline void getrt(int x,int fa,int size)
{
sz[x]=1;
mx[x]=0;
for(int i=hed[x];i;i=a[i].next)
{
int y=a[i].to;
if(y==fa||vis[y])
continue;
getrt(y,x,size);
sz[x]+=sz[y];
mx[x]=max(mx[x],sz[y]);
}
mx[x]=max(mx[x],size-sz[x]);
if(mx[x]<mx[rt])
rt=x;
}
inline void dfs1(int x,int f,int dis,int shu)
{
if(dis>k)
return;
ans=min(ans,t[k-dis]+shu);
for(int i=hed[x];i;i=a[i].next)
{
int y=a[i].to;
if(y==f||vis[y])
continue;
dfs1(y,x,dis+a[i].dis,shu+1);
}
}
inline void dfs2(int x,int f,int dis,int shu)
{
if(dis>k)
return;
t[dis]=min(t[dis],shu);
for(int i=hed[x];i;i=a[i].next)
{
int y=a[i].to;
if(y==f||vis[y])
continue;
dfs2(y,x,dis+a[i].dis,shu+1);
}
}
inline void dfs3(int x,int f,int dis)//清空
{
if(dis>k)
return;
t[dis]=1e9;
for(int i=hed[x];i;i=a[i].next)
{
int y=a[i].to;
if(y==f||vis[y])
continue;
dfs3(y,x,dis+a[i].dis);
}
}
inline void solve(int x,int size)
{
vis[x]=1;
t[0]=0;
for(int i=hed[x];i;i=a[i].next)
{
int y=a[i].to;
if(vis[y])
continue;
dfs1(y,x,a[i].dis,1);
dfs2(y,x,a[i].dis,1);
}
dfs3(x,0,0);
for(int i=hed[x];i;i=a[i].next)
{
int y=a[i].to;
if(vis[y])
continue;
rt=0;
int ji;
//之前是随便找了一个点当根dfs,所以当前size不一定是以x为根时的size
//这种情况其实只会有一个点的size不对,就是在之前当根时在x父节点的那个点
//那个点的size应该是总的size减去之前dfs时x的size
if(sz[y]<=sz[x])
ji=sz[y];
else
ji=size-sz[x];
getrt(y,0,ji);
solve(rt,ji);
}
}
int main()
{
n=read();
k=read();
for(int i=1;i<=n-1;++i)
{
int x=read(),y=read(),z=read();
++x;
++y;
add(x,y,z);
add(y,x,z);
}
for(int i=0;i<=k;++i)
t[i]=1e9;
mx[0]=2e9;
getrt(1,0,n);
solve(rt,n);
if(ans==1e9)
printf("-1\n");
else
printf("%d\n",ans);
return 0;
}