【学习笔记】二分图的边染色
定义
首先定义无向图的边着色。
对无向图 \(G\) 的边着色,要求相邻的边涂不同种颜色。
若 \(G\) 是 \(k\) - 边可着色的,但不是 \(\left(k - 1\right)\) - 边可着色的,则称 \(k\) 是 \(G\) 的边色数。记为 \(\chi^{\prime}\left(G\right)\)。
Vizing 定理
若 \(G\) 是简单图,那么有 \(\Delta \left(G\right) \le \chi^{\prime}\left(G\right) \le \Delta \left(G\right) + 1\)。
若 \(G\) 是二分图,那么有 \(\chi^{\prime}\left(G\right) = \Delta \left(G\right)\)。
其中 \(\Delta \left(G\right)\) 为 \(G\) 的最大度数。
对于简单图的最优边染色方案仍然是 NP-hard 的,因此我们主要考虑 \(G\) 为二分图的情况。
该定理存在两种证明方法,下面依次给出。
对 \(\Delta G\) 进行归纳
-
对于 \(D = 0\) 的情况,上述定理成立。
-
对于 \(D > 0\) 的情况,我们考虑将二分图 \(G\) 划分为一组匹配 \(M\) 和子图 \(G \setminus M\)。且 \(\Delta \left(G \setminus M\right) = \Delta \left(G\right) - 1\)。
首先,若该图左右部节点个数不相同,那么通过添加度数为 \(0\) 的虚拟节点使得其左右部节点个数相同。接下来若其存在度数不为 \(\Delta \left(G\right)\) 的节点,在该图中添加虚拟边使得满足所有节点度数均为 \(\Delta \left(G\right)\),由于二分图左右两侧节点个数和度数之和均相同,因此一定可以成功构造。进而我们得到了一个正则二分图 \(G^{\prime}\)。
下面我们证明对于任意 \(k\) - 正则二分图,一定存在完美匹配,进而可以证明 \(G^{\prime}\) 存在完美匹配。对于任意 \(k\) - 正则二分图,设其左部点集合为 \(L\),右部点集合为 \(R\)。那么我们假设其不存在完美匹配,根据 Hall 定理,一定存在一个集合 \(S \subseteq L\) 满足 \(\left\lvert N\left(S\right) \right\rvert < \left\lvert S \right\rvert\)。同时我们可以得到 \(S\) 和 \(N\left(S\right)\) 之间存在 \(k \times \left\lvert S \right\rvert\) 条边,而与 \(N \left(S\right)\) 相接的边的数量一定不超过 \(k \times \left\lvert N\left(S\right) \right\rvert\),矛盾。进而可以证明对于任意 \(k\) - 正则二分图,一定存在完美匹配。
设 \(M^{\prime}\) 为 \(G^{\prime}\) 的一组完美匹配,接下来我们删除 \(M^{\prime}\) 的虚拟边以得到 \(G\) 的匹配 \(M\)。由于度数为 \(\Delta \left(G\right)\) 的节点一定没有虚拟边与之相连,因此对于这些节点,一定有一条与之相连的节点在匹配 \(M\) 中,因此可以得出 \(\Delta \left(G \setminus M\right) = \Delta \left(G\right) - 1\)。
因此我们证明了对于二分图 \(G\) 有 \(\chi^{\prime}\left(G\right) = \Delta \left(G\right)\)。可以发现上述证明过程中并没有要求二分图 \(G\) 是联通的或者没有重边,因此在有重边或图不联通的情况下上述证明仍然成立。
构造性证明
考虑按某种顺序在二分图中依次加边,在加边的过程中维护最小边染色,通过证明一定可以完成构造来证明定理成立。
在加入边 \(\left(x, y\right)\) 的时候,我们分别找到对于 \(x\) 和 \(y\) 找到与其相连的边集中最小的没有出现过的颜色,记为 \(l_x, l_y\),此时颜色编号一定不超过 \(\Delta \left(G\right)\)。
若 \(l_x = l_y\),那么将边 \(\left(x, y\right)\) 的颜色设为 \(l_x\) 即可。
否则假设 \(l_x < l_y\),那么我们尝试找到一条从 \(y\) 开始,颜色为 \(l_x, l_y\) 交替出现的增广路。接下来我们将增广路上的边的颜色依次反转,具体的,若颜色为 \(l_x\) 那么将其设为 \(l_y\),若颜色为 \(l_y\) 那么将其设为 \(l_x\)。
根据二分图的性质,若 \(x\) 在增广路中当且仅当存在一条其与某个右部点连接的,颜色为 \(l_x\) 的边,这与 \(l_x\) 是与 \(x\) 相连的边集中最小的没有出现过的颜色矛盾,进而其一定不在增广路中。因此我们可以在翻转增广路之后直接将边 \(\left(x, y\right)\) 的颜色设为 \(l_x\)。
下面列出一些例题。
CF600F Edge coloring of bipartite graph
二分图边染色模板题。
按上述证明过程中的构造性证明构造即可。
Code
[SNOI2024] 拉丁方
首先考虑若 \(C = n\) 如何处理。
发现我们可以处理出每一列需要填的数字集合,且其需要满足如下限制:
- 每一行填的数字互不相同。
考虑建如下二分图:
- 左部点共 \(n\) 个点,每个点代表一个数字;
- 右部点共 \(n\) 个点,每个点代表一列;
- 若数字 \(x\) 需要在第 \(y\) 列中出现,那么建边 \(\left(x, y\right)\)。
不难发现此时每个数字均需要在 \(n - R\) 列中出现,而每列也均需要出现 \(n - R\) 个数字,因此此图为正则二分图,不难得出其最小边染色为 \(n - R\)。
因此我们考虑对该图进行边染色,若边 \(\left(x, y\right)\) 被染色,那么所有与之同色的边可以组成一行且满足上述限制。
至此我们解决了 \(C = n\) 的情况,对于 \(C < n\) 的情况我们考虑补充右上角 \(R \times n - C\) 的区域使其转化为 \(C = n\) 的情况。
考虑类似于上述的方法,我们让左部点代表数字,右部点代表行。
但是区别在于这种情况下建出的二分图不一定为正则二分图,主要体现在左部点的度数不一定为 \(n - C\),发现若存在左部点的度数大于 \(n - C\) 那么一定无解,反之一定有解。进而继续处理即可。
我们只需要求两次二分图的最小边染色即可解决这个问题。若采取增广路径的方法那么复杂度为 \(\mathcal{O}(n^3)\)。
Code
#include <bits/stdc++.h>
typedef int valueType;
typedef std::vector<valueType> ValueVector;
typedef std::vector<ValueVector> ValueMatrix;
typedef std::pair<valueType, valueType> ValuePair;
typedef std::vector<ValuePair> PairVector;
class BipartiteGraph {
private:
valueType A_, B_, N_, M_, D_;
ValueVector Degree;
PairVector edges;
ValueMatrix Color;
public:
BipartiteGraph() = default;
BipartiteGraph(valueType a, valueType b, valueType m) : A_(a), B_(b), N_(a + b), M_(m), Degree(N_ + 1, 0) {
edges.reserve(m);
}
valueType addEdge(valueType u, valueType v) {
edges.emplace_back(u, v + A_);
++Degree[u];
++Degree[v + A_];
return edges.size() - 1;
}
void solve() {
D_ = *std::max_element(Degree.begin(), Degree.end());
Color.resize(N_ + 1, ValueVector(D_ + 1, -1));
for (auto const &[u, v] : edges) {
valueType c1 = 1, c2 = 1;
while (Color[u][c1] != -1)
++c1;
while (Color[v][c2] != -1)
++c2;
Color[u][c1] = v;
Color[v][c2] = u;
if (c1 == c2)
continue;
for (valueType c = c2, x = v; x != -1; x = Color[x][c], c ^= c1 ^ c2)
std::swap(Color[x][c1], Color[x][c2]);
}
}
valueType GetColor(valueType u, valueType v) const {
for (valueType c = 1; c < Color[u].size(); ++c)
if (Color[u][c] == v)
return c;
return -1;
}
valueType GetColor(valueType i) const {
return GetColor(edges[i].first, edges[i].second);
}
valueType GetDegree(valueType u) const {
return Degree[u];
}
ValuePair GetEdge(valueType id) const {
return edges[id];
}
valueType N() const {
return N_;
}
valueType M() const {
return M_;
}
valueType D() const {
return D_;
}
};
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
std::cout.tie(nullptr);
valueType A, B, M;
std::cin >> A >> B >> M;
BipartiteGraph Graph(A, B, M);
for (valueType m = 0; m < M; ++m) {
valueType u, v;
std::cin >> u >> v;
Graph.addEdge(u, v);
}
Graph.solve();
std::cout << Graph.D() << std::endl;
for (valueType m = 0; m < M; ++m)
std::cout << Graph.GetColor(m) << ' ';
std::cout << std::endl;
}
[AGC037D] Sorting a Grid
需要找出一个策略使得三次操作后的矩阵有序。
发现若在第三次操作后可以使得矩阵变得有序,那么在第三次操作前即第二次操作后每个元素均应当处于正确的行当中。那么我们考虑如何使得元素处于正确的行当中。
发现唯一一次可以更改元素所在行的操作为第二行,但是其只能任意排列一列,也就是所在第二次操作之前矩阵每列的元素应当是所属的行互不相同。
因此我们第一次操作的目的便是为每个元素制定一个合适的列,使得每列中的元素其应属于的行互不相同。
因此我们考虑建出一个二分图:
- 左部点共 \(N\) 个点,分别代表初始矩阵的一行;
- 右部点共 \(N\) 个点,分别代表最终矩阵的一行;
- 若某个应在第 \(y\) 行的元素在初始矩阵的第 \(x\) 行,那么建边 \(\left(x, y\right)\)。
不难发现此图为正则二分图,因此求出该图的最小边染色,颜色相同的边代表的颜色在 \(B\) 矩阵的同一列中。
将 \(B\) 矩阵的每一列进行排列,使得其每个元素在对应的行出现即可得到 \(C\)。
复杂度为 \(\mathcal{O}(N^2M)\)。
Code
#include <bits/stdc++.h>
typedef int valueType;
typedef std::vector<valueType> ValueVector;
typedef std::vector<ValueVector> ValueMatrix;
typedef std::pair<valueType, valueType> ValuePair;
typedef std::vector<ValuePair> PairVector;
class BipartiteGraph {
private:
valueType A_, B_, N_, M_, D_;
ValueVector Degree;
PairVector edges;
ValueVector Color;
public:
BipartiteGraph() = default;
BipartiteGraph(valueType a, valueType b, valueType m) : A_(a), B_(b), N_(a + b), M_(m), Degree(N_ + 1, 0), Color(m) {
edges.reserve(m);
}
valueType addEdge(valueType u, valueType v) {
edges.emplace_back(u, v + A_);
++Degree[u];
++Degree[v + A_];
return edges.size() - 1;
}
void solve() {
D_ = *std::max_element(Degree.begin(), Degree.end());
ValueMatrix To(N_ + 1, ValueVector(D_ + 1, -1)), ID(N_ + 1, ValueVector(D_ + 1, -1));
for (valueType m = 0; m < M_; ++m) {
auto const &[u, v] = edges[m];
valueType c1 = 1, c2 = 1;
while (To[u][c1] != -1)
++c1;
while (To[v][c2] != -1)
++c2;
To[u][c1] = v;
To[v][c2] = u;
ID[u][c1] = m;
ID[v][c2] = m;
if (c1 == c2)
continue;
for (valueType c = c2, x = v; x != -1; x = To[x][c], c ^= c1 ^ c2) {
std::swap(To[x][c1], To[x][c2]);
std::swap(ID[x][c1], ID[x][c2]);
}
}
for (valueType i = 1; i <= N_; ++i)
for (valueType j = 1; j <= D_; ++j)
Color[ID[i][j]] = j;
}
valueType GetColor(valueType i) const {
return Color[i];
}
valueType GetDegree(valueType u) const {
return Degree[u];
}
ValuePair GetEdge(valueType id) const {
return edges[id];
}
valueType N() const {
return N_;
}
valueType M() const {
return M_;
}
valueType D() const {
return D_;
}
};
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
std::cout.tie(nullptr);
valueType N, M;
std::cin >> N >> M;
valueType const S = N * M;
ValueMatrix ID(N + 1, ValueVector(M + 1, -1));
for (valueType i = 1; i <= N; ++i)
for (valueType j = 1; j <= M; ++j)
ID[i][j] = (i - 1) * M + j;
ValueVector GoalX(S + 1, -1), GoalY(S + 1, -1);
for (valueType i = 1; i <= N; ++i) {
for (valueType j = 1; j <= M; ++j) {
GoalX[ID[i][j]] = i;
GoalY[ID[i][j]] = j;
}
}
ValueMatrix A(N + 1, ValueVector(M + 1, -1));
for (valueType i = 1; i <= N; ++i)
for (valueType j = 1; j <= M; ++j)
std::cin >> A[i][j];
ValueMatrix B(N + 1, ValueVector(M + 1, -1));
BipartiteGraph Graph(N, N, N * M);
ValueMatrix Edge(N + 1, ValueVector(M + 1, -1));
for (valueType i = 1; i <= N; ++i)
for (valueType j = 1; j <= M; ++j)
Edge[i][j] = Graph.addEdge(i, GoalX[A[i][j]]);
Graph.solve();
assert(Graph.D() == M);
for (valueType i = 1; i <= N; ++i)
for (valueType j = 1; j <= M; ++j)
B[i][Graph.GetColor(Edge[i][j])] = A[i][j];
ValueMatrix C(N + 1, ValueVector(M + 1, -1));
for (valueType i = 1; i <= N; ++i)
for (valueType j = 1; j <= M; ++j)
C[GoalX[B[i][j]]][j] = B[i][j];
for (valueType i = 1; i <= N; ++i) {
for (valueType j = 1; j <= M; ++j)
std::cout << B[i][j] << ' ';
std::cout << std::endl;
}
for (valueType i = 1; i <= N; ++i) {
for (valueType j = 1; j <= M; ++j)
std::cout << C[i][j] << ' ';
std::cout << std::endl;
}
return 0;
}
CF212A Privatization
首先可以发现,答案一定不小于
下面我们考虑构造出一组代价为此的方案。
现在问题转化为了:给定一个二分图,给边染色,使得对于每个节点与其相连的边集中颜色出现次数的极差之和最小。
而我们有希望构造出满足上式的答案,也就是所对于每个点,我们可以将与其相连的边集差分为若干子集,其中有若干个大小恰好为 \(t\) 的和不超过一个大小小于 \(t\) 的集合,满足集合内边的颜色互不相同。这启示我们直接将每个节点拆分为若干节点,满足拆分出的节点中有不超过一个节点度数小于 \(t\),其余的节点度数均为 \(t\)。
现在我们的问题变为了对拆分后的节点进行边染色,直接使用二分图边染色即可。
复杂度为 \(\mathcal{O}(\left(n + m + \frac{k}{t}\right)k)\)。
Code
#include <bits/stdc++.h>
typedef int valueType;
typedef std::vector<valueType> ValueVector;
typedef std::vector<ValueVector> ValueMatrix;
typedef std::pair<valueType, valueType> ValuePair;
typedef std::vector<ValuePair> PairVector;
class BipartiteGraph {
private:
valueType A_, B_, N_, M_, D_;
ValueVector Degree;
PairVector edges;
ValueVector Color;
public:
BipartiteGraph() = default;
BipartiteGraph(valueType a, valueType b, valueType m) : A_(a), B_(b), N_(a + b), M_(m), Degree(N_ + 1, 0), Color(m) {
edges.reserve(m);
}
valueType addEdge(valueType u, valueType v) {
edges.emplace_back(u, v + A_);
++Degree[u];
++Degree[v + A_];
return edges.size() - 1;
}
void solve() {
D_ = *std::max_element(Degree.begin(), Degree.end());
ValueMatrix To(N_ + 1, ValueVector(D_ + 1, -1)), ID(N_ + 1, ValueVector(D_ + 1, -1));
for (valueType m = 0; m < M_; ++m) {
auto const &[u, v] = edges[m];
valueType c1 = 1, c2 = 1;
while (To[u][c1] != -1)
++c1;
while (To[v][c2] != -1)
++c2;
To[u][c1] = v;
To[v][c2] = u;
ID[u][c1] = m;
ID[v][c2] = m;
if (c1 == c2)
continue;
for (valueType c = c2, x = v; x != -1; x = To[x][c], c ^= c1 ^ c2) {
std::swap(To[x][c1], To[x][c2]);
std::swap(ID[x][c1], ID[x][c2]);
}
}
for (valueType i = 1; i <= N_; ++i)
for (valueType j = 1; j <= D_; ++j)
if (ID[i][j] != -1)
Color[ID[i][j]] = j;
}
valueType GetColor(valueType i) const {
return Color[i];
}
valueType GetDegree(valueType u) const {
return Degree[u];
}
ValuePair GetEdge(valueType id) const {
return edges[id];
}
valueType N() const {
return N_;
}
valueType M() const {
return M_;
}
valueType D() const {
return D_;
}
};
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
std::cout.tie(nullptr);
valueType N, M, K, T;
std::cin >> N >> M >> K >> T;
BipartiteGraph Graph(N + K, M + K, K);
ValueVector LeftDegree(N + K, 0), RightDegree(M + K + 1, 0);
ValueVector LeftID(N + 1, 0), RightID(M + 1, 0);
valueType LeftCount = N, RightCount = M;
std::iota(LeftID.begin(), LeftID.end(), 0);
std::iota(RightID.begin(), RightID.end(), 0);
for (valueType i = 0; i < K; ++i) {
valueType u, v;
std::cin >> u >> v;
if (LeftDegree[u] % T == 0)
LeftID[u] = ++LeftCount;
if (RightDegree[v] % T == 0)
RightID[v] = ++RightCount;
++LeftDegree[u];
++RightDegree[v];
Graph.addEdge(LeftID[u], RightID[v]);
}
Graph.solve();
valueType ans = 0;
for (valueType i = 1; i <= N; ++i)
if (LeftDegree[i] % T != 0)
++ans;
for (valueType i = 1; i <= M; ++i)
if (RightDegree[i] % T != 0)
++ans;
std::cout << ans << std::endl;
for (valueType i = 0; i < K; ++i)
std::cout << Graph.GetColor(i) << ' ';
std::cout << std::endl;
return 0;
}
CF1240F Football
一定存在一组方案使得所有比赛均举行,下面给出一种构造方法。
考虑该问题的弱化版,若给出的图为二分图该如何处理,即 CF212A Privatization。
可以发现若给出的图为二分图那么我们可以将每个节点的颜色出现次数极差控制在 \(1\) 以内,因此我们考虑将原图的一般图转化为二分图。
考虑建出左右部各有 \(n\) 个点的二分图,对于边 \(\left(u, v\right)\),我们将其任意定向,例如要求其连接的是左部的 \(u\) 和右部的 \(v\)。
进而可以发现对于二分图中的每个节点我们可以将其颜色出现次数极差不超过 \(1\)。那么我们将每个节点拆分出的两个节点合并后可以得到其颜色出现次数极差一定不超过 \(2\),可以发现其满足题目要求。
复杂度为 \(\mathcal{O}(\left(n + \frac{m}{k}\right)m)\)。
Code
#include <bits/stdc++.h>
typedef int valueType;
typedef std::vector<valueType> ValueVector;
typedef std::vector<ValueVector> ValueMatrix;
typedef std::pair<valueType, valueType> ValuePair;
typedef std::vector<ValuePair> PairVector;
class BipartiteGraph {
private:
valueType A_, B_, N_, M_, D_;
ValueVector Degree;
PairVector edges;
ValueVector Color;
public:
BipartiteGraph() = default;
BipartiteGraph(valueType a, valueType b, valueType m) : A_(a), B_(b), N_(a + b), M_(m), Degree(N_ + 1, 0), Color(m) {
edges.reserve(m);
}
valueType addEdge(valueType u, valueType v) {
edges.emplace_back(u, v + A_);
++Degree[u];
++Degree[v + A_];
return edges.size() - 1;
}
void solve() {
D_ = *std::max_element(Degree.begin(), Degree.end());
ValueMatrix To(N_ + 1, ValueVector(D_ + 1, -1)), ID(N_ + 1, ValueVector(D_ + 1, -1));
for (valueType m = 0; m < M_; ++m) {
auto const &[u, v] = edges[m];
valueType c1 = 1, c2 = 1;
while (To[u][c1] != -1)
++c1;
while (To[v][c2] != -1)
++c2;
To[u][c1] = v;
To[v][c2] = u;
ID[u][c1] = m;
ID[v][c2] = m;
if (c1 == c2)
continue;
for (valueType c = c2, x = v; x != -1; x = To[x][c], c ^= c1 ^ c2) {
std::swap(To[x][c1], To[x][c2]);
std::swap(ID[x][c1], ID[x][c2]);
}
}
for (valueType i = 1; i <= N_; ++i)
for (valueType j = 1; j <= D_; ++j)
if (ID[i][j] != -1)
Color[ID[i][j]] = j;
}
valueType GetColor(valueType i) const {
return Color[i];
}
valueType GetDegree(valueType u) const {
return Degree[u];
}
ValuePair GetEdge(valueType id) const {
return edges[id];
}
valueType N() const {
return N_;
}
valueType M() const {
return M_;
}
valueType D() const {
return D_;
}
};
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
std::cout.tie(nullptr);
valueType N, M, K;
std::cin >> N >> M >> K;
{
valueType W;
for (valueType i = 1; i <= N; ++i)
std::cin >> W;
}
BipartiteGraph Graph(N + M, N + M, M);
ValueVector LeftDegree(N + M, 0), RightDegree(N + M + 1, 0);
ValueVector LeftID(N + 1, 0), RightID(N + 1, 0);
valueType LeftCount = N, RightCount = N;
std::iota(LeftID.begin(), LeftID.end(), 0);
std::iota(RightID.begin(), RightID.end(), 0);
for (valueType i = 0; i < M; ++i) {
valueType u, v;
std::cin >> u >> v;
if (LeftDegree[u] % K == 0)
LeftID[u] = ++LeftCount;
if (RightDegree[v] % K == 0)
RightID[v] = ++RightCount;
++LeftDegree[u];
++RightDegree[v];
Graph.addEdge(LeftID[u], RightID[v]);
}
Graph.solve();
for (valueType i = 0; i < M; ++i)
std::cout << Graph.GetColor(i) << '\n';
std::cout << std::endl;
return 0;
}
UOJ #444. 【集训队作业2018】二分图
考虑继续沿用上述的拆点后进行最小边染色的策略,发现重点在于如何处理删边操作。
发现若在删边时直接将边从图中删去那么得到的方案仍然满足为二分图的最小边染色。但是这可能导致某个节点的度数变化进而不符合拆点的方案。同时我们观察到对于原图的某个节点,删除最后一条与其相连边一定不会影响拆点方案。这启示我们在删除边的过程中将要删除的边与其相联通的点的边集的最后一条进行互换后删除即可。
复杂度为 \(\mathcal{O}(Q\left(n + \frac{n^2}{k}\right))\)。
Code
#include "graph.h"
#include <bits/stdc++.h>
typedef int valueType;
typedef std::vector<valueType> ValueVector;
typedef std::vector<ValueVector> ValueMatrix;
typedef std::pair<valueType, valueType> ValuePair;
typedef std::vector<ValuePair> PairVector;
typedef std::vector<PairVector> PairMatrix;
typedef std::array<ValueVector, 2> TwoValueVector;
typedef std::array<ValueMatrix, 2> TwoValueMatrix;
typedef std::vector<bool> bitset;
typedef std::vector<bitset> BitMatrix;
static valueType N, M, K, Q, S;
static TwoValueVector Degree;
static TwoValueMatrix To;
static PairMatrix Edge;
static TwoValueMatrix Graph;
static BitMatrix Exist;
static ValueMatrix Color;
void Init(int n, int k, int q) {
N = n;
M = N / k + 1;
S = (N + 1) * M;
K = k;
Q = q;
Degree[0].resize(S + 1, 0);
Degree[1].resize(S + 1, 0);
To[0].resize(S + 1, ValueVector(K + 1, -1));
To[1].resize(S + 1, ValueVector(K + 1, -1));
Edge.resize(N + 1, PairVector(N + 1, ValuePair(-1, -1)));
Graph[0].resize(S + 1);
Graph[1].resize(S + 1);
Exist.resize(N + 1, bitset(N + 1, false));
Color.resize(N + 1, ValueVector(N + 1, -1));
}
void Insert(valueType x, valueType y) {
valueType u = x * M, v = y * M;
while (Degree[0][u] == K)
++u;
while (Degree[1][v] == K)
++v;
Edge[x][y] = ValuePair(u, v);
++Degree[0][u];
++Degree[1][v];
valueType c1 = 1, c2 = 1;
while (To[0][u][c1] != -1)
++c1;
while (To[1][v][c2] != -1)
++c2;
if (c1 == c2) {
Color[x][y] = c1;
To[0][u][c1] = v;
To[1][v][c2] = u;
return;
}
valueType Pos = 0;
while (true) {
valueType const next = To[Pos ^ 1][v][c1];
To[Pos][u][c1] = v;
To[Pos ^ 1][v][c1] = u;
if (Pos == 0) {
Color[u / M][v / M] = c1;
} else {
Color[v / M][u / M] = c1;
}
if (next == -1) {
To[Pos ^ 1][v][c2] = -1;
break;
}
std::swap(c1, c2);
Pos ^= 1;
u = v;
v = next;
}
}
void Remove(valueType x, valueType y) {
auto &[u, v] = Edge[x][y];
valueType const c = Color[x][y];
--Degree[0][u];
--Degree[1][v];
assert(To[0][u][c] == v);
assert(To[1][v][c] == u);
To[0][u][c] = -1;
To[1][v][c] = -1;
Color[x][y] = -1;
u = -1;
v = -1;
}
void Rebuild(valueType pos, valueType x, valueType y) {
valueType const back = Graph[pos][x].back();
Graph[pos][x].pop_back();
if (y == back)
return;
for (auto &v : Graph[pos][x])
if (v == y)
v = back;
if (pos == 0) {
Remove(x, back);
Insert(x, back);
} else {
Remove(back, x);
Insert(back, x);
}
}
Division Modify(int x, int y) {
if (Exist[x][y]) {
Exist[x][y] = false;
Remove(x, y);
Rebuild(0, x, y);
Rebuild(1, y, x);
} else {
Exist[x][y] = true;
Graph[0][x].push_back(y);
Graph[1][y].push_back(x);
Insert(x, y);
}
Division result;
memset(result.color, 0, sizeof(result.color));
for (valueType i = 1; i <= N; ++i)
for (valueType j = 1; j <= N; ++j)
if (Color[i][j] != -1)
result.color[i][j] = Color[i][j];
return result;
}
CF547D Mike and Fish
考虑将横纵坐标视作节点,坐标系中的点视作边,这样我们就得到了一个二分图,需要进行如下操作:
将边染为两个颜色中的一种,满足对于每个点,与其相邻的边集中两种颜色出现数量之差不超过 \(1\)。
若继续沿用上述拆点的思路的话复杂度无法接受。
考虑利用颜色种类数为 \(2\) 的特殊性质去处理。
发现若某个无向图中存在欧拉回路,那么对其进行定向后每个点的出度和入度相等,因此可以按边的方向进行染色即可。
因此若该图的所有节点度数均为偶数,那么一定可以构造出一组颜色出现数量极差均为 \(0\) 的解。
一个直观的想法是对于所有度数为奇数的节点,将其一条边删除,但是这可能导致一个度数为偶数的节点边为度数为奇数的节点进而一个节点相连的边被删除多次导致不满足要求。
我们考虑额外创建一个虚拟节点,将所有度数为奇数的节点均连一条到其的边。
由于无向图节点度数之和为偶数,因此度数为奇数的点个数为偶数,进而虚拟节点的度数也为偶数,因此该图中存在欧拉回路,进而可以解决该问题。
Code
#include <bits/stdc++.h>
typedef int valueType;
typedef std::vector<valueType> ValueVector;
typedef std::vector<ValueVector> ValueMatrix;
typedef std::pair<valueType, valueType> ValuePair;
typedef std::vector<ValuePair> PairVector;
typedef std::vector<PairVector> PairMatrix;
typedef std::vector<int> bitset;
class EulerCircuit {
private:
valueType N_, M_;
bool Directed_;
bitset visited_, exist_;
ValueVector InDegree, OutDegree;
PairMatrix G_;
ValueVector path_;
public:
EulerCircuit() = default;
EulerCircuit(valueType n, valueType m, bool Directed) : N_(n), M_(m), Directed_(Directed), visited_(N_ + 1, false), exist_(M_ + 1, false), InDegree(N_ + 1, 0), OutDegree(N_ + 1, 0), G_(N_ + 1) {
path_.reserve(M_);
}
void AddEdge(valueType u, valueType v, valueType k) {
G_[u].emplace_back(v, k);
++OutDegree[u];
++InDegree[v];
if (!Directed_)
G_[v].emplace_back(u, -k);
}
private:
void dfs(valueType x) {
visited_[x] = true;
while (!G_[x].empty()) {
auto const [to, id] = G_[x].back();
G_[x].pop_back();
int const abs = std::abs(id);
if (exist_[abs])
continue;
exist_[abs] = true;
dfs(to);
path_.emplace_back(id);
}
}
public:
std::pair<bool, ValueVector> Solve() {
if (M_ == 0)
return std::make_pair(true, ValueVector());
if (Directed_) {
for (valueType i = 1; i <= N_; ++i) {
if (InDegree[i] != OutDegree[i])
return std::make_pair(false, ValueVector());
}
} else {
for (valueType i = 1; i <= N_; ++i) {
if ((InDegree[i] + OutDegree[i]) & 1)
return std::make_pair(false, ValueVector());
}
}
for (valueType i = 1; i <= N_; ++i)
if (!G_[i].empty())
dfs(i);
for (valueType i = 1; i <= N_; ++i) {
if ((InDegree[i] + OutDegree[i] > 0) && !visited_[i])
return std::make_pair(false, ValueVector());
}
std::reverse(path_.begin(), path_.end());
return std::make_pair(true, path_);
}
};
constexpr valueType V = 2e5 + 10;
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
std::cout.tie(nullptr);
valueType N;
std::cin >> N;
EulerCircuit Graph(2 * V + 10, N + 2 * V + 100, false);
valueType const Root = 2 * V + 5;
ValueVector X(V + 1, 0), Y(V + 1, 0);
for (valueType i = 1; i <= N; ++i) {
valueType x, y;
std::cin >> x >> y;
Graph.AddEdge(x, y + V, i);
++X[x];
++Y[y];
}
for (valueType i = 1; i <= V; ++i) {
if (X[i] & 1)
Graph.AddEdge(Root, i, N + (i << 1));
if (Y[i] & 1)
Graph.AddEdge(Root, i + V, N + (i << 1 | 1));
}
auto [ok, path] = Graph.Solve();
bitset IsRed(N + 1, false);
for (auto const &x : path) {
if (std::abs(x) > N)
continue;
IsRed[std::abs(x)] = x > 0;
}
for (valueType i = 1; i <= N; ++i)
std::cout << (IsRed[i] ? 'r' : 'b');
std::cout << std::endl;
return 0;
}
CF1499G Graph Coloring
考虑该题的弱化版:仅要求构造一次方案。不难发现问题实际上是:
将边染为两个颜色中的一种,满足对于每个点,与其相邻的边集中两种颜色出现数量之差不超过 \(1\)。
如果进行二分图边染色的话复杂度无法接受。
考虑利用颜色种类数为 \(2\) 的特殊性质去处理。
发现若某个无向图中存在欧拉回路,那么对其进行定向后每个点的出度和入度相等,因此可以按边的方向进行染色即可。
因此若该图的所有节点度数均为偶数,那么一定可以构造出一组颜色出现数量极差均为 \(0\) 的解。
考虑为什么找到欧拉回路并进行定向后一定可以使得颜色出现数量极差均为 \(0\),发现实质在于对于欧拉回路中的每个环上的节点,其在环上的每次出现必定导致了其出度和入度同时增加 \(1\)。因此我们退而求其次,考虑维护所有的路径,要求其上的节点颜色交替出现,并在可以合并为环的时候进行合并,由于二分图的性质,因此不会出现奇环。
由于要求每个节点的颜色出现数量极差为 \(1\),这也就要求了每个节点最多作为一个路径的端点,若其为两个路径的端点那么可以对这两条路径进行合并。
对于路径的合并可以使用启发式合并来保证复杂度。显然只有新加入边才会使得路径合并,下面讨论在加入边 \(\left(x, y\right)\) 后可能出现的几种情况。
- \(x\) 和 \(y\) 均不是任何路径的端点:将边 \(\left(x, y\right)\) 视作路径 \(x \rightarrow y\) 即可。
- \(x\) 或 \(y\) 中恰好一个节点是某条路径的端点:将边 \(\left(x, y\right)\) 与那条路径合并即可。
- \(x\) 和 \(y\) 均是某条路径的端点:先将边 \(\left(x, y\right)\) 与一条路径合并,接下来将两条路径合并即可。
下面讨论合并路径 \(P, Q\) 时可能出现的几种情况。下文假设 \(\left\lvert P \right\rvert \le \left\lvert Q \right\rvert\)。
- 首先我们可以通过交换 \(P, Q\) 的位置或将 \(P\) 翻转使得 \(P, Q\) 存在公共端点。
- 若 \(P, Q\) 连接公共端点的边颜色相同,那么翻转 \(P\) 中的每一条边的颜色。
- 接下来可以进行路径的合并,具体的,将 \(P\) 中的边移动进 \(Q\) 的起始或末尾即可。同时维护路径的端点和端点对应的路径。
由于需要支持路径的首尾修改和随机访问,因此可以使用 std::deque
维护路径。
复杂度为 \(\mathcal{O}(\left(m + q\right) \log \left(m + q\right) + n_1 + n_2)\)。
Code
#include <bits/stdc++.h>
typedef int valueType;
typedef std::vector<valueType> ValueVector;
typedef std::vector<ValueVector> ValueMatrix;
typedef std::pair<valueType, valueType> ValuePair;
typedef std::vector<ValuePair> PairVector;
typedef std::deque<ValuePair> PairDeque;
typedef std::set<valueType> ValueSet;
namespace MODINT_WITH_FIXED_MOD {
constexpr valueType MOD = 998244353;
template<typename T1, typename T2>
void Inc(T1 &a, T2 b) {
a = a + b;
if (a >= MOD)
a -= MOD;
}
template<typename T1, typename T2>
void Dec(T1 &a, T2 b) {
a = a - b;
if (a < 0)
a += MOD;
}
template<typename T1, typename T2>
T1 sum(T1 a, T2 b) {
return a + b >= MOD ? a + b - MOD : a + b;
}
template<typename T1, typename T2>
T1 sub(T1 a, T2 b) {
return a - b < 0 ? a - b + MOD : a - b;
}
template<typename T1, typename T2>
T1 mul(T1 a, T2 b) {
return (long long) a * b % MOD;
}
template<typename T1, typename T2>
void Mul(T1 &a, T2 b) {
a = (long long) a * b % MOD;
}
template<typename T1, typename T2>
T1 pow(T1 a, T2 b) {
T1 result = 1;
while (b > 0) {
if (b & 1)
Mul(result, a);
Mul(a, a);
b = b >> 1;
}
return result;
}
} // namespace MODINT_WITH_FIXED_MOD
using namespace MODINT_WITH_FIXED_MOD;
valueType N, S, M, Q, ans;
ValueSet EdgeSet;
ValueVector Pow2;
class Path {
protected:
valueType S_, T_;
PairDeque data_;
public:
Path() = default;
Path(valueType s, valueType t, valueType k, valueType c = 0) : S_(s), T_(t), data_({ValuePair(c, k)}) {
Inc(ans, mul(c, Pow2[k]));
if (c == 1)
EdgeSet.insert(k);
};
public:
void Flip() {
for (auto &[color, weight] : data_) {
if (color == 1) {
Dec(ans, mul(color, Pow2[weight]));
EdgeSet.erase(weight);
}
color ^= 1;
if (color == 1) {
Inc(ans, mul(color, Pow2[weight]));
EdgeSet.insert(weight);
}
}
}
void Reverse() {
std::reverse(data_.begin(), data_.end());
std::swap(S_, T_);
}
valueType size() const {
return data_.size();
}
bool empty() const {
return this->size() == 0;
}
valueType S() const {
return S_;
}
valueType T() const {
return T_;
}
public:
friend void MergeLeftToRight(Path &Left, Path &Right) {
if (Left.empty())
return;
assert(Left.S() == Right.S() || Left.T() == Right.S());
if (Left.S() == Right.S())
Left.Reverse();
if (Left.data_.back().first == Right.data_.front().first)
Left.Flip();
assert(Left.T_ == Right.S_);
Right.data_.insert(Right.data_.begin(), Left.data_.begin(), Left.data_.end());
Right.S_ = Left.S_;
}
friend void MergeRightToLeft(Path &Left, Path &Right) {
if (Right.empty())
return;
assert(Right.S() == Left.T() || Right.T() == Left.T());
if (Left.T() == Right.T())
Right.Reverse();
if (Left.data_.back().first == Right.data_.front().first)
Right.Flip();
assert(Left.T_ == Right.S_);
Left.data_.insert(Left.data_.end(), Right.data_.begin(), Right.data_.end());
Left.T_ = Right.T_;
}
};
typedef std::vector<Path> PathVector;
ValueVector Belong;
PathVector Paths;
valueType Merge(valueType x, valueType y) {
if (x == -1)
return y;
if (y == -1)
return x;
if (Paths[x].size() < Paths[y].size())
std::swap(x, y);
auto &u = Paths[x], &v = Paths[y];
Belong[u.S()] = -1;
Belong[u.T()] = -1;
Belong[v.S()] = -1;
Belong[v.T()] = -1;
if (u.T() == v.S() || u.T() == v.T())
MergeRightToLeft(u, v);
else
MergeLeftToRight(v, u);
if (u.S() != u.T()) {
Belong[u.S()] = x;
Belong[u.T()] = x;
}
return x;
}
void Connect(valueType x, valueType y, valueType k) {
y += N;
if (Belong[x] == -1 && Belong[y] == -1) {
Paths.emplace_back(x, y, k);
Belong[x] = Paths.size() - 1;
Belong[y] = Paths.size() - 1;
} else if (Belong[x] == -1) {
Paths.emplace_back(x, y, k);
Belong[x] = Paths.size() - 1;
Merge(Belong[x], Belong[y]);
} else if (Belong[y] == -1) {
Paths.emplace_back(x, y, k);
Belong[y] = Paths.size() - 1;
Merge(Belong[x], Belong[y]);
} else {
Paths.emplace_back(x, y, k);
valueType const id = Paths.size() - 1;
valueType const L = Belong[x];
Merge(L, Merge(id, Belong[y]));
}
}
void Print() {
std::cout << EdgeSet.size() << ' ';
for (auto const &x : EdgeSet)
std::cout << x << ' ';
std::cout << std::endl;
}
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
std::cout.tie(nullptr);
valueType A, B;
std::cin >> A >> B >> M;
N = A;
S = A + B;
Belong.resize(S + 1, -1);
static constexpr valueType const MAXM = 400000;
Pow2.resize(MAXM + 1);
Pow2[0] = 1;
for (valueType i = 1; i <= MAXM; ++i)
Pow2[i] = mul(2, Pow2[i - 1]);
for (valueType i = 1; i <= M; ++i) {
valueType x, y;
std::cin >> x >> y;
Connect(x, y, i);
}
std::cin >> Q;
for (valueType q = 0; q < Q; ++q) {
valueType type;
std::cin >> type;
if (type == 1) {
++M;
valueType x, y;
std::cin >> x >> y;
Connect(x, y, M);
std::cout << ans << std::endl;
} else {
assert(type == 2);
Print();
}
}
std::cout << std::flush;
return 0;
}
CF429E Points and Segments
首先我们可以将所有线段改写为 \(\left[l, r + 1\right)\) 的形式,然后将所有线段的 \(l\) 和 \(r + 1\) 放入一个集合中我们可以得到 \(k\) 个关键点。不妨设为 \(x_1, x_2, \cdots, x_k\),那么我们可以将坐标轴划分为 \(k + 1\) 个区间,依次为 \(\left[0, x_1\right), \left[x_1, x_2\right), \left[x_2, x_3\right), \cdots, \left[x_{k - 1}, x_k\right), \left[x_k, \infty\right)\)。不难发现对于每个区间内的所有点覆盖其的线段集合均相同,因此每个区间取一个点进行考虑即可。其中 \(\left[0, x_1\right)\) 内的点一定合法,故只需要考虑后 \(k\) 个区间内的点即可。
考虑对于某个线段 \(\left[l, r\right)\),设其覆盖的关键点集合为 \(\left\{x_i, x_{i + 1}, x_{i + 2}, \cdots, x_j\right\}\),那么我们连边 \(\left(i, j + 1\right)\)。
考虑我们首先将所有线段先染为同一种颜色,进而对于线段 \(\left[l, r\right)\),设其建出的边为 \(\left(i, j + 1\right)\),考虑关键点颜色数的差的差分序列,不难发现该边在 \(i\) 的位置为 \(1\),在 \(j + 1\) 的位置为 \(-1\)。我们的目的是通过翻转一些边的权值使得对差分序列进行前缀和后极值的绝对值不超过 \(1\)。
考虑最优情况,即我们可以使得颜色数的差均为 \(0\),即在差分序列上每个点产生贡献的 \(1\) 的数量和 \(-1\) 的数量相等。这要求每个点的度数均为偶数,那么可以发现求出其欧拉回路后定向边的方案和合法的翻转边的方案对应。
因此若此图的度数均为偶数,求出其欧拉回路并对边定向后即可得出一组方案。
对于存在节点度数为奇数的情况,我们考虑以对于每个节点至多添加一条边的代价将其转化为节点度数均为偶数的图,由于无向图节点度数之和为偶数,因此度数为奇数的点个数为偶数,因此我们可以将奇数度数的节点两两配对并连边即可。也可以选择新建一个虚拟节点并向其连边。
复杂度 \(\mathcal{O}(n \log n)\),瓶颈在于排序。
Code
#include <bits/stdc++.h>
typedef int valueType;
typedef std::vector<valueType> ValueVector;
typedef std::vector<ValueVector> ValueMatrix;
typedef std::pair<valueType, valueType> ValuePair;
typedef std::vector<ValuePair> PairVector;
typedef std::vector<PairVector> PairMatrix;
typedef std::vector<int> bitset;
class EulerCircuit {
private:
valueType N_, M_;
bool Directed_;
bitset visited_, exist_;
ValueVector InDegree, OutDegree;
PairMatrix G_;
ValueVector path_;
public:
EulerCircuit() = default;
EulerCircuit(valueType n, valueType m, bool Directed) : N_(n), M_(m), Directed_(Directed), visited_(N_ + 1, false), exist_(M_ + 1, false), InDegree(N_ + 1, 0), OutDegree(N_ + 1, 0), G_(N_ + 1) {
path_.reserve(M_);
}
void AddEdge(valueType u, valueType v, valueType k) {
G_[u].emplace_back(v, k);
++OutDegree[u];
++InDegree[v];
if (!Directed_)
G_[v].emplace_back(u, -k);
}
private:
void dfs(valueType x) {
visited_[x] = true;
while (!G_[x].empty()) {
auto const [to, id] = G_[x].back();
G_[x].pop_back();
int const abs = std::abs(id);
if (exist_[abs])
continue;
exist_[abs] = true;
dfs(to);
path_.emplace_back(id);
}
}
public:
std::pair<bool, ValueVector> Solve() {
if (M_ == 0)
return std::make_pair(true, ValueVector());
if (Directed_) {
for (valueType i = 1; i <= N_; ++i) {
if (InDegree[i] != OutDegree[i])
return std::make_pair(false, ValueVector());
}
} else {
for (valueType i = 1; i <= N_; ++i) {
if ((InDegree[i] + OutDegree[i]) & 1)
return std::make_pair(false, ValueVector());
}
}
for (valueType i = 1; i <= N_; ++i)
if (!G_[i].empty())
dfs(i);
for (valueType i = 1; i <= N_; ++i) {
if ((InDegree[i] + OutDegree[i] > 0) && !visited_[i])
return std::make_pair(false, ValueVector());
}
std::reverse(path_.begin(), path_.end());
return std::make_pair(true, path_);
}
};
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
std::cout.tie(nullptr);
valueType N;
std::cin >> N;
ValueVector Pool;
PairVector Segments(N);
for (auto &[l, r] : Segments) {
std::cin >> l >> r;
++r;
Pool.push_back(l);
Pool.push_back(r);
}
Pool.push_back(-1);
std::sort(Pool.begin(), Pool.end());
Pool.erase(std::unique(Pool.begin(), Pool.end()), Pool.end());
valueType const M = Pool.size();
EulerCircuit Graph(M, N + M, false);
ValueVector Degree(M + 1, 0);
Segments.emplace(Segments.begin(), -1, -1);
for (valueType i = 1; i <= N; ++i) {
auto &[l, r] = Segments[i];
l = std::distance(Pool.begin(), std::lower_bound(Pool.begin(), Pool.end(), l));
r = std::distance(Pool.begin(), std::lower_bound(Pool.begin(), Pool.end(), r));
Graph.AddEdge(l, r, i);
++Degree[l];
++Degree[r];
}
ValueVector OddDegree;
OddDegree.reserve(M);
for (valueType i = 1; i <= M; ++i) {
if (Degree[i] & 1)
OddDegree.push_back(i);
}
for (valueType i = 1; i < OddDegree.size(); i += 2)
Graph.AddEdge(OddDegree[i - 1], OddDegree[i], N + i);
auto [ok, path] = Graph.Solve();
assert(ok);
bitset Color(N + 1, false);
for (auto const &x : path) {
if (std::abs(x) > N)
continue;
Color[std::abs(x)] = x > 0;
}
for (valueType i = 1; i <= N; ++i)
std::cout << (Color[i] ? 1 : 0) << ' ';
std::cout << std::endl;
}