15
\$\begingroup\$

Consider a square n by n grid graph that looks like this.

grid graph

It is important to notice that this graph is 11 by 11.

At any given point a man stands at an intersection and he only ever moves vertically or horizontally by one step at a time to the next intersection. Sadly he has drunk a little too much so he chooses the direction he moves randomly from the up to 4 possible directions (up, down, left, right). This is up to 4 as if he is standing at a wall he has only 3 options of course and in a corner he only has 2.

He starts in the bottom left hand corner and his goal is to get home which is the top right hand corner. The time is simply the number of steps it takes him.

However, you are a malicious adversary who wants him to get home as slowly as possible. You can delete any number of edges from the graph at any time during during his walk. The only restriction is that you must always leave some way for him to get home and you can't delete an edge he has already used.

The challenge is to devise as malicious an adversary as possible and then test it on a 100 by 100 20 by 20 graph with a random drunken walker. Your score is simply the average time it takes the random walker to get home over 10 1000 runs.

You can use any language and libraries you like as long as they are freely available and easily installable in Linux.

What do I need to implement?

You should implement code for the random walker and also for the adversary and the code should be combined so that the output when run is simply the average of 1000 runs using your adversary code. The random walker code should be very simple to write as he just chooses from (x-1, y), (x+1, y), (x, y-1), and (x, y+1) making sure that none of those have been deleted or are out of range.

The adversary code is of course more difficult and also needs to remember which edges the drunkard has already traversed so he doesn't try to delete any of them and to make sure there is still a route home for the drunkard, which is a little trickier to do quickly.


Addendum 10 runs isn't really enough but I didn't want to punish people who managed to get really long walks. I have now increased it to 1000 due to popular request. However, if your walk is so long you can't do 1000 runs in a realistic amount of time, please just report for the maximum number of runs you can.


High score table for 100 by 100.

  • 976124.754 by Optimizer.
  • 103000363.218 by Peter Taylor.

Edit 1. Changed the graph size to 20 by 20 to help the running time of people's tests. I will make a new high table score for that size as people submit the scores.

High score table for 20 by 20.

230,794.38 (100k runs) by justhalf
227,934 by Sparr 
213,000 (approx) by Peter Taylor
199,094.3 by stokastic
188,000 (approx) by James_pic
 64,281 by Geobits
\$\endgroup\$
37
  • 2
    \$\begingroup\$ I don't understand; can't you just delete all the edges at the beginning except the ones that form the longest path? \$\endgroup\$ Sep 8, 2014 at 21:16
  • 3
    \$\begingroup\$ I don't see any rule showing that the drunkard can't re-walk the same edge twice. If he can take the same path between two points twice, and chooses turns at random, then logically isn't graph with the longest average (random) traversal the one with the most edges? That is, wouldn't the optimal (longest) graph be the one with no deleted edges? \$\endgroup\$
    – millinon
    Sep 8, 2014 at 21:19
  • 3
    \$\begingroup\$ I am not a fan of requiring every entry to reinvent the wheel (walker). If someone posts a test harness/framework then I will upvote them and use it. \$\endgroup\$
    – Sparr
    Sep 8, 2014 at 21:32
  • 1
    \$\begingroup\$ The advantage of removing a part of a path to make him go back to take the long way around is completely lost when his path is random; supposedly it's equally likely that he'll turn back at some point without needing you to remove an edge. I'd like to see some test data showing the average time with no edges removed, and then with certain edges removed as you seem to suggest. As far as this challenge, I think it would be much more interesting if the drunkard's path were deterministic. \$\endgroup\$
    – millinon
    Sep 8, 2014 at 21:42
  • 3
    \$\begingroup\$ 10 rounds is not nearly enough. Even with a static 10x10 maze, let alone an intelligent adversary and a 100x100 maze, the standard deviation is around 50% of the average case. I'm running 10000 rounds and I still wouldn't consider the results comparison-worthy. \$\endgroup\$
    – Sparr
    Sep 8, 2014 at 23:03

10 Answers 10

10
\$\begingroup\$

230,794.38 on 20x20, 100k runs

Latest Update: I finally built perfect dynamic 2-path solution. I said perfect since the previous version is actually not symmetric, it was easier to get longer path if the drunkard took one path over the other. The current one is symmetric, so it can get higher expected number of steps. After few trials, it seems to be around 230k, an improvement over the previous one which is about 228k. But statistically speaking those numbers are still within their huge deviation, so I don't claim that this is significantly better, but I believe this should be better than the previous version.

The code is at the bottom of this post. It is updated so that it's much faster than the previous version, completing 1000 runs in 23s.

Below is sample run and sample maze:

Perfect Walker
Average: 230794.384
Max: 1514506
Min:25860
Completed in 2317.374s
 _   _   _   _   _   _   _   _ _ _ _ _. 
| | | | | | | | | | | | | | |  _ _ _ _  
| | | | | | | | | | | | | | | |_ _ _ _  
| | | | | | | | | | | | | | |  _ _ _ _| 
| | | | | | | | | | | | | | | |_ _ _ _  
| | | | | | | | | | | | | | |  _ _ _ _| 
| | | | | | | | | | | | | | | |_ _ _ _  
| | | | | | | | | | | | | | |  _ _ _ _| 
| | | | | | | | | | | | | |_| |_ _ _ _  
| | | | | | | | | | | | |  _ _ _ _ _ _| 
| | | | | | | | | | | | | |_ _ _ _ _ _  
| | | | | | | | | | | | |  _ _ _ _ _ _| 
| | | | | | | | | | | | | |_ _ _ _ _ _  
| | | | | | | | | | | | |  _ _ _ _ _ _| 
| | | | | |_| |_| |_| |_| |_ _ _ _ _ _  
| | | | |  _ _ _ _ _ _ _ _ _ _ _ _ _ _| 
| | | | | |_ _ _ _ _ _ _ _ _ _ _ _ _ _  
| | | | |  _ _ _ _ _ _ _ _ _ _ _ _ _ _| 
| |_| |_| |_ _ _ _ _ _ _ _ _ _ _ _ _ _  
|_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _| 


Previous submissions

Finally I can match Sparr's result! =D

Based on my previous experiments (see bottom of this post), the best strategy is to have double path and close one as the drunkard reaches any of them, and the variable comes from how good we can dynamically predict where the drunkard will go as to increase the chance of him getting into longer path.

So based on my DOUBLE_PATH strategy, I built another one, which changes the maze (my DOUBLE_PATH maze was easily modifiable) depending on the drunkard movement. As he takes a path with more than one available options, I will close the paths so as to leave only two possible options (one from which he came, another the untravelled).

This sounds similar to what Sparr has achieved, as the result shows. The difference with his is too small for it to be considered better, but I would say that my approach is more dynamic than him, since my maze is more modifiable than Sparr's =)

The result with a sample final maze:

EXTREME_DOUBLE_PATH
Average: 228034.89
Max: 1050816
Min:34170
Completed in 396.728s
 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
|  _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
| |_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
|  _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _|
| |_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
|  _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _|
| |_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
|  _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _|
| |_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
|  _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _|
| |_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
|  _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _|
| |_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
|_ _ _ _ _ _ _ _ _ _   _ _ _ _ _ _ _ _|
 _ _ _ _ _ _ _ _ _ _| |_ _ _ _ _ _ _ _ 
|_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _  |
 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _| |
|_ _ _ _ _   _ _ _ _ _ _ _ _ _ _ _ _ _|
 _ _ _ _ _| |_ _ _ _ _ _ _ _ _ _ _ _ _ 
|_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _|


Experiments Section

The best turns out to be the same strategy as stokastic, I take pride in experimenting using various strategies and printing nice outputs :)

Each of the printed maze below is the last maze after the drunkard has reached home, so they might be slightly different from run to run due to the randomness in the drunkard movement and dinamicity of the adversary.

I'll describe each strategy:

Single Path

This is the simplest approach, which will create a single path from entry to exit.

SINGLE_PATH
Average: 162621.612
Max: 956694
Min:14838
Completed in 149.430s
 _   _   _   _   _   _   _   _   _   _
| |_| |_| |_| |_| |_| |_| |_| |_| |_| |
|_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _|
|_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _|
|_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _|
|_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _|
|_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _|
|_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _|
|_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _|
|_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _|
|_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _|

Island (level 0)

This is an approach that tries to trap the drunkard in an almost isolated island. Doesn't work as good as I expected, but this is one of my first ideas, so I include it.

There are two paths leading to the exit, and when the drunkard gets near to one of them, the adversary closes it, forcing him to find the other exit (and possibly gets trapped again in the island)

ISLAND
Average: 74626.070
Max: 428560
Min:1528
Completed in 122.512s
 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
|  _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _   
| |_|_|_|_|_|_|_|_|_|_|_|_|_|_|_|_|_| |
| |_|_|_|_|_|_|_|_|_|_|_|_|_|_|_|_|_| |
| |_|_|_|_|_|_|_|_|_|_|_|_|_|_|_|_|_| |
| |_|_|_|_|_|_|_|_|_|_|_|_|_|_|_|_|_| |
| |_|_|_|_|_|_|_|_|_|_|_|_|_|_|_|_|_| |
| |_|_|_|_|_|_|_|_|_|_|_|_|_|_|_|_|_| |
| |_|_|_|_|_|_|_|_|_|_|_|_|_|_|_|_|_| |
| |_|_|_|_|_|_|_|_|_|_|_|_|_|_|_|_|_| |
| |_|_|_|_|_|_|_|_|_|_|_|_|_|_|_|_|_| |
| |_|_|_|_|_|_|_|_|_|_|_|_|_|_|_|_|_| |
| |_|_|_|_|_|_|_|_|_|_|_|_|_|_|_|_|_| |
| |_|_|_|_|_|_|_|_|_|_|_|_|_|_|_|_|_| |
| |_|_|_|_|_|_|_|_|_|_|_|_|_|_|_|_|_| |
| |_|_|_|_|_|_|_|_|_|_|_|_|_|_|_|_|_| |
| |_|_|_|_|_|_|_|_|_|_|_|_|_|_|_|_|_| |
| |_|_|_|_|_|_|_|_|_|_|_|_|_|_|_|_|_| |
|_|_|_|_|_|_|_|_|_|_|_|_|_|_|_|_|_|_| |
|_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _|

Double Path

This is the most discussed strategy, which is to have two equal length paths to the exit, and close one of them as the drunkard gets near to one of them.

DOUBLE_PATH
Average: 197743.472
Max: 1443406
Min:21516
Completed in 308.177s
 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
|_ _ _ _ _ _ _ _ _   _ _ _ _ _ _ _ _ _ 
 _ _ _ _ _ _ _ _ _| |_ _ _ _ _ _ _ _ _ 
|_ _ _ _ _ _ _ _ _   _ _ _ _ _ _ _ _ _|
 _ _ _ _ _ _ _ _ _| |_ _ _ _ _ _ _ _ _ 
|_ _ _ _ _ _ _ _ _   _ _ _ _ _ _ _ _ _|
 _ _ _ _ _ _ _ _ _| |_ _ _ _ _ _ _ _ _ 
|_ _ _ _ _ _ _ _ _   _ _ _ _ _ _ _ _ _|
 _ _ _ _ _ _ _ _ _| |_ _ _ _ _ _ _ _ _ 
|_ _ _ _ _ _ _ _ _   _ _ _ _ _ _ _ _ _|
 _ _ _ _ _ _ _ _ _| |_ _ _ _ _ _ _ _ _ 
|_ _ _ _ _ _ _ _ _   _ _ _ _ _ _ _ _ _|
 _ _ _ _ _ _ _ _ _| |_ _ _ _ _ _ _ _ _ 
|_ _ _ _ _ _ _ _ _   _ _ _ _ _ _ _ _ _|
 _ _ _ _ _ _ _ _ _| |_ _ _ _ _ _ _ _ _ 
|_ _ _ _ _ _ _ _ _   _ _ _ _ _ _ _ _ _|
 _ _ _ _ _ _ _ _ _| |_ _ _ _ _ _ _ _ _ 
|_ _ _ _ _ _ _ _ _   _ _ _ _ _ _ _ _ _|
 _ _ _ _ _ _ _ _ _| |_ _ _ _ _ _ _ _ _ 
|_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _|

Island (level 1)

Inspired by the multiple paths of island and the high walk count in single path, we connect the island to the exit and make single path maze in the island, creating in total three paths to exit, and similar to previous case, close any of the exit as the drunkard gets near.

This works slightly better than pure single path, but still doesn't defeat the double path.

ISLAND
Average: 166265.132
Max: 1162966
Min:19544
Completed in 471.982s
 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _   _
|  _   _   _   _   _   _   _   _   _|_ 
| | |_| |_| |_| |_| |_| |_| |_| |_| |  
| |_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _  |
|  _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _| |
| |_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _  |
|  _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _| |
| |_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _  |
|  _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _| |
| |_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _  |
|  _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _| |
| |_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _  |
|  _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _| |
| |_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _  |
|  _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _| |
| |_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _  |
|  _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _| |
| |_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _  |
|  _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _| |
|_|_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _|

