昨晚十一点多吧,我在公司楼下抽烟,手机里我们组小李发了个算法题链接,说“东哥你不是天天说架构师也得会点算法么,来个变为棋盘”……我当时脑子还在想线上那个慢查询,结果一看题,哎这玩意儿还挺像排查线上问题:表面看是“能不能变”,本质是先把“不可能”的情况一刀切掉,不然你后面怎么优化都白搭,对吧。
题大概就是给你一个 N×N 的 0/1 矩阵,你只能交换任意两行、或者任意两列,问能不能把它搞成棋盘格:0101… / 1010… 这样交替的。你别急着算最少交换次数,先做“验尸”,跟看日志一样,先确认这是不是你能救的事故。
我一般先抓住一个特别“反直觉但好用”的判定:如果它真能变成棋盘,那任意一个 2×2 小块里,四个角的异或关系必须满足一致性。翻成人话就是:board[0][0] ^ board[i][0] ^ board[0][j] ^ board[i][j] 不能是 1(只要出现一次 1,直接判死刑)。 为啥?因为棋盘格的行/列模式只有两种,行与行之间要么完全一样,要么完全相反,列也一样。你拿 (0,0) 当参照,(i,0)、(0,j)、(i,j) 这四个点的关系就被锁死了,跟分布式里一致性校验一个味儿。
然后第二步才是“数数”,我当时在便利店门口蹲着算的:第一行里 1 的个数必须是 n/2 或 (n+1)/2(n 奇数的时候只能多一个或少一个),第一列同理。不然你怎么交替?你全是 1 或者 0,换到天亮也交替不出来。
最后才到“最少交换次数”,这块其实挺工程化: 你不需要真的去 swap 行列,太慢也太麻烦。你只要决定“第一行应该从 0 开始还是从 1 开始”,同理第一列。因为一旦第一行第一列定了,整个棋盘的目标形状就定了。
我常用的算发(算法…口误)是:
- 看第一列:理想情况下第 i 行的首元素应该等于
i % 2(表示从 0 开始的交替)。那当前有多少行“对上了”,就记个 rowMatch。 - 如果 n 是偶数:你可以选从 0 开始或从 1 开始,取更省交换的那个,也就是
min(match, n-match)。 - 如果 n 是奇数:起始值基本被“多数派”固定了(因为 0/1 个数不可能各一半),所以如果 match 是奇数就得反过来用
n-match。
交换次数注意还得除以 2,因为一次交换能修正两条错位(这个点小李当时就栽了,说怎么老差一倍,我说你别急,先把“match 的定义”搞清楚)。
我把完整 Java 代码贴这,你拿去直接跑就行,没用网上那种一堆 if 套 if 的写法,我尽量写得像线上能维护的那种:
publicclassSolution{publicintmovesToChessboard(int[][] board){int n = board.length;// 1) 结构一致性校验:任意(i,j)都要满足四角异或为0for (int i = 0; i < n; i++) {for (int j = 0; j < n; j++) {int v = board[0][0] ^ board[i][0] ^ board[0][j] ^ board[i][j];if (v == 1) return -1; } }// 2) 计数校验:第一行、第一列的1的数量必须合理int rowOnes = 0, colOnes = 0;for (int i = 0; i < n; i++) { rowOnes += board[0][i]; colOnes += board[i][0]; }if (!validOnesCount(rowOnes, n) || !validOnesCount(colOnes, n)) return -1;// 3) 计算与“从0开始交替”的匹配数int rowMatch = 0, colMatch = 0;for (int i = 0; i < n; i++) {if (board[i][0] == (i & 1)) rowMatch++;if (board[0][i] == (i & 1)) colMatch++; }int rowSwaps = calcMinSwapsByMatch(rowMatch, n);int colSwaps = calcMinSwapsByMatch(colMatch, n);// 每次交换修正两处错位,所以要 /2return (rowSwaps + colSwaps) / 2; }privatebooleanvalidOnesCount(int ones, int n){if ((n & 1) == 0) {return ones == n / 2; } else {return ones == n / 2 || ones == n / 2 + 1; } }privateintcalcMinSwapsByMatch(int match, int n){if ((n & 1) == 0) {return Math.min(match, n - match); } else {// 奇数时起始模式被多数派锁定,match 必须是偶数,否则用反的return (match & 1) == 0 ? match : (n - match); } }}
你看啊,这题其实挺像我们做架构时处理“看起来能靠重构解决”的问题:先做不可行性校验(结构不对直接别折腾),再做约束检查(数量不满足就别谈方案),最后才谈最小代价。小李当时听完说“懂了,就是先别急着上手术台”…我说对对对,先把 CT 拍了。
行了我不说了,刚才烟抽完了,顺手把工位那个报警也得看一眼,别又是哪个线程池默认配置没改把我半夜叫起来…