用于编程练习的回溯解决方案(拟合管道)

时间:2012-03-13 17:56:15

标签: c++ algorithm recursion backtracking

我正在审查本地编程竞赛中的编程问题。

您可以下载问题here(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;
}

2 个答案:

答案 0 :(得分:7)

原始回复

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

我没有足够的经验来提供更多详细信息(虽然如果我有时间在下周我可能会在某个时候给它一个),但如果这种事情很有意思,那就更多了another answer i wrote some time ago中的背景。

有趣的问题;感谢发布此内容。

[编辑:如果您不能使用其他库,那么您可以自己进行约束传播。有一个很棒的article by norvig,展示了如何为数独做这件事。我强烈建议阅读 - 我认为你会看到如何进行技术,即使它是数独和蟒蛇。]

更新回复(2012-04-06 - 更新了博客参考资料;旧评论有漏洞)

深度优先搜索,其中下一个空单元格填充每个可用的一致图块,并且一致性检查包括边缘约束(边缘没有管道)和最近邻居,是效率很高。我在clojure中有一个未经优化的实现,可以解决0.4ms左右的较小示例(JVM热备后360ms内的1000)和3ms中的较大示例(cedric van goethem报告优化的1ms - 但仍然是OO-java实现,这似乎是合理的)。它可以在12秒内解决10x10拼图(没有初始提示的同心圆管)。

我还花时间研究一种“智能”方法,该方法跟踪每个单元格的约束,就像上面的norvig论文一样。然后我尝试使用choco。所有这些都在blog posts here中有更详细的描述(我在这个答案的更新中确实有更多细节,但它们基于错误的代码 - 博客有更多,更好的信息)。来源也可以下载。

所有这一切的一般结论是,直接搜索可以达到10x10。之后,更多的智慧可能有所帮助,但很难确定,因为生成测试用例很难(即使给出了大量的单元格,它们也很容易模糊不清)。

答案 1 :(得分: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]包含行n中的所有8 ^ m生成的行(从1开始)与数组[n-1]中的每个项目相结合(即项目的边框与行的下边框相同) )请注意,如果您巧妙地将此条件与步骤2中的循环结合使用,则不需要任何费用。

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

  5. 如果你已经完成,请在array [n]中检查元组(空边框,全砖使用,行)。由于您的任务描述暗示它存在,因此打印出它的行。然后查看行的下边框并在数组[n-1]中搜索它并打印它,依此类推。

  6. 希望你能理解我的英语。