Island (level 2)

Trying to expand the previous idea, I created nested island, creating in total five paths, but it doesn't seem to work that well.

ISLAND
Average: 164222.712
Max: 927608
Min:22024
Completed in 793.591s
 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _   _
|  _ _ _ _ _ _ _ _ _ _ _ _ _ _ _    |_ 
| |  _   _   _   _   _   _   _   _|_|  
| | | |_| |_| |_| |_| |_| |_| |_| |   |
| | |_ _ _ _ _ _ _ _ _ _ _ _ _ _ _  | |
| |  _ _ _ _ _ _ _ _ _ _ _ _ _ _ _| | |
| | |_ _ _ _ _ _ _ _ _ _ _ _ _ _ _  | |
| |  _ _ _ _ _ _ _ _ _ _ _ _ _ _ _| | |
| | |_ _ _ _ _ _ _ _ _ _ _ _ _ _ _  | |
| |  _ _ _ _ _ _ _ _ _ _ _ _ _ _ _| | |
| | |_ _ _ _ _ _ _ _ _ _ _ _ _ _ _  | |
| |  _ _ _ _ _ _ _ _ _ _ _ _ _ _ _| | |
| | |_ _ _ _ _ _ _ _ _ _ _ _ _ _ _  | |
| |  _ _ _ _ _ _ _ _ _ _ _ _ _ _ _| | |
| | |_ _ _ _ _ _ _ _ _ _ _ _ _ _ _  | |
| |  _ _ _ _ _ _ _ _ _ _ _ _ _ _ _| | |
| | |_ _ _ _ _ _ _ _ _ _ _ _ _ _ _  | |
| |  _ _ _ _ _ _ _ _ _ _ _ _ _ _ _| | |
|_|_|_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _| |
|_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _|

Island (level 3)

Noticing that double path actually works better than single path, let's make the island in double path!

The result is an improvement over Island (level 1), but it still doesn't beat pure double path.

For comparison, the result for double path of the size of the island is 131,134.42 moves on average. So this does add quite significant number of moves (around 40k), but not enough to beat double path.

ISLAND
Average: 171730.090
Max: 769080
Min:29760
Completed in 587.646s
 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _   _
|  _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _  |_ 
| |_ _ _ _ _ _ _ _   _ _ _ _ _ _ _ _|  
|  _ _ _ _ _ _ _ _| |_ _ _ _ _ _ _ _  |
| |_ _ _ _ _ _ _ _   _ _ _ _ _ _ _ _| |
|  _ _ _ _ _ _ _ _| |_ _ _ _ _ _ _ _  |
| |_ _ _ _ _ _ _ _   _ _ _ _ _ _ _ _| |
|  _ _ _ _ _ _ _ _| |_ _ _ _ _ _ _ _  |
| |_ _ _ _ _ _ _ _   _ _ _ _ _ _ _ _| |
|  _ _ _ _ _ _ _ _| |_ _ _ _ _ _ _ _  |
| |_ _ _ _ _ _ _ _   _ _ _ _ _ _ _ _| |
|  _ _ _ _ _ _ _ _| |_ _ _ _ _ _ _ _  |
| |_ _ _ _ _ _ _ _   _ _ _ _ _ _ _ _| |
|  _ _ _ _ _ _ _ _| |_ _ _ _ _ _ _ _  |
| |_ _ _ _ _ _ _ _   _ _ _ _ _ _ _ _| |
|  _ _ _ _ _ _ _ _| |_ _ _ _ _ _ _ _  |
| |_ _ _ _ _ _ _ _   _ _ _ _ _ _ _ _| |
|  _ _ _ _ _ _ _ _| |_ _ _ _ _ _ _ _  |
| |_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _| |
|_|_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _|

Island (level 4)

Again, experimenting with nested island, and again it doesn't work so well.

ISLAND
Average: 149723.068
Max: 622106
Min:25752
Completed in 830.889s
 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _    
|  _ _ _ _ _ _ _ _ _ _ _ _ _ _ _    |_|
| |  _ _ _ _ _ _ _ _ _ _ _ _ _ _ _|_|  
| | |_ _ _ _ _ _ _   _ _ _ _ _ _ _    |
| |  _ _ _ _ _ _ _| |_ _ _ _ _ _ _  | |
| | |_ _ _ _ _ _ _   _ _ _ _ _ _ _| | |
| |  _ _ _ _ _ _ _| |_ _ _ _ _ _ _  | |
| | |_ _ _ _ _ _ _   _ _ _ _ _ _ _| | |
| |  _ _ _ _ _ _ _| |_ _ _ _ _ _ _  | |
| | |_ _ _ _ _ _ _   _ _ _ _ _ _ _| | |
| |  _ _ _ _ _ _ _| |_ _ _ _ _ _ _  | |
| | |_ _ _ _ _ _ _   _ _ _ _ _ _ _| | |
| |  _ _ _ _ _ _ _| |_ _ _ _ _ _ _  | |
| | |_ _ _ _ _ _ _   _ _ _ _ _ _ _| | |
| |  _ _ _ _ _ _ _| |_ _ _ _ _ _ _  | |
| | |_ _ _ _ _ _ _   _ _ _ _ _ _ _| | |
| |  _ _ _ _ _ _ _| |_ _ _ _ _ _ _  | |
| |_|_ _ _ _ _ _ _ _ _ _ _ _ _ _ _| | |
|_|_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _| |
|_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _|

Conclusion

All in all, this proves that having a single long path from drunkard current position to the exit works best, which is achieved by the double path strategy, since after closing an exit, the drunkard will have to travel the maximum distance possible to get to the exit.

This further hints that the basic strategy should still be double path, and we can only modify how dynamic the paths are created, which has been done by Sparr. So I believe his strategy is the way to go!

Code

import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.TreeSet;

public class Walker {

    enum Strategy{
        SINGLE_PATH,
        ISLAND,
        DOUBLE_PATH,
        EXTREME_DOUBLE_PATH,
        PERFECT_DOUBLE_PATH,
    }

    int width,height;
    int x,y; //walker's position
    int dX,dY; //destination
    Point[][] points;
    int stepCount = 0;

    public static void main(String[]args){
        int side = 20;
//      runOnce(side, Strategy.EXTREME_DOUBLE_PATH, 0);
        runOnce(side, Strategy.PERFECT_DOUBLE_PATH, 0);
//      for(Strategy strategy: Strategy.values()){
//          runOnce(side, strategy, 0);
//      }
//      runOnce(side, Strategy.ISLAND, 1);
//      runOnce(side, Strategy.ISLAND, 2);
//      Scanner scanner = new Scanner(System.in);
//      System.out.println("Enter side, strategy (SINGLE_PATH, ISLAND, DOUBLE_PATH, EXTREME_DOUBLE_PATH), and level:");
//      while(scanner.hasNext()){
//          side = scanner.nextInt();
//          Strategy strategy = Strategy.valueOf(scanner.next());
//          int level = scanner.nextInt();
//          scanner.nextLine();
//          runOnce(side, strategy, level);
//          System.out.println("Enter side, strategy (SINGLE_PATH, ISLAND, DOUBLE_PATH, EXTREME_DOUBLE_PATH), and level:");
//      }
//      scanner.close();
    }

    private static Walker runOnce(int side, Strategy strategy, int level) {
        Walker walker = null;
        long total = 0;
        int max = 0;
        int min = Integer.MAX_VALUE;
        double count = 1000;
        long start = System.currentTimeMillis();
        for(int i=0; i<count; i++){
            walker = new Walker(0,0,side,side,side-1,side-1, strategy, level, false);
            total += walker.stepCount;
            max = Math.max(walker.stepCount, max);
            min = Math.min(walker.stepCount, min);
//          System.out.println("Iteration "+i+": "+walker.stepCount);
        }
        System.out.printf("%s\nAverage: %.3f\nMax: %d\nMin:%d\n",strategy, total/count, max, min);
        System.out.printf("Completed in %.3fs\n", (System.currentTimeMillis()-start)/1000.0);
        walker.printPath();
        return walker;
    }

    private void createIsland(int botLeftX, int botLeftY, int topRightX, int topRightY){
        for(int i=botLeftY+1; i<topRightY; i++){
            if(i>botLeftY+1) deletePath(points[botLeftX][i].right());
            if(i<topRightY-1) deletePath(points[topRightX][i].left());
        }
        for(int i=botLeftX+1; i<topRightX; i++){
            if(i>botLeftX+1) deletePath(points[i][botLeftY].up());
            if(i<topRightX-1) deletePath(points[i][topRightY].down());
        }
    }

    private void createSinglePath(int botLeftX, int botLeftY, int topRightX, int topRightY){
        for(int i=botLeftY; i<topRightY; i++){
            if(i==topRightY-1 && (topRightY+1-botLeftY)%2==0){
                for(int j=botLeftX; j<topRightX; j++){
                    if(j==topRightX-1 && (j-botLeftX)%2==0){
                        deletePath(points[topRightX][topRightY].down());
                    } else {
                        deletePath(points[j][topRightY-1+((j-botLeftX)%2)].right());
                    }
                }
            } else {
                for(int j=botLeftX+(i-botLeftY)%2; j<topRightX+((i-botLeftY)%2); j++){
                    deletePath(points[j][i].up());
                }
            }
        }
    }

    private void createDoublePath(int botLeftX, int botLeftY, int topRightX, int topRightY){
        for(int i=botLeftY; i<topRightY; i++){
            if(i>botLeftY && (width%4!=1 || i<topRightY-1)) deletePath(points[width/2-1][i].right());
            if(i==topRightY-1 && (topRightY+1-botLeftY)%2==1){
                for(int j=botLeftX; j<topRightX; j++){
                    if((j-botLeftX)%2==0 || j<topRightX-1){
                        deletePath(points[j][topRightY-1+((j-botLeftX)%2)].right());
                    } else {
                        deletePath(points[topRightX-1][topRightY-1].right());
                    }
                }
            } else {
                if((i-botLeftY)%2==0){
                    for(int j=botLeftX+1; j<topRightX; j++){
                        deletePath(points[j][i].up());
                    }
                } else {
                    for(int j=botLeftX; j<topRightX+1; j++){
                        if(j!=width/2 && j!=width/2-1){
                            deletePath(points[j][i].up());
                        }
                    }
                }
            }
        }
    }

