P11022 「LAOI-6」Yet Another Graph Coloration Problem 解题报告
P11022 「LAOI-6」Yet Another Graph Coloration Problem 解题报告
1. 题目核心思想解读
首先,我们来弄懂题目到底要求我们做什么。题目要求我们将图中的节点染成黑白两色,同时满足三个条件:
- 至少有一个黑点。
- 至少有一个白点。
- 任意一个黑点
u
和一个白点v
之间,都必须有至少两条不同的简单路径。
前两个条件很简单,只要我们不把所有点染成同一种颜色就行。关键在于第三个条件。
“两点之间有至少两条路径”,在图论中,这是一个非常强烈的信号,它通常和环(Cycle)或者双连通分量有关。
想象一下,如果两点 u
和 v
在一个环上,那么从 u
到 v
天然就有两条路径:顺时针走和逆时针走。这个性质启发我们,问题的解很可能和图中的环有关。
如果一个图没有环,它就是一棵树(或者森林,即多个不连通的树)。在树上,任意两点之间的路径是唯一的。因此,如果图是一棵树,我们无论如何染色,都无法满足黑白点之间有两条路径的要求。所以,如果给定的图是树或森林,一定无解。
2. 解题策略:寻找一个“环”并利用它
既然“环”是关键,我们的策略就是找到图中的一个环,并利用它来构造一种合法的染色方案。
怎么找环呢?一个经典的方法是深度优先搜索(DFS)。
- 构建 DFS 树:我们从任意一个节点(比如 1 号点)开始进行 DFS 遍历。所有通过 DFS 访问边走过的路径会构成一棵生成树,我们称之为 “DFS 树”。
- 寻找返祖边:在 DFS 过程中,如果当前节点
u
遇到一个邻居v
,而v
之前已经被访问过,并且v
不是u
的父节点,那么边(u, v)
就是一条返祖边。这条返祖边(u, v)
和 DFS 树上从v
到u
的路径一起,构成了一个环。
(上图中,实线是 DFS 树的边,虚线 (x, p) 是一条返祖边)
找到了一个环,我们就可以构造染色方案了!题解给出了一个非常巧妙的构造方法:
- 假设我们找到了一条返祖边
(x, p)
,其中x
在 DFS 树中的深度比p
更深。 - 我们将节点
x
以及它在 DFS 树中的所有子孙节点(即x
的子树)全部染成黑色。 - 将图中的其余所有节点染成白色。
3. 为什么这个构造是正确的?
我们来验证一下这个染色方案是否满足“任意黑白点对之间都有两条路径”。
设 b
是任意一个黑点,w
是任意一个白点。根据我们的构造,b
肯定在 x
的子树里,而 w
在子树外。
-
路径一(树上路径):
由于整个图是连通的,在 DFS 树上,b
和w
之间必然存在一条唯一的路径。这条路径完全由树边构成。 -
路径二(绕行返祖边):
- 从黑点
b
出发,沿着 DFS 树向上走到x
。 - 通过返祖边
(x, p)
,我们从x
直接跳到了它的祖先p
。 - 因为
p
是x
的祖先,所以p
不在x
的子树里,因此p
是一个白点。 - 现在我们位于白点
p
,目标是另一个白点w
。我们只需沿着 DFS 树的路径从p
走到w
即可。
- 从黑点
这条新路径 b → ... → x → p → ... → w
使用了返祖边 (x, p)
,而路径一完全没有使用它。因此,这两条路径必然是不同的。这样,我们就为任意一对黑白点找到了两条不同的简单路径。
所以,只要我们能找到至少一条返祖边,就意味着图中有环,我们就能用上述方法构造出一种合法的解。
4. 无解的情况
根据上面的分析,我们可以总结出无解的情况:
- 图不连通:如果图不连通,我们把一个连通块染成黑色,另一个染成白色,那么黑白点之间根本没有路径,不满足条件。如果我们只在一个连通块内染色,那其他连通块的点颜色怎么算?为了满足“至少一个黑点”和“至少一个白点”的条件,我们必须把颜色分布在整个图中,这在不连通图上是无法满足“两条路径”条件的。
- 图连通但没有环(即图是一棵树):如前所述,树上任意两点路径唯一,无法满足条件。
一个连通的图没有环,等价于它没有返祖边。所以,无解的充要条件就是:图不连通,或者图中没有环。
5. 代码实现解析
题解中提供的代码正是实现了上述思路,它通过两次 DFS 来完成任务。
#include<bits/stdc++.h>
// ... (头文件和定义)
vector<int>q[200005]; // 邻接表存图
int vis[200005]; // 访问标记数组
int qwq[200005]; // 多功能标记数组,下面解释
int ans; // 标记是否找到了返祖边(即是否有环)
// 第一次DFS:检查连通性和寻找返祖边
void inline dfs(int x,int fa){
vis[x]=1; // 标记x已访问
for(int i=0;i<q[x].size();i++){
int v = q[x][i];
if(v==fa) continue; // 忽略到父节点的边
if(vis[v]){ // 如果邻居v已经被访问过,且不是父节点
if(ans==0) { // 找到第一条返祖边
ans=1; // 标记我们找到了环
qwq[x]=1; // 标记x是某条返祖边的“深端”点
}
} else { // 如果邻居v未被访问
dfs(v,x); // 继续DFS
}
}
}
// 第二次DFS:根据第一次的结果进行染色
void inline dfs2(int x, int y){
vis[x]=1; // 再次使用vis数组,标记已访问
// y表示x的祖先中,是否有被标记为“深端”的点
// qwq[x]继承自身或祖先的标记
qwq[x]=max(qwq[x], y);
for(int i=0;i<q[x].size();i++){
int v = q[x][i];
if(!vis[v]) {
// 将标记(qwq[x])传递给子节点
dfs2(v, max(y, qwq[x]));
}
}
}
int main(){
cin>>t;
while(t--){
// 初始化
cin>>n>>m;
ans=0;
for(int i=1;i<=n;i++) q[i].clear(), vis[i]=0, qwq[i]=0;
// 读入图
for(int i=1;i<=m;i++){ cin>>u>>v; q[u].push_back(v); q[v].push_back(u); }
// 1. 运行第一次DFS
dfs(1,0);
// 2. 检查无解情况
bool opt=false;
for(int i=1;i<=n;i++) {
if(vis[i]==0) { // 如果有节点没被访问到,说明图不连通
opt=1;
cout<<-1<<"\n";
break;
}
}
if(opt) continue;
if(ans==0){ // 如果ans仍为0,说明没找到返祖边,图是树
cout<<-1<<"\n";
continue;
}
// 3. 有解,运行第二次DFS进行染色
for(int i=1;i<=n;i++) vis[i]=0; // 重置vis数组
dfs2(1,0);
// 4. 输出结果
for(int i=1;i<=n;i++){
if(qwq[i]) cout<<"B"; // qwq[i]为1的点是黑色
else cout<<"W"; // 否则是白色
}
cout<<"\n";
}
}
代码逻辑梳理:
-
dfs
函数:- 从节点
1
开始遍历图。 vis
数组用于判断节点是否已访问。- 当发现一个已访问的邻居
v
(且不是父节点)时,就找到了一个环。ans=1
记录下这个事实。 qwq[x]=1
标记当前节点x
是我们找到的环的一部分(具体来说,是返祖边的那个深度更深的点)。dfs
结束后,检查vis
数组可以判断图是否连通。检查ans
变量可以判断图是否有环。
- 从节点
-
dfs2
函数:- 这个函数的目的是实现染色。我们的染色策略是“将
x
的子树染黑”。 - 代码的实现比这个策略更通用一些:它将所有在第一次
dfs
中被qwq
标记为 1 的节点,以及这些节点的所有后代,都染成黑色。 dfs2(x, y)
中的y
参数起到了一个“继承”的作用。如果一个节点的祖先p
被标记了(qwq[p]=1
),那么这个标记会通过y
参数一路传递给p
的所有子孙。- 最终,
qwq[i] == 1
的所有节点i
就构成了我们的黑色节点集合,其余为白色。
- 这个函数的目的是实现染色。我们的染色策略是“将
6. 总结
本题的核心是将一个看似复杂的图论问题,转化为一个经典且直观的子问题:判断图中是否存在环。
- 解题步骤:
- 通过 DFS 判断图是否连通且有环。
- 如果图不连通或是树,则无解,输出
-1
。 - 如果有环,则存在返祖边。任选一条返祖边
(x, p)
(x
是p
的后代)。 - 将
x
及其在 DFS 树中的子树染成黑色,其余染成白色,这便是一种合法的构造方案。 - 代码通过两次 DFS 高效地实现了这个判断和构造过程。
这个思路清晰地展示了如何利用图的基本性质(环、DFS树、返祖边)来解决复杂约束下的构造问题。