图像重叠 图解题解
这道题到底在问什么
- 输入
- img1=[[1,1,0],[0,1,0],[0,1,0]], img2=[[0,0,0],[0,1,1],[0,0,1]]
- 输出
- 3 (img1 向右 1、向下 1 后,有 3 个位置重叠)
- 输入
- img1=[[1]], img2=[[1]]
- 输出
- 1
先想最直接的笨办法
核心一句话:枚举两图所有 1 的配对,统计相对位移,计数最多的位移就是答案。下面每帧都在套它。(动画第 3 步)
最优解:为什么这么做
一句话答案:LeetCode 835 图像重叠:不真去平移矩阵,只把两图每个 1 配对、记下对齐所需的平移量,用哈希表数哪个平移出现最多就是最大重叠,时间 O(n⁴)、空间 O(n²)。
两张 01 方阵平移叠一起,最多对上几个 1
给两个 n×n 的 01 矩阵 img1、img2。把一张里的 1 整体往上下左右平移任意格(越界切掉、不能旋转),叠到另一张,问同一位置都是 1 的格子最多几个。例子 img1=[[1,1,0],[0,1,0],[0,1,0]]、img2=[[0,0,0],[0,1,1],[0,0,1]],img1 向右 1、向下 1 后对上 3 个,答案 3。
把矩阵真挪一遍再逐格数,账算下来多重
枚举每种平移:横纵各挪一段,约 (2n−1)² 种;每挪一次还要把整张图 n² 个格子逐个比对、清掉出界的 1。两头一乘 O(n⁴),还要真搬矩阵判越界,代码又长又易错。
为什么只盯着 1、根本不用真去平移
只盯着值为 1 的格子数,就能把平移和越界一起绕开。两个 1 叠加后要落到同一格,往哪挪多远完全由它俩坐标之差定死。设 img1 的 1 在 (i, j)、img2 的 1 在 (h, k),对齐所需的平移量就是 (i−h, j−k)。枚举所有 1 配 1 各算平移量,被最多对 1 共同需要的那个,其下同时对上的 1 就最多。这个键取 img1 坐标减 img2,是把 img2 挪到 img1 所需平移的相反数——和题面向右向下差个符号,只当分组标签、正负不必纠结。
越界为什么不用管?配不上别人的 1 只有自己一票、进不了最大值,自动被排除,无需手写清零。
拿哈希表数平移量,一趟枚举定答案
实现上开一个计数器,键是平移量、值是出现次数。四重循环,外两层走 img1 每个 1 的 (i, j)、内两层走 img2 每个 1 的 (h, k),对每一对往键 (i−h, j−k) 加一。枚举完最大值即答案,凑不出可配的 1 就返回 0。方向全程固定成 img1 减 img2,别一会儿 i−h 一会儿 h−i。
回到题面这两张图,12 对怎么数出 3
img1 的 1 在 (0,0)、(0,1)、(1,1)、(2,1),img2 的在 (1,1)、(1,2)、(2,2),共 12 对。逐对算平移量(img1 减 img2):(0,0) 得 (−1,−1)、(−1,−2)、(−2,−2);(0,1) 得 (−1,0)、(−1,−1)、(−2,−1);(1,1) 得 (0,0)、(0,−1)、(−1,−1);(2,1) 得 (1,0)、(1,−1)、(0,−1)。
归堆后 (−1,−1) 出现在 (0,0)配(1,1)、(0,1)配(1,2)、(1,1)配(2,2) 三处,(0,−1) 两次,余各一次。最大计数 3,即按 (−1,−1) 叠有三对 1 同时对上,答案 3。
四重循环的开销,和几种一票就出局的输入
复杂度上,最坏整图全 1,两图各 n² 个 1 两两配对约 n²×n² 次,时间 O(n⁴),只枚举 1 远小于上界。计数器存不同平移量,上界约 (2n−1)²,空间 O(n²)。
边界:单格 [[1]]、[[1]] 唯一平移量 (0,0) 计数 1;某张全 0 配不出对、返回 0;两图有 1 却对不齐,各平移量一票、最大计数 1;空计数器直接取最大值会报错,先判空返回 0。
▶ 动画逐步走查(共 23 步)——想跟着动画一帧帧对照就展开
- 3核心一句话:枚举两图所有 1 的配对,统计相对位移,计数最多的位移就是答案。下面每帧都在套它。
- 4绿色 = 1,蓝色 = 0,中间一列是分隔左半边是 img1,右半边是 img2,中间那列只作分隔。绿色格子是 1,蓝色是 0。我们要找一个平移,让两图的 1 尽量对上。
- 5img1 有 4 个 1先看 img1,它的 1 在 (0,0)、(0,1)、(1,1)、(2,1) 这 4 个位置,紫色圈出来。
- 6img2 有 3 个 1img2 的 1 在 (1,1)、(1,2)、(2,2) 这 3 个位置,橙色标出来。4 个 1 配 3 个 1,一共 12 对要枚举。
- 7第 1 个 img1 的 1轮到 img1 在 (0,0) 的这个 1。接下来拿它去和 img2 的 3 个 1 逐个配对,算相对位移。
- 8位移 [-1,-1]img1 的 (0,0) 对 img2 的 (1,1):位移 = (0-1, 0-1) = [-1,-1]。该位移计数变成 1。目前最高,[-1,-1] 暂列第一。
- 9位移 [-1,-2]img1 的 (0,0) 对 img2 的 (1,2):位移 = (0-1, 0-2) = [-1,-2]。该位移计数变成 1。当前最高仍是位移 [-1,-1],计数 1。
- 10位移 [-2,-2]img1 的 (0,0) 对 img2 的 (2,2):位移 = (0-2, 0-2) = [-2,-2]。该位移计数变成 1。当前最高仍是位移 [-1,-1],计数 1。
- 11第 2 个 img1 的 1轮到 img1 在 (0,1) 的这个 1。接下来拿它去和 img2 的 3 个 1 逐个配对,算相对位移。
- 12位移 [-1,0]img1 的 (0,1) 对 img2 的 (1,1):位移 = (0-1, 1-1) = [-1,0]。该位移计数变成 1。当前最高仍是位移 [-1,-1],计数 1。
- 13位移 [-1,-1]img1 的 (0,1) 对 img2 的 (1,2):位移 = (0-1, 1-2) = [-1,-1]。该位移计数变成 2。目前最高,[-1,-1] 暂列第一。
- 14位移 [-2,-1]img1 的 (0,1) 对 img2 的 (2,2):位移 = (0-2, 1-2) = [-2,-1]。该位移计数变成 1。当前最高仍是位移 [-1,-1],计数 2。
- 15第 3 个 img1 的 1轮到 img1 在 (1,1) 的这个 1。接下来拿它去和 img2 的 3 个 1 逐个配对,算相对位移。
- 16位移 [0,0]img1 的 (1,1) 对 img2 的 (1,1):位移 = (1-1, 1-1) = [0,0]。该位移计数变成 1。当前最高仍是位移 [-1,-1],计数 2。
- 17位移 [0,-1]img1 的 (1,1) 对 img2 的 (1,2):位移 = (1-1, 1-2) = [0,-1]。该位移计数变成 1。当前最高仍是位移 [-1,-1],计数 2。
- 18位移 [-1,-1]img1 的 (1,1) 对 img2 的 (2,2):位移 = (1-2, 1-2) = [-1,-1]。该位移计数变成 3。目前最高,[-1,-1] 暂列第一。
- 19第 4 个 img1 的 1轮到 img1 在 (2,1) 的这个 1。接下来拿它去和 img2 的 3 个 1 逐个配对,算相对位移。
- 20位移 [1,0]img1 的 (2,1) 对 img2 的 (1,1):位移 = (2-1, 1-1) = [1,0]。该位移计数变成 1。当前最高仍是位移 [-1,-1],计数 3。
- 21位移 [1,-1]img1 的 (2,1) 对 img2 的 (1,2):位移 = (2-1, 1-2) = [1,-1]。该位移计数变成 1。当前最高仍是位移 [-1,-1],计数 3。
- 22位移 [0,-1]img1 的 (2,1) 对 img2 的 (2,2):位移 = (2-2, 1-2) = [0,-1]。该位移计数变成 2。当前最高仍是位移 [-1,-1],计数 3。
- 23最高位移 [-1,-1] 命中 3 次12 对全部枚举完。面板里 9 种不同位移,[-1,-1] 出现 3 次最多,其余位移都只 1 到 2 次。答案就锁在这个 3。
- 24这几对就是最终重叠点把贡献位移键 [-1,-1] 的 3 对 1 同时点亮:(0,0)↔(1,1)、(0,1)↔(1,2)、(1,1)↔(2,2)。注意这个键是 img1 坐标减 img2 坐标,实际移动 img1 去对齐要取相反方向,所以 [-1,-1] 对应 img1 实际向右 1、向下 1(移动哪张图只差一个相反号,计数不受影响)。这样平移后,正好这 3 个位置和 img2 重合。
- 25最大重叠 = 3所有位移里命中次数的最大值就是最大重叠数,这里是 3。最终答案:3。
⚠️ 容易写错的地方
✗ 错:真的去平移矩阵、还手动处理越界清零
✓ 对:不平移,只记 1 配对的相对位移并计数
越界的 1 自然配不出同一个位移,无需特判清零,代码大幅简化
✗ 错:位移方向一会儿 i-h 一会儿 h-i
✓ 对:全程固定 img1 坐标减 img2 坐标
方向混用会把同一平移拆成两个位移,计数被打散,答案偏小
✗ 错:两图无可配对 1 时对空计数取 max 报错
✓ 对:计数表为空直接返回 0
max 作用在空集合上会异常,要先判空
完整代码(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 TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
class Solution:
def largestOverlap(self, img1: List[List[int]], img2: List[List[int]]) -> int:
n = len(img1)
cnt = Counter()
for i in range(n):
for j in range(n):
if img1[i][j]:
for h in range(n):
for k in range(n):
if img2[h][k]:
cnt[(i - h, j - k)] += 1
return max(cnt.values()) if cnt else 0C++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <optional>
#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;
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};
struct ListNode {
int val;
ListNode *next;
ListNode() : val(0), next(nullptr) {}
ListNode(int x) : val(x), next(nullptr) {}
ListNode(int x, ListNode *next) : val(x), next(next) {}
};
class Solution {
public:
int largestOverlap(vector<vector<int>>& img1, vector<vector<int>>& img2) {
int n = img1.size();
map<pair<int, int>, int> cnt;
int ans = 0;
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
if (img1[i][j]) {
for (int h = 0; h < n; ++h) {
for (int k = 0; k < n; ++k) {
if (img2[h][k]) {
ans = max(ans, ++cnt[{i - h, j - k}]);
}
}
}
}
}
}
return ans;
}
};Java
import java.util.*;
class TreeNode { int val; TreeNode left; TreeNode right; TreeNode(){} TreeNode(int val){this.val=val;} TreeNode(int val, TreeNode left, TreeNode right){this.val=val;this.left=left;this.right=right;} }
class ListNode { int val; ListNode next; ListNode(){} ListNode(int val){this.val=val;} ListNode(int val, ListNode next){this.val=val;this.next=next;} }
class Solution {
public int largestOverlap(int[][] img1, int[][] img2) {
int n = img1.length;
Map<List<Integer>, Integer> cnt = new HashMap<>();
int ans = 0;
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
if (img1[i][j] == 1) {
for (int h = 0; h < n; ++h) {
for (int k = 0; k < n; ++k) {
if (img2[h][k] == 1) {
List<Integer> t = Arrays.asList(i - h, j - k);
ans = Math.max(ans, cnt.merge(t, 1, Integer::sum));
}
}
}
}
}
}
return ans;
}
}复杂度
时间
O(n⁴)
最坏整图都是 1,img1 的 1 与 img2 的 1 两两配对,约 n² × n² 对
空间
O(n²)
计数表里不同位移的个数,上界约 (2n-1)²
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 图像重叠 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
只枚举 1 还是 O(n⁴),有没有更快的办法?+
有,但属于加分项。把两张图各看成一个多项式,重叠数其实等价于一种二维卷积,用快速傅里叶变换(FFT)可以把复杂度压到约 O(n² log n)。面试里通常把「枚举 1 配对 + 平移量计数」讲清就够了,能顺带点出 FFT 这条更快的路是锦上添花。
为什么平移量要固定成 img1 坐标减 img2 坐标,反过来减不行吗?+
反过来减本身能算对,怕的是同一份代码里一会儿正着减、一会儿反着减。同一种对齐,img1 减 img2 得到的键和 img2 减 img1 得到的键正好差一个负号,是两个不同的键。方向一混,本该累到同一个键上的对就被劈成两半,各自计数偏小,最大值也跟着变小,答案就错了。只要全程用一种方向,往哪个方向减都能得到正确的最大重叠数。
越界被切掉的 1 真的不用单独处理吗?+
不用。真去平移矩阵时,越界的 1 得手动清零,很繁琐;换成平移量计数后,一个 1 只有和别的 1 恰好共享同一个平移量时才会让那个键的计数变大。一对配不上任何人的 1,它算出的平移量就孤零零只有一票,永远进不了最大值,等于自己把自己排除了。所以不必写任何越界判断,代码短很多。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 图像重叠 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。