题解:AT_abc317_g [ABC317G] Rearranging
二分图边着色
对于一个无向图,定义一个着色方案为给每一条边进行染色,使得每一个点的所有邻边颜色不同。记这样需要的最少颜色数量为
Vizing 定理:
设图
G 中具有最大度数的点的度数为d(G) ,则有d(G) \le \chi(G) \le d(G) + 1 。称左侧取等的为一类图,右侧取等的为二类图。对于一般图,判别它属于哪一类是 NP 困难的。但是,所有的二分图都属于一类图。
对于二分图有构造性证明。
设二分图的左右部为
X, Y 。考虑从无边图开始,依次往图中加入一条边,并调整图的着色。我们给每种颜色进行标号。当我们加入边
(x, y) 时,设c_x, c_y 分别为x, y 邻边中没有出现的最小颜色(就是颜色中的\operatorname{mex} )。如果
c_x = c_y ,那么将这条边的颜色设为c_x 即可。否则假设
c_x \lt c_y ,我们需要将与y 相邻的c_x 边颜色改为c_y 。对此,我们可以找最长的一条增广路y - x_1 - y_1 - x_2 - y_2 - \dots ,满足y_i - x_{i + 1} 边颜色为c_x ,x_i - y_i 边颜色为c_y 。这样的增广路显然是唯一的,并且不包含x (因为x 没有c_x 边,自然不会有某个y_i 连向它)。然后,我们将这些边的颜色翻转即可。然后就可以在
c_x, c_y 之间连边了。容易发现
c_x, c_y 在x, y 度数还没达到d(G) 之前都会\le d(G) ,因此最终用到的颜色数量是d(G) 。\square
:::info[模板题与代码]{open}
CF600F Edge coloring of bipartite graph
真的就是模板。
#include <iostream>
#include <cstring>
using namespace std;
#define MAXN 2005
#define MAXM 200005
struct Edge
{
int u, v;
int col;
} e[MAXM];
int idx = 1;
int h[MAXN], d[MAXN];
int fr[MAXN][MAXN]; // fr[i][c] 表示点 i 邻边中颜色为 c 的那条
int res[MAXN][MAXN];
void add(int u, int v)
{
e[++idx] = {h[u], v};
h[u] = idx;
d[u]++;
e[++idx] = {h[v], u};
h[v] = idx;
d[v]++;
}
int a, b, m;
void dfs(int p, int o, int n) // 寻找增广路,将 p 中颜色为 o 的出边改成 n
{
if (!fr[p][o])
{
fr[p][n] = 0;
return;
}
int ei = fr[p][o];
int u = e[ei].v;
dfs(u, n, o);
fr[p][n] = ei;
fr[u][n] = ei ^ 1;
e[ei].col = e[ei ^ 1].col = n;
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cin >> a >> b >> m;
for (int i = 1; i <= m; i++)
{
int u, v;
cin >> u >> v;
add(u, v + a);
}
int mx = 0;
for (int i = 1; i <= a + b; i++)
{
mx = max(mx, d[i]);
}
cout << mx << '\n';
for (int i = 2; i < idx; i += 2)
{
int u = e[i].v, v = e[i ^ 1].v;
int lu = 1, lv = 1;
while (fr[u][lu])
{
lu++;
}
while (fr[v][lv])
{
lv++;
}
if (lu < lv)
{
dfs(v, lu, lv);
}
if (lv < lu)
{
dfs(u, lv, lu);
}
int l = min(lu, lv);
fr[u][l] = i ^ 1;
fr[v][l] = i;
e[i].col = e[i ^ 1].col = l;
}
for (int i = 2; i < idx; i += 2)
{
cout << e[i].col << ' ';
}
cout << '\n';
return 0;
}
UVA10615 Rooks
另一个模板题,边以邻接矩阵的形式给出。
#include <iostream>
#include <cstring>
using namespace std;
#define MAXN 305
#define MAXM 100005
// Vizing Theorem: Chi(G) = max Deg(i)
struct Edge
{
int u, v;
int col;
} e[MAXM];
int idx = 1;
int h[MAXN], d[MAXN];
int fr[MAXN][MAXN];
int res[MAXN][MAXN];
void add(int u, int v)
{
e[++idx] = {h[u], v};
h[u] = idx;
d[u]++;
e[++idx] = {h[v], u};
h[v] = idx;
d[v]++;
}
int n;
void dfs(int p, int o, int n)
{
if (!fr[p][o])
{
fr[p][n] = 0;
return;
}
int ei = fr[p][o];
int u = e[ei].v;
dfs(u, n, o);
fr[p][n] = ei;
fr[u][n] = ei ^ 1;
e[ei].col = e[ei ^ 1].col = n;
}
void solve()
{
memset(e, 0, sizeof(e));
memset(h, 0, sizeof(h));
memset(d, 0, sizeof(d));
memset(fr, 0, sizeof(fr));
memset(res, 0, sizeof(res));
idx = 1;
cin >> n;
for (int i = 1; i <= n; i++)
{
for (int j = n + 1; j <= 2 * n; j++)
{
char ch;
cin >> ch;
if (ch == '*')
{
add(i, j);
}
}
}
int mx = 0;
for (int i = 1; i <= 2 * n; i++)
{
mx = max(mx, d[i]);
}
cout << mx << '\n';
for (int i = 2; i < idx; i += 2)
{
int u = e[i].v, v = e[i ^ 1].v;
int lu = 1, lv = 1;
while (fr[u][lu])
{
lu++;
}
while (fr[v][lv])
{
lv++;
}
if (lu < lv)
{
dfs(v, lu, lv);
}
if (lv < lu)
{
dfs(u, lv, lu);
}
int l = min(lu, lv);
fr[u][l] = i ^ 1;
fr[v][l] = i;
e[i].col = e[i ^ 1].col = l;
}
for (int i = 2; i < idx; i += 2)
{
int u = e[i].v - n, v = e[i ^ 1].v;
res[v][u] = e[i].col;
}
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= n; j++)
{
cout << res[i][j] << ' ';
}
cout << '\n';
}
}
int main()
{
int t;
cin >> t;
while (t--)
{
solve();
}
return 0;
}
:::
本题题解
每一列都是一个排列,自然想到这是一个完美匹配状物。
我们对行和数值建点,对于给定的
我们只需要将其拆成
:::info[正则二分图必有完美匹配的证明]
由 Hall 定理,一个二分图有完美当且仅当对于左部点的任意一个子集
在正则二分图中,
:::
至于怎么求,显然我们对这个图进行边着色即可。
:::success[代码]
#include <iostream>
using namespace std;
#define MAXN 205
#define MAXM 100005
int n, m;
struct Edge
{
int u, v, rc;
int col;
} e[MAXM];
int h[MAXN];
int idx = 1;
int fr[MAXN][MAXN], ed[MAXN][MAXN];
void add(int u, int v, int rc)
{
e[++idx] = {h[u], v, rc};
h[u] = idx;
e[++idx] = {h[v], u, rc};
h[v] = idx;
}
void dfs(int p, int f, int t)
{
if (!fr[p][f])
{
fr[p][t] = 0;
return;
}
int ei = fr[p][f];
int u = e[ei].v;
dfs(u, t, f);
fr[p][t] = ei;
fr[u][t] = ei ^ 1;
e[ei].col = e[ei ^ 1].col = t;
}
int a[MAXN][MAXN];
int main()
{
cin >> n >> m;
idx = 1;
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= m; j++)
{
int v;
cin >> v;
a[i][j] = v;
v += n;
add(i, v, j);
int ic = 1, vc = 1;
while (fr[i][ic])
{
ic++;
}
while (fr[v][vc])
{
vc++;
}
if (ic < vc)
{
dfs(v, ic, vc);
}
if (ic > vc)
{
dfs(i, vc, ic);
}
int c = min(ic, vc);
fr[i][c] = idx ^ 1;
fr[v][c] = idx;
e[idx].col = e[idx ^ 1].col = c;
}
}
cout << "Yes\n";
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= m; j++)
{
cout << a[i][e[fr[i][j]].rc] << ' ';
}
cout << '\n';
}
return 0;
}
:::
拓展
:::info[AGC037D]
题目链接:AGC037D Sorting a Grid
观察到在最后一步前,我们要使得同一行内的数都在
则第一步中,我们重排列行,使得每一列都是一个排列。
第二步,我们重排列列,使得每一列都是
第三步,我们重排列行,最终得到答案。
后面两步是容易的,第一步就是二分图着色。
#include <algorithm>
#include <iostream>
using namespace std;
#define MAXN 205
#define MAXM 100005
int n, m;
struct Edge
{
int u, v, rc;
int col;
} e[MAXM];
int h[MAXN];
int idx = 1;
int fr[MAXN][MAXN], ed[MAXN][MAXN];
void add(int u, int v, int rc)
{
e[++idx] = {h[u], v, rc};
h[u] = idx;
e[++idx] = {h[v], u, rc};
h[v] = idx;
}
void dfs(int p, int f, int t)
{
if (!fr[p][f])
{
fr[p][t] = 0;
return;
}
int ei = fr[p][f];
int u = e[ei].v;
dfs(u, t, f);
fr[p][t] = ei;
fr[u][t] = ei ^ 1;
e[ei].col = e[ei ^ 1].col = t;
}
int a[MAXN][MAXN], b[MAXN][MAXN], c[MAXN];
int main()
{
cin >> n >> m;
idx = 1;
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= m; j++)
{
int v;
cin >> v;
a[i][j] = v;
v = (v - 1) / m + 1 + n;
add(i, v, j);
int ic = 1, vc = 1;
while (fr[i][ic])
{
ic++;
}
while (fr[v][vc])
{
vc++;
}
if (ic < vc)
{
dfs(v, ic, vc);
}
if (ic > vc)
{
dfs(i, vc, ic);
}
int c = min(ic, vc);
fr[i][c] = idx ^ 1;
fr[v][c] = idx;
e[idx].col = e[idx ^ 1].col = c;
}
}
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= m; j++)
{
cout << (b[i][j] = a[i][e[fr[i][j]].rc]) << ' ';
}
cout << '\n';
}
for (int j = 1; j <= m; j++)
{
for (int i = 1; i <= n; i++)
{
c[i] = b[i][j];
}
sort(c + 1, c + n + 1);
for (int i = 1; i <= n; i++)
{
b[i][j] = c[i];
}
}
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= m; j++)
{
cout << b[i][j] << ' ';
}
cout << '\n';
}
return 0;
}
:::
:::info[P9070]
题目链接:P9070 [CTS2023] 琪露诺的符卡交换
如果我们有办法重排每一行,使得每一列都是一个排列的话,那么交换
因此边着色即可。
#include <iostream>
using namespace std;
#define MAXN 405
#define MAXM 200005
int n;
struct Edge
{
int u, v, rc;
int col;
} e[MAXM];
int h[MAXN];
int idx = 1;
int fr[MAXN][MAXN], ed[MAXN][MAXN];
void add(int u, int v, int rc)
{
e[++idx] = {h[u], v, rc};
h[u] = idx;
e[++idx] = {h[v], u, rc};
h[v] = idx;
}
void dfs(int p, int f, int t)
{
if(!fr[p][f])
{
fr[p][t] = 0;
return;
}
int ei = fr[p][f];
int u = e[ei].v;
dfs(u, t, f);
fr[p][t] = ei;
fr[u][t] = ei ^ 1;
e[ei].col = e[ei ^ 1].col = t;
}
void solve()
{
cin >> n;
for (int i = 1; i <= 2 * n; i++)
{
h[i] = 0;
for (int j = 1; j <= n; j++)
{
fr[i][j] = 0;
}
}
idx = 1;
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= n; j++)
{
int v;
cin >> v;
v += n;
add(i, v, j);
int ic = 1, vc = 1;
while (fr[i][ic])
{
ic++;
}
while (fr[v][vc])
{
vc++;
}
if (ic < vc)
{
dfs(v, ic, vc);
}
if (ic > vc)
{
dfs(i, vc, ic);
}
int c = min(ic, vc);
fr[i][c] = idx ^ 1;
fr[v][c] = idx;
e[idx].col = e[idx ^ 1].col = c;
}
}
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= n; j++)
{
ed[i][j] = e[fr[i][j]].rc;
}
}
cout << n * (n - 1) / 2 << '\n';
for (int i = 1; i < n; i++)
{
for (int j = i + 1; j <= n; j++)
{
cout << i << ' ' << ed[i][j] << ' ' << j << ' ' << ed[j][i] << '\n';
}
}
}
int main()
{
int t;
cin >> t;
while (t--)
{
solve();
}
return 0;
}
:::