火柴拼正方形 图解题解
这道题到底在问什么
- 输入
- matchsticks=[1,1,2,2,2]
- 输出
- true(边长2,一边用1+1,其余三边各用一根2)
- 输入
- matchsticks=[3,3,3,3,4]
- 输出
- false
最优解:为什么这么做
一句话答案:LeetCode 473 火柴拼正方形用回溯搜索:每根火柴枚举放进四条边哪条,放不下就撤回换边;靠预判整除、降序排序、相同总长的边只试一条这三招剪枝压掉指数分支,时间 O(4ⁿ)、空间 O(n)。
什么样的一把火柴能拼成正方形
给一个数组 matchsticks,每个数是一根火柴的长度,不能折断、每根都要用上,问能不能正好拼成四条边一样长的正方形。matchsticks=[1,1,2,2,2] 能拼:总长 8、每条边凑到 2,一条边用 1 加 1、另外三条各用一根 2,返回 true;[3,3,3,3,4] 拼不成,返回 false。要的是能不能拼成的是非判断,不给具体拼法。
为什么不能一根根盲放、也不能贪心
四条边一样长,每条边的目标长度就是总长除以 4。可火柴还得真的去摆——每根有四条边可放,n 根硬枚举就是 4ⁿ 种摆法,火柴数到 15 时是十亿量级,盲放会超时。也不能贪心地哪条边缺就往哪塞:一根火柴放哪条边,会影响后面所有火柴还放不放得下,眼前塞得进不代表全局能拼成。所以用回溯——把这根放哪条边逐一枚举,放错了能退回换一条,再靠剪枝把走不通的分支尽早砍掉。
dfs 的参数和「放哪条边」怎么枚举
搜索按火柴顺序一根根来。dfs 带参数 u 表示轮到放第 u 根火柴,再用长度为 4 的数组 edges 记四条边各自的总长。放第 u 根时从边 0 到边 3 挨个试:把火柴长度加到某条边,加完不超过目标边长就递归放下一根 u 加 1;走不通再回来,把刚加的长度减掉、换下一条边。u 走到火柴总数说明每根都安置好了,返回 true。
三招剪枝分别砍掉了什么
光这样枚举还是 4ⁿ,让它跑得动的是三招剪枝(剪枝就是提前判断某条路走不通、不再往下搜)。第一招在开搜前:总长不能被 4 整除、或最长火柴超过目标边长,直接 false,省掉整棵搜索树。第二招把火柴按长度降序排好再放:大火柴先定位,撑爆某条边时浅层就发现掉头;小火柴先放会把矛盾拖到很深才暴露,白搜一大片。
第三招最容易漏:放火柴时若某条边当前总长和前一条边完全相同,就跳过它。几条总长一样的边是对称等价的——往这条放还是那条放,后面能不能拼成的结果一模一样,只试其中一条就够;不跳会重复搜出一大片等价分支。
拿 [1,1,2,2,2] 亲手放一遍火柴
总长 8 能被 4 整除,目标边长 2,最长的 2 不超,预判通过;降序排成 2、2、2、1、1,四条边起初都是 0。放第一根 2:边 0 变 2、不超留下。放第二根 2:试边 0 变 4 超了撤回,边 1 放进变 2。放第三根 2:边 0 试超,边 1 和边 0 都是 2、跳过,边 2 放进变 2。放第四根 1:边 0 变 3 超了,边 1、边 2 等长跳过,边 3 放进变 1。放第五根 1:边 0 又超、边 1 边 2 等长跳过,边 3 从 1 放到 2。五根放完,四条边都正好是 2,返回 true。
两个预判为什么必须放在开搜前
时间最坏 O(4ⁿ):每根火柴最多试四条边,n 根共 4ⁿ 个分支,靠三招剪枝实际远小于此。空间 O(n):递归最深 n 层,加上四条边的常数空间。两个预判边界最不能省:一是总长对 4 取余不为 0,四条边没法均分,直接 false;二是最长一根火柴超过目标边长,连一条边都塞不进,直接 false。不先挡住就硬搜,白跑整棵树甚至超时。
▶ 动画逐步走查(共 23 步)——想跟着动画一帧帧对照就展开
- 3记住这条主线:四个桶装四条边,大火柴优先,放进去不超目标就留、超了就撤回换边,几条一样高的边只试一条。下面每一帧都在套它。
- 4先看四条边,编号 0 到 3,现在全是空的。每条边都要恰好凑到目标长度 2。把全部 5 根火柴塞进这四条边、且每条边都不超过 2,就算拼成了。
- 5动手前把火柴从大到小排好:2、2、2、1、1。为什么先放大的?大火柴一旦让某条边超了,马上就能发现并掉头,不用等到最后才暴露,这样能尽早剪掉走不通的分支。
- 6拿起第 1 根火柴,长度 2。从编号 0 的边开始,一条一条看:把它放进去之后,这条边的总长会不会超过目标 2?不超就放下,超了就换下一条。
- 7边 0 原本是 0,放进长度 2 后变成 2,2 ≤ 2,放得下! 这根火柴就留在边 0,接着去放下一根火柴。
- 8拿起第 2 根火柴,长度 2。从编号 0 的边开始,一条一条看:把它放进去之后,这条边的总长会不会超过目标 2?不超就放下,超了就换下一条。
- 9试边 0:它现在 2,临时试放长度 2 会让它变成 4(画面里边 0 暂时显示成这个超限状态),4 > 2,超过目标了,放不下。下一帧就把这根火柴撤回来、换下一条边继续试。这一放一撤,就是回溯。
- 10边 1 原本是 0,放进长度 2 后变成 2,2 ≤ 2,放得下! 这根火柴就留在边 1,接着去放下一根火柴。
- 11拿起第 3 根火柴,长度 2。从编号 0 的边开始,一条一条看:把它放进去之后,这条边的总长会不会超过目标 2?不超就放下,超了就换下一条。
- 12试边 0:它现在 2,临时试放长度 2 会让它变成 4(画面里边 0 暂时显示成这个超限状态),4 > 2,超过目标了,放不下。下一帧就把这根火柴撤回来、换下一条边继续试。这一放一撤,就是回溯。
- 13看边 1。它现在的总长是 2,和左边的边 0 完全相同——这几条总长一样的边属于『同一批等价边』,往哪条放都是对称等价的,只需用这批里的第一条试一次就够了。边 1 是这批里后面的重复边,直接跳过(不是因为试过失败,而是和已经在试的那条等价),省掉一大片对称重复的尝试。
- 14边 2 原本是 0,放进长度 2 后变成 2,2 ≤ 2,放得下! 这根火柴就留在边 2,接着去放下一根火柴。
- 15拿起第 4 根火柴,长度 1。从编号 0 的边开始,一条一条看:把它放进去之后,这条边的总长会不会超过目标 2?不超就放下,超了就换下一条。
- 16试边 0:它现在 2,临时试放长度 1 会让它变成 3(画面里边 0 暂时显示成这个超限状态),3 > 2,超过目标了,放不下。下一帧就把这根火柴撤回来、换下一条边继续试。这一放一撤,就是回溯。
- 17看边 1。它现在的总长是 2,和左边的边 0 完全相同——这几条总长一样的边属于『同一批等价边』,往哪条放都是对称等价的,只需用这批里的第一条试一次就够了。边 1 是这批里后面的重复边,直接跳过(不是因为试过失败,而是和已经在试的那条等价),省掉一大片对称重复的尝试。
- 18看边 2。它现在的总长是 2,和左边的边 1 完全相同——这几条总长一样的边属于『同一批等价边』,往哪条放都是对称等价的,只需用这批里的第一条试一次就够了。边 2 是这批里后面的重复边,直接跳过(不是因为试过失败,而是和已经在试的那条等价),省掉一大片对称重复的尝试。
- 19边 3 原本是 0,放进长度 1 后变成 1,1 ≤ 2,放得下! 这根火柴就留在边 3,接着去放下一根火柴。
- 20拿起第 5 根火柴,长度 1。从编号 0 的边开始,一条一条看:把它放进去之后,这条边的总长会不会超过目标 2?不超就放下,超了就换下一条。
- 21试边 0:它现在 2,临时试放长度 1 会让它变成 3(画面里边 0 暂时显示成这个超限状态),3 > 2,超过目标了,放不下。下一帧就把这根火柴撤回来、换下一条边继续试。这一放一撤,就是回溯。
- 22看边 1。它现在的总长是 2,和左边的边 0 完全相同——这几条总长一样的边属于『同一批等价边』,往哪条放都是对称等价的,只需用这批里的第一条试一次就够了。边 1 是这批里后面的重复边,直接跳过(不是因为试过失败,而是和已经在试的那条等价),省掉一大片对称重复的尝试。
- 23看边 2。它现在的总长是 2,和左边的边 1 完全相同——这几条总长一样的边属于『同一批等价边』,往哪条放都是对称等价的,只需用这批里的第一条试一次就够了。边 2 是这批里后面的重复边,直接跳过(不是因为试过失败,而是和已经在试的那条等价),省掉一大片对称重复的尝试。
- 24边 3 原本是 1,放进长度 1 后变成 2,2 ≤ 2,放得下! 这根火柴就留在边 3,接着去放下一根火柴。
- 255 根火柴全部塞完,四条边的总长都正好是 2,正方形拼成了,返回 true。回头看整个过程:大火柴优先放、放不下就撤回换边、当前总长相同的边只试一条,这三招让搜索又快又稳。
⚠️ 容易写错的地方
✗ 错:不先做整除/最长边预判就硬搜
✓ 对:先判 sum % 4 ≠ 0 或 max > 目标边长直接 false
总长不能均分四份、或某根比目标边还长,根本不可能拼成,提前返回省掉整棵搜索树
✗ 错:不排序或升序放,小火柴先放
✓ 对:降序排序,大火柴优先放
大火柴先定位,放错时浅层就暴露,剪枝更早;小火柴先放会让错误拖到很深才发现,容易超时
✗ 错:相同总长的边重复试,产生大量等价分支
✓ 对:edges[i-1] 等于 edges[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 makesquare(self, matchsticks: List[int]) -> bool:
def dfs(u):
if u == len(matchsticks):
return True
for i in range(4):
if i > 0 and edges[i - 1] == edges[i]:
continue
edges[i] += matchsticks[u]
if edges[i] <= x and dfs(u + 1):
return True
edges[i] -= matchsticks[u]
return False
x, mod = divmod(sum(matchsticks), 4)
if mod or x < max(matchsticks):
return False
edges = [0] * 4
matchsticks.sort(reverse=True)
return dfs(0)C++
#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:
bool makesquare(vector<int>& matchsticks) {
int s = 0, mx = 0;
for (int& v : matchsticks) {
s += v;
mx = max(mx, v);
}
int x = s / 4, mod = s % 4;
if (mod != 0 || x < mx) return false;
sort(matchsticks.begin(), matchsticks.end(), greater<int>());
vector<int> edges(4);
return dfs(0, x, matchsticks, edges);
}
bool dfs(int u, int x, vector<int>& matchsticks, vector<int>& edges) {
if (u == matchsticks.size()) return true;
for (int i = 0; i < 4; ++i) {
if (i > 0 && edges[i - 1] == edges[i]) continue;
edges[i] += matchsticks[u];
if (edges[i] <= x && dfs(u + 1, x, matchsticks, edges)) return true;
edges[i] -= matchsticks[u];
}
return false;
}
};Java
import java.util.*;
class Solution {
public boolean makesquare(int[] matchsticks) {
int s = 0, mx = 0;
for (int v : matchsticks) {
s += v;
mx = Math.max(mx, v);
}
int x = s / 4, mod = s % 4;
if (mod != 0 || x < mx) {
return false;
}
Arrays.sort(matchsticks);
int[] edges = new int[4];
return dfs(matchsticks.length - 1, x, matchsticks, edges);
}
private boolean dfs(int u, int x, int[] matchsticks, int[] edges) {
if (u < 0) {
return true;
}
for (int i = 0; i < 4; ++i) {
if (i > 0 && edges[i - 1] == edges[i]) {
continue;
}
edges[i] += matchsticks[u];
if (edges[i] <= x && dfs(u - 1, x, matchsticks, edges)) {
return true;
}
edges[i] -= matchsticks[u];
}
return false;
}
}复杂度
时间
O(4ⁿ)
每根火柴最多试四条边,n 根共 4ⁿ 个分支,靠剪枝大幅缩小
空间
O(n)
递归深度最多 n 层,外加四个边的常数空间
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 火柴拼正方形 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
这题为什么用回溯,不能贪心?+
一根火柴放哪条边会影响后面所有火柴放不放得下,眼前把某条边填满看着合适,可能让后面某根火柴无处可放。贪心只顾当下,保证不了全局拼成,所以要回溯:把放哪条边逐一枚举,放错能退回换边,再用剪枝砍掉无用分支。
相同总长的边为什么只试一条,会不会漏掉正解?+
不会。两条边当前总长相同时,它们对后面完全对称——把当前这根放进第一条、还是放进第二条,剩下的火柴面对的局面一模一样,能拼成就都能拼成、不能就都不能。所以只需试其中一条,另一条的结果必然相同,跳过它只是省掉重复搜索,不会错过任何真正不同的摆法。
为什么要降序排序,升序不行吗?+
升序也能得到正确答案,但会慢很多。大火柴先放,一旦它让某条边超限,在很浅的层就暴露、马上掉头;小火柴先放时一堆小火柴能凑出各种组合,矛盾要等大火柴最后登场才爆发,前面白搜了一大片,数据一大就超时。降序让错误尽早出现,剪枝才砍得早。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 火柴拼正方形 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。