问题描述
二分图(Bipartite Graph)是一种特殊的无向图,其顶点可以被分成两个不相交的集合,使得图中的每一条边都连接两个不同集合中的顶点。换句话说,图中不存在奇数长度的环。
二分图判定问题:给定一个无向图,判断它是否是二分图。如果是,输出一种合法的染色方案。
算法思路
核心定理
一个无向图是二分图,当且仅当它不包含奇数长度的环。
BFS染色法
- 任选一个未染色的顶点,将其染成颜色0
- 使用BFS遍历其邻居,将邻居染成相反的颜色1
- 如果发现某个邻居已经被染成相同的颜色,则存在奇环,不是二分图
- 重复上述过程,直到所有连通分量都被处理
DFS染色法
与BFS类似,使用深度优先搜索进行染色,原理相同但实现略有差异。
Java实现
BFS染色判定
import java.util.*;
public class BipartiteCheck {
/**
* 使用BFS染色判定二分图
* colors: -1表示未染色, 0和1表示两种颜色
*
* @param graph 邻接表表示的无向图
* @return 如果是二分图返回true,否则false
*/
public static boolean isBipartiteBFS(List<Integer>[] graph) {
int n = graph.length;
int[] colors = new int[n];
Arrays.fill(colors, -1); // -1表示未染色
for (int start = 0; start < n; start++) {
if (colors[start] != -1) {
continue; // 已染色,跳过
}
Queue<Integer> queue = new LinkedList<>();
queue.offer(start);
colors[start] = 0; // 起始点染成颜色0
while (!queue.isEmpty()) {
int u = queue.poll();
for (int v : graph[u]) {
if (colors[v] == -1) {
// 邻居未染色,染成相反颜色
colors[v] = colors[u] ^ 1;
queue.offer(v);
} else if (colors[v] == colors[u]) {
// 邻居已染成相同颜色,存在奇环
return false;
}
}
}
}
return true;
}
/**
* 返回染色方案,如果不是二分图返回null
*/
public static int[] getColoring(List<Integer>[] graph) {
int n = graph.length;
int[] colors = new int[n];
Arrays.fill(colors, -1);
for (int start = 0; start < n; start++) {
if (colors[start] != -1) continue;
Queue<Integer> queue = new LinkedList<>();
queue.offer(start);
colors[start] = 0;
while (!queue.isEmpty()) {
int u = queue.poll();
for (int v : graph[u]) {
if (colors[v] == -1) {
colors[v] = colors[u] ^ 1;
queue.offer(v);
} else if (colors[v] == colors[u]) {
return null; // 不是二分图
}
}
}
}
return colors;
}
/**
* 使用DFS染色判定二分图
*/
public static boolean isBipartiteDFS(List<Integer>[] graph) {
int n = graph.length;
int[] colors = new int[n];
Arrays.fill(colors, -1);
for (int i = 0; i < n; i++) {
if (colors[i] == -1) {
if (!dfs(graph, i, 0, colors)) {
return false;
}
}
}
return true;
}
private static boolean dfs(List<Integer>[] graph, int u, int color, int[] colors) {
colors[u] = color;
for (int v : graph[u]) {
if (colors[v] == -1) {
if (!dfs(graph, v, color ^ 1, colors)) {
return false;
}
} else if (colors[v] == color) {
return false;
}
}
return true;
}
public static void main(String[] args) {
// 测试1:二分图(偶环)
List<Integer>[] graph1 = new ArrayList[4];
for (int i = 0; i < 4; i++) graph1[i] = new ArrayList<>();
graph1[0].add(1); graph1[1].add(0);
graph1[1].add(2); graph1[2].add(1);
graph1[2].add(3); graph1[3].add(2);
graph1[3].add(0); graph1[0].add(3);
System.out.println("图1(偶环C4)是二分图:" + isBipartiteBFS(graph1));
int[] coloring1 = getColoring(graph1);
System.out.println("染色方案:" + Arrays.toString(coloring1));
// 测试2:非二分图(奇环)
List<Integer>[] graph2 = new ArrayList[3];
for (int i = 0; i < 3; i++) graph2[i] = new ArrayList<>();
graph2[0].add(1); graph2[1].add(0);
graph2[1].add(2); graph2[2].add(1);
graph2[2].add(0); graph2[0].add(2);
System.out.println("\n图2(奇环C3)是二分图:" + isBipartiteBFS(graph2));
// 测试3:非连通二分图
List<Integer>[] graph3 = new ArrayList[6];
for (int i = 0; i < 6; i++) graph3[i] = new ArrayList<>();
graph3[0].add(1); graph3[1].add(0);
graph3[2].add(3); graph3[3].add(2);
graph3[4].add(5); graph3[5].add(4);
System.out.println("\n图3(三个不连通的边)是二分图:" + isBipartiteBFS(graph3));
int[] coloring3 = getColoring(graph3);
System.out.println("染色方案:" + Arrays.toString(coloring3));
}
}
扩展:二分图最大匹配(匈牙利算法)
public class HungarianAlgorithm {
/**
* 匈牙利算法求解二分图最大匹配
* @param graph 左侧顶点到右侧顶点的邻接表
* @param nLeft 左侧顶点数
* @param nRight 右侧顶点数
* @return 最大匹配数
*/
public static int maxMatching(List<Integer>[] graph, int nLeft, int nRight) {
int[] matchR = new int[nRight]; // matchR[j]表示右侧顶点j匹配的左侧顶点
Arrays.fill(matchR, -1);
int result = 0;
for (int u = 0; u < nLeft; u++) {
boolean[] seen = new boolean[nRight];
if (bpm(graph, u, seen, matchR)) {
result++;
}
}
return result;
}
private static boolean bpm(List<Integer>[] graph, int u, boolean[] seen, int[] matchR) {
for (int v : graph[u]) {
if (seen[v]) continue;
seen[v] = true;
if (matchR[v] < 0 || bpm(graph, matchR[v], seen, matchR)) {
matchR[v] = u;
return true;
}
}
return false;
}
public static void main(String[] args) {
// 示例:5个工人和5个任务
int nLeft = 5, nRight = 5;
List<Integer>[] graph = new ArrayList[nLeft];
for (int i = 0; i < nLeft; i++) graph[i] = new ArrayList<>();
graph[0].addAll(Arrays.asList(0, 1));
graph[1].addAll(Arrays.asList(0, 2));
graph[2].addAll(Arrays.asList(1, 2, 3));
graph[3].addAll(Arrays.asList(2, 4));
graph[4].addAll(Arrays.asList(3, 4));
System.out.println("最大匹配数:" + maxMatching(graph, nLeft, nRight));
}
}
扩展:判断一般图的奇环
public class OddCycleDetection {
/**
* 使用BFS染色检测图中是否存在奇环
* 返回值:如果存在奇环返回true
*/
public static boolean hasOddCycle(List<Integer>[] graph) {
return !BipartiteCheck.isBipartiteBFS(graph);
}
/**
* 找出一个奇环上的顶点(如果存在)
*/
public static List<Integer> findOddCycle(List<Integer>[] graph) {
int n = graph.length;
int[] colors = new int[n];
int[] parent = new int[n];
Arrays.fill(colors, -1);
Arrays.fill(parent, -1);
for (int start = 0; start < n; start++) {
if (colors[start] != -1) continue;
Queue<Integer> queue = new LinkedList<>();
queue.offer(start);
colors[start] = 0;
while (!queue.isEmpty()) {
int u = queue.poll();
for (int v : graph[u]) {
if (colors[v] == -1) {
colors[v] = colors[u] ^ 1;
parent[v] = u;
queue.offer(v);
} else if (colors[v] == colors[u]) {
// 找到奇环,回溯路径
List<Integer> cycle = new ArrayList<>();
int x = u, y = v;
// 找到x和y的最近公共祖先
Set<Integer> pathX = new HashSet<>();
while (x != -1) {
pathX.add(x);
x = parent[x];
}
while (!pathX.contains(y)) {
cycle.add(y);
y = parent[y];
}
cycle.add(y); // LCA
x = u;
List<Integer> pathFromU = new ArrayList<>();
while (x != y) {
pathFromU.add(x);
x = parent[x];
}
Collections.reverse(pathFromU);
cycle.addAll(pathFromU);
return cycle;
}
}
}
}
return null;
}
}
复杂度分析
| 操作 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| BFS染色判定 | O(V + E) | O(V) |
| DFS染色判定 | O(V + E) | O(V) |
| 匈牙利算法(最大匹配) | O(V × E) | O(V) |
实际应用
- 任务分配:将任务和工人建模为二分图,最大匹配即最优分配
- 棋盘覆盖:棋盘黑白染色本身就是二分图,相关覆盖问题可转化
- 冲突消解:将冲突双方分为两组,判断是否存在合法分组
- 推荐系统:用户-物品二分图是协同过滤的基础模型
总结
二分图判定是图论入门的重要问题。BFS染色法利用二分图的本质特征(无奇环),通过两种颜色的交替染色,在线性时间内完成判定。这一思想可以推广到更复杂的图论问题,如最大匹配、最小点覆盖等。掌握二分图的相关算法,是深入理解图论和网络流的基础。