题意梳理
- (n* n) 棋盘,m 个车,初始无两车同行同列;
- 单次操作:移动一个车任意横竖距离,移动后仍不能同行同列;
- 目标:让所有车放到主对角线 (i,i),求最小移动次数;
- 关键:(m<n),棋盘存在空行空列。
Problem - 1411C - Codeforces
一、为什么要把题目转换成图结构
-
原始棋盘限制 所有车不会同行、同列,也就是说每一个列数字只出现一次,每一个行数字只出现一次。 把列编号、行编号全部当成普通数字点,一辆车坐标 (x,y),就代表数字 x 和数字 y 存在绑定关系。
-
连通块等价含义 如果两个数字在同一个连通块里,代表块内所有行列上的车,依靠题目允许的移动规则,可以自由调换位置,没有永久冲突。 我们最终目标是让数字 k 的列匹配数字 k 的行,也就是位置 (k,k)。
-
两种连通块带来不同代价
- 链型块:块里点的数量 > 块里车的数量 没有互相卡死的情况,每一辆不在对角线上的车,只需要移动一次就能归位,没有额外开销。
- 环型块:块里点的数量 = 块里车的数量 环里所有车辆互相占用对方需要的行 / 列,直接移动一定会冲突。题目保证 m<n,棋盘存在空行列,必须先挪一辆车去空位中转,每一个环额外多消耗一次操作。
- 特殊车辆处理 如果一辆车坐标 (x,x),本身就在主对角线上,不需要移动,不参与建图、不计入移动总量。
二、文字符号定义(纯文本,可直接复制)
- R:全部车辆集合,每个元素是一对数字 (x,y) cnt:满足 x≠y 的车辆总数量,每一台基础消耗 1 次移动
R(x≠y) = cnt
-
G:仅由 x≠y 的车辆构建的无向图 S:图 G 里任意一个连通块 |S|:连通块 S 包含的数字点总数 E (S):连通块 S 内部包含的车辆(边)总数
-
判断一个连通块是环的条件 E (S) > 0 并且 |S| == E (S)
-
loop:图 G 中所有满足上面条件的环连通块总个数
-
最终答案计算式 ans = cnt + loop
三、并查集数组对应含义
fa u:数字 u 在并查集中的父节点 siz rt:根为 rt 的连通块,包含的点总数,对应 | S| cntE rt:根为 rt 的连通块,包含的有效车辆数量,对应 E (S) vis rt:标记该连通块根是否已经统计过,防止重复计算环
#include <cstdio>
using namespace std;
const int MAX = 100010;
int fa[MAX], siz[MAX], cntE[MAX];
bool vis[MAX];
int find(int x) {
if (fa[x] != x) fa[x] = find(fa[x]);
return fa[x];
}
int main() {
int t; scanf("%d", &t);
while (t--) {
int n, m; scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i) {
fa[i] = i, siz[i] = 1, cntE[i] = 0, vis[i] = 0;
}
int cnt = 0;
for (int i = 0; i < m; ++i) {
int x, y; scanf("%d%d", &x, &y);
if (x == y) continue;
cnt++;
int fx = find(x), fy = find(y);
if (fx != fy) {
fa[fy] = fx;
siz[fx] += siz[fy];
cntE[fx] += cntE[fy];
}
cntE[fx]++;
}
int loop = 0;
for (int i = 1; i <= n; ++i) {
int rt = find(i);
if (!vis[rt]) {
vis[rt] = 1;
if (cntE[rt] && siz[rt] == cntE[rt]) loop++;
}
}
printf("%d\n", cnt + loop);
}
return 0;
}