题目描述

给定一个 $N$ 行 $M$ 列的方格表,每个小方格中都有一个数,所有的数恰好构成 $1 \sim N M$ 的一个排列。记第 $i$ 行第 $j$ 列的数为 $A_{i, j}$。

你需要通过下面三种操作将方格表中的数重新排列

  1. 首先,对于每一,你可以将该行的 $M$ 个数随意变换顺序。
  2. 然后,对于每一,你可以将该列的 $N$ 个数随意变换顺序。
  3. 最后,对于每一,你可以将该行的 $M$ 个数随意变换顺序。

当然,你希望在重排完成后,第 $i$ 行第 $j$ 列的数恰好是 $M \times \left( i - 1 \right) + j$

你需要输出一组方案,可以证明一定存在可行的方案

输入格式

第一行包含两个正整数 $N, M$ ($N, M \leq 100$),表示方格表的行数和列数。

接下来 $N$ 行,每行 $M$ 个正整数 $A_{i, j}$ ($1 \leq A_{i, j} \leq N M$),描述这个方格表。保证所有 $A_{i, j}$ 构成 $1 \sim N M$ 的一个排列。

输出格式

输出 $2 N$ 行,每行 $M$ 个整数。前 $N$ 行表示第 1 步完成后的方格表,后 $N$ 行表示第 2 步完成后的方格表,具体格式同输入格式。

题解

注意到前后都有 "行操作",因此我们不妨把每一行的元素看成一个 (无序的) 集合。

我们对两个方格表中的每行指定一种颜色,比如第 $i$ 行的颜色为 $c_i$,将其染为对应的颜色。于是,我们容易求出每个数起始颜色最终颜色

那么考虑第二次操作的每一列,相当于原先的 $N$ 个元素,它们的颜色是 $c_1, c_2, \cdots, c_N$ 的排列,而将它们重排后,颜色仍为 $c_1, c_2, \cdots, c_N$ 的排列。

也就是说,对于每一列,我们都需要在当前剩下的数中寻找 $N$ 个,使得它们的起始颜色最终颜色分别形成 $c_1, c_2, \cdots, c_N$ 的排列。

如果你们对这种排列问题比较敏感,立马可以发现这就是一个经典的二分图匹配模型。

我们在两部各建立 $N$ 个点,分别表示起始颜色点 $u_i$ 和最终颜色点 $v_i$。

然后对于一个数 $t$,设它的起始颜色为 $A$,最终颜色为 $B$ (注意这是已知的),我们在 $u_A$ 和 $v_B$ 之间连接一条边,这条边就表示数 $t$。

于是,每一列的 $N$ 个数就对应到这个二分图的一个完美匹配 ($1-$因子)。

考虑整张二分图,它每一部均有 $N$ 个点,共有 $N M$ 条边,我们需要找到它的 $M$ 个完美匹配 ($1-$因子)。换句话说,我们就需要找到这张二分图的一个 $1-$因子分解

那它是否有 $1-$因子分解呢?考虑每个点的度数,对于左部的一个点 $u_i$,它的每一条出边都表示第 $i$ 行的一个数,因此它共有 $M$ 条出边 $\Rightarrow \deg \left( u_i \right) = M$。于是,每个点的度数均为 $M$,从而二分图是 $M-$正则二分图

由 Hall 定理推论可知正则二分图一定存在 $1-$因子分解。于是,我们只需要暴力通过匈牙利算法找到 $M$ 组匹配,输出即可。

时间复杂度 $O \left( N^2 M^2 \right)$ (当然可以通过网络流或其它 $1-$因子分解算法达到 $O \left( N^{3/2} M^2 \right)$ 或者 $O \left( N^2 M \right)$)。

代码

#include <bits/stdc++.h>
#define EB emplace_back

typedef std::pair <int, int> pr;
typedef std::vector <pr> vecpr;
const int N = 108, N2 = 10054;

int R, C;
int A1[N2], A2[N2], ans1[N][N], ans2[N][N];
int matL[N], matR[N];
bool used[N];
vecpr G[N];

bool dfs(int x) {
	int y;
	for (const pr &p : G[x])
		if (!used[y = p.first])
			if (used[y] = true, !~matL[y] || dfs(matL[y]))
				return matL[y] = x, matR[x] = y, true;
	return false;
}

int main() {
	int i, j, x, n, match;
	scanf("%d%d", &R, &C);
	for (i = 0; i < R; ++i) {
		for (j = 0; j < C; ++j) scanf("%d", &x), A1[--x] = i;
		std::fill(A2 + i * C, A2 + (i + 1) * C, i);
	}
	for (i = 0; i < R * C; ++i) G[ A1[i] ].EB(A2[i], i + 1);
	for (match = 0; match < C; ++match) {
		memset(matL, -1, R << 2), memset(matR, -1, R << 2);
		for (i = 0; i < R; ++i) memset(used, false, R), assert( dfs(i) );
		for (i = 0; i < R; ++i)
			for (n = G[i].size(), j = 0; j < n; ++j)
				if (G[i][j].first == matR[i]) {
					ans1[i][match] = ans2[ matR[i] ][match] = G[i][j].second;
					G[i].erase(G[i].begin() + j); break;
				}
	}
	for (i = 0; i < R; ++i) for (j = 0; j < C; ++j) printf("%d%c", ans1[i][j], j == C - 1 ? 10 : 32);
	for (i = 0; i < R; ++i) for (j = 0; j < C; ++j) printf("%d%c", ans2[i][j], j == C - 1 ? 10 : 32);
	return 0;
}

坑1:二分图匹配 (匈牙利算法) 的时候注意 used[] 数组清空的时间,以保证复杂度。

坑2:输出时注意格式,不要把行和列搞混了。