题解:洛谷 AT_abc451_f Make Bipartite 3
【题目来源】
洛谷:AT_abc451_f [ABC451F] Make Bipartite 3 - 洛谷
【题目描述】
有一个无向图 \(G\),它有 \(N\) 个顶点和 \(0\) 条边,顶点的编号从 \(1\) 到 \(N\)。
给定 \(Q\) 个查询。在第 \(i\) 个查询中,向图 \(G\) 中添加一条连接顶点 \(u_i\) 和顶点 \(v_i\) 的边。
在处理完每个查询后的图 \(G\) 中,请判断是否可以将 \(G\) 的每个顶点涂成白色或黑色中的一种颜色,且满足以下条件;如果可行,则求出被涂成黑色的顶点数的最小可能值。
- 对于每一条边,其两端的顶点都被涂上了不同的颜色。
【输入】
输入以以下形式从标准输入中给出。
\(N\) \(Q\) \(u_1\) \(v_1\) \(u_2\) \(v_2\) \(\cdots\) \(u_Q\) \(v_Q\)。
【输出】
请输出 \(Q\) 行。
在第 \(i\) 行中,输出处理完第 \(i\) 个查询后的图 \(G\) 的以下数值。
- 如果存在满足条件的涂色方式,则输出黑色顶点的最小可能数量。
- 如果不存在满足条件的涂色方式,则输出 \(-1\)。
【输入样例】
4 4
1 2
2 3
3 4
1 3
【输出样例】
1
1
2
-1
【核心思想】
-
问题分析:给定 \(N\) 个孤立顶点,依次添加 \(Q\) 条无向边。每次添加后立即判断图是否为二分图,若是则求黑色顶点的最小数量。这是一个动态二分图判定 + 扩展域并查集(带权并查集 / 种类并查集)问题,核心在于维护每个连通分量的染色方案,并在加边时检测奇环。
-
算法选择:
- 扩展域并查集:每个顶点 \(u\) 拆分为 \(u\)(代表白色)和 \(u+n\)(代表黑色)。若 \(u\) 和 \(v\) 之间有边,则 \(u\) 与 \(v\) 异色,即 \(u\)(白)与 \(v+n\)(黑)同集合,\(u+n\)(黑)与 \(v\)(白)同集合
- 贪心染色:对每个连通分量,选择黑白两种颜色中数量较少的那种作为黑色顶点数,累加得到全局最小黑色数
-
关键步骤:
- 初始化:\(p[i] = i\)(\(1 \leq i \leq 2n\)),\(sz[i] = 1\)(\(1 \leq i \leq n\)),\(ans = 0\)
- 处理每条边 \((u, v)\):
- 若之前已判定非二分图(\(ans = -1\)),直接输出 \(-1\)
- 若 \(find(u) = find(v+n)\)(\(u\) 和 \(v\) 已在同一集合,即同色),则形成奇环,\(ans \leftarrow -1\)
- 若 \(find(u) = find(v)\)(已在不同集合的异色连通块),则合并两个连通块:
- 减去原贡献:\(ans \leftarrow ans - \min(sz[pu], sz[pun]) - \min(sz[pv], sz[pvn])\)
- 合并:\(p[pvn] = pu\),\(sz[pu] += sz[pvn]\);\(p[pv] = pun\),\(sz[pun] += sz[pv]\)
- 加上新贡献:\(ans \leftarrow ans + \min(sz[pu], sz[pun])\)
- 输出 \(ans\)
-
时间/空间复杂度:
- 时间复杂度:\(O(Q \cdot \alpha(N))\),并查集操作近似 \(O(1)\)
- 空间复杂度:\(O(N)\),扩展域并查集数组
-
扩展域并查集与动态二分图的核心思想:
- 颜色对立关系建模:\(u\) 与 \(v\) 异色 \(\Leftrightarrow\) \(u\) 和 \(v+n\) 同集合、\(u+n\) 和 \(v\) 同集合。通过并查集的合并操作,将"异色约束"转化为"同集合约束"
- 奇环检测:若 \(find(u) = find(v)\),说明 \(u\) 和 \(v\) 已经是同一颜色,再添加边 \((u,v)\) 必然形成奇环,图不再是二分图
- 连通块合并的最优染色:两个异色连通块合并后,新的连通块中每个原连通块的黑白集合被重新组合。\(sz[pu]\) 和 \(sz[pun]\) 分别表示以 \(pu\) 为根的集合中"白域"和"黑域"的大小,\(\min\) 取较小者即为该连通块的最小黑色数
- 增量维护答案:合并前先减去两个原连通块的贡献,合并后加上新连通块的贡献,保证 \(ans\) 始终为当前所有连通块最小黑色数之和
- 适用于动态图二分性判定、二分图染色优化、种类并查集类问题
【算法标签】
普及+ #并查集
【代码详解】
#include <bits/stdc++.h>
using namespace std;
const int N = 200005 * 2; // 两倍空间
int n, q, ans; // n: 节点数, q: 操作数, ans: 当前答案
int p[N], sz[N]; // 并查集数组和大小数组
int find(int x) // 并查集查找,带路径压缩
{
if (p[x] != x) // 如果不是根
{
p[x] = find(p[x]); // 路径压缩
}
return p[x];
}
int main()
{
cin >> n >> q; // 输入节点数和操作数
// 初始化
for (int i = 1; i <= n; i++)
{
sz[i] = 1; // 初始每个集合大小为1
}
for (int i = 1; i <= n + n; i++) // 初始化2n个并查集元素
{
p[i] = i; // 每个元素是自己的父节点
}
while (q--) // 处理每个操作
{
int u, v;
cin >> u >> v; // 输入边(u,v)
// 如果已经违反二分图条件,或之前已违反
if (find(u) == find(v + n) || ans == -1)
{
cout << ans << endl; // 直接输出当前答案
continue;
}
int pu = find(u), pv = find(v); // 找到u和v的根
int pun = find(u + n), pvn = find(v + n); // 找到u'和v'的根
// 检查u和v是否已经在同一集合(同色)
if (pu == pv)
{
ans = -1; // 违反二分图条件
cout << ans << endl;
continue;
}
// 减去原来两个连通块的贡献
ans = ans - min(sz[pu], sz[pun]) - min(sz[pv], sz[pvn]);
// 合并集合:u和v'合并,u'和v合并
p[pvn] = pu; // 将v'合并到u
sz[pu] += sz[pvn]; // 更新大小
p[pv] = pun; // 将v合并到u'
sz[pun] += sz[pv]; // 更新大小
// 加上新连通块的贡献
ans += min(sz[pu], sz[pun]);
cout << ans << endl; // 输出当前答案
}
return 0;
}
【运行结果】
4 4
1 2
1
2 3
1
3 4
2
1 3
-1
浙公网安备 33010602011771号