排列搜索(英雄会在线编程题)解答和搜索可视化(已贴出代码)
题目出自:http://hero.csdn.net/Question/Details?ID=292&ExamID=287
设数组a包含n个元素恰好是0..n - 1的一个排列,给定b[0],b[1],b[2],b[3]问有多少个0..n-1的排列a,满足(a[a[b[0]]]*b[0]+a[a[b[1]]]*b[1]+a[a[b[2]]]*b[2]+a[a[b[3]]]*b[3])%n==k ?
输入包含5个参数:N,K,B0,B1,B2,B3,其中 4<= N<12, 0 <= K,B0,B1,B2,B3 < N。
【思路分析】
最直观的思路就是,每输出一个0~n的排列a[],就验证是否满足条件,满足则ans++。当输出0~n的所有的排列时也就求出了ans。
写过全排列的都知道,n个数的全排列需要计算n!次。一般n>9,用个人电脑计算的话,都会感受到计算时间了。n==12果断会超时。
当然你也可以按这个思路写一次,可以与优化算法后的程序对拍,验证结果。
用回溯法写全排列很简单,可以看我过去写的文章:http://www.cnblogs.com/zhanghaiba/p/3533614.html
换个思路,只枚举B0~B3所指的那些a[],考虑到B0~B3还可以重复,所以最多需要给4个位置枚举。
是这样吗?别忘了公式加法因子左边并不是a[b[0]],而是再多一层映射,是a[a[b[0]]]。
假设现在x = {a[b[i]]}( i = [0, 3])已经被枚举出来了,但它们映射后对应四个a[x]却不一定被枚举过,
所以最坏的情况是还需要对4个位置枚举。
综上可见最多枚举8个位置,也就是说程序最坏的时间等同于求0~7的全排列的时间。
//补充:第一次阅读可跳过
最坏时间效率情况,有读者指出应当是相当于求排列A(N, 8)。
我想,最极端坏的情况是N==12时,出现需要枚举的刚好是最后8个位置[4,11]。那么枚举平均次数应该(5+12)/2 = 8.5,也很接近A(8, 8)效率。
还有从整体出发,若第一次映射枚举4个位置(肯定不同),这4个位置映射的新位置也必然不同,从剩下8个数字枚举4个数字作为新位置,碰撞的概率是1/2。
碰撞1次就相当于当前搜索路径的深度减1。
综上,无论如何,最坏效率不会高于A(8.5, 8),平均效率由于我是新手,不会求哈==!,目测挺好的,求大神指点。
最好的情况是b[0~3]都是同一个数,而且有a[b[0]] == a[a[b[0]]],那么只需要枚举1个位置,也就是计算机执行n次。
假如n == 11,总共枚举了5个位置,那么剩下11-5 == 6个位置没有枚举过,
此时5个位置足够计算是否满足条件了,如果满足条件,则ans += 6!
因为剩下6个位置的元素可以随便放,总的可能情况有6!。
【来一个详细例子你就完全明白了】
输入4 0 1 1 1 0
即n == 4, k == 0, b[] = {1, 1, 1, 0}
搜索空间树应该是是这样的(字典序枚举):
root ___________________________|___________________________ | | | | 0 1 2 3 ______|______ ______|______ ______|______ ______|______ | | | | | | | | | | | | 1 2 3 0 2 3 0 1 3 0 1 2 _|__ _|__ _|__ _|__ _|__ _|__ _|__ _|__ _|__ _|__ | | | | | | | | | | | | | | | | | | | | 1 3 1 2 0 3 0 2 1 3 0 3 0 1 1 2 0 2 0 1 | | | | 1 0 1 0
树中节点(除总的树根)就是在各个位置上枚举出来的数字。
初始化a[]使每个元素为-1,值为-1的元素表示这个位置没有枚举过。
(1)先看第一层映射a[b[i]]需要枚举的位置
由于b[i]中有三个重复,也就是说有2个位置b[0]和b[3]需要枚举
按字典序,a[b[0]]枚举为0,a[b[3]]枚举为1,(上图红色数字所示路径)
此时第一层隐射枚举完毕
(2)第二层映射a[a[b[i]]]
因为a[a[b[0]]] == a[0] != -1,也就是枚举过了,不需要再枚举(扩展节点)
同样a[ab[3]]] == a[1] != -1,也不需要再枚举。所以搜索的深度是2
读者可以验证蓝色数字所在路径,是需要枚举4次的。
一楼读者建议AC代码在比赛结束后给出,我觉得有道理,等比赛结束再贴完整代码。
//贴出代码如下:2014-02-27增加
1 #include <stdio.h> 2 #include <string.h> 3 4 int fac[12] = { 1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880, 3628800, 39916800 }; 5 int a[12], v[12], n, k, b[4], ans, cnt; 6 7 void dfs2(int step) 8 { 9 int i; 10 11 if (step == 4) { 12 if ( (a[a[b[0]]]*b[0] + a[a[b[1]]]*b[1] + a[a[b[2]]]*b[2] + a[a[b[3]]]*b[3]) % n == k ) { 13 ans += fac[n-cnt]; 14 } 15 return; 16 } 17 if (a[a[b[step]]] != -1) { 18 dfs2(step+1); 19 return; //! 20 } 21 for (i = 0; i < n; ++i) { 22 if (!v[i]) { 23 v[i] = 1; 24 cnt++; 25 a[ a[b[step]] ] = i; 26 dfs2(step+1); 27 a[ a[b[step]] ] = -1; 28 cnt--; 29 v[i] = 0; 30 } 31 } 32 } 33 34 void dfs1(int step) 35 { 36 int i; 37 38 if (step == 4) { 39 dfs2(0); 40 return; 41 } 42 if (a[b[step]] != -1) { 43 dfs1(step+1); 44 return; 45 } 46 for (i = 0; i < n; ++i) { 47 if (!v[i]) { 48 v[i] = 1; 49 cnt++; 50 a[ b[step] ] = i; 51 dfs1(step+1); 52 a[ b[step] ] = -1; 53 cnt--; 54 v[i] = 0; 55 } 56 } 57 } 58 59 int howmany (int N,int K,int B0,int B1,int B2,int B3) 60 { 61 n = N, k = K, b[0] = B0, b[1] = B1, b[2] = B2 ,b[3] = B3; 62 memset(a, -1, sizeof a); 63 ans = cnt = 0; 64 dfs1(0); 65 return ans; 66 } 67 68 int main(void) 69 { 70 printf("%d\n", howmany(5, 2, 1, 2, 3, 4)); 71 return 0; 72 }
由于是做题代码不考虑代码复用性和鲁棒性。
其中fac数组保存阶乘,fac[5] == 5!,其中fac[0]应该为1。
v数组标记i是否枚举过,v[i]==1表示数字i已经没枚举过了。
这里DFS1对应第一层映射需要枚举的位置,DFS2对应第二层。
DFS看起来做固定4次枚举(扩展节点),其实对于重复情况直接跳到下一层。但记得回溯时要return。
cnt变量记录搜索深度(也就是枚举的位置有几个)。最后满足条件则ans += fac[n-cnt]。
很容易把两层映射的DFS合并,只需设置一个a[]下标x表示当前要枚举的位置。
第一层映射(step < 4),x当然是b[step],第二层x就是a[b[step-4]]了。这样写代码不仅短了,同样也保持了清晰易懂。
【简化后代码如下】
1 /* 2 *CopyRight (C) Zhang Haiba 3 *Date: 2014-02-12 4 *FileName: csdn10_reduce.c 5 * 6 *this prog to solve the problem http://hero.csdn.net/Question/Details?ID=292&ExamID=287 7 *and reduce code form csdn10.c 8 */ 9 10 11 #include <stdio.h> 12 #include <string.h> 13 14 int fac[12] = { 1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880, 3628800, 39916800 }; 15 int a[12], v[12], n, k, b[4], ans, cnt; 16 17 void dfs(int step) 18 { 19 int i; 20 int x = step < 4 ? b[step] : a[b[step-4]]; 21 22 if (step == 8) { 23 if ( (a[a[b[0]]]*b[0] + a[a[b[1]]]*b[1] + a[a[b[2]]]*b[2] + a[a[b[3]]]*b[3]) % n == k ) 24 ans += fac[n-cnt]; 25 } else if (a[x] != -1) { 26 dfs(step+1); 27 } else { 28 for (i = 0; i < n; ++i) { 29 if (!v[i]) { 30 v[i] = 1; 31 cnt++; 32 a[x] = i; 33 dfs(step+1); 34 a[x] = -1; 35 cnt--; 36 v[i] = 0; 37 } 38 } 39 } 40 } 41 42 int main(void) 43 { 44 memset(a, -1, sizeof a); 45 scanf("%d%d%d%d%d%d", &n, &k, &b[0], &b[1], &b[2], &b[3]); 46 ans = cnt = 0; 47 dfs(0); 48 printf("%d\n", ans); 49 return 0; 50 }
【最后,我们通过打印搜索空间树,看看这个算法对于不同输入的表现】
我们修改一下代码就可以借助tree工具来打印,修改代码如下:
1 /* 2 *CopyRight (C) Zhang Haiba 3 *Date: 2014-02-12 4 *FileName: csdn10_tree_show.c 5 * 6 *this prog to solve the problem http://hero.csdn.net/Question/Details?ID=292&ExamID=287 7 *and using tree tools to print search space tree 8 */ 9 10 #include <stdio.h> 11 #include <string.h> 12 #include <stdlib.h> //for system() 13 #define CMD_LEN 128 //for char cmd[] 14 15 int fac[12] = { 1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880, 3628800, 39916800 }; 16 int a[12], v[12], n, k, b[4], ans, cnt; 17 18 void dfs(int step, FILE* fd) //add FILE* fd 19 { 20 int i; 21 22 int x = step < 4 ? b[step] : a[b[step-4]]; 23 if (step == 8) { 24 if ( (a[a[b[0]]]*b[0] + a[a[b[1]]]*b[1] + a[a[b[2]]]*b[2] + a[a[b[3]]]*b[3]) % n == k ) 25 ans += fac[n-cnt]; 26 } else if (a[x] != -1) 27 dfs(step+1, fd); 28 else { 29 for (i = 0; i < n; ++i) { 30 if (!v[i]) { 31 v[i] = 1; 32 cnt++; 33 a[x] = i; 34 fprintf(fd, "(%d", i); //add 35 dfs(step+1, fd); 36 fprintf(fd, ")"); //add 37 a[x] = -1; 38 cnt--; 39 v[i] = 0; 40 } 41 } 42 } 43 } 44 45 void show_by_tree(void) 46 { 47 char cmd[CMD_LEN]; 48 49 sprintf(cmd, "rm -f ./tree_src.txt"); 50 system(cmd); 51 52 FILE *fd = fopen("./tree_src.txt", "a+"); 53 fprintf(fd, "\n\t\\tree(root"); 54 dfs(0, fd); 55 fprintf(fd, ")\n\n"); 56 fclose(fd); 57 58 sprintf(cmd, "cat ./tree_src.txt | ~/tree/tree"); 59 system(cmd); 60 } 61 62 int main(void) 63 { 64 memset(a, -1, sizeof a); 65 scanf("%d%d%d%d%d%d", &n, &k, &b[0], &b[1], &b[2], &b[3]); 66 ans = cnt = 0; 67 show_by_tree(); 68 printf("%d\n", ans); 69 return 0; 70 }
关于tree工具的使用,前面的文章也有介绍,可以看:二叉排序树删除、搜索、插入的递归实现 link(public)
代码测试示范:
ZhangHaiba-MacBook-Pro:code apple$ ./a.out 4 0 3 2 1 0 root ___________________________|___________________________ | | | | 0 1 2 3 ______|______ ______|______ ______|______ ______|______ | | | | | | | | | | | | 1 2 3 0 2 3 0 1 3 0 1 2 _|__ _|__ _|__ _|__ _|__ _|__ _|__ _|__ _|__ _|__ _|__ _|__ | | | | | | | | | | | | | | | | | | | | | | | | 2 3 1 3 1 2 2 3 0 3 0 2 1 3 0 3 0 1 1 2 0 2 0 1 | | | | | | | | | | | | | | | | | | | | | | | | 3 2 3 1 2 1 3 2 3 0 2 0 3 1 3 0 1 0 2 1 2 0 1 0 4 ZhangHaiba-MacBook-Pro:code apple$ ./a.out 4 0 1 1 0 0 root ___________________________|___________________________ | | | | 0 1 2 3 ______|______ ______|______ ______|______ ______|______ | | | | | | | | | | | | 1 2 3 0 2 3 0 1 3 0 1 2 _|__ _|__ _|__ _|__ _|__ _|__ _|__ _|__ _|__ _|__ | | | | | | | | | | | | | | | | | | | | 1 3 1 2 0 3 0 2 1 3 0 3 0 1 1 2 0 2 0 1 | | | | 1 0 1 0 8 ZhangHaiba-MacBook-Pro:code apple$ ./a.out 4 0 0 0 0 0 root _____________|______________ | | | | 0 1 2 3 ___|___ ___|___ ___|___ | | | | | | | | | 0 2 3 0 1 3 0 1 2 24 ZhangHaiba-MacBook-Pro:code apple$ ./a.out 5 1 0 0 0 0 root ________________________|________________________ | | | | | 0 1 2 3 4 ____|_____ ____|_____ ____|_____ ____|_____ | | | | | | | | | | | | | | | | 0 2 3 4 0 1 3 4 0 1 2 4 0 1 2 3 0
看以看到——
第1个用例b[i]全部不重复,必定当且仅当枚举4个位置,打印出来的其实就是一个0~3的全排列搜索空间树;
第2个用例实际b[i]有2个不重复,所以枚举位置个数2~4个都有,搜索空间树是参差不齐的;
第3个用例实际b[i]只有1个不重复,搜索空间范围更小了;
第4个用例有5个位置,b[i]却只有1个,实际上枚举空间也非常小,当然这个时候也是最好的情况。
@Author: 张海拔
@Update: 2014-2-27
@Link: http://www.cnblogs.com/zhanghaiba/p/3548602.html