[CF1713E] Cross Swapping
题目简介
给一个 \(n \times n\) 的矩阵,可以进行任意次操作。
每次操作可以任选一个数字 \(k \le n\) ,交换第 \(k\) 行与第 \(k\) 列,即:
// choose the number k
for(int i = 1; i <= n; ++i)
swap(a[i][k], a[k][i]);
要求最后矩阵的字典序最小,求字典序最小的矩阵。
题目中矩阵的字典序定义为以 \(a_{(i-1) \times n + j}\) 为序(从上往下每一行,从左往右),像比较字符串那样一个一个比较。
正解
考虑选择了 \(k\) 对某一个单独的位置 \(a_{i, j}\) 产生的影响。
如果 \(k\) 等于 \(i\) 或 \(j\),那么 \(a_{i, j}\) 与 \(a_{j, i}\) 交换,再选择一次 \(k\) 则影响消除(恢复原来的样子)。
那么其实 \(a_{i, j}\) 最多出现在两个位置,而且每一个 \(k\) 最多选择一次就够了。
按字典序影响从大到小来考虑,枚举右上角的上三角矩阵(因为位置对称的可以一起考虑到)。
如果 \(a_{i, j} < a_{j, i}\),那么 \(i\) 和 \(j\) 要么同时选,要么同时不选。
如果 \(a_{i, j} > a_{j, i}\),那么 \(i\) 和 \(j\) 选择的情况是相反的(一个 0 一个 1)。
如果 \(a_{i, j} = a_{j, i}\),其实对 \(i\) 和 \(j\) 之间选择的关系没有影响,无所谓的。
这个东西选和不选可以用并查集巧妙的维护,而且并查集还可以做到优先满足前面的约束。
如果 \(x\) 和 \(y\) 属于同一个集合,令 fa[find(x)] = find(y)。
否则令 fa[find(x)] = -find(y)。
但是要注意一下正负号,并查集的代码就按下面这样写:
int fa[N];
int find(int u) {
if(u < 0) return -find(-u);
return fa[u] == u ? u : fa[u] = find(fa[u]);
}
void merge(int u, int v) {
u = find(u), v = find(v);
if(abs(u) != abs(v)) {
if(u > 0) fa[u] = v;
else fa[-u] = -v;
}
}
代码
#include <bits/stdc++.h>
using namespace std;
const int N = 1005;
int a[N][N], fa[N];
int find(int u) {
if(u < 0) return -find(-u);
return fa[u] == u ? u : fa[u] = find(fa[u]);
}
void merge(int u, int v) {
u = find(u), v = find(v);
if(abs(u) != abs(v)) {
if(u > 0) fa[u] = v;
else fa[-u] = -v;
}
}
void solve() {
int n;
cin >> n;
for(int i = 1; i <= n; ++i) {
fa[i] = i;
for(int j = 1; j <= n; ++j)
cin >> a[i][j];
}
for(int i = 1; i <= n; ++i)
for(int j = 1; j <= n; ++j) {
if(a[i][j] < a[j][i]) merge(i, j);
if(a[i][j] > a[j][i]) merge(i, -j);
}
for(int i = 1; i <= n; ++i) {
if(find(i) < 0) continue;
for(int k = 1; k <= n; ++k)
swap(a[i][k], a[k][i]);
}
for(int i = 1; i <= n; ++i) {
for(int j = 1; j <= n; ++j)
cout << a[i][j] << ' ';
cout << '\n';
}
}
int main() {
ios::sync_with_stdio(false);
int T;
cin >> T;
while(T--) solve();
system("pause");
return 0;
}