排布二进制网格的最少交换次数 图解题解
这道题到底在问什么
- 输入
- grid=[[0,0,1],[1,1,0],[1,0,0]]
- 输出
- 3
- 输入
- grid=[[1,0,0],[1,1,0],[1,1,1]]
- 输出
- 0
最优解:为什么这么做
一句话答案:LeetCode 1536 排布二进制网格的最少交换:每次只能交换相邻两行,要让主对角线右上方全为 0。先数每行末尾连续的 0,再从上到下给每个位置就近挑一行够格的冒泡上来、累加移动距离,做不到返回 −1,时间 O(n²)。
网格要摆成什么样才算合格
给一个 n 行 n 列、只含 0 和 1 的网格,每次只能交换相邻两行,最少换几次能让主对角线右上方全为 0,做不到返回 −1。右上方指第 i 行第 i+1 列往右的格子,对角线和左边随便,也就是第 i 行末尾得有足够多连着的 0,位置越靠上需要的越多。
挨个试所有行排法为什么走不通
n 行有 n! 种先后顺序,全排开检查哪些合法、再挑交换最少的,n 才十几就爆了。而且每次只能换相邻两行,把一行挪到目标位得逐格挪,代价是移动距离。得找条不枚举全排列的路。
先数每行末尾的 0,就近挑一行上移
每行并不用摆到某个确定名次,只要末尾连续的 0 够盖住它所在位置的右上方就行。先量每行末尾连着几个 0,从右端往左数、碰到第一个 1 就停;位置 i 需要 n−1−i 个,谁够多谁往上放。
参考代码里换了个等价的量,记每行最右那个 1 在第几列、存进 pos 数组,末尾连续 0 就是 n−1 减去这列号,整行全 0 记 −1。位置 i 要末尾 0 够 n−1−i 个,换成列号看就是最右的 1 落在第 i 列或更左,两把尺子等价。之后就近取够格的行冒上来,总交换最省。
从上到下逐个位置安排够格的行
从位置 0 排到位置 n−1。轮到位置 i,从第 i 行起往下找第一个够格的行,记作第 j 行,把它一步步和上面相邻行对调、冒到第 i 位,一共 j−i 次交换累加进答案,被挤过的行整体下移一格。
冒上来的行就此锁定,就近取第一个不会亏:被换下的那些末尾 0 更多的行,落到更靠下、需求更松的位置照样够用。从第 i 行往下找不到够格的就返回 −1。
拿题面这张 3×3 网格从头排一遍
用题面的 grid=[[0,0,1],[1,1,0],[1,0,0]],答案 3。数末尾 0:第 0 行 0、0、1 从右往左碰到第一个 1 就停、有 0 个;第 1 行 1、1、0 有 1 个;第 2 行 1、0、0 有 2 个。三行从上到下是 0、1、2。
位置 0 要不少于 2 个:前两行的 0 和 1 都不够,第 2 行的 2 个够格、离位置 0 有 2 步,累加 2、冒到顶,末尾 0 成 2、0、1。位置 1 要不少于 1 个:紧挨的行是 0 不够,下一行 1 够格、隔 1 步,累加 1,末尾 0 成 2、1、0。位置 2 只要 0 个、当前行本就 0 步。合计 2 加 1 加 0,答案 3。
末尾 0 只数右端连续段,别贪最多的行
求每行末尾 0 从右往左扫是 O(n²);贪心阶段 n 个位置、每个往下找够格行和冒泡各最多 O(n) 次,也是 O(n²)。只额外开长度为 n 的数组记每行最右的 1,原地交换,空间 O(n)。
几个边界:单行网格没有右上方格子、天然合格,0 次;整张全 0 每行都管够,也 0 次;某位置往下凑不出够格行就返回 −1。最常写坏的:对角线那格不要求为 0,需求是 n−1−i 不是 n−i;末尾 0 只数右端连续那段,像 1、0、1、0、0 中间的 0 被 1 挡着不算、只有 2 个;也别去抢末尾 0 最多的行,挑最近刚够格的。
▶ 动画逐步走查(共 23 步)——想跟着动画一帧帧对照就展开
- 3记住这套打法:先数每行末尾连续 0 的个数,再从上到下给每个位置就近找一个够格的行冒泡上来,交换次数累加距离,中途找不到就是 -1。下面先把三行的末尾 0 都数出来。
- 4橙=待安排,灰=已就位这就是 3 行 3 列的网格。先看要求面板:位置 0 末尾要有 ≥ 2 个 0,位置 1 要 ≥ 1 个,位置 2 要 ≥ 0 个,也就是位置越靠上、末尾要的 0 越多。咱们先把每一行末尾连着几个 0 数出来。
- 5高亮=主对角线右上方,要求全 0高亮的这几个格子就是主对角线右上方的区域,合格网格要它们全为 0。第 0 行有两个、第 1 行有一个、第 2 行一个都没有。一行末尾连着的 0 越多,就越能盖住右上方这片,所以越要往上放。
- 6这一行末尾0 = 0看第 0 行 0、0、1,从右往左数,碰到第一个 1 就停。它末尾连着的 0 有 0 个。接着数下一行。
- 7这一行末尾0 = 1看第 1 行 1、1、0,从右往左数,碰到第一个 1 就停。它末尾连着的 0 有 1 个。接着数下一行。
- 8这一行末尾0 = 2看第 2 行 1、0、0,从右往左数,碰到第一个 1 就停。它末尾连着的 0 有 2 个。三行都数完了,末尾0 分别是 0、1、2。
- 9需求 2 / 1 / 0,现在还对不上三行末尾0 从上到下是 0、1、2,而位置 0、1、2 的需求是 2、1、0,正好反着来。现在从最上面的位置开始安排,需求最高的放最上面。
- 10在位置 0 及其下方找最近的够格行现在轮到位置 0,它需要末尾0 不少于 2 个。就从位置 0 自己开始往下看,找最近的、末尾0 够 2 的那一行。
- 11末尾0 0 不足 2,跳过第 0 行末尾0 只有 0,不到 2,盖不住位置 0 右上方的格子,跳过它,继续往下看。
- 12末尾0 1 不足 2,跳过第 1 行末尾0 只有 1,不到 2,盖不住位置 0 右上方的格子,跳过它,继续往下看。
- 13够格,选它冒泡到位置 0第 2 行末尾0 是 2,达到了 2,够格。它离位置 0 有 2 步,要用 2 次相邻交换把它一格一格冒泡上来。总交换次数先加上 2。
- 14继续往上冒泡把它和上面那一行对调一次,它就往上挪了一格,现在到了第 1 行。还没到位置 0,再换一次。
- 15够格行已到位置 0把它和上面那一行对调一次,它就往上挪了一格,现在到了第 0 行。正好落到位置 0,这一轮的交换做完了。
- 16累计交换 2位置 0 安排好了,这一行末尾0 是 2,满足 ≥ 2,右上方该为 0 的格子都盖住了。它就此锁定,后面的安排只在它下面进行。累计交换到现在是 2 次。
- 17在位置 1 及其下方找最近的够格行现在轮到位置 1,它需要末尾0 不少于 1 个。就从位置 1 自己开始往下看,找最近的、末尾0 够 1 的那一行。
- 18末尾0 0 不足 1,跳过第 1 行末尾0 只有 0,不到 1,盖不住位置 1 右上方的格子,跳过它,继续往下看。
- 19够格,选它冒泡到位置 1第 2 行末尾0 是 1,达到了 1,够格。它离位置 1 有 1 步,要用 1 次相邻交换把它一格一格冒泡上来。总交换次数先加上 1。
- 20够格行已到位置 1把它和上面那一行对调一次,它就往上挪了一格,现在到了第 1 行。正好落到位置 1,这一轮的交换做完了。
- 21累计交换 3位置 1 安排好了,这一行末尾0 是 1,满足 ≥ 1,右上方该为 0 的格子都盖住了。它就此锁定,后面的安排只在它下面进行。累计交换到现在是 3 次。
- 22在位置 2 及其下方找最近的够格行现在轮到位置 2,它需要末尾0 不少于 0 个。就从位置 2 自己开始往下看,找最近的、末尾0 够 0 的那一行。这次需求是 0,任何行都够格。
- 23够格,选它冒泡到位置 2第 2 行末尾0 是 0,达到了 0,够格。它离位置 2 有 0 步,本来就在位置上,不用交换。总交换次数先加上 0。
- 24累计交换 3位置 2 安排好了,这一行末尾0 是 0,满足 ≥ 0,右上方该为 0 的格子都盖住了。它就此锁定,后面的安排只在它下面进行。累计交换到现在是 3 次。
- 25主对角线右上方已全为 0三个位置都安排妥当,回看整张网格,主对角线右上方的格子全是 0,合格了。一路上做了 2 次再加 1 次相邻交换,一共 3 次,这就是最少交换次数,答案 3。
⚠️ 容易写错的地方
✗ 错:把主对角线本身也要求为 0
✓ 对:只要求严格在对角线右上方的格子为 0
第 i 行只管第 i+1 列往右,第 i 列自己以及左边随便,所以位置 i 的需求是 n-1-i 不是 n-i
✗ 错:末尾0 误数成整行 0 的总数
✓ 对:只数从右端起连续的 0,碰到第一个 1 就停
像 1、0、1、0、0 末尾连续 0 只有 2 个,中间那个 0 被 1 挡住不算
✗ 错:贪心去抢末尾0 最多的那一行
✓ 对:要选最近的、刚好够格的那一行
就近上移交换代价最小;被换下来的更远够格行放到更靠下的位置,那里需求更低,它照样够格,不影响后续
✗ 错:以为能直接跨行交换
✓ 对:每次只能换相邻两行,代价就是距离
把第 j 行挪到第 i 行要 j 减 i 次相邻交换,答案累加的正是这个距离之和
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class Solution:
def minSwaps(self, grid: List[List[int]]) -> int:
n = len(grid)
pos = [-1] * n
for i in range(n):
for j in range(n - 1, -1, -1):
if grid[i][j] == 1:
pos[i] = j
break
ans = 0
for i in range(n):
k = -1
for j in range(i, n):
if pos[j] <= i:
ans += j - i
k = j
break
if k == -1:
return -1
while k > i:
pos[k], pos[k - 1] = pos[k - 1], pos[k]
k -= 1
return ansC++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
class Solution {
public:
int minSwaps(vector<vector<int>>& grid) {
int n = grid.size();
vector<int> pos(n, -1);
for (int i = 0; i < n; ++i) {
for (int j = n - 1; j >= 0; --j) {
if (grid[i][j] == 1) {
pos[i] = j;
break;
}
}
}
int ans = 0;
for (int i = 0; i < n; ++i) {
int k = -1;
for (int j = i; j < n; ++j) {
if (pos[j] <= i) {
ans += j - i;
k = j;
break;
}
}
if (k == -1) {
return -1;
}
for (; k > i; --k) {
swap(pos[k], pos[k - 1]);
}
}
return ans;
}
};Java
import java.util.*;
class Solution {
public int minSwaps(int[][] grid) {
int n = grid.length;
int[] pos = new int[n];
Arrays.fill(pos, -1);
for (int i = 0; i < n; ++i) {
for (int j = n - 1; j >= 0; --j) {
if (grid[i][j] == 1) {
pos[i] = j;
break;
}
}
}
int ans = 0;
for (int i = 0; i < n; ++i) {
int k = -1;
for (int j = i; j < n; ++j) {
if (pos[j] <= i) {
ans += j - i;
k = j;
break;
}
}
if (k == -1) {
return -1;
}
for (; k > i; --k) {
int t = pos[k];
pos[k] = pos[k - 1];
pos[k - 1] = t;
}
}
return ans;
}
}复杂度
时间
O(n²)
n 是网格边长。先求每行的 pos 要把每行从右往左扫,最坏是 O(n²);贪心阶段有 n 个位置,每个位置往下找够格行最多看 O(n) 行、冒泡又最多 O(n) 次相邻交换,合起来也是 O(n²)。整体由这两段平方主导,是 O(n²)
空间
O(n)
只额外开了一个长度为 n 的 pos 数组,记每行最右 1 的列号,按峰值就是 O(n)。没有按网格面积 n² 去开额外空间,交换是在原数组上原地进行
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 排布二进制网格的最少交换次数 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么就近上移一定最优,不会漏掉更省的排法?+
可以用交换论证说明。位置 i 必须放一个末尾 0 够 n−1−i 的行,假设最优解放的不是最近那个够格行,而是更靠下的一个,那最近的够格行此刻还压在下面。把这两行的角色对调,让最近的上去、远的留在下面,上移的距离只会更短或持平;而那行更远的够格行既然本来能放到位置 i,说明它末尾 0 至少满足位置 i 的需求,如今挪到更靠下、需求更松的位置照样够格,不影响后面。这样一步步把任何最优解掰成『每个位置都取最近够格行』且代价不增,就近贪心因此最优。
这题和用相邻交换把数组排成目标顺序是一类吗?+
本质相通。相邻交换把一行挪到目标位,代价就是它移动的距离,这和用相邻交换排序、代价是逆序对个数是同一套想法。不同的是这里不用把行完全排成某个固定顺序,只要每个位置的行末尾 0 够用就行,所以用就近贪心去取,而不是去数逆序对;但『相邻交换的代价等于移动距离』这个核心是一样的。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 排布二进制网格的最少交换次数 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。