473. 火柴拼正方形 - 力扣(LeetCode) (leetcode-cn.com)
题目:
是否能利用给定数组中的所有数拼出一个正方形。
思考过程:
看题目的数据范围就知道是用状态压缩。当然,这类题目很多时候都可以用dfs + 剪枝技巧 来做。包括我自己以前都是喜欢一股脑地上dfs,但是这次专门挑战下就用状态压缩dp。原因有两个:1)处于锻炼自身技能的目的 2)dfs经常要搭配一些剪枝技巧,有时候并不是很容易找出来剪枝技巧的思路。
这道题属于十分经典的状态压缩类的(分组)问题。典型的套路就是:将原问题拆解为子问题的解。还有一道比较类似的可以参考:1681. 最小不兼容性 - 力扣(LeetCode) (leetcode-cn.com)
在这里的情况就是将数组中所有数分到4组,看是否每一组的和都相等。所以我们可以提前计算出数组总和,然后除以4就能求出边长,也即每个分组的和
递推公式如下:
f[k][mask]∣=f[k−1][mask⊕subset],其中valid[subset]=true,表示数组中的某个子集subset代表的数之和是否等于正方形的边长f[k][mask] \quad |= f[k-1][mask \oplus subset],其中valid[subset] = true,表示数组中的某个
\子集subset代表的数之和是否等于正方形的边长f[k][mask]∣=f[k−1][mask⊕subset],其中valid[subset]=true,表示数组中的某个子集subset代表的数之和是否等于正方形的边长
顺便说一下两个位运算的经典作用:
1.求所有子集
1 | ini复制代码int subset = mask; |
2.将选定的数置0
1 | css复制代码mask = mask ^ subset; |
我们可以先预处理valid数组,看哪些数字组合可以放到一组中,然后再利用上面的递推式枚举所有状态,最后答案是:
f[k][(1<<nums.length)−1]f[k][(1 << nums.length) - 1]f[k][(1<<nums.length)−1]
一开始的代码如下:
1 | java复制代码 public boolean makesquare(int[] matchsticks) { |
2300多ms通过你敢信。。。?
当然本着学习的态度,这显然是无法接受的。
于是我在原来的代码上加了很多输出,帮助自己寻找“重复”的计算,类似于:
1 | ini复制代码 public boolean makesquare(int[] matchsticks) { |
终于,在观察了半天打印出来的输出之后,我发现:其实我一直迷惑了自己,双层for循环其实是“不重不漏”的,没有地方重复计算了,每一步都是“合理”的,比如,当要求f[3][mask]
的时候,就需要知道f[2][mask ^ subset]
的各种情况。
真正耗时的是由很多计算其实是不需要的,所以只需要加上判断:
1 | css复制代码if (sidesum[j] != (i + 1) * sidelen) { |
就能够提减少运算。
它的含义是在当前k=i的情况下,只有那些sidesum[j] == (i + 1) * sidelen
才有可能 为true
完整代码如下:
1 | java复制代码public boolean makesquare(int[] matchsticks) { |
另外,其实可以把双层循环改成一层就够了,如下:
1 | java复制代码public boolean makesquare(int[] matchsticks) { |
上面的两个代码我加了count来对比是否计算量一致,最后发现是一致的。
最终提交的时间:
其实跟dfs+剪枝比还是不快的,但是这种套路应用性好一点,毕竟有些剪枝技巧实在是不容易看出来。
本文转载自: 掘金