【问题标题】:Fill 2D grid with single path用单一路径填充 2D 网格
【发布时间】:2013-04-09 09:57:37
【问题描述】:

如何用数字填充方形二维数组,以便创建从 1 到 (edge length)2 的升序连续数字的(随机)路径?

我正在尝试用 JavaScript 编写 Hidato(又名 Hidoku)生成器。它不一定是最好的语言,但这就是我目前使用的语言。游戏板最初只是部分填充。显示的唯一保证数字是路径中的第一个和最后一个数字。该游戏的理念是通过网格(垂直、水平或对角线)创建一条数字路径,从而形成一个连续的递增数字链。由于考虑了对角线,链可能会重叠。

我被困在电路板生成部分。一个有效的网格必须具有从1 到(grid size)2 的连续数字的(单个、非分支)路径。我看了又看,但没有发现任何可能有帮助的东西。有没有一种路径跟踪算法可以用由连续数字组成的单个路径填充二维数组?

我最初的幼稚方法是用值填充二维数组并交换值,直到网格成为有效的 Hidato 谜题。这将花费很长时间来计算并且效率非常低,所以我放弃了这个想法。

我的下一个想法是使用回溯路径跟踪器用连续值填充网格,但是我不确定如何实现这样的跟踪器。生成路径很容易(选择一个随机的相邻单元格并移动到它直到 2D 数组已满),但我这里的问题是算法的“回溯性”,或者其他一些始终确保存在随机整个网格中连续数字的路径。我想到了一个迷宫追踪器,但这并不处理没有分叉或死胡同的单一路径。

我该如何从这里开始?我应该考虑路径跟踪器或其他类似算法之外的其他选项吗?

相关问题:

【问题讨论】:

  • 我猜对了吗:你把国王棋子放在棋盘的一个随机单元格上,每个单元格只访问一次。然后你拍下这个棋盘的“快照”,并按遍历顺序用数字重新计算所有单元格?
  • 是的,没错。但是,单元格必须相邻 - 我不能随意跳棋
  • 你需要细胞有共同的边缘吗?就像棋子 Rook 移动,但最多只能移动一个单元格?
  • 我不确定你的意思。这里的棋子只能向任意方向移动一个格
  • 哪条规则为真:“您只能向左、上、右或下移动一个单元格”或“您可以在所有八个方向上移动:左上、上、右上、左、右、左下、下和右下”?

标签: javascript algorithm


【解决方案1】:

事实证明,由于 Angluin 和 Valiant (1977) 的汉密尔顿路径局部搜索算法在这方面非常出色,即使没有非随机图的证据。这是一个示例方块

  99  98 101 103 105 106 129 132 133 140 135 136
  97 100 102 104 107 130 131 128 141 134 139 137
  95  96 109 108 112 122 127 126 125 142 143 138
  80  94 110 111 121 113 123 124  40  39  36 144
  79  81  93 120 116 115 114  48  41  38  37  35
  78  82  92  90 119 117  47  46  49  42  33  34
  77  83  84  91  89 118  45  58  43  50  32  31
  76   1  85  87  88  60  59  44  57  51  30  28
  75   2  86   4   6  63  61  54  52  56  29  27
  73  74   3   7   5  64  62  53  55  22  24  26
  72  69  67   8  65  11  12  14  15  23  21  25
  70  71  68  66   9  10  13  16  17  18  19  20

以及制作它的(有些仓促的)Java 代码。

import java.util.*;

