【BFS + Hash】拼图——携程2017春招编程题2
写在前面
前天参加了携程的网测……还是感觉自己太!渣!了! _(:з」∠)_
时光匆匆啊,已经到了开始思考人生的时候了(算了不矫情了)……总之写个博客来督促一下自己。之前太懒了,很多时候都是输在了“开始”这一步上了,顺便用一句前几天看到的鸡汤来警醒一下自己,”你不需要很厉害,才可以开始;而是要开始,才可以很厉害”,共勉。
说正经的
经典的拼图(八数码/九宫格)问题,典型BFS,但这次考试是我第一次做希望大家不要嘲笑,考试的时候想当然的觉得每次交换必须把其中一个数字复位,所以就简单写了写,没想到居然过了75%(问号脸)。
回过头来发现能拿分算是上苍保佑了,不多啰嗦了下面是正题。原题链接点这里。
题目描述
拼图,是一个老少皆宜的益智游戏,在打乱的3*3的范围中,玩家只能每次将空格(0)和相邻的数字格(上、下、左、右)交换,最终调整出一个完整的拼图。
完整拼图为:
1 2 3 4 5 6 7 8 0 输入
测试数据共3行,每行3个数字 ,包括数字0-8(无重复)
输出
还原完整拼图的最少移动次数。如不需要调整,则输出0;如无解,则输出-1。
例如:
0 1 3 4 2 5 7 8 6 依次移动1,2,5,6,即可还原为正确拼图,移动次数为4
限制
- 时间限制:C/C++语言:2000MS; 其它语言:4000MS
- 内存限制:C/C++语言:10240KB; 其它语言:534528KB
解题思路
拼图的每个状态作为一个节点,每移动一次即到达下一层。因此对于每个节点,与之相连的下一层最多有4个节点(分别对应上下左右移动),在这道题中,每个节点须同时保存截至当前的移动次数,为了方便还可以加上空格的位置(可以根据上层的空格位置更新,不需要重新遍历求取),如下:
struct State {
vector<int> nums;
int blank;
int steps;
};
接下来就是BFS过程了。注意保存一下当前节点,然后获取新节点并加入队列(满足条件的前提下)。搜索过程中会有大量重复情况出现,所以判重就非常重要了。
首先想到的是用 STL 中的 set 来保存已经搜索过的节点,然鹅,内存爆了(MLE)……再试下把 vector<int> 换成 string 吧,还是MLE(= =)…… 其实set 的内部存储结构是红黑树(Red-Black Tree),插入和搜索效率大为提升,但必然是以空间的损失为代价的。
一番搜索之后,找到解决方案——康托展开。康托展开将n个元素的某种全排列映射成一个唯一的正整数 X,表示该全排列在所有全排列方案中排第X位(从小到大,序列号从0开始)。具体公式这里不展开了,举个栗子,比如 [1, 2, 3] 的全排列中,要求[2, 3, 1] 的康托展开结果,如下:
X = 1*2!+1*1!+0*0! = 3
从公式形式来看跟某进制的展开差不多,只是考虑到全排列的性质,相应的把幂运算换成了阶乘。OK下面是代码:
int cantor(vector<int> nums)
{
int fact[] = {, , , , , , , , , };
int ans = ;
for (int i = ; i < ; ++i) {
int tmp = ;
for (int j = i + ; j < ; ++j)
if (nums[j] < nums[i]) ++tmp;
ans += tmp * fact[ - i - ];
}
return ans;
}
Cantor Expansion
这样就可以通过一个长度为 9!的数组来实现判重啦!
附上AC源代码
#include <iostream>
#include <vector>
#include <queue>
#include <memory.h>
#define MAXN 362880
using namespace std; struct State {
vector<int> nums;
int blank;
int steps;
}; int cantor(vector<int> nums)
{
int fact[] = {, , , , , , , , , };
int ans = ;
for (int i = ; i < ; ++i) {
int tmp = ;
for (int j = i + ; j < ; ++j)
if (nums[j] < nums[i]) ++tmp;
ans += tmp * fact[ - i - ];
}
return ans;
} bool valid(const vector<int>& nums)
{
for (int i = ; i < nums.size() - ; ++i)
if (nums[i] != i + ) return false;
if (nums[nums.size() - ] != ) return false;
return true;
} int bfs(State state)
{
int dirs[] = {-, -, , };
int steps = ; queue<State> q;
bool visited[MAXN];
memset(visited, false, MAXN); q.push(state);
visited[cantor(state.nums)] = true; State next; while (!q.empty()){
state = q.front(); q.pop(); if (valid(state.nums)) return state.steps;
if (state.steps > ) return -; for (int d : dirs) {
int nextb = state.blank + d;
if (nextb < || nextb >= || (d == && nextb % == )
|| (d == - && nextb % == )) continue; next = state;
swap(next.nums[next.blank], next.nums[next.blank + d]);
next.blank += d;
++next.steps; int c = cantor(next.nums);
if (!visited[c]) {
q.push(next);
visited[c] = true;
}
}
}
} int main()
{
vector<int> nums;
int num, blank, dist = ;
for (int i = ; i < ; ++i) {
cin >> num;
if (num == ) blank = i;
nums.push_back(num);
} State state;
state.nums = nums;
state.blank = blank;
state.steps = ; int res = bfs(state);
cout << res << endl; return ;
}
其他
- 双向广搜貌似效率更高一些,立个flag,周末补充一下 √
- 代码还是太弱了,一到比赛考试就蒙圈,还是得好好练习呀 = =
- 希望大家多多支持,多多批评
补充
双向广搜(DBFS)
基本思路是从其实状态和目标状态两个方向出发,交替进行广搜。
需要维护两个队列 q1 和 q2 ,分别代表从起始节点出发和从目标节点出发。同时使用 visited 整型数组来表示节点的访问情况:0表示未访问,1表示被正向队列访问过,2表示被反向队列访问过,3表示双向均访问过(即找到了一条可行路径),看上去是有点low嗯……
对两个队列交替进行BFS,目的是寻找交汇节点,分别返回到交汇节点需要的步数,之和即为所求结果。
考虑终止条件。我们假设 q1 中当前出队的节点为 now ,下一个被扩展的未访问的节点为 next ,如果 next已经在 q2 中被访问过(即 visited[cantor(next.nums)] == ),说明这时 next 就是两个队列交汇的节点。此时需要处理队列 q2中的该节点(判断 visited==3即可)。
最终返回两个队列的BFS结果,相加,OK~
在系统上边提交了一下发现效果还是很明显的(上边是双向广搜,下边是普通广搜):

