排列搜索(英雄会在线编程题)解答和搜索可视化(已贴出代码)

题目出自: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

 

posted @ 2014-02-13 19:21  张海拔  阅读(1173)  评论(8编辑  收藏  举报