题目
题解
记每个旅馆为 (rest_i,1 le i le r)。
奶牛从 (a) 到 (b) 的路径可以分为两种:一种是直接 (a->b);另一种是中间经过若干((c) 间)旅馆 (a -> rest_{s_1}->...-> rest_{s_c} -> b)。其中一个点到另一个点的路径长度不超过 (k)。
换言之如果 (rest_i) 可以到 (rest_j),且 (a) 可以到 (rest_i),(b) 可以到 (rest_j)(都不超过 (k) 步),那么 (a) 可以到 (b)。
这启发我们如果两间旅馆 (rest_i) 可以到 (rest_j),那么我们可以把这两间旅馆合并。最后,每个“旅馆连通块”内的旅馆可以互相通。
但是这个算法的瓶颈在于,我们每次要搜索分别从 (a,b) 出发蓑鲉可到达“旅馆连通块”集合 (A,B),再判断是否有相同的“旅馆联通块”(即 (A cap B ot = emptyset))。
考虑用贪心来优化这个算法。因为如果最后的路径是 (a -> rest_{s_1}->...-> rest_{s_c} -> b),我们直接从 (a) 向 (b) 的方向走 (k) 步到 (go_a),(b) 向 (a) 的方向走 (k) 步到 (go_b),看 (go_a) 和 (go_b) 是否在同一个“旅馆连通块”中。很容易可以发现这样贪心是错误的,因为走了 (k) 步之后还不一定可以走到恰好是旅馆的点。因此可能会把一些可行的路径判为不可行(不可行的路径一定会判为不可行)。
我们来分析一下这样贪心为什么是错误的:
首先做一步转化,我们可以假设奶牛走 (x) 步就要休息,这样的话对于每个旅馆( (rest_i,1 le i le r)),向外面走 (k-x) 步的地方都可以称为“旅馆”,我们把 (rest_i) 为中心,向外辐射 (k-x) 步的“旅馆”集合记做 (S_i)。原来的 “(rest_i) 在 (k) 步内可以到 (rest_j)” 就可以描述为 “(S_i) 中的某个点在 (2x-k) 步内可以到 (S_j) 中的某个点”(简记为 (S_i) 可以到 (S_j))
((2x-k) 可以结合 (S_i) 的意义推出,具体的是由 (k-2(k-x)) 得到的。由此可以发现 (x) 有意义的范围是 (frac{k}{2} le x le k)。)
贪心错误的原因在于,(S_i) 可以到 (S_j),中间经过了一段长度 (le 2x-k) 的不是旅馆的路径。 我们贪心 (a) 直接往 (b) 走 (x) 步(注意我们在讨论转化后,已经不是走 (k) 步) ,最后可能落在这段 长度 (le 2x-k) 的不是旅馆的路径上。
这启发我们令 (2x-k=0),即 (x = frac{k}{2}),则贪心 (a) 直接往 (b) 走 (x) 步后,不会落在不是旅馆的路径上。这样,我们就可以用贪心优化掉这个算法的瓶颈。
我们可以在每条边中间加一个点,这样原条件就变为“奶牛走 (2k) 步就要休息”,于是解决了 (k) 不为偶数的问题。
合并旅馆联通块的过程,我们可以以 (rest_i,1 le i le r) 为起点同时出发跑 (k) 步,将可达的旅馆联通块合并。
时间复杂度 (O((n+q) log n))。
代码
Talk is cheap.Show me the code.
#include<bits/stdc++.h>
#define mp make_pair
#define fi first
#define se second
using namespace std;
inline int read() {
int x = 0, f = 1; char ch = getchar();
while(ch<'0' || ch>'9') { if(ch=='-') f=-1; ch=getchar(); }
while(ch>='0'&&ch<='9') { x=(x<<3)+(x<<1)+(ch^48); ch=getchar(); }
return x * f;
}
typedef pair<int,int> PII;
const int N = 4e5+7;
int n,k,r,cnt,tot;
int head[N],rest[N];
bool fg[N];
struct Edge {
int next,to;
}edge[N<<1];
inline void add(int u,int v) {
edge[++cnt] = (Edge)<%head[u],v%>;
head[u] = cnt;
}
namespace UF {
int fa[N];
void Init(int n) {
for(int i=1;i<=n;++i) fa[i] = i;
}
int Find(int x) {
return (x==fa[x] ? x : fa[x]=Find(fa[x]));
}
void Join(int x,int y) {
int fx = Find(x), fy = Find(y);
if(fx != fy) {
fa[fx] = fy;
}
}
}
bool vis[N];
void Bfs() {
queue<PII> q;
for(int i=1;i<=r;++i) {
q.push(mp(rest[i],0)); vis[rest[i]] = 1;
}
while(!q.empty()) {
int u = q.front().fi, step = q.front().se;
q.pop();
if(step >= k) break;
for(int i=head[u];i;i=edge[i].next) {
int v = edge[i].to;
UF::Join(u,v);
if(!vis[v]) {
vis[v] = 1;
q.push(mp(v,step+1));
}
}
}
}
const int lgN = 20;
int fa[N][lgN],dep[N],lg[N];
void Init() {
for(int i=2;i<N;++i) lg[i] = lg[i>>1] + 1;
}
void Dfs1(int u,int fath) {
fa[u][0] = fath; dep[u] = dep[fath] + 1;
for(int i=1;i<=lg[dep[u]];++i)
fa[u][i] = fa[fa[u][i-1]][i-1];
for(int i=head[u];i;i=edge[i].next) {
int v = edge[i].to;
if(v != fath) {
Dfs1(v,u);
}
}
}
int LCA(int x,int y) {
if(dep[x] < dep[y]) swap(x,y);
while(dep[x] > dep[y]) x = fa[x][lg[dep[x]-dep[y]]];
if(x == y) return x;
for(int i=lg[dep[x]];i>=0;--i)
if(fa[x][i] != fa[y][i]) x = fa[x][i], y = fa[y][i];
return fa[x][0];
}
int walk(int x,int y,int lca) {
int res = -1, now = -1;
if(k <= dep[x]-dep[lca]) {
res = x;
now = k;
} else {
res = y;
now = (dep[y]-dep[lca]) - (k-(dep[x]-dep[lca]));
}
int up = lg[now];
for(int i=up;i>=0;--i) {
if(now >= (1<<i)) {
res = fa[res][i];
now -= (1<<i);
}
}
return res;
}
int main()
{
n = read(), k = read(), r = read();
tot = n;
for(int i=1;i<n;++i) {
int u = read(), v = read();
++tot;
add(u,tot), add(tot,u);
add(v,tot), add(tot,v);
}
for(int i=1;i<=r;++i) {
int x = read();
fg[x] = 1;
rest[i] = x;
}
UF::Init(tot);
Bfs();
Init();
Dfs1(1,0);
int Q = read();
while(Q--) {
int x = read(), y = read();
int lca = LCA(x,y);
int gox = walk(x,y,lca), goy = walk(y,x,lca);
if((dep[x]+dep[y]-2*dep[lca]<=2*k) || (UF::Find(gox)==UF::Find(goy))) puts("YES");
else puts("NO");
}
return 0;
}
/*
8 3 3
1 2
2 3
3 4
4 5
4 6
6 7
7 8
2 5 8
2
7 1
8 1
YES
NO
*/
总结
这个性质非常强,用 (frac{k}{2}) 作为奶牛可以走的距离,即方便了旅馆间的合并,又方便了贪心的正确!