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


    题目出自: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
    ", 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
    ", 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, "
    	\tree(root");
    54     dfs(0, fd);
    55     fprintf(fd, ")
    
    ");
    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
    ", 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

  • 相关阅读:
    loj2042 「CQOI2016」不同的最小割
    loj2035 「SDOI2016」征途
    luogu2120 [ZJOI2007]仓库建设
    luogu3195 [HNOI2008]玩具装箱TOY
    51nod 1069 Nim游戏 + BZOJ 1022: [SHOI2008]小约翰的游戏John(Nim游戏和Anti-Nim游戏)
    HDU 5723 Abandoned country(最小生成树+边两边点数)
    BZOJ 1497: [NOI2006]最大获利(最大权闭合图)
    51nod 1615 跳跃的杰克
    SPOJ 839 Optimal Marks(最小割的应用)
    UVa 11107 生命的形式(不小于k个字符串中的最长子串)
  • 原文地址:https://www.cnblogs.com/zhanghaiba/p/3548602.html
Copyright © 2020-2023  润新知