Java中用于解决填字游戏的递归回溯

时间:2017-06-21 21:11:09

标签: java algorithm recursion backtracking recursive-backtracking

我需要解决一个填字游戏,给出初始网格和单词(单词可以多次使用或根本不使用)

初始网格看起来像这样:

++_+++
+____+
___+__
+_++_+
+____+
++_+++

这是一个示例单词列表:

pain
nice
pal
id

任务是填充占位符(水平或垂直,长度> 1),如下所示:

++p+++
+pain+
pal+id
+i++c+
+nice+
++d+++

任何正确的解决方案都是可以接受的,并且保证有解决方案。

为了开始解决问题,我将网格存储在2-dim中。 char数组和我按照它们的长度将单词存储在集合列表中:List<Set<String>> words,以便例如words.get(4)

可以访问长度为4的单词

然后我从网格中提取所有占位符的位置,并将它们添加到占位符列表(堆栈)中:

class Placeholder {
    int x, y; //coordinates 
    int l; // the length
    boolean h; //horizontal or not
    public Placeholder(int x, int y, int l, boolean h) {
        this.x = x;
        this.y = y;
        this.l = l;
        this.h = h;
    }
}

算法的主要部分是solve()方法:

char[][] solve (char[][] c, Stack<Placeholder> placeholders) {
    if (placeholders.isEmpty())
        return c;

    Placeholder pl = placeholders.pop();
    for (String word : words.get(pl.l)) {
        char[][] possibleC = fill(c, word, pl); // description below
        if (possibleC != null) {
            char[][] ret = solve(possibleC, placeholders);
            if (ret != null)
                return ret;
        }
    }
    return null;
}

函数fill(c, word, pl)只返回一个新的填字游戏,其当前单词写在当前占位符 pl 上。如果 word pl 不兼容,则函数返回null。

char[][] fill (char[][] c, String word, Placeholder pl) {

    if (pl.h) {
        for (int i = pl.x; i < pl.x + pl.l; i++)
            if (c[pl.y][i] != '_' && c[pl.y][i] != word.charAt(i - pl.x))
                return null;
        for (int i = pl.x; i < pl.x + pl.l; i++)
            c[pl.y][i] = word.charAt(i - pl.x);
        return c;

    } else {
        for (int i = pl.y; i < pl.y + pl.l; i++)
            if (c[i][pl.x] != '_' && c[i][pl.x] != word.charAt(i - pl.y))
                return null;
        for (int i = pl.y; i < pl.y + pl.l; i++)
            c[i][pl.x] = word.charAt(i - pl.y);
        return c;
    }
}

以下是Rextester上的完整代码。

问题是我的回溯算法效果不佳。让我们说这是我的初始网格:

++++++
+____+
++++_+
++++_+
++++_+
++++++

这是单词列表:

pain
nice

我的算法将垂直放置单词pain,但是当意识到这是一个错误的选择时它会回溯,但到那时初始网格将会改变并且占位符的数量将减少。您认为该算法如何修复?

2 个答案:

答案 0 :(得分:4)

这可以通过两种方式解决:

  • fill开头创建矩阵的a deep copy,修改并返回(保留原文完整)。

    鉴于您已经传递了矩阵,这不需要任何其他更改。

    这很简单但效率很低,因为每次尝试填写单词时都需要复制矩阵。

  • 创建一个unfill方法,该方法恢复在fill中所做的更改,在每个for循环迭代结束时调用。

    for (String word : words.get(pl.l)) {
        if (fill(c, word, pl)) {
            ...
            unfill(c, word, pl);
        }
    }
    

    注意:我根据下面的注释更改了fill

    当然,只是试图擦除所有字母可能会删除其他放置字母的字母。为了解决这个问题,我们可以计算每个字母所属单词的数量。

    更具体地说,有一个int[][] counts(也需要传递或以其他方式访问),每当您更新c[x][y]时,也会增加counts[x][y]。要还原展示位置,请将该展示位置中每个字母的数量减少1,并仅删除计数为0的字母。

    这有点复杂,但比上述方法更有效。

    就代码而言,您可以在fill中添加类似的内容:
    (在第一部分中,第二部分是相似的)

    for (int i = pl.x; i < pl.x + pl.l; i++)
        counts[pl.y][i]++;
    

    并且unfill看起来像这样:(仅针对第一部分)

    for (int i = pl.x; i < pl.x + pl.l; i++)
        counts[pl.y][i]--;
    for (int i = pl.x; i < pl.x + pl.l; i++)
        if (counts[pl.y][i] == 0)
            c[pl.y][i] = '_';
    // can also just use a single loop with "if (--counts[pl.y][i] == 0)"
    

请注意,如果采用上述第二种方法,只需让fill返回booleantrue,如果成功)并仅传递{{1}转到c的递归调用。 solve可以返回unfill,因为它不会失败,除非你有错误。

您的代码中只传递了一个数组,您正在做的就是更改其名称。

另见Is Java "pass-by-reference" or "pass-by-value"?

答案 1 :(得分:1)

您自己确定了:

  

它将回溯,但到那时初始网格已经存在   改变

该网格应该是局部矩阵,而不是全局矩阵。这样,当您返回null时,来自父级调用的网格仍然完好无损,准备尝试for循环中的下一个单词。

您的终止逻辑是正确的:当您找到解决方案时,立即将该网格传回堆栈。