题目描述
小蓝准备去星际旅行,出发前想在本星系采购一些零食,星系内有 n 颗星球,由 n − 1 条航路连接为连通图,第 i 颗星球卖第 ci 种零食特产。小蓝想出了 q 个采购方案,第 i 个方案的起点为星球 si ,终点为星球 ti ,对于每种采购方案,小蓝将从起点走最短的航路到终点,并且可以购买所有经过的星球上的零食(包括起点终点),请计算每种采购方案最多能买多少种不同的零食。
输入格式
输入的第一行包含两个正整数 n, q ,用一个空格分隔。
第二行包含 n 个整数 c1, c2, · · · , cn ,相邻整数之间使用一个空格分隔。
接下来 n − 1 行,第 i 行包含两个整数 ui, vi ,用一个空格分隔,表示一条航路将星球 ui 与 vi 相连。接下来 q 行,第 i 行包含两个整数 si, ti ,用一个空格分隔,表示一个采购方案。
输出格式
输出 q 行,每行包含一个整数,依次表示每个采购方案的答案。
样例输入
4 2
1 2 3 1
1 2
1 3
2 4
4 3
1 4
样例输出
3
2
提示
【样例说明】
第一个方案路线为 {4, 2, 1, 3} ,可以买到第 1, 2, 3 种零食;第二个方案路线为 {1, 2, 4} ,可以买到第 1, 2 种零食。
【评测用例规模与约定】
对于 20% 的评测用例,1 ≤ n, q ≤ 5000 ;对于所有评测用例,1 ≤ n, q ≤ 10^5,1 ≤ ci ≤ 20,1 ≤ ui, vi ≤ n,1 ≤ si, ti ≤ n。
整体思路
看到了树与路径的题目,可以考虑倍增 LCA 算法,零食的种类数用二进制维护,每一位表示一种不同的零食,用或运算模拟两段路径的合并,最终的零食种类数就是二进制中含有 1 的个数。
#include <bits/stdc++.h>
#define endl '\n'
using namespace std;
const int N = 1e5 + 5;
int num[N]; // 零食编号
vector<int> edge[N];
int dep[N], fa[N][25][2], lg[N];
// fa[i][j][k]中 k 为 0 表示 2^j 级祖先结点编号,为 1 表示该路径上的零食种类的二进制
void dfs(int u, int father)
{
dep[u] = dep[father] + 1;
fa[u][0][0] = father;
fa[u][0][1] = num[father];
for(int i = 1; i <= lg[dep[u]]; i++)
{
fa[u][i][0] = fa[fa[u][i - 1][0]][i - 1][0];
fa[u][i][1] = fa[fa[u][i - 1][0]][i - 1][1] | fa[u][i - 1][1];
}
for(int v : edge[u])
{
if(v != father)
{
dfs(v, u);
}
}
}
int cnt(int x)
{
int r = 0;
while(x)
{
if(x & 1)
{
r++;
}
x >>= 1;
}
return r;
}
int lca(int u, int v)
{
int tmp = num[u] | num[v]; // 起点和终点在深搜的过程中没有被统计,需要手动添加
if(dep[u] < dep[v])
{
swap(u, v);
}
for(int i = lg[dep[u] - dep[v]] - 1; i >= 0; i--)
{
if(dep[fa[u][i][0]] >= dep[v])
{
tmp |= fa[u][i][1];
u = fa[u][i][0];
}
}
if(u == v)
{
return cnt(tmp);
}
for(int i = lg[dep[u]] - 1; i >= 0; i--)
{
if(fa[u][i][0] != fa[v][i][0])
{
tmp |= fa[u][i][1];
tmp |= fa[v][i][1];
u = fa[u][i][0];
v = fa[v][i][0];
}
}
tmp |= fa[u][0][1];
return cnt(tmp);
}
int main()
{
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n, q;
cin >> n >> q;
for(int i = 1; i <= n; i++)
{
int c;
cin >> c;
num[i] = 1 << (c - 1);
}
for(int i = 0; i < n - 1; i++)
{
int u, v;
cin >> u >> v;
edge[u].push_back(v);
edge[v].push_back(u);
}
for(int i = 1; i <= n; i++)
{
lg[i] = lg[i - 1] + ((1 << lg[i - 1]) == i);
}
dfs(1, 0);
for(int i = 0; i < q; i++)
{
int s, t;
cin >> s >> t;
cout << lca(s, t) << endl;
}
return 0;
}
具体步骤
1. 从起点走最短的航路到终点的问题,可以等效为从起点通过两个结点的最近公共祖先到达终点。
2. 从一个结点到其 2^k 级祖先结点的路径上的零食种类,可以分解为以下两段:
-
从该结点的 2^(k−1) 级祖先结点到 2^k 级祖先结点的路径上的零食种类。
-
从该结点到该结点的 2^(k−1) 级祖先结点的路径上的零食种类。
3. 在寻找公共祖先的同时,利用或运算的特性记录跳跃路径上的零食种类,最终返回零食种类计数器 tmp 二进制中 1 的个数。