    public Walker(int startingX,int startingY, int Width, int Height, int destinationX, int destinationY, Strategy strategy, int level, boolean animate){
        width = Width;
        height = Height;
        dX = destinationX;
        dY = destinationY;
        x=startingX;
        y=startingY;
        points = new Point[width][height];
        for(int y=0; y<height; y++){
            for(int x=0; x<width; x++){
                points[x][y] = new Point(x,y);
            }
        }
        for(int y=0; y<height; y++){
            for(int x=0; x<width; x++){
                if(x<width-1) new Edge(points[x][y], points[x+1][y]);
                if(y<height-1) new Edge(points[x][y], points[x][y+1]);
            }
        }

        if(strategy == Strategy.SINGLE_PATH) createSinglePath(0,0,width-1,height-1);

        if(strategy == Strategy.DOUBLE_PATH) createDoublePath(0,0,width-1,height-1);

        List<EdgeList> edgeLists = new ArrayList<EdgeList>();
        if(strategy == Strategy.ISLAND){
            List<Edge> edges = new ArrayList<Edge>();
            if(level==0){
                createIsland(0,0,width-1,height-1);
                deletePath(points[width-2][height-2].right());
                deletePath(points[width-2][height-2].up());
            } else {
                for(int i=0; i<level; i++){
                    createIsland(i,i,width-1-i, height-1-i);
                }
                createDoublePath(level,level,width-1-level,height-1-level);
                for(int i=height-1; i>=height-level; i--){
                    edges.add(points[i-2][i].right());
                    edges.add(points[i][i-2].up());
                    edgeLists.add(new EdgeList(points[i-1][i].right(), points[i][i-1].up()));
                }
            }
            edges.add(points[width-1-level][height-1-level].down());
            edges.add(points[width-1-level][height-1-level].left());
            edgeLists.add(new EdgeList(edges.toArray(new Edge[0])));
        }

        int[] availableVerticals = new int[height];
        if(strategy == Strategy.EXTREME_DOUBLE_PATH){
            for(int i=1; i<width-1; i++){
                deletePath(points[i][0].up());
            }
            availableVerticals[0] = 2;
            for(int i=1; i<height; i++){
                availableVerticals[i] = width;
            }
        }

        boolean[][] available = new boolean[width][height];
        if(strategy == Strategy.PERFECT_DOUBLE_PATH){
            for(int x=0; x<width; x++){
                for(int y=0; y<height; y++){
                    if(x%2==1 && y%2==1){
                        available[x][y] = true;
                    } else {
                        available[x][y] = false;
                    }
                }
            }
        }
//      printPath();
        while(!walk()){
            if(animate)try{Thread.sleep(500);}catch(InterruptedException e){}
            if(strategy == Strategy.ISLAND){
                if(x==y && (x==1 || (x>=2 && x<=level))){
                    if(!hasBeenWalked(points[x][x].down())){
                        deletePath(points[x][x].down());
                    } else if(!hasBeenWalked(points[x][x].left())){
                        deletePath(points[x][x].left());
                    }
                }
            }
            if(strategy == Strategy.EXTREME_DOUBLE_PATH){
                Point cur = points[x][y];
                int untravelled = 0;
                for(Edge edge: cur.edges) if(edge!=null && !edge.walked) untravelled++;
                if(untravelled>1){
                    if(cur.up()!=null && availableVerticals[y]>2 && !cur.up().walked){
                        deletePath(cur.up());
                        availableVerticals[y]--;
                    }
                    if(cur.down()!=null && !cur.down().walked){
                        deletePath(cur.down());
                        availableVerticals[y-1]--;
                    }
                    if(cur.up()!=null && cur.left()!=null && !cur.left().walked){
                        deletePath(cur.left());
                        deletePath(points[x][y+1].left());
                    }
                    if(cur.up()!=null && cur.right()!=null && !cur.right().walked){
                        deletePath(cur.right());
                        if(y<height-1) deletePath(points[x][y+1].right());
                    }
                }
            }
            if(strategy == Strategy.PERFECT_DOUBLE_PATH){
                Point cur = points[x][y];
                int untravelled = 0;
                for(Edge edge: cur.edges) if(edge!=null && !edge.walked) untravelled++;
                if(x%2!=1 || y%2!=1){
                    if(untravelled>1){
                        if(cur.down()==null && hasBeenWalked(cur.right())){
                            if(canBeDeleted(cur.up())) deletePath(cur.up());
                        }
                        if(cur.down()==null && hasBeenWalked(cur.left())){
                            if(x%2==0 && y%2==1 && canBeDeleted(cur.right())) deletePath(cur.right());
                            else if(cur.right()!=null && canBeDeleted(cur.up())) deletePath(cur.up());
                        }
                        if(cur.left()==null && hasBeenWalked(cur.up())){
                            if(canBeDeleted(cur.right())) deletePath(cur.right());
                        }
                        if(cur.left()==null && hasBeenWalked(cur.down())){
                            if(x%2==1 && y%2==0 && canBeDeleted(cur.up())) deletePath(cur.up());
                            else if (cur.up()!=null && canBeDeleted(cur.right())) deletePath(cur.right());
                        }
                    }
                } else {
                    if(!hasBeenWalked(cur.left())){
                        if(x>1 && available[x-2][y]){
                            if(untravelled>1){
                                available[x-2][y] = false;
                                deletePath(cur.up());
                            }
                        } else if(cur.up()!=null){
                            if(canBeDeleted(cur.left())) deletePath(cur.left());
                            if(canBeDeleted(points[x][y+1].left())) deletePath(points[x][y+1].left());
                        }
                    }
                    if(!hasBeenWalked(cur.down())){
                        if(y>1 && available[x][y-2]){
                            if(untravelled>1){
                                available[x][y-2] = false;
                                deletePath(cur.right());
                            }
                        } else if(cur.right()!=null){
                            if(canBeDeleted(cur.down())) deletePath(cur.down());
                            if(canBeDeleted(points[x+1][y].down())) deletePath(points[x+1][y].down());
                        }
                    }
                }
            }
            if(strategy == Strategy.DOUBLE_PATH || strategy == Strategy.EXTREME_DOUBLE_PATH
                    || strategy == Strategy.PERFECT_DOUBLE_PATH){
                if(x==width-2 && y==height-1 && points[width-1][height-1].down()!=null){
                    deletePath(points[width-1][height-1].left());
                }
                if(x==width-1 && y==height-2 && points[width-1][height-1].left()!=null){
                    deletePath(points[width-1][height-1].down());
                }
            } else if(strategy == Strategy.ISLAND){
                for(EdgeList edgeList: edgeLists){
                    boolean deleted = false;
                    for(Edge edge: edgeList.edges){
                        if(edge.start.x == x && edge.start.y == y){
                            if(!hasBeenWalked(edge)){
                                deletePath(edge);
                                edgeList.edges.remove(edge);
                                if(edgeList.edges.size() == 1){
                                    edgeLists.remove(edgeList);
                                }
                                deleted = true;
                                break;
                            }
                        }
                    }
                    if(deleted) break;
                }
            }
            if(animate)printPath();
        }
    }

    public boolean hasBeenWalked(Edge edge){
        if(edge == null) return false;
        return edge.walked;
    }

    public boolean canBeDeleted(Edge edge){
        if(edge == null) return false;
        return !edge.walked;
    }

    public List<Edge> getAdjacentUntravelledEdges(){
        List<Edge> result = new ArrayList<Edge>();
        for(Edge edge: points[x][y].edges){
            if(edge!=null && !hasBeenWalked(edge)) result.add(edge); 
        }
        return result;
    }

    public void printPath(){
        StringBuilder builder = new StringBuilder();
        for(int y=height-1; y>=0; y--){
            for(int x=0; x<width; x++){
                Point point = points[x][y];
                if(this.x==x && this.y==y){
                    if(point.up()!=null) builder.append('?');
                    else builder.append('.');
                } else {
                    if(point.up()!=null) builder.append('|');
                    else builder.append(' ');
                }
                if(point.right()!=null) builder.append('_');
                else builder.append(' ');
            }
            builder.append('\n');
        }
        System.out.print(builder.toString());
    }

    public boolean walk(){
        ArrayList<Edge> possibleMoves = new ArrayList<Edge>();
        Point cur = points[x][y];
        for(Edge edge: cur.edges){
            if(edge!=null) possibleMoves.add(edge);
        }
        int random = (int)(Math.random()*possibleMoves.size());
        Edge move = possibleMoves.get(random);
        move.walked = true;
        if(move.start == cur){
            x = move.end.x;
            y = move.end.y;
        } else {
            x = move.start.x;
            y = move.start.y;
        }
        stepCount++;
        if(x==dX && y == dY){
            return true;
        } else {
            return false;
        }
    }

    public boolean isSolvable(){
        TreeSet<Point> reachable = new TreeSet<Point>();
        Queue<Point> next = new LinkedList<Point>();
        next.offer(points[x][y]);
        reachable.add(points[x][y]);
        while(next.size()>0){
            Point cur = next.poll();
            ArrayList<Point> neighbors = new ArrayList<Point>();
            if(cur.up()!=null) neighbors.add(cur.up().end);
            if(cur.right()!=null) neighbors.add(cur.right().end);
            if(cur.down()!=null) neighbors.add(cur.down().start);
            if(cur.left()!=null) neighbors.add(cur.left().start);
            for(Point neighbor: neighbors){
                if(!reachable.contains(neighbor)){
                    if(neighbor == points[dX][dY]) return true;
                    reachable.add(neighbor);
                    next.offer(neighbor);
                }
            }
        }
        return false;
    }

    public boolean deletePath(Edge toDelete){
        if(toDelete == null) return true;
//      if(toDelete.walked){
//          System.err.println("Edge already travelled!");
//          return false;
//      }
        int startIdx = toDelete.getStartIdx();
        int endIdx = toDelete.getEndIdx();
        toDelete.start.edges[startIdx] = null;
        toDelete.end.edges[endIdx] = null;
//      if(!isSolvable()){
//          toDelete.start.edges[startIdx] = toDelete;
//          toDelete.end.edges[endIdx] = toDelete;
//          System.err.println("Invalid deletion!");
//          return false;
//      }
        return true;
    }

    static class EdgeList{
        List<Edge> edges;

        public EdgeList(Edge... edges){
            this.edges = new ArrayList<Edge>();
            this.edges.addAll(Arrays.asList(edges));
        }
    }

    static class Edge implements Comparable<Edge>{
        Point start, end;
        boolean walked;

        public Edge(Point start, Point end){
            walked = false;
            this.start = start;
            this.end = end;
            this.start.edges[getStartIdx()] = this;
            this.end.edges[getEndIdx()] = this;
            if(start.compareTo(end)>0){
                Point tmp = end;
                end = start;
                start = tmp;
            }
        }

        public Edge(int x1, int y1, int x2, int y2){
            this(new Point(x1,y1), new Point(x2,y2));
        }

        public boolean exists(){
            return start.edges[getStartIdx()] != null || end.edges[getEndIdx()] != null;
        }

        public int getStartIdx(){
            if(start.x == end.x){
                if(start.y < end.y) return 0;
                else return 2;
            } else {
                if(start.x < end.x) return 1;
                else return 3;
            }
        }

        public int getEndIdx(){
            if(start.x == end.x){
                if(start.y < end.y) return 2;
                else return 0;
            } else {
                if(start.x < end.x) return 3;
                else return 1;
            }
        }

        public boolean isVertical(){
            return start.x==end.x;
        }

        @Override
        public int compareTo(Edge o) {
            int result = start.compareTo(o.start);
            if(result!=0) return result;
            return end.compareTo(o.end);
        }
    }

    static class Point implements Comparable<Point>{
        int x,y;
        Edge[] edges;

        public Point(int x, int y){
            this.x = x;
            this.y = y;
            edges = new Edge[4];
        }

        public Edge up(){ return edges[0]; }
        public Edge right(){ return edges[1]; }
        public Edge down(){ return edges[2]; }
        public Edge left(){ return edges[3]; }

        public int compareTo(Point o){
            int result = Integer.compare(x, o.x);
            if(result!=0) return result;
            result = Integer.compare(y, o.y);
            if(result!=0) return result;
            return 0;
        }
    }
}
\$\endgroup\$
12
  • \$\begingroup\$ This is very impressive. How long does it take to run? If the winning entries remain this close we will have to increase the number of runs to see if we can separate them. \$\endgroup\$
    – user9206
    Sep 10, 2014 at 9:05
  • 1
    \$\begingroup\$ The timing is already included in the snippet. Around 400s for 1000 runs. That's including solvability check at each path deletion. I can remove that to have around 170s for 1000 runs. So I can do 20k runs in about an hour. \$\endgroup\$
    – justhalf
    Sep 10, 2014 at 9:10
  • \$\begingroup\$ Actually optimizing further I might be able to run 100k in 3.5 hours. \$\endgroup\$
    – justhalf
    Sep 10, 2014 at 9:45
  • \$\begingroup\$ My score is with 100k runs and took 10 minutes. @justhalf very nice on the more flexible double path maze. I know how to make an even better one, but I don't have the patience to implement it right now. \$\endgroup\$
    – Sparr
    Sep 10, 2014 at 14:55
  • 2
    \$\begingroup\$ Happy to see the symmetric solution implemented. I've got yet another idea to improve this solution, and this time I think I might implement it myself :) \$\endgroup\$
    – Sparr
    Sep 15, 2014 at 21:55
9
\$\begingroup\$

227934 (20x20)

My third attempt. Uses the same general approach as @stokastic with two paths to the exit. When the walker reaches the end of one path, it closes, requiring him to return to get to the end of the other path to exit. My improvement is to generate the paths as the walker progresses, so that whichever path he is progressing further along in the first half of the process will end up being longer than the other path.

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <math.h>
#include <iostream>

#define DEBUG 0
#define ROUNDS 10000

#define Y 20
#define X 20
#define H (Y*2+1)
#define W (X*2+1)

int maze[H][W];
int scores[ROUNDS];

int x, y;

void print_maze(){
    char line[W+2];
    line[W+1]=0;
    for(int row=0;row<H;row++) {
        for(int col=0;col<W;col++) {
            switch(maze[row][col]) {
                case 0:
                    line[col]=' ';
                    break;
                case 1:
                    line[col]=row%2?'-':'|';
                    break;
                case 8:
                    line[col]=(row==y*2+1&&col==x*2+1)?'@':'?';
                    break;
                case 9:
                    line[col]=(row==y*2+1&&col==x*2+1)?'@':'*';
                    break;
            }
        }
        line[W]='\n';
        printf("%s",line);
    }
    printf("%d %d\n",y,x);
}

