[NOIP 2013] 华容道

当年我省只有vfk一人AC此题(其实是AK),学长记载的时候猜测他一定用了很高级的算法。做完之后发现特定算法方面它只用到了BFS和SPFA,不禁想起自己去年NOIP,三个小时没做出一道DFS(得了10分)。

读完题后,判断它是搜索。棋子的移动等价于空格的移动。要不要使用启发式搜索?怎样表示一个状态?我发现只要标记出空格的位置和指定棋子的位置。这样一来,状态只有不到810000个,60分应该有了。500次查询是怎么回事?4亿多的计算量……启发式搜索能让有解的情况快一点,无解还是得搜索整个状态空间。这是一个上界,也许实际情况达不到这么坏?然而,吃过几次亏了,对数据没有明确说明的特性不要乱假设……

和WZH学长交流了一下,结果他看题解去了(嘿嘿)……看完题解的WZH学长,每句话都带着强烈的启发性。虽然不是很明白他在说什么,但他强调了四连通。

先回到自己的思路。如果使用启发式搜索,启发函数我会选择指定棋子到终点的曼哈顿距离。这样不能体现空格和棋子之间的联系,移动的是空格,然而醉翁之意不在酒,棋子才是我的目标。如果开局的时候空格离指定棋子很远,会扩展出很多无用的状态,因为是让空格走,这个不影响启发函数。也许我应该把空格和棋子的距离加入启发函数?这样仍然不能避免无解时搜索整个状态空间。我也考虑过让空格先走到棋子附近,再搜索,把空格处于四连通或八连通位置作为初始状态。W学长的话又促使我思考,发现四连通就够了。但是还是禁不住巨大的搜索量。

我得从根本上加以改进,减少状态数。先前想的那些策略,都是为了让空格尽量接近棋子,不走冤枉路,不喧宾夺主。能不能让棋子自己主导搜索?它想往旁边移一格,怎么办?借助于空格。空格先到那里,再和它交换一下位置。必须是这样。是否可以规定状态中棋子和空格是相邻的?这样使状态数减少到3600个。怎么扩展呢?每次BFS?很可能会搜索整个棋盘,不优。联想到题目中一个有点怪的约定:每组数据中棋盘是固定的,变的只是开局时空格的位置和游戏的指定棋子起始、终止的位置。方便了预处理。

然后写代码。一开始有点不专心,造成了之后调试的困难。考虑不周……我写着写着才发现要用SPFA。还有其他错误:字母打错、不必要的限制条件、改了一处错误却没改配套的地方、SPFA使用了struct Node里的dist而不是更新后的、在外面加点忘记置inq为true、初始化数组的位置不对、莫名地在每次询问后把预处理出来的cost数组清零了(是说大数据怎么一堆-1……)。手里有数据,我是对着数据调的,作弊啦。练习了一下GDB的使用。一开始,学长推荐GDB,我是拒绝的。最近发现还是挺方便,可以让程序在恰当的地方停下来,查看一些变量。要综合百家之长啊。

对于我的代码做一些说明。两个idx函数用于把多个维度压缩成一维,因为状态数比较少,就没有用哈希表。namespace pre有两个用处:一是用于预处理,修改dist数组,在main里面用这些信息计算cost;二是用于让空格走到棋子附近。发现它们的需求是相似的,就共用了一段代码。namespace search用于针对每一局来search,状态之间转移的代价来自cost。在W学长的提醒下加上了对起点终点是否重合的判断。

#include <cstdio>
#include <queue>
#include <cmath>
#include <cstring>
#include <algorithm>
using namespace std;
const int dr[] = {-1, 1, 0, 0}, dc[] = {0, 0, -1, 1}, INF = 0x3f3f3f3f;
int n, m, cost[1<<14], dist[32][32];
bool M[32][32];

// 棋子在(i, j),空格在k方向,棋子转移到p方向
inline int idx(int i, int j, int k, int p)
{
    return i<<9 | j<<4 | k<<2 | p;
}

// 棋子在(i, j),空格在k方向
inline int idx(int i, int j, int k)
{
    return i<<7 | j<<2 | k;
}

namespace pre {
    struct Node {
        int r, c;
    };