public class AV {
    public static void main(String[] args) {
        // construct an n-by-n grid
        int n = 12;
        Node[][] node = new Node[n][n];
        List<Node> nodes = new ArrayList<Node>();
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                nodes.add((node[i][j] = new Node()));
            }
        }
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                if (i >= 1) {
                    if (j >= 1) {
                        node[i - 1][j - 1].addEdge(node[i][j]);
                    }
                    node[i - 1][j].addEdge(node[i][j]);
                    if (j < n - 1) {
                        node[i - 1][j + 1].addEdge(node[i][j]);
                    }
                }
                if (j >= 1) {
                    node[i][j - 1].addEdge(node[i][j]);
                }
            }
        }
        findPath(nodes);
        labelPath(nodes);
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                System.out.printf("%4d", node[i][j].label);
            }
            System.out.println();
        }
    }

    private static void findPath(List<Node> nodes) {
        for (Node node : nodes) {
            node.isOnPath = false;
        }
        Random random = new Random();
        Node sink = nodes.get(random.nextInt(nodes.size()));
        sink.isOnPath = true;
        int isNotOnPathCount = nodes.size() - 1;
        while (isNotOnPathCount > 0) {
            sink.pathOut = sink.out.get(random.nextInt(sink.out.size()));
            sink = sink.pathOut.head;
            if (sink.isOnPath) {
                // rotate
                sink = sink.pathOut.head;
                Arc reverse = null;
                Node node = sink;
                do {
                    Arc temp = node.pathOut;
                    node.pathOut = reverse;
                    reverse = temp.reverse;
                    node = temp.head;
                } while (node != sink);
            } else {
                // extend
                sink.isOnPath = true;
                isNotOnPathCount--;
            }
        }
    }

    private static void labelPath(Collection<Node> nodes) {
        for (Node node : nodes) {
            node.isSource = true;
        }
        for (Node node : nodes) {
            if (node.pathOut != null) {
                node.pathOut.head.isSource = false;
            }
        }
        Node source = null;
        for (Node node : nodes) {
            if (node.isSource) {
                source = node;
                break;
            }
        }
        int count = 0;
        while (true) {
            source.label = ++count;
            if (source.pathOut == null) {
                break;
            }
            source = source.pathOut.head;
        }
    }
}

class Node {
    public final List<Arc> out = new ArrayList<Arc>();
    public boolean isOnPath;
    public Arc pathOut;
    public boolean isSource;
    public int label;

    public void addEdge(Node that) {
        Arc arc = new Arc(this, that);
        this.out.add(arc.reverse);
        that.out.add(arc);
    }
}

class Arc {
    public final Node head;
    public final Arc reverse;

    private Arc(Node head, Arc reverse) {
        this.head = head;
        this.reverse = reverse;
    }

    public Arc(Node head, Node tail) {
        this.head = head;
        this.reverse = new Arc(tail, this);
    }
}

【讨论】:

  • 非常感谢您非常的帮助。你能用伪代码总结一下算法吗?
  • 在每个时间点,都有一条通往sink 的简单路径。从sink 开始,尝试使用随机出局弧来扩展路径。如果该弧的头部不在路径上,则延长路径。否则,新弧完成一个循环,其连接节点是该弧的头部。删除与连接节点相关的另一个循环弧,重新定向循环中的弧,然后继续使用等于删除弧的头部的sink。搜索“Angluin Valiant Hamilton cycle”会找到更多解释。
  • @DavidEisenstat 您是否知道任何使用网格中缺失单元格的 AV 示例?想象一下,您想要一个像这样的 3x3 网格:N N N, N X N, N N N 其中 N 是一个数字,X 不会包含在图表中?
  • @SpencerEvans AV 根本不需要方形网格或任何特定的图形属性。它在任意图上可能不会很快成功,因为已知只有随机图的情况有正式证明(除非有后续论文),但值得一试。
  • @DavidEisenstat 我最终在评论后大约 5 分钟弄清楚了如何编辑示例 - 只需不要为任何缺失的网格单元添加节点/边。你是对的,它并不总是在短时间内找到解决方案。我给了它一个非常短的时间限制,如果它失败了就重试。再次感谢您发布此示例!即使在 JS 中也能像魅力一样工作并且速度非常快。
猜你喜欢
  • 1970-01-01
  • 2019-07-06
  • 2013-04-16
  • 1970-01-01
  • 1970-01-01
  • 2017-03-24
  • 2017-08-14
  • 2015-06-04
  • 1970-01-01
相关资源
最近更新 更多