11

How can I fill in a square 2D array with numbers so that a (random) path of consecutive numbers in ascending order is created from 1 to (edge length)2?

I'm trying to write a Hidato (aka Hidoku) generator in JavaScript. It's not necessarily the best language to write it in, but that's what I'm using currently. The game board is initially only partially filled. The only guaranteed numbers shown are the first and last numbers in the path. The idea of the game is to create a single path of numbers through the grid (vertically, horizontally or diagonally) so that there is a consecutive ascending chain of numbers. The chain can overlap due to the diagonals being taken into account.

I'm stuck on the board generation part. A valid grid must have a (single, non-branching) path of consecutive numbers going from 1 to (grid size)2. I've looked and looked but found nothing that might've helped. Is there an path tracing algorithm that can fill a 2D array with a single path made up of consecutive numbers?

My initial, naive approach was to fill a 2D array with values and swap values until the grid was a valid Hidato puzzle. This would take forever to compute and would be very inefficient, so I scrapped that idea.

My next thought was to use a backtracking path tracer to fill in the grid with consecutive values, however I'm unsure how to implement such a tracer. Generating a path is easy enough (choose a random adjacent cell and move to it until the 2D array is full), but my issue here is the "backtracking-ness" of the algorithm, or some other way to always ensure there is a random path of consecutive numbers throughout the grid. I thought about a maze tracer but that doesn't deal with single paths with no forking or dead ends.

How can I proceed from here? Should I be considering other options than a path tracer or other similar algorithm?

Related questions:

Community
  • 1
  • 1
Bojangles
  • 99,427
  • 50
  • 170
  • 208
  • Did I get it right: you put King chess piece on a random cell of the chessboard and visit each cell only once. Then you take a "snapshot" of this chessboard and re-numerate all cells with numbers in traversal order? – izogfif Apr 09 '13 at 11:03
  • Yes, that's correct. The cells must be adjacent, however - I can't randomly jump the chess piece around – Bojangles Apr 09 '13 at 11:23
  • do you need cells to have the common edge? Like the chess piece Rook moves, but only to one cell maximum? – izogfif Apr 09 '13 at 11:26
  • I'm not sure what you mean. The chess piece here can only move one cell in any direction – Bojangles Apr 09 '13 at 11:27
  • Which rule is true: "you can only move one cell left, top, right or bottom" or "you can move in all eight directions: up-left, up, up-right, left, right, down-left, down, and down-right"? – izogfif Apr 09 '13 at 11:30
  • "you can move in all eight directions: up-left, up, up-right, left, right, down-left, down, and down-right" `:)` – Bojangles Apr 09 '13 at 11:56
  • This may help your research efforts: your problem is equivalent to finding a [Hamiltonian Path](http://en.wikipedia.org/wiki/Hamiltonian_path) in your grid. – Kevin Apr 09 '13 at 12:02
  • @Kevin: this will narrow the possible solutions: there is no need the last cell to be adjacent with the first one. – izogfif Apr 09 '13 at 12:23
  • @Bojangles: did you try DFS (deep-first search) algorithm? – izogfif Apr 09 '13 at 12:52

1 Answers1

8

It turns out that the local search algorithm for Hamilton path due to Angluin and Valiant (1977) is pretty good at this, even though there's no proof for non-random graphs. Here's a sample square

  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

and the (somewhat hastily written) Java code that made it.

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);
    }
}
David Eisenstat
  • 64,237
  • 7
  • 60
  • 120
  • Thank you _very_ much for your help. Would you be able to summarise the algorithm in pseudocode? – Bojangles Apr 09 '13 at 14:51
  • At every point in time, there's a simple path to `sink`. From `sink`, try to extend the path with a random outgoing arc. If the head of that arc is not on the path, extend the path. Otherwise, the new arc completes a loop whose junction node is that arc's head. Delete the other loop arc incident to the junction node, reorient the arcs in the loop, and continue with `sink` equal to the head of the deleted arc. Searching for "Angluin Valiant Hamilton cycle" will find more explanations. – David Eisenstat Apr 09 '13 at 15:13
  • @DavidEisenstat Are you aware of any examples of AV that work with missing cells in the grid? Imagine you wanted a 3x3 grid like this: N N N, N X N, N N N Where N is a number and X won't be included in the graph? – Spencer Evans Jan 24 '20 at 03:27
  • 1
    @SpencerEvans AV doesn't need a square grid, or any particular graph properties at all. It may not succeed quickly on arbitrary graphs, since only the case of a random graph is known to have a formal proof (unless there have been subsequent papers), but it's worth a try. – David Eisenstat Jan 24 '20 at 17:32
  • @DavidEisenstat I ended up figuring out how to edit the example about 5 mins after commenting- Simply don't add nodes/edges for any missing grid cell. You're right that it doesn't always find a solution in a short period of time. I gave it a very short time limit and just retry if it fails. Thanks again for posting this example! Worked like a charm and is pretty fast, even in JS. – Spencer Evans Jan 25 '20 at 20:54