状态定义有时要服务于剪枝策略
题目链接
http://poj.org/problem?id=2362
题意
输入M个棍子(\(4\le M\le 20\)),问这M个棍子是否能首尾相接形成一个正方形。
题解
本质就是找一个数组的全排列,使得从头到尾能形成4段长度相等的区域。
由于只需找到一个符合条件的排列,想到直接使用DFS。
如果直接暴力搜索,复杂度是\(O(n!)\)的,显然会爆。必然需要剪枝。
定义stick[M]数组为:所有木棍及其长度。
搜索状态的定义
基本的状态
假设木棍总长度为target,则正方形边长是side = target / 4。
如果存在某个排列,那么在搜索到这个排列的过程中,应该有5个阶段:
- 已组建完成0条边
- 已组建完成1条边
- 已组建完成2条边
- 已组建完成3条边
- 已组建完成4条边
因此,在状态描述中,必须有已经完成多少条边这一状态,记为finished_side
然后考虑上述每个阶段,都有一个当前边已有的长度这一状态,记为len
(其实上述两种状态可以使用当前的总长度sum来表示,sum % side表示len,sum / side表示已经完成多少边,为清晰期间,还是分成两个量表示)
visit数组标记已用过的木棍,用于表示当前状态下,还可以使用哪些木棍扩展子节点(visit值为false的点)
服务于剪枝的状态
按道理来说,以上三个状态已经足够完成搜索了,但是如果仅仅这样搜索的话,无法完全支持后续的剪枝策略。这里引入另一个状态描述——当前状态下,用于构建子节点的木棍的起始下标,记为start_index。
为何引入这个状态,在下文剪枝策略中描述。
剪枝策略
首先是全局的判断(甚至称不上是剪枝)
- 总长度不为4的倍数,肯定不行
··废话·· - 最长的棍子大于边长,肯定不行
然后几个重点的剪枝策略:
- 当finished_side == 3时,就可直接判断为可行了,因为只要总长度是4的倍数,最后一条边一定能构建成功
- 构建某条边时,如果长度等于fail_length的木棍构建正方形失败,那么所有长度为fail_length的木棍都不能用于构建 ====> 剪去了在相同的长度木棍下重复搜索(也就是,每种长度的棍子,只需做一次拓展)
- 这里为了判断方便,可以事先将棍子排序,让相同长度的都在一起
- 如果本状态构造的边和父状态构造的边时同一条边,则只需扩展本状态后面(这里的“后面”指stick数组中)的木棍。
- 这里解释了start_index这个状态存在的意义
- 每次扩展子状态i之前,判断是否开启了一条新边:如果不是,则子状态的扩展起点只需在后面即可(start_index=i +1);如果是,则扩展起点需重置为0(start_index=0)。
对最后一个剪枝策略的正确性作一个证明:
在构建同一条边的情况下,当前木棍(下标记做i)的前面的木棍(下标记做k)(k < i),无非两种状态:
- 已经被选择了,visit[k]值为true。这样的话,本来就不该选
- 未被选择,visit[k]值为false。按照不剪枝的策略,木棍k本来是要被用来扩展子节点的,为什么不呢?
- 由于每次都是按照stick数组的从前到后的顺序扩展的,k肯定在当前节点i的某个祖先节点扩展子节点时,曾经被选中过。且当时选中之后,k也曾把i拿去用于扩展子节点(即,选k又选i的情况,早就出现过了)。但显然那种情况是不可行的,否则早就成功返回了。因此,选择k肯定是不可行的。
但是当不是构建同一条边的时候,就应该从0开始重新找了。直观地看,构建上一条边舍弃的某些木棍,可能可以用于构建下一条边。
AC代码
1 |
|