题意简述
给一个带权值的树(点数<=1e5,边权<=1e9),Q(Q<=1e5)个询问,每次有一个起点 v i v_i vi和长度 k i k_i ki(1<=vi<=n,ki<=1e9),求v点到多少点的路径上最短的那个边权(树上路径是唯一的,所以这个值是确定的)>=k。
数据
输入:
4 3
1 2 3
2 3 2
2 4 4
1 2
4 1
3 1
输出:
3
0
2
样例解释
从2开始到1是3,到3是2,到4是4,3个>=1
从1开始到3是2,到4是3,到2是3,0个>=4,2个>=3
思路
暴力模拟
显然不行
UPD on 2019.4.1(愚人节)
(今天给学妹学弟做这个题,给他按原来的讲,发现没有讲懂。我们写博客的要站在不会这个题的读者角度上多想想,所以我决定改一下写法)。
但是我们保留这个想法,想想如何优化这个暴力。
暴力:
对于询问中的点 v v v,我们需要枚举一个终点 j ( j ! = v ) j(j !=v ) j(j!=v),判断 v v v到 j j j的最短的边是否 > = k >=k >=k,是则统计答案
但是我们仔细想想,发现统计路径很慢,代码也很长(要写LCA),wdnmd这tm是暴力!!!
那么,如何快速(时间少+代码短)的统计i到多少点距离>=k呢?
并查集。
把读入的边离线存下来,对于一个 v v v,先初始化并查集,枚举每一条边,如果这个边的长度>=k,就在并查集上合并这个边连的两个点(代码中是E[i].u和E[i].v)。最后统计点v所在的集合大小-1(为什么-1?因为v自己不能算)
代码短了,但是复杂度依然是O(很大)(实际上是
n
×
q
n\times q
n×q)。
那么,我们如何继续优化这个思路呢?
我们发现,其实不用每次都初始化。设想这样一个场景:如果我们把询问也离线,按某种玄学的东西排一下序,然后我们每次只需要在原来的基础上新加边,而不是每次都初始化然后重新加,那就珂以优化到
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)(如果要排序的话)了。
如何实现这个设想呢?
把边按w(边长)降序排序,把询问按k降序排序,就珂以实现了。
证明(不太严谨,凑合看看):
设询问是Q,有2个成员变量是k和v
对于询问Q[i],如果有一个边的长度>=Q[i-1].k,又因为我们按降序排序,所以一定满足Q[i-1].k>=Q[i].k,根据不等式的传递性我们知道:该边长>=Q[i].k,所以我们就不用重复算了,只要算新加的即可。
这样下来我们的复杂度就是
O
(
n
l
o
g
n
+
q
l
o
g
q
+
n
)
O(nlogn+qlogq+n)
O(nlogn+qlogq+n)
(排序是nlogn+qlogq,处理询问是n)
但是我们要按输入顺序回答询问,所以我们只需要再加两个成员变量id和ans,id保存这是第几个询问,ans表示该询问的答案。最后按id排一下序,顺序输出ans即可。
代码:
#include<bits/stdc++.h>
#define N 1001000
using namespace std;
class DSU
{
private:
int Father[N];
public:
int Cnt[N];
void Init()
{
for(int i=0;i<N;i++)
{
Father[i]=i;
Cnt[i]=1;
}
}
int Find(int x)
{
return x==Father[x]?x:Father[x]=Find(Father[x]);
}
void Merge(int x,int y)
{
int ax=Find(x),ay=Find(y);
if (Cnt[ax]<Cnt[ay])
{
Cnt[ay]+=Cnt[ax];
Father[ax]=ay;
}
else
{
Cnt[ax]+=Cnt[ay];
Father[ay]=ax;
}
}
}D;
struct tmp1
{
int u,v,w;
const bool operator<(const tmp1 CompWith) const
{
return w>CompWith.w;
}
}E[N];
struct tmp2
{
int k,v,id,ans;
const bool operator<(const tmp2 CompWith) const
{
return k>CompWith.k;
}
}Q[N];bool cmp(tmp2 x,tmp2 y) {return x.id<y.id;}
//询问结构体要写两个比较函数,一个按k降序,一个按id升序
int n,q;
void Input()
{
scanf("%d%d",&n,&q);
for(int i=1;i<n;i++)
{
int u,v,w;
if (u>v) u^=v^=u^=v;
scanf("%d%d%d",&u,&v,&w);
E[i]=(tmp1){u,v,w};//离线存储边
}
for(int i=1;i<=q;i++)
{
int k,v;
scanf("%d%d",&k,&v);
Q[i]=(tmp2){k,v,i,0};//离线存储询问
}
}
void Solve()
{
sort(E+1,E+n);
sort(Q+1,Q+q+1);//排序
int j=1;//表示我们当前用到了哪条边,每次只要考虑j后面即可
for(int i=1;i<=q;i++)
{
while(E[j].w>=Q[i].k)//重复加边
{
int u=E[j].u,v=E[j].v;
if (D.Find(u)!=D.Find(v)) D.Merge(u,v);
j++;
}
Q[i].ans=D.Cnt[D.Find(Q[i].v)]-1;//还记得为什么-1么(不记得的emmm。。。)
}
sort(Q+1,Q+q+1,cmp);
for(int i=1;i<=q;i++)
{
printf("%d\n",Q[i].ans);//按输入顺序回答询问
}
}
main()
{
D.Init();
Input();
Solve();
return 0;
}