int main(){
    srand (time(NULL));
    long long total_turns = 0;
    for(int round=0;round<ROUNDS;round++) {
        for (int r=0;r<H;r++) {
            for (int c=0;c<W;c++) {
                maze[r][c]=0;
            }
        }
        maze[1][1]=9;
        maze[1][2]=1;
        maze[2][1]=1;
        maze[1][3]=8;
        maze[3][1]=8;
        int progress_l = 0;
        int progress_r = 0;
        int side = 0;
        int closed_exit = 0;
        x=0;
        y=0;
        if (DEBUG) print_maze();
        long long turn = 0;
        int in = 0;
        while (x!=X-1||y!=Y-1) {
            turn++;
            int r = y*2+1;
            int c = x*2+1;
            int dx=0, dy=0;
            if (DEBUG) {
                std::cin>>in;
                switch(in) {
                    case 0:
                        dy=-1; dx=0; break;
                    case 1:
                        dy=0; dx=1; break;
                    case 2:
                        dy=1; dx=0; break;
                    case 3:
                        dy=0; dx=-1; break;
                    default:
                        dy=0; dx=0; break;
                }
            } else {
                int exits = maze[r-1][c] + maze[r][c+1] + maze[r+1][c] + maze[r][c-1];
                int exit_choice = -1;
                do {
                    if (rand()%exits == 0) {
                        exit_choice = exits;
                        break;
                    } else {
                        exits--;
                    }
                }while(exits);

                --exits;

                if (maze[r-1][c]&&!dx&&!dy) {
                    if (exits) {
                        --exits;
                    } else {
                        dy = -1;
                        dx = 0;
                    }
                }
                if (maze[r][c+1]&&!dx&&!dy) {
                    if (exits) {
                        --exits;
                    } else {
                        dy = 0;
                        dx = 1;
                    }
                }
                if (maze[r+1][c]&&!dx&&!dy) {
                    if (exits) {
                        --exits;
                    } else {
                        dy = 1;
                        dx = 0;
                    }
                }
                if (maze[r][c-1]&&!dx&&!dy) {
                    if (exits) {
                        --exits;
                    } else {
                        dy = 0;
                        dx = -1;
                    }
                }
            }

            x+=dx;
            y+=dy;

            if(x==X-1 && y==Y-1) continue;

            if (x==0&&y==1) side=-1;
            if (x==1&&y==0) side=1;
            if (maze[y*2+1][x*2+1]==8) { // room needs another exit, maybe
                if (side==-1) { // left half of maze
                    if (y==1) { // top of a column
                        if (x%2) { // going up, turn right
                            maze[y*2+1][x*2+2]=1;
                            maze[y*2+1][x*2+3]=8;
                        } else { // going right, turn down
                            maze[y*2+2][x*2+1]=1;
                            maze[y*2+3][x*2+1]=8;
                        }
                    } else if (y==Y-1) { // bottom of a column
                        if (x%2 && x<(X-progress_r-3)) { // going right, turn up if there's room
                            maze[y*2+0][x*2+1]=1;
                            maze[y*2-1][x*2+1]=8;
                            progress_l=x+1;
                        } else { // going down or exiting, go right
                            if (x!=X-2 or closed_exit==1) {
                                maze[y*2+1][x*2+2]=1;
                                maze[y*2+1][x*2+3]=8;
                            } else {
                                closed_exit = -1;
                            }
                        }
                    } else { // in a column
                        if (maze[y*2+0][x*2+1]) { // going down
                            maze[y*2+2][x*2+1]=1;
                            maze[y*2+3][x*2+1]=8;
                        } else { // going up
                            maze[y*2+0][x*2+1]=1;
                            maze[y*2-1][x*2+1]=8;
                        }
                    }
                } else { // right half of maze
                    if (y==0) { // top row
                        if (x<X-1) { // go right
                            maze[y*2+1][x*2+2]=1;
                            maze[y*2+1][x*2+3]=8;
                        } else { // go down
                            maze[y*2+2][x*2+1]=1;
                            maze[y*2+3][x*2+1]=8;
                        }
                    } else if (y==Y-2) { // heading right to the exit
                        if (x<X-1) { // go right
                            maze[y*2+1][x*2+2]=1;
                            maze[y*2+1][x*2+3]=8;
                        } else { // go down
                            if (x!=X-1 or closed_exit==-1) {
                                maze[y*2+2][x*2+1]=1;
                                maze[y*2+3][x*2+1]=8;
                            } else {
                                closed_exit = 1;
                            }
                        }
                    } else if (y==Y-3) { // bottom of a column
                        if (x>progress_l+1) { // do we have room for another column?
                            if (!(x%2)&&y>1) { // going left, turn up
                                maze[y*2+0][x*2+1]=1;
                                maze[y*2-1][x*2+1]=8;
                            } else { // going down, turn left
                                maze[y*2+1][x*2+0]=1;
                                maze[y*2+1][x*2-1]=8;
                                progress_r=X-x-1;
                            }
                        } else { // abort, move down to escape row
                            maze[y*2+2][x*2+1]=1;
                            maze[y*2+3][x*2+1]=8;
                        }
                    } else if (y==1) { // top of a column
                        if (!(x%2)) { // going up, turn left
                            maze[y*2+1][x*2+0]=1;
                            maze[y*2+1][x*2-1]=8;
                        } else { // going left, turn down
                            maze[y*2+2][x*2+1]=1;
                            maze[y*2+3][x*2+1]=8;
                        }
                    } else { // in a column
                        if (maze[y*2+0][x*2+1]) { // going down
                            maze[y*2+2][x*2+1]=1;
                            maze[y*2+3][x*2+1]=8;
                        } else { // going up
                            maze[y*2+0][x*2+1]=1;
                            maze[y*2-1][x*2+1]=8;
                        }
                    }

                }
                maze[y*2+1][x*2+1]=9;
            }

            if (DEBUG) print_maze();
        }
        // print_maze();
        printf("turns:%lld\n",turn);
        scores[round] = turn;
        total_turns += turn;
    }
    printf("%d rounds in a %d*%d maze\n",ROUNDS,X,Y);
    long long avg = total_turns/ROUNDS;
    printf("average: % 10lld\n",avg);
    long long var = 0;
    for(int r=0;r<ROUNDS;r++){
        var += (scores[r]-avg)*(scores[r]-avg);
    }
    var/=ROUNDS;
    // printf("variance: %lld\n",var);
    int stddev=sqrt(var);
    printf("stddev:  % 10d\n",stddev);

}

output (with time):

...
turns:194750
turns:506468
turns:129684
turns:200712
turns:158664
turns:156550
turns:311440
turns:137900
turns:86948
turns:107134
turns:81806
turns:310274
100000 rounds in a 20*20 maze
average:     227934
stddev:      138349
real    10m54.797s
...

example of my maze, with roughly equal length halves to the path, showing the left/lower path cut off from the exit (bottom right):

  _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
 |  _   _   _   _   _   _   _   _   _  |
 | | | | | | | | | | | | | | | | | | | |
 | | | | | | | | | | | | | | | | | | | |
 | | | | | | | | | | | | | | | | | | | |
 | | | | | | | | | | | | | | | | | | | |
 | | | | | | | | | | | | | | | | | | | |
 | | | | | | | | | | | | | | | | | | | |
 | | | | | | | | | | | | | | | | | | | |
 | | | | | | | | | | | | | | | | | | | |
 | | | | | | | | | | | | | | | | | | | |
 | | | | | | | | | | | | | | | | | | | |
 | | | | | | | | | | | | | | | | | | | |
 | | | | | | | | | | | | | | | | | | | |
 | | | | | | | | | | | | | | | | | | | |
 | | | | | | | | | | | | | | | | | | | |
 | | | | | | | | | | | | | | | | | | | |
 | | | | | | | | | | |_| |_| |_| |_| |_|
 | | | | | | | | | |_ _ _ _ _ _ _ _ _ _
 |_| |_| |_| |_| |_ _ _ _ _ _ _ _ _ _  !

PS: I am aware of a very minor improvement to this algorithm that requires more clever code to generate a different shape for the two paths, staircases instead of consistent height zig zags.

\$\endgroup\$
20
  • \$\begingroup\$ Color me impressed. You have my vote sir! \$\endgroup\$
    – stokastic
    Sep 10, 2014 at 0:35
  • 1
    \$\begingroup\$ That's pretty impressive. Remember when we just drew on the drunks' faces? \$\endgroup\$
    – Dennis
    Sep 10, 2014 at 0:40
  • \$\begingroup\$ It's pretty hard to discern your graph, perhaps you can change your graph printing to something similar to mine? \$\endgroup\$
    – justhalf
    Sep 10, 2014 at 2:39
  • 1
    \$\begingroup\$ @justhalf your wish is my command \$\endgroup\$
    – Sparr
    Sep 10, 2014 at 3:42
  • 1
    \$\begingroup\$ @justhalf I've got it drawn out on paper. Just need to write the logic. If I don't get it done in a couple more days, I'll give you the sketch? :) \$\endgroup\$
    – Sparr
    Sep 19, 2014 at 17:11
6
\$\begingroup\$

135,488,307.9 for 98x98

199094.3 for 20x20

I have implemented a solution that creates two paths to the finish, and closes exactly one of them once the drunkard reaches it. This simulates a path length which at the very least will be 1.5x the length of a single path from start to end. After 27 runs I hit an average of about 135 million. Unfortunately it takes several minutes per walk, so I will have to run it for the next few hours. One caveat - my double path generator only works if the size of the graph is in the form 4*n + 2, meaning the closest I can get to 100 is 102 or 98. I am going to post results using 98, which I expect to still surpass the zigzag path method. I will work on a better pathing system later. Currently outputs results in the form of (numSteps, currentAverage) after each walk.

EDIT: fixed so code now works on graph sizes that are any multiple of 2, rather than 4*n + 2.

Code: (add 'True' argument to walker constructor on line 187 for turtle drawing of the graph).

import random
import turtle

WIDTH  = 20
HEIGHT = 20
L, U, R, D = 1, 2, 4, 8

def delEdge(grid, x1, y1, x2, y2):

    # check that coordinates are in-bounds
    if not (0 <= x1 < WIDTH):  return False
    if not (0 <= y1 < HEIGHT): return False
    if not (0 <= x2 < WIDTH):  return False
    if not (0 <= y2 < HEIGHT): return False

    # swap order such that x1 <= x2 and y1 <= y2
    if x2 < x1:
        x2 ^= x1
        x1 ^= x2
        x2 ^= x1
    if x2 < x1: print "Swap failure: {}, {}".format(x1, x2)

    if y2 < y1:
        y2 ^= y1
        y1 ^= y2
        y2 ^= y1
    if y2 < y1: print "Swap failure: {}, {}".format(y1, y2)

    # check that only one of the deltas is = 1
    dx = x2 - x1
    dy = y2 - y1

    if dx and dy:       return False
    if not (dx or dy):  return False
    if dx > 1:          return False
    if dy > 1:          return False

    #print "<{}, {}>, <{}, {}>".format(x1, y1, x2, y2)

    if dx > 0:
        try: grid[x1][y1].remove(R)
        except: pass
        try: grid[x2][y2].remove(L)
        except: pass
    if dy > 0:
        try: grid[x1][y1].remove(D)
        except: pass
        try: grid[x2][y2].remove(U)
        except: pass

    return True

def newGrid():

    grid = [[[] for y in xrange(HEIGHT)] for x in xrange(WIDTH)]

    for x in xrange(WIDTH):
        for y in xrange(HEIGHT):
            if x > 0:
                grid[x][y].append(L)
            if x < WIDTH-1:
                grid[x][y].append(R)
            if y > 0:
                grid[x][y].append(U)
            if y < HEIGHT-1:
                grid[x][y].append(D)

    return grid

class walker:

    def __init__(self, grid, mode, draw=False):
        self.x  = 0
        self.y  = 0
        self.dx = WIDTH-1
        self.dy = HEIGHT-1

        self.grid     = grid
        self.mode     = mode
        self.draw     = draw
        self.numSteps = 0

        self.initGrid()

    def initGrid(self):
        if self.mode == 0:
            #pass
            if self.draw: drawGrid(grid)

        elif self.mode == 1:

            for y in xrange(HEIGHT-1):
                if y % 2 == 0:
                    for x in xrange(WIDTH - 1):
                        delEdge(grid, x, y, x, y+1)
                else:
                    for x in xrange(1, WIDTH):
                        delEdge(grid, x, y, x, y+1)
            if self.draw: drawGrid(grid)

        elif self.mode == 2:
            for y in xrange(HEIGHT/2):
                if y % 2 == 0:
                    for x in xrange(1, WIDTH-1):
                        delEdge(grid, x, y, x, y+1)
                else:
                    for x in xrange(2, WIDTH):
                        delEdge(grid, x, y, x, y+1)
            for y in xrange(HEIGHT/2, HEIGHT-1):
                if y%2 == 0:
                    for x in xrange(1, WIDTH-1):
                        delEdge(grid, x, y, x, y+1)
                else:
                    for x in xrange(0, WIDTH-2):
                        delEdge(grid, x, y, x, y+1)
            for y in xrange(1, HEIGHT-1):
                midpoint = HEIGHT/2
                if HEIGHT % 4 == 0: 
                    midpoint = HEIGHT/2 + 1
                if y < midpoint:
                    delEdge(grid, 0, y, 1, y)
                else:
                    delEdge(grid, WIDTH-1, y, WIDTH-2, y)
            if self.draw: drawGrid(grid)

    def walk(self):
        self.numSteps += 1
        choices = grid[self.x][self.y]
        direction = random.choice(choices)
        #print (self.x, self.y), grid[self.x][self.y], direction
        if direction   == L: self.x -= 1
        elif direction == U: self.y -= 1
        elif direction == R: self.x += 1
        elif direction == D: self.y += 1

    def main(self):
        hasBlocked = False
        while (self.x, self.y) != (self.dx, self.dy):
            #print (self.x, self.y), (self.dx, self.dy)
            self.walk()
            if self.mode == 2:
                if not hasBlocked:
                    if (self.x, self.y) == (WIDTH-2, HEIGHT-1):
                        delEdge(self.grid, WIDTH-2, HEIGHT-1, WIDTH-1, HEIGHT-1)
                        hasBlocked = True
                    elif (self.x, self.y) == (WIDTH-1, HEIGHT-2):
                        delEdge(self.grid, WIDTH-1, HEIGHT-1, WIDTH-1, HEIGHT-2)
                        hasBlocked = True

        return self.numSteps

