目录
一、题目
二、思路
1. 问题转化:同步DFS走树
2. 优化:同步DFS匹配
3. 状态设计:dfs参数含义
4. 匹配过程:用 map 建立权值索引
5. 终止条件:无法匹配则更新答案
6. 总结
三、完整代码
四、知识点总结
1. 邻接表建树
2. DFS模板(树)
3. map统计映射
五、优化建议
一、题目
题目链接:蓝桥杯2024年第十五届省赛真题-团建 - C语言网
标签:树、DFS、映射、最长公共前缀
【题目抽象过来就是】:
-
两棵树分别从根结点1出发
-
每棵树走一条路径,从根走到叶子
-
只要路径上对应位置的权值一致,前缀继续,直到不一致
-
求任意一对路径中最长的公共前缀长度
【等价描述】:
从树1的结点i和树2的结点j同时出发,若其子节点中存在相同权值的点,则同步走向下一个匹配点,继续搜索;否则终止,更新答案
二、思路
本质上就是是树上路径问题,目标是找出两棵树中一对路径,其权值前缀最长,且两个路径需从根走到某个叶子
1. 问题转化:同步DFS走树
我们将这个“团建”问题转化为更形式化的问题:
-
设第一棵树为 T1,第二棵树为 T2
-
从T1的根结点(编号 1)出发,走到任意叶节点形成一个权值路径 P1
-
从T2 的根结点(编号 1)出发,走到任意叶节点形成另一个权值路径 P2
-
目标是找出一对路径 P1, P2,使它们的最长公共前缀(权值完全相同)长度最大
这个问题很容易想到暴力做法,枚举所有从根到叶的路径组合,比较公共前缀,但这会非常低效,因为路径组合的数量是指数级的
2. 优化:同步DFS匹配
我们注意到只要两棵树当前所在的节点权值一致,就可以继续尝试向下匹配,于是我们可以设计一个双树同步DFS的过程:
-
从两棵树的根结点出发(必须权值相同才开始);
-
进入递归函数
dfs(i, j, pi, pj, cnt)
,表示当前在第一棵树的结点i
,第二棵树的结点j
,pi
、pj
是其父节点,用于避免走回头路; -
当前的公共前缀长度为
cnt
; -
然后尝试“配对子节点”:
-
对
i
的所有子节点(除了父节点)建立map<int,int>
表(key=权值,value=结点编号); -
遍历
j
的所有子节点(同样跳过父节点),如果它的权值在上面的 map 中出现,说明两个子节点具有相同的权值; -
则递归调用
dfs(新i, 新j, i, j, cnt + 1)
,继续向下探索;
-
-
若当前无法继续匹配,说明一条公共前缀路径终止,更新
ans = max(ans, cnt)
这其实相当于构造了一个“公共路径树”:每次 DFS 都在尝试走向匹配路径的更深层
3. 状态设计:dfs参数含义
dfs(i, j, pi, pj, cnt)
-
i
,j
:当前分别在两棵树的哪个结点 -
pi
,pj
:各自结点的父结点,用于防止重复访问(因为树是无向图) -
cnt
:当前公共前缀的长度,也就是成功“匹配”的层数
每次进入 dfs,相当于说:“我已经找到了
cnt
层的共同路径,现在看看是否能进入下一层”
4. 匹配过程:用 map
建立权值索引
由于要找到第二棵树某个子节点的权值是否和第一棵树子节点的权值相同,为了快速判断和定位,我们用 map<int, int>
来做映射
map<int, int> m;
for (int newi : edge1[i]) {
if (newi == pi) continue; //避免回头
m[c[newi]] = newi; //权值 → 节点编号
}
然后对于 j
的所有子节点,判断它们的权值是否在 m
中出现:
for (int newj : edge2[j]) {
if (newj == pj) continue;
if (m.count(d[newj])) {
//匹配成功,进入下一层
dfs(m[d[newj]], newj, i, j, cnt + 1);
}
}
这种方式效率高,而且灵活地实现了“同步匹配”的过程
5. 终止条件:无法匹配则更新答案
一旦某一层匹配失败(即 j
的子节点权值无法在 i
的子节点中找到对应值),这条同步路径就结束了,此时需要更新全局最大值:
ans = max(ans, cnt);
这句代码放在 DFS 末尾,保证所有路径尝试都会记录最长前缀
6. 总结
-
我们不需要预先构造所有路径
-
只需从根节点出发,通过匹配下一层的子节点权值,构造公共前缀路径(相当于剪枝)
-
整个过程由 DFS 驱动,使用
map
或unordered_map
快速匹配子结点 -
最终输出最长的前缀长度
ans
三、完整代码
#include <iostream>
#include <vector>
#include <map>
using namespace std;
const int N=2e5+10;
int n,m;
int c[N];//第一棵树的权值
int d[N];//第二棵树的权值
vector<vector<int>> edge1(N);//第一棵树邻接表,大小为N,存储的类型为vector
vector<vector<int>> edge2(N);//第二棵树
int ans=0;
//i:第一棵树的当前节点
//j:第二棵树的当前节点
//pi:当前结点的父结点
//pj:同理
//cnt:当前前缀长度
void dfs(int i,int j,int pi,int pj,int cnt)
{
//存储第一棵树的相邻节点权值
//first:权值,second:出现的次数
//map可以用来统计出现的次数
map<int,int>m;
for(int newi:edge1[i])
{
//避免回头走,父结点跳过
if(newi==pi) continue;
//记录权值对应位置
m[c[newi]]=newi;
}
//找第二棵树
for(int newj:edge2[j])
{
if(newj==pj) continue;
//碰到相同权值点,继续向下走
//count统计的是次数
if(m.count(d[newj]))
{
dfs(m[d[newj]],newj,i,j,cnt+1);
}
}
//当前结点找不到相同权值,结束
//更新答案
ans=max(ans,cnt);
return;
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>c[i];
for(int i=1;i<=m;i++) cin>>d[i];
for(int i=1;i<=n-1;i++)
{
int x,y;cin>>x>>y;
edge1[x].push_back(y);
edge1[y].push_back(x);
}
for(int i=1;i<=m-1;i++)
{
int x,y;cin>>x>>y;
edge2[x].push_back(y);
edge2[y].push_back(x);
}
//根节点相同才能进入dfs
if(c[1]==d[1]) dfs(1,1,-1,-1,1);
cout<<ans;
return 0;
}
四、知识点总结
1. 邻接表建树
vector<vector<int>> tree(N);
tree[u].push_back(v);
tree[v].push_back(u); //因为是无向图建树
2. DFS模板(树)
void dfs(int u, int parent) {
for (int v : tree[u]) {
if (v == parent) continue;
dfs(v, u);
}
}
3. map统计映射
map<int, int> m;
m[val] = index; //记录权值和对应结点编号
五、优化建议
-
如果运行时常数过大,可以用
unordered_map
替代map
加快哈希效率 -
若题目限制非常紧,也可以采用前缀哈希或 Trie 树进行路径存储优化
如果你觉得有收获,欢迎点赞收藏支持!
后续将持续更新算法题解,也欢迎留言交流~