您的位置:首页 > 其它

pku acm 2362 square 解题报告

2009-05-20 08:34 302 查看

[b]acm 2362 square


http://acm.pku.edu.cn/JudgeOnline/problem?id=2362

给定一些有特定长度的小棍,问能否拼成正方形。很经典的深度优先搜索+剪枝的题目。

1.    首先不能被4 整除的不行
排序后最长的木棍大于总长度/4的也不行,也就是大于唯一可能的正方形边长的。

2.     然后就是深度优先搜索,
举例说如果,长度总和,除以 4 就是正方形边长,我们设定为len
我们要找出是否存在一个分组,将小木棍分成四组,每组长度和为len

3.	两个典型的测试用例
1 3 4 5 4  7 8   可以        1 7     3 5      4 4     8   注意不能  1 3 4 凑成一组
5 4 7 8 8          不可以

4.	基本的DFS搜索思路

对于每个木棍,可以选或者不选两种状态,顺序扫描所有的木棍,然后找出所有可能的组合,使得长度和可能为len

(对于给的例子即为8),

对于一个给定的长度为8的木棍组,继续递归寻找另一组,直到能找到所有组则成功,但是如果后续失败了,

还能返回上面,继续尝试

如 找到  1 3 4 然后 找到 8 然后不成功
还可以退回来 找到  1  7

5. 	剪枝优化

5.1.其实我们只要找3个分组,对于总和为32的来说,找3个长度为8 的分组成功的话就成功了

5.2.	每次选组的时候,首元素确定。为什么要确定首元素,剪枝,(和排序无关)
因为本质上我们要找的4个分组是不分顺序的!!
abc   def  ghi  jkl

和 def jkl  ghi abc是一个解 找到就可以

所以 对于 a 开头的组

如果我们试图找 (a,..) (...)(...) (...)失败了 那么没有必要找 (!a,....)(...)(...)(...)了
假如能找到 后面的成立 那么 a 必然在后面某一组 ,调到前面来也应该成立 矛盾了

这样每次找某个组的时候剪枝50%

5.3.	还有什么地方可以剪枝呢(这里也和排序无关)
当到达某个元素选定它的时候正好长度达到要求,如上面的例子 总和32,这个选的分组包含该元素正好长度为8.

1 3 4 4 5 7 8

扫描到4 发现长度和为8了 然后 找 (...) (...) (...)失败了
这时候只要退出到上一层继续就好了
不需要考虑 该层不选4的情况
当然对于 从大到小排列 很清楚 后面的更大 更不可能

6  2   1  1    1 1 1 1 1 1 1 1

对于这种情况,假如 扫描到2 发现正好为8,然后查找后续3组(...) (...)(...)失败
同样可以剪枝,不需要考虑 6 !2 。。。。。。。。。。。。  这种情况
因为加入后面的需要考虑 那么  令     x = 2   2前面的集合为 U
sum( U   +  x) =   len

最后假设分组成功 ,肯定   存在某个分组  sum(U + Z)  = len   sum(V+x) = len
那么完全可以互换  Z组和 x,这于前面   U+x 然后向后寻找其他组失败是矛盾的。

5.4.为什么要从大到小排列
POJ的测试数据证明了从大到小排序,快速的多,意味着更少的搜索。

首先从大到小,是先找数目少的集合,能较快的确定某个长度为len 的组,
而且确定的过程中经过许多单分支(仅 false)

8  7   5   4    4    3   1
如   找到  7                                     1
true  false   false false   false     true

而考虑  1    3   4    4   5  7   8
会先找到  1   3  4  后续分组不成功 退回来找  到 1     7
这个过程大量的搜索因为绝大多数分组都是2分的(选中或者不选),两个向下的分支。
下面注意是考虑从小到大排序, 0  右子树也会被剪枝掉,

0                                      7
1                          0                              7
4           1              4            0                         7
8        4      5     1      8     4      4     0                           7
4  9  5                                                            7
12  5                                                     8

5.5  找到后立即退出,也算剪枝。