def drawGrid(grid):
    size = 3
    turtle.speed(0)
    turtle.delay(0)
    turtle.ht()
    for x in xrange(WIDTH):
        for y in xrange(HEIGHT):
            dirs = grid[x][y]
            for dir in dirs:
                if dir == L:
                    turtle.pu()
                    turtle.setpos((x*4, y*4))
                    turtle.pd()
                    turtle.setpos(((x-1)*4, y*4))
                elif dir == R:
                    turtle.pu()
                    turtle.setpos((x*4, y*4))
                    turtle.pd()
                    turtle.setpos(((x+1)*4, y*4))
                elif dir == U:
                    turtle.pu()
                    turtle.setpos((x*4, y*4))
                    turtle.pd()
                    turtle.setpos((x*4, (y-1)*4))
                elif dir == D:
                    turtle.pu()
                    turtle.setpos((x*4, y*4))
                    turtle.pd()
                    turtle.setpos((x*4, (y+1)*4))
    turtle.mainloop()

numTrials  = 100
totalSteps = 0.0
i = 0
try:
    while i < numTrials:
        grid = newGrid()

        w = walker(grid, 2)
        steps = w.main()
        totalSteps += steps
        print steps, totalSteps/(i+1)
        i += 1

    print totalSteps / numTrials

except KeyboardInterrupt:
    print totalSteps / i

Raw data: (current numSteps, running average)

358796490 358796490.0
49310430 204053460.0
106969130 171692016.667
71781702 146714438.0
49349086 127241367.6
40874636 112846912.333
487607888 166384194.571
56423642 152639125.5
71077302 143576700.667
101885368 139407567.4
74423642 133499937.818
265170542 144472488.167
59524778 137938048.923
86919630 134293876.143
122462528 133505119.6
69262650 129489965.25
85525556 126903823.529
161165512 128807250.667
263965384 135920836.632
128907594 135570174.5
89535930 133378067.619
97344576 131740181.636
98772132 130306788.174
140769524 130742735.5
198274280 133443997.28
95417374 131981434.846
226667006 135488307.852
\$\endgroup\$
8
  • \$\begingroup\$ I reduced the graph size to 20 by 20 to make run times quicker. I hope it helps. \$\endgroup\$
    – user9206
    Sep 9, 2014 at 18:12
  • \$\begingroup\$ You are currently winning :) \$\endgroup\$
    – user9206
    Sep 9, 2014 at 19:08
  • \$\begingroup\$ Is your 20 by 20 score over 1000 runs? \$\endgroup\$
    – user9206
    Sep 9, 2014 at 19:20
  • \$\begingroup\$ @Lembik yes it is. \$\endgroup\$
    – stokastic
    Sep 9, 2014 at 19:28
  • 1
    \$\begingroup\$ @Dennis au contraire :) \$\endgroup\$
    – Sparr
    Sep 9, 2014 at 23:30
6
\$\begingroup\$

4-path approach, 213k

The one-path approach is

Straight line from S to E

and scores an average of N^2.

The two-path approach is

Loop with S and E opposite each other

but then the first time the drunkard gets within reach of the end point, it's cut:

Loop is cut to give a curved line from S to E

It scores an average of (N/2)^2 + N^2.

The four-path approach uses two cuts:

Nested loops, joined in two forks, one either side of E Cut one of the forks on the E side On the other side, cut the fork on the non-E side. This leaves one convoluted path

Assume that the outer loop is of length xN and the inner loop of length (1-x)N. For simplicity, I'll normalise to N=1.

From start to the first cut scores an average of (x/2)^2. From first cut to second cut has two options, of lengths x and 1-x; this gives an average of (1-x)x^2 + x(1-x)^2 = x-x^2. Finally the remaining path gives 1. So the total score is N^2 (1 + x - 3/4 x^2).

I initially assumed that keeping the available paths of equal length at each step would be optimal, so my initial approach used x = 1/2 giving a score of 1.3125 N^2. But after doing the above analysis it turns out that the optimal split is given when x = 2/3 with score 1.3333 N^2.

1000 walks with average 210505.738 in 202753ms

1000 walks with average 212704.626 in 205191ms

with code

import java.awt.Point;
import java.util.*;

// http://codegolf.stackexchange.com/q/37484/194
public class RandomWalker {
    private static final int SIZE = 19;
    private static final Point dest = new Point(SIZE, SIZE);

    private final Random rnd = new Random();
    private Point p = new Point(0, 0);
    private int step = 0;
    private Set<Set<Point>> edges;
    private Map<Set<Point>, String> cuttableEdgeNames;
    private Set<String> cutSequences;
    private String cutSequence = "";

    public static void main(String[] args) {
        long start = System.nanoTime();
        long total = 0;
        int walks = 0;
        while (walks < 1000 && total < 1L << 40) {
            RandomWalker rw = new RandomWalker();
            total += rw.walk();
            walks++;
        }

        long timeTaken = System.nanoTime() - start;
        System.out.println(walks + " walks with average " + total / (double)walks + " in " + (timeTaken / 1000000) + "ms");
    }

    RandomWalker() {
        loadMaze(
            "+-+ +-+ +-+ +-+ +-+ +-+ +-+-+-+-+-+-+-+",
            "| | | | | | | | | | | | |             |",
            "+ + + + + + + + + + + + + +-+ +-+ +-+ +",
            "| | | | | | | | | | | | | | | | | | | |",
            "+ + + + + + + + + + + + + + + + + + + +",
            "| | | | | | | | | | | | | | | | | | | |",
            "+ + + + + + + + + + + +-+ + + + + + + +",
            "| | | | | | | | | | |     | | | | | | |",
            "+ + + + + + + + + + + +-+-+ + + + + + +",
            "| | | | | | | | | | | |     | | | | | |",
            "+ + + + + + + + + + + + +-+ + + + + + +",
            "| | | | | | | | | | | | | | | | | | | |",
            "+ + + + + + + + + + + + + + + + + + + +",
            "| | | | | | | | | | | | | | | | | | | |",
            "+ + + + + + + + + + + + + + + + + + + +",
            "| | | | | | | | | | | | | | | | | | | |",
            "+ + + + + + + + + + + + + + + + + + + +",
            "| | | | | | | | | | | | | | | | | | | |",
            "+ +-+ +-+ +-+ +-+ +-+ + + + + + + + + +",
            "|                     | | | | | | | | |",
            "+ +-+ +-+ +-+ +-+ +-+ + + + + + + + + +",
            "| | | | | | | | | | | | | | | | | | | |",
            "+ + + + + + + + + + + + + + + + + + + +",
            "| | | | | | | | | | | | | | | | | | | |",
            "+ + + + + + + + + + + + + + + + + + + +",
            "| | | | | | | | | | | | | | | | | | | |",
            "+ + + + + + + + + + + + + + + + + + + +",
            "| | | | | | | | | | | | | | | | | | | |",
            "+ + + + + + + + + + + + + + + + + + + +",
            "| | | | | | | | | | | | | | | | | | | |",
            "+ + + + + + + + + + + +-+ + + + + + + +",
            "| | | | | | | | | | |     | | | | | | |",
            "+ + + + + + + + + + + +-+ + + + + + + +",
            "| | | | | | | | | | | | | | | | | | | d",
            "+ + + + + + + + + + + + + + +-+ +-+ +c+",
            "| | | | | | | | | | | | | |           |",
            "+ + + + + + + + + + + + + +-+-+-+-+-+ +",
            "| | | | | | | | | | | | |           f b",
            "+-+ +-+ +-+ +-+ +-+ +-+ +-+-+-+-+-+e+a+"
        );
        cutSequences = new HashSet<String>();
        cutSequences.add("ac");
        cutSequences.add("ad");
        cutSequences.add("be");
        cutSequences.add("bf");
    }

    private void loadMaze(String... row) {
        edges = new HashSet<Set<Point>>();
        cuttableEdgeNames = new HashMap<Set<Point>, String>();

        // Horizontal edges
        for (int y = 0; y <= SIZE; y++) {
            for (int x0 = 0; x0 < SIZE; x0++) {
                char ch = row[y * 2].charAt(x0 * 2 + 1);
                if (ch == ' ') continue;
                Set<Point> edge = new HashSet<Point>();
                edge.add(new Point(x0, y));
                edge.add(new Point(x0 + 1, y));
                edges.add(edge);
                if (ch != '-') cuttableEdgeNames.put(edge, "" + ch);
            }
        }

        // Vertical edges
        for (int y0 = 0; y0 < SIZE; y0++) {
            for (int x = 0; x <= SIZE; x++) {
                char ch = row[y0 * 2 + 1].charAt(x * 2);
                if (ch == ' ') continue;
                Set<Point> edge = new HashSet<Point>();
                edge.add(new Point(x, y0));
                edge.add(new Point(x, y0 + 1));
                edges.add(edge);
                if (ch != '|') cuttableEdgeNames.put(edge, "" + ch);
            }
        }
    }

    int walk() {
        while (!p.equals(dest)) {
            List<Point> neighbours = neighbours(p);
            int idx = rnd.nextInt(neighbours.size());
            p = neighbours.get(idx);
            step++;
        }

        return step;
    }

    List<Point> neighbours(Point p) {
        List<Point> rv = new ArrayList<Point>();
        if (p.x > 0) handlePossibleNeighbour(rv, p, new Point(p.x - 1, p.y));
        if (p.x < SIZE) handlePossibleNeighbour(rv, p, new Point(p.x + 1, p.y));
        if (p.y > 0) handlePossibleNeighbour(rv, p, new Point(p.x, p.y - 1));
        if (p.y < SIZE) handlePossibleNeighbour(rv, p, new Point(p.x, p.y + 1));
        return rv;
    }

    private void handlePossibleNeighbour(List<Point> neighbours, Point p1, Point p2) {
        if (edgeExists(p1, p2)) neighbours.add(p2);
    }

    private boolean edgeExists(Point p1, Point p2) {
        Set<Point> edge = new HashSet<Point>();
        edge.add(p1);
        edge.add(p2);

        // Is it cuttable?
        String id = cuttableEdgeNames.get(edge);
        if (id != null) {
            String prefix = cutSequence + id;
            for (String seq : cutSequences) {
                if (seq.startsWith(prefix)) {
                    // Cut it
                    cutSequence = prefix;
                    edges.remove(edge);
                    return false;
                }
            }
        }

        return edges.contains(edge);
    }
}
\$\endgroup\$
16
  • \$\begingroup\$ Ah, I see, that's why my island approach doesn't work, I didn't make the path length balanced. Just to clarify my understanding, the length from f to c in your code is about N/2, be it through e (and d) or not, right? \$\endgroup\$
    – justhalf
    Sep 10, 2014 at 17:14
  • \$\begingroup\$ how is the y-E path length N instead of length N/2? \$\endgroup\$
    – Sparr
    Sep 10, 2014 at 19:52
  • \$\begingroup\$ @justhalf, yes. There are 400 vertices, so there are 401 edges (after one cut the graph is a Hamiltonian cycle); the two outer paths are 100 edges each, and the inner loop is therefore 101 edges. \$\endgroup\$ Sep 10, 2014 at 20:51
  • \$\begingroup\$ got it. two observations: a) larger mazes would benefit from greater 2^n paths. b) if you make your path length dynamic, you'll beat the current leaders with dynamic two-path solutions (myself and @justhalf) \$\endgroup\$
    – Sparr
    Sep 10, 2014 at 20:59
  • \$\begingroup\$ @Sparr: it's N^2, not 2^N. And yes, making this dynamic will make it the best, the challenge is how to make it dynamic while keeping the four-path property. @PeterTaylor: Nice images! \$\endgroup\$
    – justhalf
    Sep 11, 2014 at 2:17
5
\$\begingroup\$

I experimented with slicing the grid almost entirely across every k rows. This effectively converts it into something similar to a random walk on a k by N * N/k grid. The most effective option is to slice every row so that we force the drunkard to zigzag.

For the 20x20 case (SIZE=19) I have

time java RandomWalker 
1000 walks with average 148577.604

real    0m14.076s
user    0m13.713s
sys     0m0.360s

