题目:
你将得到一个整数数组matchsticks,其中 matchsticks[i]是第i个火柴棒的长度。你要用所有的火柴棍拼成一个正方形。你不能折断任何一根火柴棒,但你可以把它们连在一起,而且每根火柴棒必须使用一次。如果你能使这个正方形,则返回 true ,否则返回 false 。
示例
输入: matchsticks = [1,4,2,3,5,2,3]
输出: true
解释: 能拼成一个边长为2的正方形,每边两根火柴。
题解
首先来分析一下题目具体是什么问题。题目的意思是给定一组长短不同的火柴,利用这些火柴判断是否能拼成一个正方形。正方形就是四条边均相等,这个问题的抽象化表示就是给定一个数组,是否能把数据分成四个子集,每个子集的数的和相同。为了有效说明以上面的示例为例来分析一下该题目的解题方法。
给定一组可选择的火柴,把这火柴均分到四条边中。显然根据火柴的列表可以确定每一个边的目标值是6。
那么我们先对“可选火柴”中的第一个数据进行分配。显然该数据放在任何一个边中均可。
如上图所示,把1放在边1中。这里边1当前已有,还需要再填充5。由于如果结果是真的话就一定有一组数据与1一起正好构成一条值为6的边,因此我们只需要逐个的填满每条边就好。这里需要加一个状态值来记录,当前填充边填充了多少。接下来处理第2个数据4。从目前状态来看4是可以放到边1中的,因为1与4的全为5显然小于6。我们先假定把4放到边1中。
我们发现继续遍历2一直到最后也无法填充满边1。显然把4放到边1中无法找到有效的分配结果。退回原来状态,跳过4。
2显然也是能放到边1中的,先尝试把2放到边1中。
当前的3也是能放到边1中,这样边1正好达到目标值6,由于当前边已填充完,开始填充边2了这里当前边已有值就变成0了。
把5放到边2中。
但是到这里我们发现我们无法找到能与5一起填充边的值了,显然,从2开始我们的填充。我们开始回退。这里直接回退到。
再重新开始,重这些操作直到达到如下状态。
那么我们现在开始整理我们的思路。我们每一步的输入是“可选火柴”与当前已有值,第一步的选择都是不断的尝试,最终是通过“可选火柴”与当前已有值来判断选择该步数值是否能让所有边都填充完。这显然是一个递归过程,每一步的选择都依赖于对剩下状态的递归。
递归函数
- 输入:“可选火柴”,当前已有值
- 输出:是否可正好填充完各边
- 递归过程:
1 终止条件,当“可选火柴”无数据可选择时终止
2 等价问题,选择完一个数之后,判断当前状态是否可填充完各边
代码如下:
class Solution:
def makesquare(self, matchsticks: List[int]) -> bool:
#边界确定
bucket_value,div_value=divmod(sum(matchsticks),4)
matchsticks.sort(reverse=True)
if div_value:
return False
n=len(matchsticks)
@cache
def dfs(state,curr_value):
#终止条件
if state==0:
return True
for i in range(n):
#判断i位置是否可选
if state>>i&1:
#判断该位置数据是否能与curr_value分到一个释桶
if matchsticks[i]+curr_value<=bucket_value:
#判断分完i个数后剩下的数据是否满足要求
if dfs(state^(1<<i),(curr_value+matchsticks[i])%bucket_value):
return True
return False
return dfs((1<<n)-1,0)
代码关键部分介绍
bucket_value,div_value=divmod(sum(matchsticks),4)
matchsticks.sort(reverse=True)
if div_value:
return False
这一块其实就是判断给定的list是否是能被4整除。如果不能整除直接就返回False。
这里使用了状态压缩,这里状态压缩就是用一个数来表示“可选火柴”,怎么表示呢。在遍历可选火柴列表时,已选过的数标记为0表示不能再选了,标记为1的数才可以遍历。如下图所示,只有数据1与数据3的状态表示为1,表示只有剩下这两个数待填充。
这种表示是一一对应的形式,为了书写方便通常表示倒顺的表示。
为什么要这么写,比如判断i个位置是否可选择使用state>>i&1,如果要是第一种对应的话就得变成state>>(n-i)&1。后面的state^(1<<i)表示把第i位置的数据为成0表示已选择过不可选了。
计算复杂度
- 时间复杂度: O ( n × 2 n ) O(n×2^n) O(n×2n) ,其中 n 为 数组 的长度,共有 2 n 2^n 2n个状态,每一个状态进行了 n 次尝试。
- 空间复杂度: O ( 2 n ) O(2^n) O(2n),其中n 为数组 nums 的长度,主要为状态数组的空间开销。