不过感觉这个网站上的测试样例是有点少 = = 所以代码侥幸过了也未必不可能,如果有错误或者其他意见建议欢迎大家提出!再来一遍原题链接!
最后是源代码(太懒,注释写(ya)的(gen)不(mei)多(xie),求轻拍):
/**
Double BFS
*/
#include <iostream>
#include <vector>
#include <queue>
#include <memory.h>
#define MAXN 362880
using namespace std;
struct State {
vector<int> nums;
int blank;
int steps;
};
/* Cantor expansion : judging duplicate situations */
int cantor(vector<int> nums)
{
int fact[] = {, , , , , , , , , };
int ans = ;
for (int i = ; i < ; ++i) {
int tmp = ;
for (int j = i + ; j < ; ++j)
if (nums[j] < nums[i]) ++tmp;
ans += tmp * fact[ - i - ];
}
return ans;
}
int bfs(queue<State>& q, int* visited, int dir)
{
int dirs[] = {-, -, , };
int steps = ;
State now;
now = q.front();
q.pop();
if (now.steps > ) return ;
if (visited[cantor(now.nums)] == ) return now.steps;
State next;
for (int d : dirs) {
int nextb = now.blank + d;
if (nextb < || nextb >= || (d == && nextb % == )
|| (d == - && nextb % == )) continue;
next = now;
swap(next.nums[next.blank], next.nums[next.blank + d]);
next.blank += d;
++next.steps;
int c = cantor(next.nums);
// positive queue
if (dir == ) {
if (visited[c] != ) {
if (visited[c] == ) {
visited[c] = ;
return next.steps;
}
q.push(next);
visited[c] = ;
}
}
// negative queue
else {
if (visited[c] != ) {
if (visited[c] == ){
visited[c] = ;
return next.steps;
}
q.push(next);
visited[c] = ;
}
}
}
return -;
}
// Double BFS
int dbfs(State s1, State s2)
{
if (s1.nums == s2.nums) return ;
queue<State> q1, q2;
int visited[MAXN];
memset(visited, , MAXN);
q1.push(s1);
q2.push(s2);
visited[cantor(s1.nums)] = ;
visited[cantor(s2.nums)] = ;
int ret1 = -, ret2 = -;
while (!q1.empty() || !q2.empty()) {
if (!q1.empty() && ret1 < ) ret1 = bfs(q1, visited, );
if (!q2.empty() && ret2 < ) ret2 = bfs(q2, visited, );
if (ret1 == && ret2 == ) return -;
if (ret1 != - && ret2 != -) return ret1 + ret2;
}
}
int main()
{
vector<int> nums1, nums2;
int num, blank = ;
for (int i = ; i < ; ++i) {
cin >> num;
if (num == ) blank = i;
nums1.push_back(num);
nums2.push_back((i + ) % );
}
State s1, s2;
s1.nums = nums1; s1.blank = blank; s1.steps = ;
s2.nums = nums2; s2.blank = ; s2.steps = ;
int ret = dbfs(s1, s2);
cout << ret << endl;
return ;
}
【BFS + Hash】拼图——携程2017春招编程题2的更多相关文章
- 算法是什么我记不住,But i do it my way. 解一道滴滴出行秋招编程题。
只因在今日头条刷到一篇文章,我就这样伤害我自己,手贱. 刷头条看到一篇文章写的滴滴出行2017秋招编程题,后来发现原文在这里http://www.cnblogs.com/SHERO-Vae/p/588 ...
- ACM-ICPC(10 / 10)——(完美世界2017秋招真题)
今天学了莫比乌斯反演,竟然破天荒的自己推出来了一个题目!有关莫比乌斯反演的题解,和上次的01分数规划的题解明天再写吧~~~ 学长们都在刷面试题,我也去试了试,120分钟,写出6题要有一点熟练度才行.先 ...
- Leetcode - 剑指offer 面试题29:数组中出现次数超过一半的数字及其变形(腾讯2015秋招 编程题4)
剑指offer 面试题29:数组中出现次数超过一半的数字 提交网址: http://www.nowcoder.com/practice/e8a1b01a2df14cb2b228b30ee6a92163 ...
- 百度2017春招<空间中最大三角形面积的问题>
题目: 三维空间中有N个点,每个点可能是三种颜色的其中之一,三种颜色分别是红绿蓝,分别用'R', 'G', 'B'表示. 现在要找出三个点,并组成一个三角形,使得这个三角形的面积最大.但是三角形必须满 ...
- 百度2017春招<度度熊回家问题>
题目: 一个数轴上共有N个点,第一个点的坐标是度度熊现在位置,第N-1个点是度度熊的家.现在他需要依次的从0号坐标走到N-1号坐标.但是除了0号坐标和N-1号坐标,他可以在其余的N-2个坐标中选出一个 ...
- 小米2017秋招真题——电话号码分身问题(Java版)
原题描述如下: 通过对各个数字对应的英文单词的分析,可以发现一些规律: 字母Z为0独占,字母W为2独占,字母U为4独占,字母X为6独占,字母G为8独占: 在过滤一遍0.2.4.6.8后,字母O为1独占 ...
- 第K个幸运数(京东2017秋招真题)
题目 4和7是两个幸运数字,我们定义,十进制表示中,每一位只有4和7两个数的正整数都是幸运数字.前几个幸运数字为:4,7,44,47,74,77,444,447... 现在输入一个数字K,输出第K个幸 ...
- 2017阿里Java编程题第2题
题意是给一组数字+符号(自增1:^,相乘*,相加+)和一个长度为16的stack.栈空取数返回-1,栈满推数返回-2. 输入样例是1 1 + 2 ^ 3 * 这样子,做的时候紧张忽略了空格,用char ...
- poj1182 and 携程预赛2第一题 带权并查集
题意: 动物王国中有三类动物A,B,C,这三类动物的食物链构成了有趣的环形.A吃B, B吃C,C吃A. 现有N个动物,以1-N编号.每个动物都是A,B,C中的一种,但是我们并不知道它到底 ...
随机推荐
- 清空file文件域的方法
我们在实际应用中经常希望能把文件域给清空,比如使用change事件时,因为不清空再次选择同一文件时将不在触发change事件. 但是在IE中,由于安全设置的原因,是不允许更改文件域的值的,也就是不能使 ...
- 微信小程序入门之构建一个简单TODOS应用
最近开始了解微信小程序,虽然小程序已经出了很久了,刚出的那段时间很火,看到很多关于小程序的技术文章,不过现在似乎没那么火了,anyway,我们还是可以学习下的. 一.了解微信小程序 1.理念:小程序开 ...
- android开发艺术探索读书笔记之-------view的事件分发机制
View的点击事件的分发,其实就是对MotionEvent事件的分发过程,即当一个MotionEvent产生后,系统需要把这个事件传递给一个具体的View,而这个过程就是分发过程. 分发过程主要由以下 ...
- java-信息安全(四)-数据签名、数字证书
概述 信息安全基本概念: 数字签名 数字证书 数字签名 数字签名(又称公钥数字签名.电子签章)是一种类似写在纸上的普通的物理签名,但是使用了公钥加密领域的技术实现,用于鉴别数字信息的方法.一套数字签名 ...
- 表达式计算 java 后缀表达式
题目: 问题描述 输入一个只包含加减乖除和括号的合法表达式,求表达式的值.其中除表示整除. 输入格式 输入一行,包含一个表达式. 输出格式 输出这个表达式的值. 样例输入 1-2+3*(4-5) 样例 ...
- 浅谈EL
一.了解EL 1.EL是从 JavaScript 脚本语言得到启发的一种表达式语言,它借鉴了 JavaScript 多类型转换无关性的特点.在使用 EL 从 scope 中得到参数时可以自动转换类型, ...
- cheatsheet——mac 上的一款可以显示软件所有快捷键的小工具
https://www.mediaatelier.com/CheatSheet/ 发现一款可以显示 mac 上各种软件所有快捷键的小工具:cheatsheet,只要长按 command 键就可以了~ ...
- .Net轻量级ORM-NPoco的使用方法-摘自NPoco国外官方Wiki
文章引用自NPoco官方Wiki,地址:https://github.com/schotime/NPoco/wiki,因公司网络不稳定,有时无法访问,特将其摘抄. Home Adam Schroder ...
- windows phone 8.1开发:触控和指针事件1
原文出自:http://www.bcmeng.com/windows-phone-touch/ UIElement类的触控事件: ManipulationStarting:当用户将手指放在 IsMan ...
- JVM中GC浅解:垃圾回收的了解
1.为什么要有GC 没有GC的世界,我们需要手动进行内存管理,但是内存管理是纯技术活,又容易出错.但是我们写码的目的是为了解决业务问题,所以可以把这种纯技术活自动化,当然自动化也是有代价的. 2.垃圾 ...