with code

import java.awt.Point;
import java.util.*;

// http://codegolf.stackexchange.com/q/37484/194
// This handles a simpler problem where the grid is mutilated before the drunkard starts to walk.
public class RandomWalker {
    private static final int SIZE = 19;
    private final Random rnd = new Random();

    public static void main(String[] args) {
        RandomWalker rw = new RandomWalker();
        long total = 0;
        int walks = 0;
        while (walks < 1000 && total < 1L << 40) {
            total += rw.walk();
            walks++;
        }

        System.out.println(walks + " walks with average " + total / (double)walks);
    }

    int walk() {
        Point dest = new Point(SIZE, SIZE);
        Point p = new Point(0, 0);
        int step = 0;

        while (!p.equals(dest)) {
            List<Point> neighbours = neighbours(p);
            int idx = rnd.nextInt(neighbours.size());
            p = neighbours.get(idx);
            step++;
        }

        return step;
    }

    List<Point> neighbours(Point p) {
        List<Point> rv = new ArrayList<Point>();
        if (p.x > 0) handlePossibleNeighbour(rv, p, new Point(p.x - 1, p.y));
        if (p.x < SIZE) handlePossibleNeighbour(rv, p, new Point(p.x + 1, p.y));
        if (p.y > 0) handlePossibleNeighbour(rv, p, new Point(p.x, p.y - 1));
        if (p.y < SIZE) handlePossibleNeighbour(rv, p, new Point(p.x, p.y + 1));
        return rv;
    }

    private void handlePossibleNeighbour(List<Point> neighbours, Point p1, Point p2) {
        if (edgeExists(p1, p2)) neighbours.add(p2);
    }

    private boolean edgeExists(Point p1, Point p2) {
        return p1.x != p2.x || p1.x == SIZE * (Math.max(p1.y, p2.y) & 1);
    }
}
\$\endgroup\$
5
  • \$\begingroup\$ Am I right in thinking all the edge deletion happens before the walk starts in your solution? \$\endgroup\$
    – user9206
    Sep 9, 2014 at 10:47
  • \$\begingroup\$ @Lembik, yes. I thought the comment at the top would make that clear. \$\endgroup\$ Sep 9, 2014 at 10:57
  • \$\begingroup\$ It does, thank you. I wonder how much difference you can make by deleting edges during the walk. \$\endgroup\$
    – user9206
    Sep 9, 2014 at 10:58
  • \$\begingroup\$ Out of curiosity, how long does this take to run (altogether and per run)? \$\endgroup\$
    – stokastic
    Sep 9, 2014 at 16:53
  • \$\begingroup\$ @stokastic, about 3 seconds per run. \$\endgroup\$ Sep 9, 2014 at 17:28
3
\$\begingroup\$

For those who don't want to reinvent the wheel

Don't worry! I'll reinvent it for you :)

This is in Java, by the way.

I created a Walker class that deals with walking randomly. It also includes a helpful method for determining if a move is valid (if it has already been walked upon).

I am assuming all of you smart people can figure out to put random numbers in for the constructor, I left it up to you so you could test certain cases. Also, just call walk() function to (you guessed it!) make the drunkard walk (randomly).

I will implement canComeHome() function some other time. Preferably after I look up the best way to do that.

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Queue;
import java.util.TreeSet;

public class Walker {
    int width,height;
    int x,y; //walker's position (does anyone else keep thinking about zombies?!?)
    int dX,dY; //destination
    TreeSet<Edge> pathsNoLongerAvailable = new TreeSet<Edge>();
    TreeSet<Edge> previouslyTraveled = new TreeSet<Edge>();
    int stepCount = 0;

    public static void main(String[]args){
        int side = 10;
        Walker walker = null;
        int total = 0;
        double count = 1000;
        for(int i=0; i<count; i++){
            walker = new Walker(0,0,side,side,side-1,side-1);
            total += walker.stepCount;
            System.out.println("Iteration "+i+": "+walker.stepCount);
        }
        System.out.printf("Average: %.3f\n", total/count);
        walker.printPath();
    }

    public Walker(int startingX,int startingY, int Width, int Height, int destinationX, int destinationY){
        width = Width;
        height = Height;
        dX = destinationX;
        dY = destinationY;
        x=startingX;
        y=startingY;
        while(!walk()){
            // Do something
        }
    }

    public void printPath(){
        for(int i=0; i<width-1; i++){
            if(!pathsNoLongerAvailable.contains(new Edge(i,height-1,i+1,height-1))){
                System.out.print(" _");
            } else {
                System.out.print("  ");
            }
        }
        System.out.println();
        for(int i=height-2; i>=0; i--){
            for(int j=0; j<2*width-1; j++){
                if(j%2==0){
                    if(!pathsNoLongerAvailable.contains(new Edge(j/2,i,j/2,i+1))){
                        System.out.print("|");
                    } else {
                        System.out.print(" ");
                    }
                } else {
                    if(!pathsNoLongerAvailable.contains(new Edge(j/2,i,j/2+1,i))){
                        System.out.print("_");
                    } else {
                        System.out.print(" ");
                    }
                }
            }
            System.out.println();
        }
    }

    public boolean walk(){
        ArrayList<int[]> possibleMoves = new ArrayList<int[]>();
        if(x!=0 && !pathsNoLongerAvailable.contains(new Edge(x,y,x-1,y))){
            possibleMoves.add(new int[]{-1,0});
        }
        if(x!=width-1 && !pathsNoLongerAvailable.contains(new Edge(x,y,x+1,y))){
            possibleMoves.add(new int[]{1,0});
        }
        if(y!=0 && !pathsNoLongerAvailable.contains(new Edge(x,y,x,y-1))){
            possibleMoves.add(new int[]{0,-1});
        }
        if(y!=height-1 && !pathsNoLongerAvailable.contains(new Edge(x,y,x,y+1))){
            possibleMoves.add(new int[]{0,1});
        }
        int random = (int)(Math.random()*possibleMoves.size());
        int[] move = possibleMoves.get(random);
        previouslyTraveled.add(new Edge(x,y,x+move[0],y+move[1]));
        x+=move[0];
        y+=move[1];
        stepCount++;
        if(x==dX && y == dY){
            return true;
        } else {
            return false;
        }
    }

    public boolean isSolvable(){
        TreeSet<Point> reachable = new TreeSet<Point>();
        Queue<Point> next = new LinkedList<Point>();
        next.offer(new Point(x,y));
        reachable.add(new Point(x,y));
        while(next.size()>0){
            Point cur = next.poll();
            int x = cur.x;
            int y = cur.y;
            ArrayList<Point> neighbors = new ArrayList<Point>();
            if(x!=0 && !pathsNoLongerAvailable.contains(new Edge(x,y,x-1,y))){
                neighbors.add(new Point(x-1, y));
            }
            if(x!=width-1 && !pathsNoLongerAvailable.contains(new Edge(x,y,x+1,y))){
                neighbors.add(new Point(x+1, y));
            }
            if(y!=0 && !pathsNoLongerAvailable.contains(new Edge(x,y,x,y-1))){
                neighbors.add(new Point(x, y-1));
            }
            if(y!=height-1 && !pathsNoLongerAvailable.contains(new Edge(x,y,x,y+1))){
                neighbors.add(new Point(x, y+1));
            }
            for(Point neighbor: neighbors){
                if(!reachable.contains(neighbor)){
                    if(neighbor.compareTo(new Point(dX, dY))==0){
                        return true;
                    }
                    reachable.add(neighbor);
                    next.offer(neighbor);
                }
            }
        }
        return false;
    }

    public boolean hasBeenWalked(int x1, int y1, int x2, int y2){
        return previouslyTraveled.contains(new Edge(x1, y1, x2, y2));
    }

    public boolean hasBeenWalked(Edge edge){
        return previouslyTraveled.contains(edge);
    }

    public void deletePath(int startX, int startY, int endX, int endY){
        Edge toAdd = new Edge(startX,startY,endX,endY);
        if(hasBeenWalked(toAdd)){
            System.out.println("Edge already travelled!");
            return;
        }
        pathsNoLongerAvailable.add(toAdd);
        if(!isSolvable()){
            pathsNoLongerAvailable.remove(toAdd);
            System.out.println("Invalid deletion!");
        }
    }

    static class Edge implements Comparable<Edge>{
        Point start, end;

        public Edge(int x1, int y1, int x2, int y2){
            start = new Point(x1, y1);
            end = new Point(x2, y2);
            if(start.compareTo(end)>0){
                Point tmp = end;
                end = start;
                start = tmp;
            }
        }

        @Override
        public int compareTo(Edge o) {
            int result = start.compareTo(o.start);
            if(result!=0) return result;
            return end.compareTo(o.end);
        }
    }

    static class Point implements Comparable<Point>{
        int x,y;
        public Point(int x, int y){
            this.x = x;
            this.y = y;
        }
        public int compareTo(Point o){
            int result = Integer.compare(x, o.x);
            if(result!=0) return result;
            result = Integer.compare(y, o.y);
            if(result!=0) return result;
            return 0;
        }
    }
}
\$\endgroup\$
7
  • \$\begingroup\$ This contains some bugs and inconsistencies. previouslyTraveled.add(new int[]{x,y,move[0],move[1]}) should be x+move[0] and y+move[1]. The Width-1 and Height-1, and inefficiency in checking the deleted paths. I've edited your code (with additional function to print the maze). Feel free to rollback if you think that's inappropriate. \$\endgroup\$
    – justhalf
    Sep 9, 2014 at 7:34
  • \$\begingroup\$ Your Edge doesn't correctly implement Comparable<Edge>. If you want edges to compare as equals even if you reverse them, you need to take into account the reversal as well in the non-equal case. The easiest way of doing this would be to change the constructor to keep the points ordered. \$\endgroup\$ Sep 9, 2014 at 8:16
  • \$\begingroup\$ @PeterTaylor: Thanks for the heads up. I thought about the non-equal case a bit, but couldn't get myself to understand why it matters. Do you know where I can look for implementation requirement for Comparable? \$\endgroup\$
    – justhalf
    Sep 9, 2014 at 8:22
  • 1
    \$\begingroup\$ docs.oracle.com/javase/7/docs/api/java/lang/Comparable.html The key is that it needs to define a total ordering. But if A and B are the same edge reversed and C is different, you can get A.compareTo(B) == B.compareTo(A) == 0 but A.compareTo(C) < 0 and B.compareTo(C) > 0. \$\endgroup\$ Sep 9, 2014 at 8:24
  • \$\begingroup\$ How about now? I added another class. And I added function to check if it's solvable (or canComeHome()) \$\endgroup\$
    – justhalf
    Sep 9, 2014 at 8:30
3
\$\begingroup\$

64,281

Update since the grid was changed from 100x100 to 20x20 (1000 tests). Score on 100x100 (100 tests) was roughly 36M.

While this isn't going to beat a 1D walk, I wanted to play with an idea I had.

The basic idea is that the grid is split into square rooms, with only one path leading 'homeward' from each. The open path is whichever the drunk gets close to last, meaning he has to explore every possible exit, only to have all but one of them slammed in his face.

After playing with room sizes, I came to the same conclusion as Peter, slicing it up smaller is better. The best scores come with a room size of 2.

Average score over 100 trials: 36051265

The code is sloppy, don't mind the mess. You can flip on the SHOW switch and it will show an image of the paths every SHOW_INT steps so you can watch it in action. A completed run looks something like:

enter image description here

(This is the image from the previous 100x100 grid. 20x20 is just like this, but, well, smaller. Code below has been updated for new size/runs.)

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.image.*;
import java.util.*;
import javax.swing.*;

public class DrunkWalk {

    boolean SHOW = false;
    int SHOW_INT = 10;
    int SIZE = 20;
    Random rand = new Random();
    Point pos;
    int[][] edges;
    int[][] wally;
    int[] wallx;
    int roomSize = 2;
    JFrame frame;
    final BufferedImage img;

    public static void main(String[] args){
        long total=0,runs=1000;
        for(int i=0;i<runs;i++){
            int steps = new DrunkWalk().run();
            total += steps;
            System.out.println("("+i+") "+steps);
        }
        System.out.println("\n Average " + (total/runs) + " over " + runs + " trials.");
    }

    DrunkWalk(){
        edges = new int[SIZE][SIZE];
        for(int x=0;x<SIZE;x++){
            for(int y=0;y<SIZE;y++){
                if(x>0) edges[x][y] |= WEST;
                if(x+1<SIZE) edges[x][y] |= EAST;
                if(y>0) edges[x][y] |= NORTH;
                if(y+1<SIZE) edges[x][y] |= SOUTH;
            }
        }
        wallx = new int[SIZE/roomSize+1];
        wally = new int[SIZE/roomSize+1][SIZE/roomSize+1];
        pos = new Point(SIZE-1,SIZE-1);
        img = new BufferedImage(SIZE*6+1,SIZE*6+1, BufferedImage.TYPE_INT_RGB);
        frame = new JFrame(){
            public void paint(Graphics g) {
                g.drawImage(img, 50, 50, null);
            }
        };
        frame.setSize(700,700);
        if(SHOW)
            frame.show();
    }

