排列搜索(英雄会在线编程题)解答和搜索可视化(已贴出代码)
2014-02-13 19:21
204 查看
题目出自: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果断会超时。
当然你也可以按这个思路写一次,可以与优化算法后的程序对拍,验证结果。
用回溯法写全排列很简单,可以看我过去写的文章:/article/6991308.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}
搜索空间树应该是是这样的(字典序枚举):
树中节点(除总的树根)就是在各个位置上枚举出来的数字。
初始化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增加
由于是做题代码不考虑代码复用性和鲁棒性。
其中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]]了。这样写代码不仅短了,同样也保持了清晰易懂。
【简化后代码如下】
【最后,我们通过打印搜索空间树,看看这个算法对于不同输入的表现】
我们修改一下代码就可以借助tree工具来打印,修改代码如下:
关于tree工具的使用,前面的文章也有介绍,可以看:二叉排序树删除、搜索、插入的递归实现 link(public)
代码测试示范:
看以看到——
第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
题目详情
设数组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果断会超时。
当然你也可以按这个思路写一次,可以与优化算法后的程序对拍,验证结果。
用回溯法写全排列很简单,可以看我过去写的文章:/article/6991308.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增加
#include <stdio.h> #include <string.h> int fac[12] = { 1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880, 3628800, 39916800 }; int a[12], v[12], n, k, b[4], ans, cnt; void dfs2(int step) { int i; if (step == 4) { 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 ) { ans += fac[n-cnt]; } return; } if (a[a[b[step]]] != -1) { dfs2(step+1); return; //! } for (i = 0; i < n; ++i) { if (!v[i]) { v[i] = 1; cnt++; a[ a[b[step]] ] = i; dfs2(step+1); a[ a[b[step]] ] = -1; cnt--; v[i] = 0; } } } void dfs1(int step) { int i; if (step == 4) { dfs2(0); return; } if (a[b[step]] != -1) { dfs1(step+1); return; } for (i = 0; i < n; ++i) { if (!v[i]) { v[i] = 1; cnt++; a[ b[step] ] = i; dfs1(step+1); a[ b[step] ] = -1; cnt--; v[i] = 0; } } } int howmany (int N,int K,int B0,int B1,int B2,int B3) { n = N, k = K, b[0] = B0, b[1] = B1, b[2] = B2 ,b[3] = B3; memset(a, -1, sizeof a); ans = cnt = 0; dfs1(0); return ans; } int main(void) { printf("%d\n", howmany(5, 2, 1, 2, 3, 4)); return 0; }
由于是做题代码不考虑代码复用性和鲁棒性。
其中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]]了。这样写代码不仅短了,同样也保持了清晰易懂。
【简化后代码如下】
/* *CopyRight (C) Zhang Haiba *Date: 2014-02-12 *FileName: csdn10_reduce.c * *this prog to solve the problem http://hero.csdn.net/Question/Details?ID=292&ExamID=287 *and reduce code form csdn10.c */ #include <stdio.h> #include <string.h> int fac[12] = { 1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880, 3628800, 39916800 }; int a[12], v[12], n, k, b[4], ans, cnt; void dfs(int step) { int i; int x = step < 4 ? b[step] : a[b[step-4]]; if (step == 8) { 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 ) ans += fac[n-cnt]; } else if (a[x] != -1) { dfs(step+1); } else { for (i = 0; i < n; ++i) { if (!v[i]) { v[i] = 1; cnt++; a[x] = i; dfs(step+1); a[x] = -1; cnt--; v[i] = 0; } } } } int main(void) { memset(a, -1, sizeof a); scanf("%d%d%d%d%d%d", &n, &k, &b[0], &b[1], &b[2], &b[3]); ans = cnt = 0; dfs(0); printf("%d\n", ans); return 0; }
【最后,我们通过打印搜索空间树,看看这个算法对于不同输入的表现】
我们修改一下代码就可以借助tree工具来打印,修改代码如下:
/* *CopyRight (C) Zhang Haiba *Date: 2014-02-12 *FileName: csdn10_tree_show.c * *this prog to solve the problem http://hero.csdn.net/Question/Details?ID=292&ExamID=287 *and using tree tools to print search space tree */ #include <stdio.h> #include <string.h> #include <stdlib.h> //for system() #define CMD_LEN 128 //for char cmd[] int fac[12] = { 1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880, 3628800, 39916800 }; int a[12], v[12], n, k, b[4], ans, cnt; void dfs(int step, FILE* fd) //add FILE* fd { int i; int x = step < 4 ? b[step] : a[b[step-4]]; if (step == 8) { 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 ) ans += fac[n-cnt]; } else if (a[x] != -1) dfs(step+1, fd); else { for (i = 0; i < n; ++i) { if (!v[i]) { v[i] = 1; cnt++; a[x] = i; fprintf(fd, "(%d", i); //add dfs(step+1, fd); fprintf(fd, ")"); //add a[x] = -1; cnt--; v[i] = 0; } } } } void show_by_tree(void) { char cmd[CMD_LEN]; sprintf(cmd, "rm -f ./tree_src.txt"); system(cmd); FILE *fd = fopen("./tree_src.txt", "a+"); fprintf(fd, "\n\t\\tree(root"); dfs(0, fd); fprintf(fd, ")\n\n"); fclose(fd); sprintf(cmd, "cat ./tree_src.txt | ~/tree/tree"); system(cmd); } int main(void) { memset(a, -1, sizeof a); scanf("%d%d%d%d%d%d", &n, &k, &b[0], &b[1], &b[2], &b[3]); ans = cnt = 0; show_by_tree(); printf("%d\n", ans); return 0; }
关于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
相关文章推荐
- 排列搜索-c#求解-英雄会在线编程题目
- 英雄会第一届在线编程大赛:单词博弈 (C++代码) ---miss若尘
- Matlab官方在线代码搜索网站,很多开源matlab代码
- HTML-DEV-ToolLink(常用的在线字符串编解码、代码压缩、美化、JSON格式化、正则表达式、时间转换工具、二维码生成与解码等工具,支持在线搜索和Chrome插件。)
- 去哪儿网2017校招在线笔试(前端工程师)编程题及JavaScript代码
- MSN在线状态代码
- ASP统计在线人数代码
- 网络选摘之[javamail在线发邮件全部代码,包括身份验证部分]
- JSP显示在线人数代码服务
- 枚举数组中所有可能排列的代码实现 [2004年7月8日 1:57 ]
- 【在线运行代码】特效
- 站点中,实现: 在线签名 功能 (示例代码下载)
- php实现站内搜索的代码
- 零行代码把搜索栏searchBar的英文-cancel改为中文-取消
- HTML/CSS/Javascript代码在线压缩、格式化(美化)工具
- Java 2实用教程(第三版)实验指导与习题解答and实验模版代码及答案 (二)
- Google工程师详述Google的搜索结果排列算法
- 一个dht网络的“磁力链接”搜索python代码
- 阿里巴巴集团2017暑期实习Java研发工程师在线编程题-数组分片