    // 从(r, c)到(r1, c1)周围四连通的位置,且不过(r1, c1);至多有cnt个目标点
    void cal(int r, int c, int r1, int c1, int cnt)
    {
        memset(dist, -1, sizeof(dist));
        M[r1][c1] = false;
        queue<Node> Q;
        Q.push((Node){r, c});
        dist[r][c] = 0;
        Node u;
        while (!Q.empty()) {
            u = Q.front();
            Q.pop();
            if (abs(u.r-r1) == 1 && abs(u.c-c1) == 1) {
                if (--cnt == 0)
                    break;
            }
            for (int i = 0; i < 4; ++i) {
                Node v = (Node){u.r+dr[i], u.c+dc[i]};
                if (M[v.r][v.c] && dist[v.r][v.c] == -1) {
                    dist[v.r][v.c] = dist[u.r][u.c] + 1;
                    Q.push(v);
                }
            }
        }
        M[r1][c1] = true;
    }
}

namespace search {
    struct Node {
        int r, c, dir;
    };
    int d[1<<12];
    bool inq[1<<12];

    int spfa(int ti, int tj, queue<Node>& Q)
    {
        Node u; 
        while (!Q.empty()) {
            u = Q.front();
            Q.pop();
            int hu = idx(u.r, u.c, u.dir);
            inq[hu] = false;
            for (int k = 0; k < 4; ++k) {
                Node v = (Node){u.r+dr[k], u.c+dc[k], k^1};
                int c = cost[idx(u.r, u.c, u.dir, k)], hv = idx(v.r, v.c, v.dir);
                if (M[v.r][v.c] && c && d[hv] > d[hu] + c) {
                    d[hv] = d[hu] + c;
                    if (!inq[hv]) {
                        Q.push(v);
                        inq[hv] = true;
                    }
                }
            }
        }
        int ans = INF;
        for (int k = 0; k < 4; ++k)
            ans = min(ans, d[idx(ti, tj, k)]);
        return ans == INF ? -1 : ans;
    }
}

int main()
{
    int q;
    scanf("%d %d %d", &n, &m, &q);
    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= m; ++j) {
            int x;
            scanf("%d", &x);
            M[i][j] = x;
        }

    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= m; ++j)
            if (M[i][j]) // blank
                for (int k = 0; k < 4; ++k) {
                    int i1 = i+dr[k], j1 = j+dc[k]; // 棋子
                    if (M[i1][j1]) {
                        pre::cal(i, j, i1, j1, 4);
                        for (int p = 0; p < 4; ++p) {
                            int i2 = i1+dr[p], j2 = j1+dc[p]; // 目标
                            if (dist[i2][j2] != -1) {
                                cost[idx(i1, j1, k^1, p)] = dist[i2][j2] + 1;
                            }
                        }
                    }
                }

    int ei, ej, si, sj, ti, tj;
    while (q--) {
        using namespace search;
        scanf("%d %d %d %d %d %d", &ei, &ej, &si, &sj, &ti, &tj);
        if (si == ti && sj == tj) {
            puts("0");
            continue;
        }   
        pre::cal(ei, ej, si, sj, 4);
        memset(d, 0x3f, sizeof(d));
        queue<Node> Q;
        for (int k = 0; k < 4; ++k) {
            int i = si+dr[k], j = sj+dc[k];
            if (dist[i][j] != -1) {
                Q.push((Node){si, sj, k});
                inq[idx(si, sj, k)] = true;
                d[idx(si, sj, k)] = dist[i][j];
            }
        }
        printf("%d\n", spfa(ti, tj, Q));
    }
    return 0;
}

体会:大胆构思,仔细实现。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
noip2013普及组初赛是全国信息学奥林匹克联赛的一场选拔赛。该比赛旨在选拔初学者,对编程和算法有一定基础的学生,通过比赛形式来考察他们的知识水平和解题能力。 比赛题目通常会涉及各个领域的算法和数据结构,如图论、动态规划、数论等。题目难度逐步增加,从简单的输出结果,到复杂的程序设计与代码实现,考察选手的逻辑思维和编程能力。 参赛选手需要通过自己的思考和编程实现来解决题目,同时时间也是一个重要因素。比赛中,选手需要在规定的时间内独立完成所有题目,对于复杂的题目需要迅速想出解题思路并进行编码。因此,在比赛中,选手的临场发挥和解题速度也是需要考虑的因素。 noip2013普及组初赛的结果将作为选拔阶段的一个重要依据,选出表现出色的选手进入到更高阶段的比赛,对于他们来说,这是一次展示自己实力的机会。 此外,noip2013普及组初赛,也给了参赛选手一个交流的平台。选手们可以通过比赛结交同好,相互切磋,共同进步。同时,比赛结束后,还有详细的解题分析和讲解,有助于参赛选手对自己在比赛中的不足进行反思与改进。 总之,noip2013普及组初赛是一个考察学生编程和算法能力的选拔赛,通过比赛的形式来选拔出优秀的选手。这对于参赛选手来说,是一次展示自己才华的机会,也是一个展示自己实力和提高自己能力的平台。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值