    void draw(){
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        Graphics g = img.getGraphics();
        g.setColor(Color.WHITE);
        g.clearRect(0, 0, img.getWidth(), img.getHeight());
        for(int x=0;x<SIZE;x++){
            for(int y=0;y<SIZE;y++){
                if((edges[x][y]&EAST)==EAST)
                    g.drawLine(x*6, y*6, x*6+5, y*6);
                if((edges[x][y]&SOUTH)==SOUTH)
                    g.drawLine(x*6, y*6, x*6, y*6+5);
            }
        }
        g.setColor(Color.RED);
        g.drawOval(pos.x*6-2, pos.y*6-2, 5, 5);
        g.drawOval(pos.x*6-1, pos.y*6-1, 3, 3);
        frame.repaint();
    }

    int run(){
        int steps = 0;
        Point home = new Point(0,0);
        while(!pos.equals(home)){
            if(SHOW&&steps%SHOW_INT==0){
                System.out.println(steps);
                draw();
            }
            step();
            adversary();
            steps++;
        }
        if(SHOW)
            draw();
        return steps;
    }

    void adversary(){
        int rx = pos.x / roomSize;
        int ry = pos.y / roomSize;
        int maxWalls = roomSize - 1;
        if(wally[rx][ry] < maxWalls){
            if(pos.y%roomSize==0)
                if(delete(pos.x,pos.y,NORTH))
                    wally[rx][ry]++;
        }
        maxWalls = SIZE-1;
        if(pos.x%roomSize==0){
            if(wallx[rx] < maxWalls)
                if(delete(pos.x, pos.y,WEST))
                    wallx[rx]++;


        }       
    }

    void step(){
        List<Integer> choices = getNeighbors(pos);
        Collections.shuffle(choices);
        int dir = choices.get(0);
        pos.x += dir==WEST?-1:dir==EAST?1:0;
        pos.y += dir==NORTH?-1:dir==SOUTH?1:0;
    }

    boolean delete(int x, int y, int dir){
        if((edges[x][y] & dir) != dir)
            return false;
        edges[x][y] -= dir;
        if(dir == NORTH)
            if(y>0) edges[x][y-1] -= SOUTH;
        if(dir == SOUTH)
            if(y+1<SIZE) edges[x][y+1] -= NORTH;
        if(dir == EAST)
            if(x+1<SIZE) edges[x+1][y] -= WEST;
        if(dir == WEST)
            if(x>0) edges[x-1][y] -= EAST;
        return true;
    }

    List<Integer> getNeighbors(Point p){
        if(p.x==SIZE || p.y==SIZE){
            System.out.println("wtf");
            System.exit(0);
        }
        List<Integer> choices = new ArrayList<Integer>();
        if((edges[p.x][p.y] & NORTH) == NORTH)
            choices.add(NORTH);
        if((edges[p.x][p.y] & SOUTH) == SOUTH)
            choices.add(SOUTH);
        if((edges[p.x][p.y] & EAST) == EAST)
            choices.add(EAST);
        if((edges[p.x][p.y] & WEST) == WEST)
            choices.add(WEST);
        return choices;
    }

    final static int NORTH=1,EAST=2,SOUTH=4,WEST=8;
}
\$\endgroup\$
3
  • \$\begingroup\$ I just noticed that he should be going from bot/left->top/right, while mine goes bot/right->top/left. I can change it if it really matters, but... \$\endgroup\$
    – Geobits
    Sep 9, 2014 at 16:00
  • \$\begingroup\$ This is very nice and is the first dynamic solution I think. I am interested that your path isn't quite as long as the static ones yet. \$\endgroup\$
    – user9206
    Sep 9, 2014 at 16:51
  • \$\begingroup\$ If by "not quite as long" you mean ~1/3 as long as one and ~36x as long the other? :P \$\endgroup\$
    – Geobits
    Sep 9, 2014 at 16:53
3
\$\begingroup\$

188k, with 2 paths

The best entries all seem to take the approach of generating 2 paths, and then cutting one off when the drunk nears the end of the path. I don't think I can beat justhalf's entry, but I couldn't help but wonder: Why 2 paths? Why not 3, or 5, or 20?

TL;DR: 2 paths seems to be optimal

So I did an experiment. Based on Stretch Maniac's framework, I wrote an entry to test various numbers of paths. You can tweak the featureSize parameter to vary the number of paths. A featureSize of 20 give 1 path, 10 gives 2 paths, 7 gives 3, 5 gives 4, and so on.

import java.util.ArrayList;
import java.util.BitSet;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.Objects;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ThreadLocalRandom;

public class Walker {
    final int width,height;
    int x,y; //walker's position (does anyone else keep thinking about zombies?!?)
    final int dX,dY; //destination
    final int featureSize;
    Set<Edge> pathsNoLongerAvailable = new HashSet<>();
    Set<Edge> previouslyTraveled = new HashSet<>();
    int stepCount = 0;
    private final BitSet remainingExits;

    public static void main(String[]args){
        int side = 20;
        Walker walker = null;
        int total = 0;
        int featureSize = 10;
        double count = 1000;
        for(int i=0; i<count; i++){
            walker = new Walker(0,0,side,side,side-1,side-1, featureSize);
            total += walker.stepCount;
            System.out.println("Iteration "+i+": "+walker.stepCount);
        }
        System.out.printf("Average: %.3f\n", total/count);
        walker.printPath();
    }

    public Walker(int startingX,int startingY, int Width, int Height, int destinationX, int destinationY, int featureSize){
        width = Width;
        height = Height;
        dX = destinationX;
        dY = destinationY;
        x=startingX;
        y=startingY;
        this.featureSize = featureSize;

        deleteBars();

        remainingExits = new BitSet();
        for (int yy = 0; yy < height; yy++) {
            if (!pathsNoLongerAvailable.contains(new Edge(width - 2, yy, width - 1, yy))) {
                remainingExits.set(yy);
            }
        }

        while(!walk()){
            if (x == width - 2
                    && remainingExits.get(y)
                    && remainingExits.cardinality() > 1) {
                deletePath(x, y, x + 1, y);
                remainingExits.set(y, false);
            }
        }
    }

    private void deleteBars() {
        for (int xx = 0; xx < width - 1; xx++) {
            for (int yy = 0; yy < height / featureSize + 1; yy++) {
                if (xx != 0) deletePath(xx, featureSize * yy + featureSize - 1, xx, featureSize * yy + featureSize);
                boolean parity = xx % 2 == 0;
                if (yy == 0) parity ^= true; // First path should be inverted
                for (int i = 0; i < featureSize && featureSize * yy + i < height; i++) {
                    if (i == 0 && !parity) continue;
                    if ((i == featureSize - 1 || featureSize * yy + i == height - 1) && parity) continue;
                        deletePath(xx, featureSize * yy + i, xx + 1, featureSize * yy + i);
                }
            }
        }
    }

    public void printPath(){
        for(int i=0; i<width-1; i++){
            if(!pathsNoLongerAvailable.contains(new Edge(i,height-1,i+1,height-1))){
                System.out.print(" _");
            } else {
                System.out.print("  ");
            }
        }
        System.out.println();
        for(int i=height-2; i>=0; i--){
            for(int j=0; j<2*width-1; j++){
                if(j%2==0){
                    if(!pathsNoLongerAvailable.contains(new Edge(j/2,i,j/2,i+1))){
                        System.out.print("|");
                    } else {
                        System.out.print(" ");
                    }
                } else {
                    if(!pathsNoLongerAvailable.contains(new Edge(j/2,i,j/2+1,i))){
                        System.out.print("_");
                    } else {
                        System.out.print(" ");
                    }
                }
            }
            System.out.println();
        }
    }

    public boolean walk(){
        ArrayList<int[]> possibleMoves = new ArrayList<int[]>();
        if(x!=0 && !pathsNoLongerAvailable.contains(new Edge(x,y,x-1,y))){
            possibleMoves.add(new int[]{-1,0});
        }
        if(x!=width-1 && !pathsNoLongerAvailable.contains(new Edge(x,y,x+1,y))){
            possibleMoves.add(new int[]{1,0});
        }
        if(y!=0 && !pathsNoLongerAvailable.contains(new Edge(x,y,x,y-1))){
            possibleMoves.add(new int[]{0,-1});
        }
        if(y!=height-1 && !pathsNoLongerAvailable.contains(new Edge(x,y,x,y+1))){
            possibleMoves.add(new int[]{0,1});
        }
        int random = ThreadLocalRandom.current().nextInt(possibleMoves.size());
        int[] move = possibleMoves.get(random);
        previouslyTraveled.add(new Edge(x,y,x+move[0],y+move[1]));
        x+=move[0];
        y+=move[1];
        stepCount++;
        if(x==dX && y == dY){
            return true;
        } else {
            return false;
        }
    }

    public boolean isSolvable(){
        Set<Point> reachable = new HashSet<>();
        Queue<Point> next = new LinkedList<>();
        next.offer(new Point(x,y));
        reachable.add(new Point(x,y));
        while(next.size()>0){
            Point cur = next.poll();
            int x = cur.x;
            int y = cur.y;
            ArrayList<Point> neighbors = new ArrayList<>();
            if(x!=0 && !pathsNoLongerAvailable.contains(new Edge(x,y,x-1,y))){
                neighbors.add(new Point(x-1, y));
            }
            if(x!=width-1 && !pathsNoLongerAvailable.contains(new Edge(x,y,x+1,y))){
                neighbors.add(new Point(x+1, y));
            }
            if(y!=0 && !pathsNoLongerAvailable.contains(new Edge(x,y,x,y-1))){
                neighbors.add(new Point(x, y-1));
            }
            if(y!=height-1 && !pathsNoLongerAvailable.contains(new Edge(x,y,x,y+1))){
                neighbors.add(new Point(x, y+1));
            }
            for(Point neighbor: neighbors){
                if(!reachable.contains(neighbor)){
                    if(neighbor.compareTo(new Point(dX, dY))==0){
                        return true;
                    }
                    reachable.add(neighbor);
                    next.offer(neighbor);
                }
            }
        }
        return false;
    }

    public boolean hasBeenWalked(int x1, int y1, int x2, int y2){
        return previouslyTraveled.contains(new Edge(x1, y1, x2, y2));
    }

    public boolean hasBeenWalked(Edge edge) {
        return previouslyTraveled.contains(edge);
    }

    public void deletePath(int startX, int startY, int endX, int endY){
        Edge toAdd = new Edge(startX,startY,endX,endY);
        if(hasBeenWalked(toAdd)){
            System.out.println("Edge already travelled!");
            return;
        }
        pathsNoLongerAvailable.add(toAdd);
        if(!isSolvable()){
            pathsNoLongerAvailable.remove(toAdd);
            System.out.println("Invalid deletion!");
        }
    }

    public static class Edge implements Comparable<Edge>{
        Point start, end;

        public Edge(int x1, int y1, int x2, int y2){
            start = new Point(x1, y1);
            end = new Point(x2, y2);
            if(start.compareTo(end)>0){
                Point tmp = end;
                end = start;
                start = tmp;
            }
        }

        @Override
        public int compareTo(Edge o) {
            int result = start.compareTo(o.start);
            if(result!=0) return result;
            return end.compareTo(o.end);
        }

        @Override
        public String toString() {
            return start.toString() + "-" + end.toString();
        }

        @Override
        public int hashCode() {
            int hash = 7;
            hash = 83 * hash + Objects.hashCode(this.start);
            hash = 83 * hash + Objects.hashCode(this.end);
            return hash;
        }

        @Override
        public boolean equals(Object obj) {
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            final Edge other = (Edge) obj;
            if (!Objects.equals(this.start, other.start)) {
                return false;
            }
            if (!Objects.equals(this.end, other.end)) {
                return false;
            }
            return true;
        }


    }

    static class Point implements Comparable<Point>{
        int x,y;
        public Point(int x, int y){
            this.x = x;
            this.y = y;
        }
        public int compareTo(Point o){
            int result = Integer.compare(x, o.x);
            if(result!=0) return result;
            result = Integer.compare(y, o.y);
            if(result!=0) return result;
            return 0;
        }
        @Override
        public String toString() {
            return "(" + x + "," + y + ")";
        }

        @Override
        public int hashCode() {
            int hash = 7;
            hash = 23 * hash + this.x;
            hash = 23 * hash + this.y;
            return hash;
        }

        @Override
        public boolean equals(Object obj) {
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            final Point other = (Point) obj;
            if (this.x != other.x) {
                return false;
            }
            if (this.y != other.y) {
                return false;
            }
            return true;
        }


    }
}

There are a few optimisations that I could do but haven't, and it doesn't support any of the adaptive trickery that justhalf uses.

Anyway, here's the results for various featureSize values:

