F. Graph Without Long Directed Paths - 题解
比赛与标签
比赛: Codeforces Round 550 (Div. 3) 标签: dfs and similar, graphs 难度: *1700
喵喵,题目在说什么呀?
主人,你好喵~ 这道题是关于图的一个非常有趣的问题哦!(ฅ'ω'ฅ)
题目给了我们一个 连通的无向图,有很多节点和连接它们的边。我们的任务呢,就是给每一条边都指定一个方向,把它变成一个 有向图。
但是有一个很严格的规定:在改造后的有向图里,不能存在长度为2或更长的路径!也就是说,像 A → B → C
这样的三点一线路径是绝对不可以出现的!
如果能找到一种满足条件的方向方案,我们就输出 "YES",然后用一个由 '0' 和 '1' 组成的字符串来表示这个方案。字符串的第 i
个字符对应输入中的第 i
条边 (u, v)
,'0' 代表方向是 u → v
,'1' 代表方向是 v → u
。如果怎么都做不到,那就只能遗憾地输出 "NO" 啦。
猫娘的思考时间!
呐呐,主人请想一下,A → B → C
这样的路径为什么会被禁止呢?关键点就在中间的那个节点 B
啦!它有一条边进来(A → B
),又有一条边出去(B → C
)。
所以呀,为了不出现这种长路径,我们必须遵守一个核心规则:图里的每一个节点,要么所有与它相连的边都指向它(成为一个“汇点”),要么所有与它相连的边都从它出发(成为一个“源点”)。绝对不能既有入边又有出边!
这样一来,所有的节点就被分成了两类啦:
- 源点 (Source):所有边都从它出发,只出不入。
- 汇点 (Sink):所有边都指向它,只入不出。
是不是很像要把所有节点分成两拨呢喵?( ´∀`)
现在我们再考虑任意一条边 (u, v)
。
- 如果
u
和v
都是源点,那这条边该是什么方向呢?u → v
的话v
就不是纯源点了,v → u
的话u
就不是纯源点了。矛盾了喵! - 如果
u
和v
都是汇点,也会发生同样的矛盾。 - 所以,唯一的可能性就是:任何一条边,都必须连接一个源点和一个汇点!
喵!这不就是... 二分图 的定义嘛!一个图的顶点可以被分成两个独立的集合,使得每一条边都连接着两个集合中的顶点。在这里,一个集合就是所有的“源点”,另一个集合就是所有的“汇点”。
所以,这道题的核心就转化成了:判断给定的图是不是一个二分图。
判断二分图有一个非常经典的方法,那就是 染色法!我们可以用BFS(广度优先搜索)或者DFS(深度优先搜索)来实现。就像给小猫分队,规定相邻的小猫不能是同一个颜色的队伍~
我们的策略是:
- 使用BFS来进行二分图染色。我们准备两种颜色,比如颜色1和颜色2。
- 因为题目保证图是连通的,我们从任意一个节点开始(比如节点1),把它染成颜色1,然后放进队列里。
- 当队列不为空时,取出一个节点
u
。遍历它的所有邻居v
:- 如果
v
还没有被染色,就把它染成和u
相反的颜色(如果u
是颜色1,v
就染成颜色2,反之亦然),然后把v
加入队列。 - 如果
v
已经被染色了,而且颜色和u
相同,那就糟糕了!这说明我们找到了一个奇数长度的环,这个图就不是二分图。此时直接判定无解,输出 "NO"。
- 如果
- 如果BFS顺利结束,没有发现任何颜色冲突,那就说明这个图是二分图,一定有解!我们输出 "YES"。
那么怎么生成那个01字符串呢?既然我们已经成功地把所有节点分成了颜色1和颜色2两组,我们可以简单地定一个规则:所有边都从颜色1的节点指向颜色2的节点。这样,所有颜色1的节点都成了源点,所有颜色2的节点都成了汇点,完美满足了题目的要求!
我们只需要遍历一遍输入时记录的原始边 (u, v)
:
- 如果
u
被我们染成了颜色1,那么方向就是u → v
,输出 '0'。 - 如果
u
被我们染成了颜色2,那么v
必然是颜色1,方向就应该是v → u
,输出 '1'。
这样问题就解决啦,喵~
把思路变成代码喵!
#include <iostream>
#include <vector>
#include <string>
#include <queue>
#include <utility>
// 喵~ 一个小技巧,让C++的输入输出变得更快!
void setup_io() {
std::ios_base::sync_with_stdio(false);
std::cin.tie(NULL);
}
int main() {
setup_io();
int n, m;
std::cin >> n >> m;
// 用一个vector来记录原始边的输入顺序,这样我们才能按顺序输出01串呐
// pair里存的是输入给的 {u, v}
std::vector<std::pair<int, int>> edges;
edges.reserve(m);
// 喵~ 这是邻接表,用来存图的结构,方便我们遍历一个点的所有邻居
std::vector<std::vector<int>> adj(n + 1);
for (int i = 0; i < m; ++i) {
int u, v;
std::cin >> u >> v;
edges.push_back({u, v});
adj[u].push_back(v);
adj[v].push_back(u);
}
// 问题的关键是判断图是否为二分图
// 我们用BFS来进行二分图染色判定
// color[i] = 0: 代表还没被染色
// color[i] = 1: 代表染成了颜色1
// color[i] = 2: 代表染成了颜色2
std::vector<int> color(n + 1, 0);
std::queue<int> q;
bool is_bipartite = true;
// 题目保证图是连通的,所以我们只需要从一个点(比如1)开始BFS就能遍历整个图了
q.push(1);
color[1] = 1; // 给起点染上颜色1
while (!q.empty() && is_bipartite) {
int u = q.front();
q.pop();
for (int v : adj[u]) {
if (color[v] == 0) {
// 如果邻居v还没被染色,就给它染上和u相反的颜色
color[v] = 3 - color[u]; // 这是一个小技巧, 3-1=2, 3-2=1
q.push(v);
} else if (color[v] == color[u]) {
// 糟糕!如果邻居v已经被染色了,而且颜色和自己一样
// 这就意味着我们找到了一个奇数环,它不是二分图!
is_bipartite = false;
break;
}
}
}
if (!is_bipartite) {
std::cout << "NO\n";
} else {
std::cout << "YES\n";
std::string result_string;
result_string.reserve(m);
// 如果图是二分图,我们就规定所有边都从颜色1的节点指向颜色2的节点
// 这样颜色1的都是源点,颜色2的都是汇点
for (const auto& edge : edges) {
int u = edge.first;
// int v = edge.second; // v其实在这里用不到
// 题目要求 (u, v) 这条边,u->v是'0',v->u是'1'
// 如果 u 是颜色1,根据我们的规则,方向是 u -> v
if (color[u] == 1) {
result_string += '0';
} else { // 如果 u 是颜色2,那 v 肯定是颜色1,方向是 v -> u
result_string += '1';
}
}
std::cout << result_string << "\n";
}
return 0;
}
分析一下时间和空间吧~
- 时间复杂度: O(N + M) 的说。这里的 N 是顶点数,M 是边数。因为我们用BFS来遍历图,每个顶点和每条边都只会被访问一次,所以效率非常高!
- 空间复杂度: O(N + M) 的说。我们需要邻接表来存储整个图,这需要 O(N + M) 的空间。此外,
color
数组和BFS用的队列q
都需要 O(N) 的空间。所以总的空间复杂度和图的规模成正比。
喵呜~ 总结时间到!
这道题真是一次有趣的思维体操呢!主人是不是也觉得很有收获呀?
核心思想转换: 这道题最巧妙的地方在于,它把一个看起来很独特的约束(“无长度大于等于2的路径”)成功地转化为了一个我们非常熟悉的经典图论模型——二分图判定。能够看出这种转化是解题的关键一步哦!
二分图的判断: 判断一个图是否是二分图的充要条件是 图中不包含任何奇数长度的环。而实现上,最常用的方法就是 BFS或DFS染色法。
构造解: 一旦确定了图是二分图并完成了染色,构造一个合法的解就变得非常简单了。我们只需要定义一个统一的规则(比如,所有边都从颜色1指向颜色2),就能保证所有节点要么是源点,要么是汇点,从而满足题目的要求。
实现细节:
- 因为题目保证了图是 连通 的,所以我们只需要进行一次BFS/DFS就能给所有节点染色。如果图不连通,就需要对每个连通分量都跑一遍染色过程。
- 注意要 按输入顺序 输出结果,所以需要一个额外的数据结构(比如
vector<pair<int, int>>
)来存储原始的边。
把抽象的问题转化为经典的算法模型,是解题的一大乐趣呢!主人也要多多练习,以后碰到类似的题就能一眼看穿啦!继续加油喵~ (๑•̀ㅂ•́)و✧