编程练习(安装管道)的回溯解决方案

13

我正在审查一道来自本地编程竞赛的编程问题。

您可以在此处下载这个问题(pdf文件)。虽然是荷兰语,但图片会帮助理解。

输入一个m * m的网格,其中有些地方有管道,有些地方缺失(用问号表示)。 剩下的管道必须被放置在网格中,以便与其他管道连接起来。

每个管道都表示为一个字母(请参见第2页上的图片)。字母'A'的值为1,'B'的值为2,...

我尝试使用回溯算法解决它(但还不够完善)。但由于该网格的大小可以达到10x10,因此速度太慢。 有人能提出更好(更快)的解决方案/方法吗?

#include <iostream>
#include <stdio.h>
#include <vector>
using namespace std;

#define sz(a) int((a).size())
#define pb push_back

int m, found;
string letters;
vector<int> done;
vector<string> a;

int ok(char letter, int c, int r)
{
    int val = letter - 'A' + 1;

    //checking if no side goes outside
    if (r == 0 && (val & 1))
        return 0;
    if (r == m - 1 && (val & 4))
        return 0;
    if (c == 0 && (val & 8))
        return 0;
    if (c == m - 1 && (val & 2))
        return 0;

    //check if the side is connected the other pipe on the grid
    if (r > 0 && a[r - 1][c] != '?' && (a[r - 1][c] & 4) && !(val & 1))
        return 0;
    if (c > 0 && a[r][c - 1] != '?' && (a[r][c - 1] & 2) && !(val & 8))
        return 0;
    if (r < m - 1 && a[r + 1][c] != '?' && (a[r + 1][c] & 1) && !(val & 4))
        return 0;
    if (c < m - 1 && a[r][c + 1] != '?' && (a[r][c + 1] & 8) && !(val & 2))
        return 0;

    return 1;
}

void solve(int num_placed, int pos)
{
    if (found) return;

    //done
    if (num_placed == sz(letters)) {
        for (int i = 0; i < m; ++i)
            cout << a[i] << endl;
        found = 1;
        return;
    }

    int c = pos % m;
    int r = pos / m;
    if (a[r][c] != '?')
        solve(num_placed, pos + 1);

    //try all the pipes on this position
    for (int i = 0; i < sz(letters); ++i) {
        if (!done[i] && ok(letters[i], c, r)) {
            a[r][c] = letters[i];
            done[i] = 1;
            solve(num_placed + 1, pos + 1);
            done[i] = 0;
            a[r][c] = '?';
        }
    }
}

int main()
{
    freopen("input.txt", "r", stdin);

    int n;
    cin >> n;

    while (n--) {
        cin >> m;
        cin >> letters;

        cout << m << endl;
        a.clear();
        for (int i = 0; i < m; ++i) {
            string line;
            cin >> line;
            a.pb(line);
        }

        done = vector<int>(sz(letters), 0);

        found = 0;
        solve(0, 0);
    }

    return 0;
}

1
你链接到了输入,而不是问题描述。 - Bart
2
看起来问题描述是:http://www.vlaamseprogrammeerwedstrijd.be/2011/opgaves/cat2-2011/loodgieter.pdf - fgb
2
这个问题让我想起了NetWalk游戏。虽然不一样,但仍然很有趣。 - Dialecticus
这里有一些“硬”测试数据吗? - MGwynne
输入:http://www.vlaamseprogrammeerwedstrijd.be/2011/opgaves/cat2-2011/loodgieter/bonus.input;输出:http://www.vlaamseprogrammeerwedstrijd.be/2011/opgaves/cat2-2011/loodgieter/bonus.output;输入:http://www.vlaamseprogrammeerwedstrijd.be/2011/opgaves/cat3-2011/loodgieter/wedstrijd.input;输出:http://www.vlaamseprogrammeerwedstrijd.be/2011/opgaves/cat3-2011/loodgieter/wedstrijd.output;输入:http://www.vlaamseprogrammeerwedstrijd.be/2011/opgaves/cat2-2011/loodgieter/wedstrijd.input;输出:http://www.vlaamseprogrammeerwedstrijd.be/2011/opgaves/cat2-2011/loodgieter/wedstrijd.output。 - andrew cooke
显示剩余4条评论
2个回答

