【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;
};
View Code

接下来就是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[] = {1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880};
    int ans = 0;
    for (int i = 0; i < 9; ++i) {
        int tmp = 0;
        for (int j = i + 1; j < 9; ++j)
            if (nums[j] < nums[i]) ++tmp;
        ans += tmp * fact[9 - i - 1];
    }
    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[] = {1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880};
    int ans = 0;
    for (int i = 0; i < 9; ++i) {
        int tmp = 0;
        for (int j = i + 1; j < 9; ++j)
            if (nums[j] < nums[i]) ++tmp;
        ans += tmp * fact[9 - i - 1];
    }
    return ans;
}

bool valid(const vector<int>& nums)
{
    for (int i = 0; i < nums.size() - 1; ++i)
        if (nums[i] != i + 1) return false;
    if (nums[nums.size() - 1] != 0) return false;
    return true;
}

int bfs(State state)
{
    int dirs[4] = {-3, -1, 3, 1};
    int steps = 0;

    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 > 20) return -1;

        for (int d : dirs) {
            int nextb = state.blank + d;
            if (nextb < 0 || nextb >= 9 || (d == 1 && nextb % 3 == 0)
                || (d == -1 && nextb % 3 == 2)) 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 = 0;
    for (int i = 0; i < 9; ++i) {
        cin >> num;
        if (num == 0) blank = i;
        nums.push_back(num);
    }

    State state;
    state.nums = nums;
    state.blank = blank;
    state.steps = 0;

    int res = bfs(state);
    cout << res << endl;

    return 0;
}
View Code

其他

  • 双向广搜貌似效率更高一些,立个flag,周末补充一下    
  • 代码还是太弱了,一到比赛考试就蒙圈,还是得好好练习呀 = =
  • 希望大家多多支持,多多批评

 


 

补充

双向广搜(DBFS)

基本思路是从其实状态和目标状态两个方向出发,交替进行广搜。

需要维护两个队列 q1 和 q2 ,分别代表从起始节点出发和从目标节点出发。同时使用 visited 整型数组来表示节点的访问情况:0表示未访问,1表示被正向队列访问过,2表示被反向队列访问过,3表示双向均访问过(即找到了一条可行路径),看上去是有点low嗯……

对两个队列交替进行BFS,目的是寻找交汇节点,分别返回到交汇节点需要的步数,之和即为所求结果。

考虑终止条件。我们假设 q1 中当前出队的节点为 now ,下一个被扩展的未访问的节点为 next ,如果 next已经在 q2 中被访问过(即 visited[cantor(next.nums)] == 2 ),说明这时 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[] = {1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880};
    int ans = 0;
    for (int i = 0; i < 9; ++i) {
        int tmp = 0;
        for (int j = i + 1; j < 9; ++j)
            if (nums[j] < nums[i]) ++tmp;
        ans += tmp * fact[9 - i - 1];
    }
    return ans;
}


int bfs(queue<State>& q, int* visited, int dir)
{
    int dirs[4] = {-3, -1, 3, 1};
    int steps = 0;
    State now;

    now = q.front();
    q.pop();

    if (now.steps > 10) return 0;
    if (visited[cantor(now.nums)] == 3) return now.steps;

    State next;
    for (int d : dirs) {
        int nextb = now.blank + d;

        if (nextb < 0 || nextb >= 9 || (d == 1 && nextb % 3 == 0)
            || (d == -1 && nextb % 3 == 2)) 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 == 1) {
            if (visited[c] != 1) {
                if (visited[c] == 2) {
                    visited[c] = 3;
                    return next.steps;
                }
                q.push(next);
                visited[c] = 1;
            }
        }
        // negative queue
        else {
            if (visited[c] != 2) {
                if (visited[c] == 1){
                    visited[c] = 3;
                    return next.steps;
                }
                q.push(next);
                visited[c] = 2;
            }
        }
    }

    return -1;
}


// Double BFS
int dbfs(State s1, State s2)
{
    if (s1.nums == s2.nums) return 0;

    queue<State> q1, q2;
    int visited[MAXN];
    memset(visited, 0, MAXN);

    q1.push(s1);
    q2.push(s2);

    visited[cantor(s1.nums)] = 1;
    visited[cantor(s2.nums)] = 2;

    int ret1 = -1, ret2 = -1;
    while (!q1.empty() || !q2.empty()) {
        if (!q1.empty() && ret1 < 0) ret1 = bfs(q1, visited, 1);
        if (!q2.empty() && ret2 < 0) ret2 = bfs(q2, visited, 2);

        if (ret1 == 0 && ret2 == 0) return -1;
        if (ret1 != -1 && ret2 != -1) return ret1 + ret2;
    }

}

int main()
{
    vector<int> nums1, nums2;
    int num, blank = 0;
    for (int i = 0; i < 9; ++i) {
        cin >> num;
        if (num == 0) blank = i;
        nums1.push_back(num);

        nums2.push_back((i + 1) % 9);
    }

    State s1, s2;
    s1.nums = nums1; s1.blank = blank; s1.steps = 0;
    s2.nums = nums2; s2.blank = 8;     s2.steps = 0;

    int ret = dbfs(s1, s2);

    cout << ret << endl;

    return 0;
}
View Code

 

posted on 2017-04-13 20:52  simplc  阅读(749)  评论(2编辑  收藏  举报

导航