一、朴素做法:图的DFS
#include <iostream>
#include <vector>
#include <set>
using namespace std;
vector<vector<int>> adj; // 邻接表表示树
vector<int> c; // 每个星球的零食种类
vector<int> path; // 存储从起点到终点的路径
bool found = false; // 标记是否找到路径
// DFS查找从u到v的路径
void dfs(int u, int v, vector<int>& currentPath, vector<bool>& visited) {
visited[u] = true;
currentPath.push_back(u);
if (u == v) {
path = currentPath; // 找到路径
found = true;
return;
}
for (int neighbor : adj[u]) {
if (!visited[neighbor]) {
dfs(neighbor, v, currentPath, visited);
if (found) return; // 如果找到路径,直接返回
}
}
currentPath.pop_back(); // 回溯。由于是树,vis可以不用回溯
}
int main() {
int n, q;
cin >> n >> q;
adj.resize(n + 1);
c.resize(n + 1);
// 输入每个星球的零食种类
for (int i = 1; i <= n; ++i) {
cin >> c[i];
}
// 输入n-1条边,构建树
for (int i = 1; i < n; ++i) {
int u, v;
cin >> u >> v;
adj[u].push_back(v);
adj[v].push_back(u);
}
// 处理每个查询
for (int i = 0; i < q; ++i) {
int s, t;
cin >> s >> t;
// 初始化
path.clear();
found = false;
vector<bool> visited(n + 1, false);
vector<int> currentPath;
// 查找从s到t的路径
dfs(s, t, currentPath, visited);
// 统计路径上的零食种类
set<int> kinds;
for (int node : path) {
kinds.insert(c[node]);
}
// 输出结果
cout << kinds.size() << endl;
}
return 0;
}
可通过20%,其他的超时。
二、更优解:LCA
1.LCA基本模版
LCA,即最近公共祖先。参考:https://www.bilibili.com/video/BV1N7411G7JD/?spm_id_from=333.337.search-card.all.click&vd_source=0252428a85afe838b167a93feafd7c42
对于两个点,找他们的最近公共祖先有两种情况:
- 让深度较大的跳到与深度小的同一深度,如果已重合,重合点即为LCA,结束
- 再让它们同时尽可能向上跳,跳到同一深度,但不能重合,跳后所在两点的父亲即为LCA
那么我们如何实现“尽可能向上跳但不能重合”?——我们可以先试探最大的跳跃高度,然后慢慢缩小。实质上,是把跳的高度表示成2的次方的和的形式(与快速幂、ST表的思想类似)。为了实现快速跳跃,我们需要准备2个东西:
- 各点的深度
- 跳跃表
跳跃表的递推式:
i表示目前所在的点,j表示跳跃的高度的log2,表格的值记作f[i][j]表示跳后到达的点。
这两个东西可以通过一次dfs得到:
void dfs(int v,int u) {//u是v的父节点
depth[v] = depth[u] + 1;
f[v][0] = u;
for (int j = 1; (1 << j) < depth[v]; j++) {//1 << j相当于2的j次方
f[v][j] = f[f[v][j - 1]][j - 1];
}
for (auto it : g[v]) {
if (it == u) continue;
f[it][0] = v;
dfs(it, v);
}
}
准备好后就可以上LCA
int LCA(int x,int y) {
//x赋成深度大的点
if (depth[x] < depth[y]) swap(x, y);
//x跳到y的同一深度
for (int i = 20; i >= 0; i--) {//题中所给的n对应的最大深度是20
if (depth[f[x][i]] <= depth[y]) x = f[x][i];
}
if (x == y) return x;
//一起跳
for (int i = 20; i >= 0; i--) {
if (f[x][i] != f[y][i])
x = f[x][i], y = f[y][i];
}
return f[x][0];
}
2.详细解法
cnt[x][i] 表示从根节点到节点x的路径上零食种类i的数量,注意题干规定:i的范围是1到20。如果找到了起点、终点的lca,我们就可以统计起点a到终点b的路径上每一种零食的数量:
而cnt在DFS时就可得到:每个点的各种类的零食数都与其父节点的相同,除了它拥有的那一种要+1。
完整题解:
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
vector<int> g[N]; // 邻接表表示树
int c[N], depth[N], f[N][21], cnt[N][21];//cnt[x][i] 表示从根节点到节点x的路径上零食种类i的数量,注意题干规定:i的范围是1到20
void dfs(int v,int u) {//u是v的父节点
depth[v] = depth[u] + 1;
f[v][0] = u;
for (int i = 1; i <= 20; i++) {
cnt[v][i] = cnt[u][i];
}
cnt[v][c[v]]++;
for (int j = 1; (1 << j) < depth[v]; j++) {//1 << j相当于2的j次方
f[v][j] = f[f[v][j - 1]][j - 1];
}
for (auto it : g[v]) {
if (it == u) continue;
dfs(it, v);
}
}
int LCA(int x,int y) {
//x赋成深度大的点
if (depth[x] < depth[y]) swap(x, y);
//x跳到y的同一深度
for (int i = 20; i >= 0; i--) {
if (depth[f[x][i]] >= depth[y]) x = f[x][i];
}
if (x == y) return x;
//一起跳
for (int i = 20; i >= 0; i--) {
if (f[x][i] != f[y][i])
x = f[x][i], y = f[y][i];
}
return f[x][0];
}
int main() {
int n, q;
cin >> n >> q;
// 输入每个星球的零食种类
for (int i = 1; i <= n; ++i) {
cin >> c[i];
}
// 输入n-1条边,构建树
for (int i = 1; i < n; ++i) {
int u, v;
cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u);
}
dfs(1, 0);
int a, b;
while (q--) {
int ans = 0;
cin >> a >> b;
int lca = LCA(a, b);
for (int i = 1; i <= 20; i++) {
int p = cnt[a][i] + cnt[b][i] - cnt[lca][i] - cnt[f[lca][0]][i];
if (p) ans++;
}
cout << ans << endl;
}
return 0;
}