D. Valid BFS? - 题解
比赛与标签
比赛: Manthan, Codefest 18 (rated, Div. 1 + Div. 2) 标签: dfs and similar, graphs, shortest paths, trees 难度: *1700
题目大意喵~
主人你好呀~ 这道题是关于图论的呢!(ฅ'ω'ฅ)
题目给了我们一棵有 n
个节点的树,还有一串长度为 n
的节点序列 a
。我们的任务是判断,这串序列 a
有没有可能是一次从节点 1
开始的广度优先搜索(BFS)的遍历结果呐。
BFS 的规则是:
- 从队列里拿出队首的节点
v
。 - 把它记录到遍历序列里。
- 以任意顺序,把
v
的所有还没访问过的邻居节点,加入到队列的末尾。
因为第三步的顺序是任意的,所以从同一个节点开始的 BFS 可能会产生很多种不同的遍历序列。我们就是要检查给定的序列 a
是不是其中一种合法的可能~
解题思路,喵! (My Purr-fect Strategy!)
要解决这个问题,我们首先要抓住 BFS 的核心性质!BFS 最重要的特点就是分层遍历,的说。
想象一下,从节点 1 出发,所有和 1直接相连的节点(深度为1)一定会在节点 1 之后、并且在深度为 2 的任何节点之前被访问。这是一个雷打不动的规矩,喵!
更进一步地,对于同一个父节点 u
,它的所有子节点(在 BFS 树中,也就是 u
的邻居且深度比 u
大 1 的节点)在遍历序列中,不一定紧跟在 u
后面,但它们一定会被打包在一起,形成一个连续的区块。这个区块内部的顺序可以是任意的,但这个区块整体一定出现在更深层节点的区块之前。
举个栗子!(๑•̀ω•́)و✧ 假设节点 u
和 v
都是同一层的,u
在 v
之前被访问。那么在遍历序列中,会先出现 u
,然后是 u
的所有孩子们(顺序任意),然后是 v
,然后是 v
的所有孩子们(顺序任意)。
所以,我们的策略就清晰啦:
确定基准结构:我们先自己从节点 1 跑一次标准的 BFS,目的是确定每个节点的深度(depth)。这个深度是唯一的,任何合法的 BFS 遍历都必须严格遵守这个深度顺序。
逐层验证序列
a
:现在我们来检查给定的序列a
是不是遵守了这个结构。我们可以用两个指针(或者叫游标)来模拟这个过程:parent_cursor
:指向序列a
中当前正在处理的父节点。child_cursor
:指向序列a
中,当前父节点的孩子们应该开始的位置。
我们从
parent_cursor = 0
开始(也就是节点a[0]
,必须是 1 哦!)。设当前父节点是u = a[parent_cursor]
。- 首先,我们在原图中找到
u
的所有“真正的”孩子。哪些是它的孩子呢?就是那些与u
相邻,并且深度比u
大 1 的节点。我们把这些孩子节点收集起来。 - 然后,我们看看序列
a
。假设u
有k
个孩子,那么序列a
中从child_cursor
开始的k
个节点,就应该是u
的这k
个孩子。 - 因为孩子们的访问顺序是任意的,所以我们不需要关心这
k
个节点的顺序,只需要关心集合是否相等。也就是说,a
中声明的这k
个孩子,和我们从图里找到的k
个孩子,是不是同一批节点。 - 使用
std::set
来比较两个集合是不是相等,简直是太方便了,喵~ - 如果检查通过,我们就把
parent_cursor
向后移动一位,去处理下一个父节点。同时,child_cursor
要跳过刚刚验证过的k
个孩子,即child_cursor += k
。 - 如果在任何一步检查失败,或者
child_cursor
超出了序列a
的范围,那就说明这个序列不合法,直接输出 "No"。
如果 parent_cursor
成功地遍历完了整个序列 a
,都没有发现任何问题,那就说明这个序列是完全合法的!输出 "Yes" 就好啦~
代码实现,一起来写吧!
#include <iostream>
#include <vector>
#include <queue>
#include <algorithm>
#include <set>
void solve() {
int n;
std::cin >> n;
// 用邻接表来存这棵树,喵~
std::vector<std::vector<int>> adj(n + 1);
for (int i = 0; i < n - 1; ++i) {
int u, v;
std::cin >> u >> v;
adj[u].push_back(v);
adj[v].push_back(u);
}
// 读入需要检查的序列 a
std::vector<int> a(n);
for (int i = 0; i < n; ++i) {
std::cin >> a[i];
}
// BFS 必须从节点 1 开始,这是第一条规矩!
if (a[0] != 1) {
std::cout << "No\n";
return;
}
// 步骤 1: 跑一次标准的 BFS,来确定每个节点的深度。
// 这会给我们一个基准的 BFS 树结构,任何合法的遍历都必须遵守这些层级关系。
std::vector<int> depth(n + 1, -1);
std::queue<int> q;
depth[1] = 0;
q.push(1);
while (!q.empty()) {
int u = q.front();
q.pop();
for (int v : adj[u]) {
if (depth[v] == -1) { // 如果邻居还没被访问过
depth[v] = depth[u] + 1; // 它的深度就是父节点深度+1
q.push(v);
}
}
}
// 步骤 2: 验证给定的序列 `a`。
// 我们遍历 `a`,把它看作是出队顺序。
// 对于从 `a` 中取出的每个节点 `u`,它的孩子们必须组成 `a` 中的下一个区块。
int parent_cursor = 0; // 指向 `a` 中当前父节点的索引
int child_cursor = 1; // 指向 `a` 中当前父节点的孩子区块的起始索引
while (parent_cursor < n) {
int u = a[parent_cursor];
// 找到 `u` 在我们基准 BFS 树中的所有孩子。
// 也就是 `u` 的邻居中,深度比 `u` 大 1 的那些节点。
std::set<int> children_from_graph;
for (int v : adj[u]) {
if (depth[v] > depth[u]) {
children_from_graph.insert(v);
}
}
int num_children = children_from_graph.size();
// `a` 中接下来的 `num_children` 个元素必须是这些孩子。
// 如果 `a` 的剩余长度不够,那肯定不对啦。
if (child_cursor + num_children > n) {
std::cout << "No\n";
return;
}
// 把 `a` 中声称是孩子的那个区块收集起来。
std::set<int> children_from_a;
for (int i = 0; i < num_children; ++i) {
children_from_a.insert(a[child_cursor + i]);
}
// 关键一步:比较这两个孩子的集合是否完全相同。顺序不重要,所以用 set 最好了!
if (children_from_graph != children_from_a) {
std::cout << "No\n";
return;
}
// 验证通过!移动指针,准备检查下一个父节点和它的孩子区块。
parent_cursor++;
child_cursor += num_children;
}
// 如果整个序列 `a` 都被成功处理完,并且所有检查都通过了,那它就是合法的 BFS 序列!
std::cout << "Yes\n";
}
int main() {
std::ios_base::sync_with_stdio(false);
std::cin.tie(NULL);
solve();
return 0;
}
复杂度分析 (How Fast is My Paw-some Code?)
时间复杂度: O(N log N) 的说
- 第一次标准 BFS 用来计算深度,访问每个节点和每条边一次,因为是树,所以是 O(N)。
- 验证过程中的
while
循环,parent_cursor
从 0 遍历到 N-1。在循环内部,我们为每个父节点u
构建了两个set
。构建children_from_graph
时,我们遍历u
的所有邻居,总的来说所有边会被访问两次,这部分是 O(N)。构建children_from_a
也是 O(N)。将元素插入set
的时间复杂度是对数级的。总的来说,所有插入操作加起来,最坏情况是 O(N log N)。所以,总的时间复杂度由验证步骤主导,是 O(N log N) 呐。
空间复杂度: O(N) 的说
- 邻接表
adj
需要 O(N) 空间(因为树有 N-1 条边)。 depth
数组和输入的a
数组都需要 O(N) 空间。- 队列
q
和两个set
在最坏的情况下(比如一个星形图),也可能需要 O(N) 的空间。 - 所以总的空间复杂度是 O(N) 哦。
- 邻接表
知识点与总结 (What We've Learned, Nya!)
这道题真的非常考验对 BFS 本质的理解呢!通过这道题,我们学到了:
- BFS 的不变性: 无论邻居的访问顺序如何,BFS 的分层特性是绝对不变的。所有深度为
d
的节点一定在深度为d+1
的节点之前被访问。 - 验证型问题的思路: 很多时候,我们不需要去模拟所有可能性。而是可以先建立一个“黄金标准”(比如本题中的节点深度),然后去验证给定输入是否符合这个标准。这是一种非常高效的解题思想!
- 巧用数据结构:
std::set
在这里大放异彩!它能自动排序并去重,完美地解决了“顺序任意,但集合必须相同”的判断需求。如果不用set
,我们也可以把两个子序列分别排序然后比较,效果是一样的。 - 双指针/游标思想: 使用
parent_cursor
和child_cursor
两个游标来同步处理序列和其内部的逻辑区块,是一种非常清晰和常见的技巧,在处理序列和字符串问题时特别有用。
希望这篇题解能帮到你,主人!如果还有问题,随时可以再来问我哦~ 喵~ (づ。◕‿‿◕。)づ