473. 火柴拼正方形

题目描述

给一个数组nums,nums中元素代表火柴棒的长度。
给的要求是火柴棒可以连接,不能折断,每根必须用一次,问能不能连成正方形?

基本分析

f1-状态压缩+动态规划

  1. 火柴数组长度不超过15暗示什么?可以用二进制数s表示数组中索引对应的火柴的使用情况
  2. 火柴数组什么情况可以提前排除?(1)长度总和不是4的倍数;(2)有某根长度>边长
  3. 火柴使用情况用什么表示?二进制的数字state
  4. dp的状态怎么定义?利用f[state]表示正方形未被放满的边的当前长度
  5. 状态的初始化和最终结果怎么定义?初始化f[0]=0表示不放火柴时,当前长度是0;最终结果是f[(1<<n)-1],表示都用的时候,未被放满边的长度,如果要满足条件,需要f[-1]==0
  6. 状态怎么进行转移?对当前的状态s,找到包含1的所有位置。以遍历到i为例,对应的火柴长度是v。考虑转移时,去掉i位置放置火柴的情况,对应状态为s1,当f[s1]+v<=边长时,表示可以转移f[s] = (f[s1]+v)%line, 此时可以不再继续遍历i,可以break,直接去讨论下一个状态;否则当f[s1]+v > 边长line时,不能进行转移,只能考虑其他别的放置情况。

f2-回溯

  1. 排除简单不满足的情况?和上面一样
  2. 怎么在遍历的是减少搜索量?对火柴从大到小排序
  3. 怎么定义辅助的数组表示状态?定义了一个数组edges,其中每个元素edges[i]表示当前i边的长度
  4. dfs的参数需要什么?定义idx表示处理到了哪个索引,如果idx=火柴长度,返回True,表示到栈尾
  5. 回溯的中间逻辑是什么?对idx没有到达最终长度时,对饮的值是v。 遍历每个边0-4,先把值加到对应的边,新长度是edges[i]+v,看新长度是不是满足要求且后面也满足要求,如果是,可以返回True,需要在i边的放置,分析放到下一个边的情况
  6. 怎么开始遍历?dfs(0),表示从索引0开始。

代码

f1-状态压缩+动态规划

class Solution:
def makesquare(self, matchsticks: List[int]) -> bool:
n = len(matchsticks)
total = sum(matchsticks)
if total % 4 != 0:
return False
line = total // 4
if any(num > line for num in matchsticks):
return False
final = 1<<n
f = [-1] * final
f[0] = 0
for s in range(1, final):
for i, k in enumerate(matchsticks):
if s & (1<<i) == 0:
continue
s1 = s & (~(1<<i))
if f[s1] >=0 and f[s1] + k <=line:
f[s] = (f[s1] + k) % line
break
return f[-1]==0

f2-回溯

class Solution:
def makesquare(self, matchsticks: List[int]) -> bool:
n = len(matchsticks)
total = sum(matchsticks)
if total % 4 != 0:
return False
line = total / 4
if any(num > line for num in matchsticks):
return False
matchsticks.sort(reverse=True)
edges = [0] *4
def dfs(idx):
if idx == n:
return True
for i in range(4):
edges[i] += matchsticks[idx]
if edges[i] <= line and dfs(idx+1):
return True
edges[i] -= matchsticks[idx]
return False
return dfs(0)

复杂度

f1-状态压缩+动态规划
时间:O(n2n), n是火柴数组的长度,有2n个状态,每个状态内在遍历火柴需要O(n)
空间:O(2n) ,保存dp数组需要O(2n)的空间
f2-回溯
时间:O(4n)
空间:O(n),递归栈需要O(n)

总结

f1-状态压缩+动态规划

  1. 能看出来火柴数组长度不大于15,考虑用二进制来表示对应索引火柴的使用情况,进行状态压缩
  2. 能利用简单规则排除明显不满足的情况
  3. 定义dp状态时,定义的放置方式是依次放入,一条边满后再放另一条。对应的dp状态定义为f[s]s状态对应的,正方形未放满边的当前长度
  4. 状态转移时先遍历每个状态,再遍历状态内1对应的i(在代码中是枚举i,不满足s % (1<<i)==1时跳过);下面需要去找i处不放的状态s1,具体实现是s1,s & (~(1<<i))。这里s1一定能转移到s吗?不一定。(1)如果能转移,需要满足f[s1]>=0(代表s1合法),f[s1]+v <=line,表示s的放法不会超过边长,要吗不够继续放,要么刚好满。(2)不能转移有哪些情况?可能是遍历火柴后,不存在合法的s1;也可能是f[s1]+v > line, 不能进行转移
  5. 对某个s,找到合适的s1转以后,为啥直接break?已经找到结果了,这个结果也不会再变,直接去找下一个state就行了。

f2-回溯

  1. 简单排除不满足的情况
  2. 对火柴排序,减少搜索空间
  3. 定义edges辅助数组,表示每个边当前的长度,用来配合dfs
  4. dfs时,需要的参数是idx。退出的出口是idx等于数组长;对每个idx遍历在每个边的放置情况;每个边内的逻辑是(1)放+(2)判断+(3)撤回
  5. 调用时,从索引0开始考虑,就是dfs(0)
posted @   zhangk1988  阅读(104)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示