20 (1 path):  156284 
10 (2 paths): 188553
7 (3 paths):  162279
5 (4 paths):  152574
4 (5 paths):  134287
3 (7 paths):  118843
2 (10 paths): 94171
1 (20 paths): 64515

And here's a map with 3 paths:

 _   _   _   _   _   _   _   _   _    
| | | | | | | | | | | | | | | | | | | |
| | | | | | | | | | | | | | | | | | | |
| | | | | | | | | | | | | | | | | | | |
| | | | | | | | | | | | | | | | | | | |
| |_| |_| |_| |_| |_| |_| |_| |_| |_| |
|_   _   _   _   _   _   _   _   _   _|
| | | | | | | | | | | | | | | | | | | |
| | | | | | | | | | | | | | | | | | | |
| | | | | | | | | | | | | | | | | | | |
| | | | | | | | | | | | | | | | | | | |
| | | | | | | | | | | | | | | | | | | |
| |_| |_| |_| |_| |_| |_| |_| |_| |_| |
|  _   _   _   _   _   _   _   _   _  |
| | | | | | | | | | | | | | | | | | | |
| | | | | | | | | | | | | | | | | | | |
| | | | | | | | | | | | | | | | | | | |
| | | | | | | | | | | | | | | | | | | |
| | | | | | | | | | | | | | | | | | | |
|_| |_| |_| |_| |_| |_| |_| |_| |_| | |
\$\endgroup\$
5
  • \$\begingroup\$ Thanks for this. It seems all the money is in the adaptive trickery now :) \$\endgroup\$
    – user9206
    Sep 10, 2014 at 12:24
  • \$\begingroup\$ Why do you cut the path at the bottom? You can cut the path between the lower path and the middle path for better score, I think. \$\endgroup\$
    – justhalf
    Sep 10, 2014 at 17:01
  • \$\begingroup\$ @justhalf Yes, I expect it would. I decided not to, as it would have made the code more complicated, and it wouldn't have been a winning entry either way. \$\endgroup\$
    – James_pic
    Sep 10, 2014 at 18:59
  • 1
    \$\begingroup\$ The three paths (assuming optimal 3-path) will on average be the same as single path: let N be the path length (which is n^2-1), single path will on average require N^2 moves, while the three paths (N/3)^2 + (2N/3)^2 + (2N/3)^2 = N^2 plus some relatively small value, so three paths has no significant gain over single path, let alone double path. (The calculation is based on probability result which states that random movement on 1-D path of length N requires on average N^2 movement from one end to the other.) \$\endgroup\$
    – justhalf
    Sep 11, 2014 at 2:13
  • \$\begingroup\$ @justhalf Nice. I was struggling to come up with a good first-principles argument for why 2 was best, but this nails it. \$\endgroup\$
    – James_pic
    Sep 11, 2014 at 8:08
2
\$\begingroup\$

131k (20x20)

My first attempt was to remove all of the horizontal edges except the top and bottom row, then each time the walker reached the bottom of a column I would remove the edge ahead of him, until he had visited the bottom of every column and would finally be able to reach the exit. This resulted in an average of 1/8 as many steps as @PeterTaylor's 1d walk approach.

Next I decided to try something a bit more circuitous. I have split the maze into a series of nested hollow chevrons, and require him to traverse the perimeter of each chevron at least 1.5 times. This has an average time of about 131k steps.

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <iostream>
#include <math.h>

#define DEBUG 0
#define ROUNDS 10000

#define Y 20
#define X 20
#define H (Y*2+1)
#define W (X*2+1)

int maze[H][W];
int scores[ROUNDS];

int x, y;

void print_maze(){
    char line[W+2];
    line[W+1]=0;
    for(int row=0;row<H;row++) {
        for(int col=0;col<W;col++) {
            switch(maze[row][col]) {
                case 0:
                    line[col]=' ';
                    break;
                case 1:
                    line[col]=row%2?'-':'|';
                    break;
                case 9:
                    line[col]=(row==y*2+1&&col==x*2+1)?'@':' ';
                    break;
            }
        }
        line[W]='\n';
        printf("%s",line);
    }
    printf("%d %d\n",y,x);
}

int main(){
    srand (time(NULL));
    long long total_turns = 0;
    for(int round=0;round<ROUNDS;round++) {
        for (int r=0;r<H;r++) {
            for (int c=0;c<W;c++) {
                if (r==0 || r==H-1 || c==0 || c==W-1) maze[r][c]=0; // edges
                else if (r%2) { // rows with cells and E/W paths
                    if (c%2) maze[r][c] = 9; // col with cells
                    else if (r==1 || r==H-2) maze[r][c]=1; // E/W path on N/Smost row
                    else if (c>r) maze[r][c]=1; // E/W path on chevron perimeter
                    else maze[r][c]=0; // cut path between cols
                } else { // rows with N/S paths
                    if (c%2==0) maze[r][c] = 0; // empty space
                    else if (c==1 || c==W-2) maze[r][c]=1; // N/S path on E/Wmost row
                    else if (r>c) maze[r][c]=1; // N/S path on chevron perimeter
                    else maze[r][c]=0;
                }
            }
        }
        int progress = 0;
        int first_cut = 0;
        x=0;
        y=0;
        if(DEBUG) print_maze();
        long long turn = 0;
        while (x!=X-1||y!=Y-1) {
            if(DEBUG) std::cin.ignore();
            turn++;
            int r = y*2+1;
            int c = x*2+1;
            int exits = maze[r-1][c] + maze[r][c+1] + maze[r+1][c] + maze[r][c-1];
            int exit_choice = -1;
            do {
                if (rand()%exits == 0) {
                    exit_choice = exits;
                    break;
                } else {
                    exits--;
                }
            }while(exits);
            int dx=0, dy=0;
            --exits;
            if (maze[r-1][c]&&!dx&&!dy) {
                if (exits) {
                    --exits;
                } else {
                    dy = -1;
                    dx = 0;
                }
            }
            if (maze[r][c+1]&&!dx&&!dy) {
                if (exits) {
                    --exits;
                } else {
                    dy = 0;
                    dx = 1;
                }
            }
            if (maze[r+1][c]&&!dx&&!dy) {
                if (exits) {
                    --exits;
                } else {
                    dy = 1;
                    dx = 0;
                }
            }
            if (maze[r][c-1]&&!dx&&!dy) {
                if (exits) {
                    --exits;
                } else {
                    dy = 0;
                    dx = -1;
                }
            }
            x+=dx;
            y+=dy;
            if (first_cut==0) {
                if(x==X-1 && y==progress*2+1) {
                    first_cut = 1;
                    maze[y*2+2][x*2+1]=0;
                }
                if(y==Y-1 && x==progress*2+1) {
                    first_cut = 2;
                    maze[y*2+1][x*2+2]=0;
                }
            }
            else if (first_cut==1) {
                if (y==Y-1 && x==progress*2) {
                    maze[y*2+1][x*2+2]=0;
                    progress++;
                    first_cut=0;
                }
                else if (y==Y-2 && x==progress*2+1) {
                    maze[y*2+2][x*2+1]=0;
                    progress++;
                    first_cut=0;
                }
            }
            else if (first_cut==2) {
                if (x==X-1 && y==progress*2) {
                    maze[y*2+2][x*2+1]=0;
                    progress++;
                    first_cut=0;
                }
                else if (x==X-2 && y==progress*2+1) {
                    maze[y*2+1][x*2+2]=0;
                    progress++;
                    first_cut=0;
                }
            }
            if(DEBUG) print_maze();
        }
        // printf("turns:%lld\n",turn);
        scores[round] = turn;
        total_turns += turn;
    }
    long long avg = total_turns/ROUNDS;
    printf("average: % 10lld\n",avg);
    long long var = 0;
    for(int r=0;r<ROUNDS;r++){
        var += (scores[r]-avg)*(scores[r]-avg);
    }
    var/=ROUNDS;
    // printf("variance: %lld\n",var);
    int stddev=sqrt(var);
    printf("stddev:  % 10d\n",stddev);

}
\$\endgroup\$
0
\$\begingroup\$

Do Nothing

Since the man moves randomly, one might think that removing any node will only increase his chances of getting home in the long term.

First, lets have a look at the one-dimensional case, this can be achieved by removing nodes until you end up with a squiggly path, without deadends or cycles, that visits (almost) every gridpoint. On an N x N grid the maximal length of such a path is L = N*N - 2 + N%2 (98 for a 10x10 grid). Walking along the path can be described by a transition matrix as generated by T1d. transition matrix

(The slight asymmetry makes it hard to find an analytical solution, except for very small or infinite matrices, but we obtain a numerical solution faster than it would take to diagonalize the matrix anyway).
The state vector has a single 1 at the starting position and after K steps (T1d**K) * state gives us the probability distribution of being at a certain distance from the start (that is equivalent to averaging over all 2**K possible walks along the path!)

Running the simulation for 10*L**2 steps and saving the last element of the state vector after each step which gives us the probability of having made it to the goal after a certain total number of steps - the cumulative probability distribution cd(t). Differentiating it gives us the probability p of reaching the goal exactly at a certain time. To find the average time we integrate t*p(t) dt
The average time to reach the goal is proportional to L**2 with a factor that goes very quickly to 1. The standard deviation is almost constant at around 79% of the average time.
This graph shows the average time to reach the goal for different path lengths (corresponding to grid sizes of 5x5 to 15x15) enter image description here

Here is how the probability of reaching the goal looks like. The second curve looks filled out because at every odd timestep the position is odd and therefore cannot be at the goal. enter image description here

From that we can see that the balanced dual-path strategy works best here. For larger grids, where the overhead of making more paths is negligible compared to their size, we might be better off increasing the number of paths, similar to how Peter Taylor described it, but keeping the lengths balanced

What if we dont remove any nodes at all?

Then we would have twice as many walkable nodes, plus four possible directions instead of two. It would seem that it makes it very unlikely to ever get anywhere. However, simulations show otherwise, after just 100 steps on a 10x10 grid the man is pretty likely to reach his goal, so trappin him in islands is a futile attempt, since you are trading a potential N**2 long winding path with an average completion time of N**4 for an island which is passed in N**2 steps

probability of a walk on 2d grid

from numpy import *
import matplotlib.pyplot as plt

def L(N): # maximal length of a path on an NxN grid
    return N*N - 2 + N%2

def T1d(N): # transition along 1d path
    m = ( diag(ones(N-1),1) + diag(ones(N-1),-1) )/2.
    m[1,0] = 1
    m[-2,-1] = 0
    m[-1,-1] = 1
    return m

def walk(stepmatrix, state, N):
    data = zeros(N)
    for i in xrange(N):
        data[i] = state[-1]
        state = dot(stepmatrix, state)
    return data

def evaluate(data):
    rho = diff(data)/data[-1]
    t = arange(len(rho))
    av = sum(rho*t)
    stdev = sum((t-av)**2 * rho)**.5
    print 'average: %f\nstd: %f'%(av, stdev)
    return rho, av, stdev

gridsize = 10
M = T1d(L(gridsize))
initpos = zeros(L(gridsize))
initpos[0] = 1
cd = walk(M, initpos, L(gridsize)**2*5)

plt.subplot(2,1,1)
plt.plot(cd)
plt.title('p of reaching the goal after N steps')
plt.subplot(2,1,2)
plt.plot(evaluate(cd)[0])
plt.title('p of reaching the goal at step N')
plt.show()


''' 
# uncomment to run the 2D simulation
# /!\ WARNING /!\ generates a bunch of images, dont run on your desktop

def links(k,n):
    x = [k-n, k+n]
    if k%n != 0: x.append(k-1)
    if k%n != n-1: x.append(k+1)
    x = [i for i in x if 0<= i <n*n]
    return x

N = 10 # gridsize    

MM = zeros((N*N, N*N)) # build transition matrix
for i in range(N*N):
    temp = links(i,N)
    for j in temp: MM[i,j] = 1./len(temp)
MM[:,-1] = zeros(N*N)
MM[-1,-1] = 1

pos = zeros(N*N)
pos[0] = 1
for i in range(N*N):
    plt.imsave('grid_%.2d'%i, kron(pos.reshape((N,N)), ones((10,10))), cmap='gray')
    pos = dot(MM, pos)
'''
\$\endgroup\$
2
  • \$\begingroup\$ +1 for effort and nice graphs. But this doesn't answer the question, and the first two words is contradictory with the conclusion of your analysis. And, you really should label the axes of your graphs. For what grid size is your probability graph applicable? \$\endgroup\$
    – justhalf
    Sep 17, 2014 at 2:59
  • \$\begingroup\$ Lovely pictures but I am not sure you have the question right. For example "Since the man moves randomly, one might think that removing any node will only increase his chances of getting home in the long term.". 1) the man will always get home eventually under the rules that are set so this doesn't seem relevant and 2) we are removing edges not nodes. \$\endgroup\$
    – user9206
    Sep 17, 2014 at 16:47

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.