7

原回答

你是否必须自己编写所有代码,或者有兴趣探索其他工具?因为我建议看一下约束传播/线性规划。您已经有很多边界约束-外边缘不能有管道,加上内部边缘-所以我想这将非常有效。此外,约束看起来将是简单的等式,因此设置应该相当容易。

我在这方面没有足够的经验,无法在此处提供更多详细信息(尽管如果我有时间在下周的某个时候可能会尝试),但是如果这种事情有趣,那么在另一个我之前写过的答案中有更多背景。

ps有趣的问题;感谢发布。

[编辑:如果您无法使用其他库,则可以自己进行约束传播。Norvig撰写了一篇精彩的文章,展示了如何针对数独执行此操作。我强烈建议阅读该文章-我认为您将看到如何跨越技术,即使它是数独和Python。]

更新回答(2012-04-06-使用博客引用更新;旧评论存在错误)

深度优先搜索,其中下一个空单元格填充每个可用的一致瓷砖,并且一致性检查包括边缘约束(不要在边缘上放置管道)和最近的邻居,效率相当高。我在Clojure中有一个未经优化的实现,可以在大约0.4ms(JVM预热后1000个为360ms)内解决较小的示例,而在3ms内解决较大的示例(Cedric Van Goethem报告了1ms的优化-但仍然是OO-Java实现,这似乎是合理的)。它可以在12s内解决10x10的难题(没有初始提示的同心圆管)。

我还花时间研究了“智能”方法,该方法跟踪每个单元格上的约束,就像Norvig上面的论文中一样。然后我尝试使用Choco。所有这些都在此处的博客文章中更详细地描述(我在对此答案进行更新时有更多详细信息,但是它们基于有缺陷的代码-博客提供了更多,更好的信息)。源代码也可以下载。

所有这些的一般结论是,直接搜索对于10x10以下的情况都很好。在那之后,更多的智能可能会有所帮助,但是很难确定,因为生成测试用例很困难(即使给出了许多单元格,它们往往是模棱两可的)。


是的,它属于那个问题类别,但我不能使用非标准库。 - Jasper
2
Norvig写了一篇非常棒的文章,介绍了如何将回溯搜索与其他约束传播技术相结合来解决数独问题,而不使用专门的软件包。目前,您已经有了搜索功能,但没有利用其他约束(例如管道不跨越边界)。因此,我建议阅读http://norvig.com/sudoku.html,在那里他详细描述了如何解决这种类型的问题,并且阅读体验非常好。 - andrew cooke
谢谢你,安德鲁,写得非常好。这可能会有所帮助。 - Jasper

2
不错的问题。我已经找到了一个O(n·m·8^m)的解决方案,这似乎足够好。
  1. 关注第一行和第二行之间的边界。有2^m种可能性(每个侧面都有出线或不出线)。这将是上边界线,下边界线将没有连接。

  2. 对于每对下边界线和上边界线(将是2^m·2^m = 4^m对),计算适合的每一行。如果从左边进入,则给出左侧、顶部和底部,因此只有2种可能性。如果您查看的瓷砖在地图中固定,请检查其是否适合并中止操作。递归调用此过程,每个包含2^m行或总共为4^m*2^m=8^m。

  3. 虽然最后一步是纯暴力,但这次我们使用DP。将元组(边框、使用的砖块、行)保存在数组的数组中。array[0]将包含单个元组(空边框、未使用砖块、无)。array[n]包含所有生成的8^m行在第n行(从1开始)与每个项组合起来它适合(即项目的边框与行的下边框相同)。请注意,如果您巧妙地将此条件与步骤2中的循环相结合,则不会耗费任何东西。

  4. 删除需要使用比可用数量更多的一种砖块的所有元组,并对数组进行排序。然后继续执行步骤2,直到处理完所有行。

  5. 如果已完成,请在array[n]中查找元组(空边框、所有砖块都使用、行)。由于您的任务描述暗示它存在,请打印出其行。然后查看行的下边界并在array[n-1]中搜索它并打印它,以此类推。

希望您理解我的英语。

谢谢回复,但恐怕我不理解你的解决方案。 - Jasper
他所说的DP是指动态规划。 - Rusty Rob

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接