方法.1 抛出异常
2. 适当的 return 语句        if (dfs(next))  return, 这样成功后立即向上逐层直接退出而不进入后续分支
choosen()
if (dfs(next) return true
not choosen()
if dfs(next)  return  true

return false

5. 6	一个小的剪枝

当两个元素相同,排序后紧挨着,前面的元素如果为被选中,后面的也不需要选。
否则重复了。
4  !4
!4   4    是相同的情形。



//代码主要参考了http://www.cppblog.com/jhpjhyx/archive/2009/05/14/82981.html 表示感谢:)

#include<iostream>

#include <algorithm>
using namespace std;
int test_num;
int sl[20];
bool choosen[20];
int stick_num;
bool find_square;
int sum;
int len;
struct cmper{
inline bool operator ()(const int &a, const int &b)const{
return a > b;
}
};
//确保了最大的要小于= len,也正确 但递归调用较多,考虑用下面的循环减少递归
//void find_group(int set_num, int depth) {
//    if (depth == stick_num)
//        return;
//    if (set_num == 0) {
//        find_square = true;
//        throw true;
//    }
//
//    if (choosen[depth] == true)
//        find_group(set_num, depth + 1);
//    else {
//            if (depth && !sl[depth - 1] && sl[depth] == sl[depth -1]) {
//                find_group(set_num, depth + 1);
//                return;
//            }
//            int sum_now = sl[depth] + sum;
//            if (sum_now < len) {
//                choosen[depth] = true;
//                sum = sum_now;
//                find_group(set_num, depth + 1);
//                sum = sum_now - sl[depth];
//                choosen[depth] = false;
//                if (sum != 0)                          //剪枝,最大的只能被选中,不需要考虑不被选中的情况,剪枝50%
//                    find_group(set_num, depth + 1);
//            }
//            else if (sum_now == len) {
//                choosen[depth] = true;
//                sum = 0;
//                find_group(set_num - 1, 0);
//                choosen[depth] = false;
//
//            }
//            else {
//                find_group(set_num, depth + 1);    //sum_now > len 只能不选当前的
//            }
//
//    }
//}
bool find_group(int set_num, int depth , int sum = 0) {
if (set_num == 1)             //选好3组了,返回成功
return true;

for (int i = depth; i < stick_num; i++) {
if (choosen[i] == true)           //前面的选好的组已经用到该元素,略过继续查找。
continue;
if (i && !sl[i - 1] && sl[i] == sl[i -1])  //剪枝,作用不大
continue;
int sum_now = sl[i] + sum;
if (sum_now < len) {
choosen[i] = true;         //选中当前元素,继续后续查找当前组组元素
if (find_group(set_num, i + 1, sum_now))
return true;
choosen[i] = false;  //不选中当期元素的情形(注意向上返回时都已置回false),继续后续查找当前组元素
}
else if (sum_now == len) {
choosen[i] = true;
if (find_group(set_num - 1, 0, 0))  //找到1组,查找后续分组,成功的话返回true
return true;
choosen[i] = false;
}
if (sum_now == len || sum == 0)  //剪枝,首元素要被选中,当包含首元素的组查找其它分组失败即为失败5.2
break;         //当选中当前元素,能构成一个组的时候,如果查找后续分组失败即为失败5.3
}
return false;  //当前选择的情形,已经扫描到末尾仍为成功失向上层返回标识为失败
}
bool group_stick() {
find_square = false;
for (int i = 0; i < 20; i++)
choosen[i] = false;
//try{
return   find_group(4, 0);
//} catch(bool nothing){ }
//return find_square;
}
int main(int argc, char *argv[])
{
cin >> test_num;
int sum;
for (int i = 0; i < test_num; i++) {
cin >> stick_num;
sum = 0;
for (int j = 0; j < stick_num; j++) {
cin >> sl[j];
sum += sl[j];
//choosen[i] = false;
}
if ((sum % 4) != 0)
cout << "no" << endl;
else {
sort(sl, sl + stick_num, cmper());
len = sum / 4;
if (sl[0] > len)
cout << "no" << endl;
else {
if (group_stick())
cout << "yes" << endl;
else
cout << "no" << endl;
}
}
}
return 0;
}

[/b]
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