行相等的最少多米诺旋转 图解题解
这道题到底在问什么
- 输入
- tops=[2,1,2,4,2,2], bottoms=[5,2,6,2,3,2]
- 输出
- 2 (把上行统一成 2,翻两张)
- 输入
- tops=[3,5,1,2,3], bottoms=[3,6,3,3,4]
- 输出
- -1 (没有数字能盖住每一列)
- 输入
- tops=[1,1,1], bottoms=[1,2,3]
- 输出
- 0 (上行已经全是 1)
最优解:为什么这么做
一句话答案:LeetCode 1007 行相等的最少多米诺旋转:要让一整行全相同,能成的目标值只可能是第一列的上下两个数,逐候选各扫一遍数翻转取最少、都不行返回 -1,时间 O(n)。
两行多米诺,要让某一行的点数全相同
给两个等长数组 tops 和 bottoms,tops[i] 是第 i 列上面的点数,bottoms[i] 是下面的点数。每次操作能把某一列翻过来(上下点数对调),问最少翻几列,能让上行或下行全相同;做不到返回 -1。题面给 tops=[2,1,2,4,2,2]、bottoms=[5,2,6,2,3,2],答案是 2;若像 tops=[3,5,1,2,3]、bottoms=[3,6,3,3,4] 没有数盖得住每一列,就返回 -1;而 tops=[1,1,1] 上行本就全是 1,一次不用翻,返回 0。
要统一成哪个数,难道 1 到 6 挨个试
点数就 1 到 6 六种,一个笨办法是拿每个数轮流当目标 x,扫一遍全列数需要翻几列,六个候选挑翻得最少的。这能算出答案,但多数候选一上手就没戏、白扫一趟,得先看清哪些 x 不可能成。
能成的目标值,只可能是第一列的两个数
盯住第 0 列:上是 2、下是 5。整行若要全变成 x,第 0 列也必须出得了 x——要么上面本就是 x,要么翻过来让下面的 x 顶上去。可这列只有 2 和 5,x 就只能是 2 或 5,别的值第 0 列出不来、整行凑不齐。六选一坍缩成二选一:把 x=tops[0] 和 x=bottoms[0] 各验一遍,谁翻得少取谁,都不行才返回 -1。
定住一个候选 x,上行下行各要翻几列
验候选 x 时,top、bottom 两个计数器都从 0 起,分别记「上行全变成 x 要翻几列」和「下行全变成 x 要翻几列」。逐列看,只要上或下有一个是 x 就有救——上面已是 x 则 top 不动、否则加 1,下面已是 x 则 bottom 不动、否则加 1。某列上下都不是 x,候选当场作废。走完全列,min(top, bottom) 就是 x 的代价。
x=2 和 x=5 各要数几张翻转
先试 x=2。第 0 列上 2 下 5,下行要翻,top=0、bottom=1;第 1 列上 1 下 2,上行要翻,top=1、bottom=1;第 2 列上 2 下 6,下行翻,top=1、bottom=2;第 3 列上 4 下 2,上行翻,top=2、bottom=2;第 4 列上 2 下 3,下行翻,top=2、bottom=3;第 5 列上下都是 2,两边不动,top=2、bottom=3,全列过关,min(2,3)=2。
再试 x=5。第 0 列上 2 下 5,上面要翻,top=1、bottom=0;到第 1 列上 1 下 2 都不是 5,这列出不来 5,候选 5 作废。两候选里 x=2 要 2 张、x=5 灭,答案就是 2。
把 -1 当成 0,或只算半边账
枚举 1 到 6 每个点数当候选纯属白费,第 0 列出不了的值整行都出不来,锁死成两个候选就够。同一个 x 若只数上行、忘了下行,可能上行翻 3 列而下行只翻 1 列,更省的那半就漏了。某列上下都不是 x 时把无解写成返回 0 也不对:0 是本来就相同、-1 是根本做不到,两者反着来。全程最多两个候选各扫一遍 n 列,共 2n 次,时间 O(n),空间 O(1)。
▶ 动画逐步走查(共 22 步)——想跟着动画一帧帧对照就展开
- 3记住这套「候选只有第 0 列的两个数 → 对每个候选扫一遍数翻转」,下面每帧都在套它。
- 4先看第 0 列:上是 2、下是 5。整行要统一成 x,第 0 列必须出得了 x,所以 x 只能是 2 或 5。我们先试 x = 2。
- 5开扫候选 x=2:用 top 记「想统一上行需翻几张」,bottom 记「想统一下行需翻几张」,两个都从 0 开始。
- 6看第 0 列:上 2、下 5。只要上或下里有一个是 2,这列就有救。
- 7第 0 列:上是 2、下是 5:上行不用翻;若想让下行变 2,这列得翻一张(bottom+1)。 累计 top=0、bottom=1。
- 8看第 1 列:上 1、下 2。只要上或下里有一个是 2,这列就有救。
- 9第 1 列:上是 1(不是 2),但下是 2:若想让上行变 2,这列得翻一张(top+1);下行本就是 2,下行不用翻。 累计 top=1、bottom=1。
- 10看第 2 列:上 2、下 6。只要上或下里有一个是 2,这列就有救。
- 11第 2 列:上是 2、下是 6:上行不用翻;若想让下行变 2,这列得翻一张(bottom+1)。 累计 top=1、bottom=2。
- 12看第 3 列:上 4、下 2。只要上或下里有一个是 2,这列就有救。
- 13第 3 列:上是 4(不是 2),但下是 2:若想让上行变 2,这列得翻一张(top+1);下行本就是 2,下行不用翻。 累计 top=2、bottom=2。
- 14看第 4 列:上 2、下 3。只要上或下里有一个是 2,这列就有救。
- 15第 4 列:上是 2、下是 3:上行不用翻;若想让下行变 2,这列得翻一张(bottom+1)。 累计 top=2、bottom=3。
- 16看第 5 列:上 2、下 2。只要上或下里有一个是 2,这列就有救。
- 17第 5 列:上下都是 2,怎么放都行,两边都不用翻。 累计 top=2、bottom=3。
- 18候选 x=2 全列都过关。凑上行要翻 2 张、凑下行要翻 3 张,取较小的 → 2 张。这就是 x=2 的代价。
- 19别忘了还有第二个候选:把整行统一成下行第 0 个的值 x=5。计数器清零,从第 0 列重新扫。
- 20候选 5:看第 0 列,上 2、下 5。这一列里有没有 5?
- 21第 0 列有 5,本列过关。累计 top=1、bottom=0。
- 22候选 5:看第 1 列,上 1、下 2。这一列里有没有 5?
- 23第 1 列上 1、下 2,两个都不是 5:无论翻不翻,这列都出不来 5。候选 5 当场出局(红色那列卡死)。
- 24两个候选里:x=2 要 2 张,x=5 出局。取能成的最小值 → 答案 2。若两个候选都出局,则返回 -1。
⚠️ 容易写错的地方
✗ 错:枚举 1 到 6 所有点数当候选
✓ 对:候选只可能是 tops[0] 或 bottoms[0]
第 0 列出不了 x,整行就不可能全是 x,其它值不用试
✗ 错:只算「上行变 x」忘了「下行变 x」
✓ 对:同一个 x 分别算 top 与 bottom 取最小
统一上行和统一下行翻的张数不同,必须都算
✗ 错:某列上下都不是 x 还继续算,或无解返回 0
✓ 对:该列立即判候选无解;两候选都无解才返回 -1
0 是本就相同,-1 是做不到,含义完全不同
完整代码(Python / C++ / Java)
Python
from typing import List
class Solution:
def minDominoRotations(self, tops: List[int], bottoms: List[int]) -> int:
def check(x):
top = bottom = 0
for a, b in zip(tops, bottoms):
if a != x and b != x:
return 10**9
if a != x:
top += 1
if b != x:
bottom += 1
return min(top, bottom)
ans = min(check(tops[0]), check(bottoms[0]))
return -1 if ans == 10**9 else ansC++
#include <algorithm>
#include <vector>
using namespace std;
class Solution {
public:
int minDominoRotations(vector<int>& tops, vector<int>& bottoms) {
auto check = [&](int x) {
int top = 0, bottom = 0;
for (int i = 0; i < (int)tops.size(); ++i) {
if (tops[i] != x && bottoms[i] != x) return 1000000000;
if (tops[i] != x) top++;
if (bottoms[i] != x) bottom++;
}
return min(top, bottom);
};
int ans = min(check(tops[0]), check(bottoms[0]));
return ans == 1000000000 ? -1 : ans;
}
};Java
import java.util.*;
class Solution {
public int minDominoRotations(int[] tops, int[] bottoms) {
int ans = Math.min(check(tops, bottoms, tops[0]), check(tops, bottoms, bottoms[0]));
return ans >= 1_000_000_000 ? -1 : ans;
}
private int check(int[] tops, int[] bottoms, int x) {
int top = 0, bottom = 0;
for (int i = 0; i < tops.length; i++) {
if (tops[i] != x && bottoms[i] != x) return 1_000_000_000;
if (tops[i] != x) top++;
if (bottoms[i] != x) bottom++;
}
return Math.min(top, bottom);
}
}复杂度
时间
O(n)
最多两个候选,每个扫一遍 n 列,共 2n
空间
O(1)
只用 top、bottom 几个计数器
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 行相等的最少多米诺旋转 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么只试 tops[0] 一个候选会漏答案?+
因为最优的目标值有可能在第 0 列只出现在下面。假设第 0 列上是 2、下是 5,要统一成 5 就得先把第 0 列翻过来,这个 5 等于 bottoms[0] 而不是 tops[0];如果只拿 tops[0]=2 去验,就永远试不到统一成 5 这条路,可能因此漏掉更少的翻转甚至唯一可行的解。所以 tops[0] 与 bottoms[0] 两个候选都得各验一遍,再取较小的。
同一个候选 x,为什么要分别算 top 和 bottom 两笔账?+
因为「让上行全变成 x」和「让下行全变成 x」翻的列数往往不一样。某一列如果上面是 x、下面不是,想统一上行这列不用翻,想统一下行这列就得翻;反过来也是。所以同一个 x 下 top 和 bottom 会各自累加成不同的数,必须都算完再取 min,只算一笔就可能把更省的那半漏掉。
返回 0 和返回 -1 有什么区别,别弄反了?+
0 表示某一行本来就全相同,一次都不用翻,比如 tops=[1,1,1] 上行已是全 1。-1 表示无论怎么翻都做不到——存在某一列上下都不是候选值,两个候选全灭。两者含义正相反:0 是已经达成的最好情况,-1 是根本达不成,把无解误写成返回 0 就会把「做不到」当成「白捡的 0 次」报出去。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 行相等的最少多米诺旋转 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。