How to slow down a drunkard on his way home

15

3

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

user9206

Posted 2014-09-08T20:50:53.963

Reputation:

2I don't understand; can't you just delete all the edges at the beginning except the ones that form the longest path? – Peter Olson – 2014-09-08T21:16:50.347

3I 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? – millinon – 2014-09-08T21:19:58.770

I think you could make it so that he'll never reach home. For example remove all but the bottom most and right most edges. As he approaches the right most edge disable the right most edges and enable the left most and top most edges. Switch as required. He should end up wandering around on the bottom edge of the grid indefinitely – MickyT – 2014-09-08T21:28:47.300

@millinon We talked a bit about it in chat. For example, assume you have a full grid, and let the drunkard walk until he gets one space from the finish. Then delete that edge forcing him to find the other edge leading to finish. It was previously a 1/3 chance he'd finish on that turn, now it's zero, so on average it would be longer. – Geobits – 2014-09-08T21:29:22.937

@millinon We discussed this in the chat room before this was posted. There is definitely a better approach than simply deleting all the edges to make the longest single path. Consider this very simple better approach: Delete edges leaving TWO equal length paths to the exit. Wait for him to get to the end of one of those paths, then delete the edge between him and the exit. Now he has to traverse a single path back to start and to the exit, almost as long as your originally proposed single path, on top of the traversal of a half-length path he's already made.

There are much better solutions. – Sparr – 2014-09-08T21:29:35.900

@MickyT You can't add paths back in. – Geobits – 2014-09-08T21:29:48.103

@MickyT you cannot create/enable edges, only delete/close/disable them. – Sparr – 2014-09-08T21:30:01.193

3I 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. – Sparr – 2014-09-08T21:32:26.793

1The 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. – millinon – 2014-09-08T21:42:51.577

@Geobits I thought I must of misread it – MickyT – 2014-09-08T21:50:45.427

@millinon I argued the same at first (and would still like to see the comparison, to be honest). In my example, though, the odds that he'll turn around are not the same without removing an edge. Before removing, he has a 2/3 chance of walking away from the final spot. After removing, he has a 100% chance of walking away. A deterministic path wouldn't be as interesting, because there would (I believe) be one optimal path. – Geobits – 2014-09-08T22:01:46.293

1I definitely see your point about the probability of redirecting him. However, I'd say that writing code to defeat a random walker would be much less interesting than writing code to defeat a walker that uses some kind of predictable mechanism. For example, I would prevent him from walking the same edge twice, and would introduce a backtracking behavior when he's unable to proceed from a certain point. – millinon – 2014-09-08T22:08:42.137

310 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. – Sparr – 2014-09-08T23:03:33.393

1Also, I'm finding plenty of depth of strategy on a 10x10 grid. Making it 100x100 just makes testing slower. I don't think the larger grid makes the challenge any deeper. – Sparr – 2014-09-08T23:56:18.523

It's really (n+1) by (n+1). – feersum – 2014-09-09T01:31:27.327

1@millinon I agree that the code to defeat a random walker will be 'boring', but as a comparison of the difference between steps in a complete and partially deleted grid: Average steps for the complete grid (100 trials) ~ 120,000 (st.dev. ~ 115,000); Average steps for a grid reduced to a 110 step 1-D trail ~ 97,000,000 (st. dev. ~86,000,000); So the edge deleted path takes almost 1000 times as long to complete.. – Penguino – 2014-09-09T02:57:00.743

@Penguino Awesome, thanks! I'm legitimately surprised by the result - it's an interesting problem to think about. – millinon – 2014-09-09T03:17:27.873

@Sparr You are right that 10 rounds may not be enough to distinguish cases. I set it low in case anyone manages to get the walks to be really long. I'll add something about this to the question. – None – 2014-09-09T06:34:28.063

In that case, I think it's better to use 10x10 maze, and repeat 10,000 times for more consistent scoring, instead of using 100x100 and repeating it less times. And the restriction of not deleting a path he already walked really limit the possibility of strategy. Can you remove that one? I think the restriction of not being able to reinclude path is already sufficient to make this interesting. – justhalf – 2014-09-09T07:05:48.107

@justhalf I would rather not decrease the size of the maze for the competition. You can of course test your code on different size mazes and report them and I am sure other people would be interested. One advantage of not deleting an edge he has already walked is that combined with the other rule you only ever need to test if there is still a route from the bottom left to the top right (you don't need to check if you can get back to the beginning). It's also more realistic in a sense. – None – 2014-09-09T07:11:07.463

Actually I would even suggest to use a set of small mazes (say, 4x4, 5x5, and 6x6) for the competition. And I don't agree that this makes it more realistic. But anyway, this is your competition, so I'll follow. Btw, to clarify, in your grid pictured above, is it 10x10 or 11x11? – justhalf – 2014-09-09T07:17:04.380

@justhalf I think those smaller examples would be very interesting. They just won't add to the final score. The graph is 11x11. – None – 2014-09-09T07:18:22.677

I think you really need to change the maze size. Even in 10x10 maze, my adversary took a few minutes to complete 1000 runs, resulting in average of 165,746 moves. I tried on 100x100 and it hasn't even finish one iteration. – justhalf – 2014-09-09T07:24:45.323

@justhalf I am amazed you got 165,746 moves for 10x10! Just run 100x100 for as many iterations as you can and report it. You can even report a histogram of times. It seems you may have found a very clever adversary! – None – 2014-09-09T07:28:44.580

That's actually far worse compared to the obvious single path, which is about 5mil (100 runs). So I think this question is not that balanced. – justhalf – 2014-09-09T08:25:29.073

1Hmm, sorry, I think there was some bugs in my previous implementations (this is another reason why giving the Walker class is important). It's only 7k instead of 165k. – justhalf – 2014-09-09T08:48:01.640

@justhalf 7k makes much more sense. Good catch! – None – 2014-09-09T09:23:28.847

@Optimizer I will run the code for my own interest and also to make sure the answers make sense. I find the whole problem fascinating . – None – 2014-09-09T11:57:39.210

It turns out that the question isn't clear enough about what it means by an "n by n graph". Assuming that the walker starts at (0, 0), is he trying to get to (99, 99) or (100, 100)? Different answers have made different assumptions. – Peter Taylor – 2014-09-09T17:34:42.097

@PeterTaylor He is trying to get to (99,99). – None – 2014-09-09T19:43:06.137

1A potential problem I see is the quality of the PRNGs. C's rand() varies with the libraries, but most (if not all) are pretty awful. Java seems to be better, but not by much. Forgive me for beating a dead horse, but this is one of the issues that could have been prevented by using a single walker. – Dennis – 2014-09-10T19:48:30.370

This would have been more fun if you have to remove one edge at a time, until you cannot remove any more edges (paths) per the rules. – ja72 – 2014-09-12T17:00:38.287

@ja72 That potentially makes a nice follow up question :) How different are the best answers to following that rule? – None – 2014-09-12T17:01:41.380

Since you won't be able to remove any path from the current location to home it becomes increasingly likely that the random path would take him home. It would be a futile race to delay the arrival. – ja72 – 2014-09-12T17:16:29.967

Has anybody tried a fractal pattern to maximize length to area ratio? – ja72 – 2014-09-12T17:17:22.537

The maximum length-to-area ratio is n^2-1, which can be simply achieved by creating single path, like most answers have done. Also a good dynamic solution will remove zero, one, or two edges at a time, so I don't think it's that different from current best answer, perhaps just the score will be lower if you require exactly one edge to be removed at each step, since it basically removes the dynamicity of the walker. – justhalf – 2014-09-15T08:06:35.130

All of the current good solutions can be implemented as removing a single edge at a time. The scores would be effectively identical. – Sparr – 2014-09-15T21:46:21.770

Answers

10

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;
        }
    }
}

justhalf

Posted 2014-09-08T20:50:53.963

Reputation: 2 070

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. – None – 2014-09-10T09:05:29.810

1The 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. – justhalf – 2014-09-10T09:10:14.860

Actually optimizing further I might be able to run 100k in 3.5 hours. – justhalf – 2014-09-10T09:45:07.213

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. – Sparr – 2014-09-10T14:55:19.333

Argh, I ran 100k a few hours ago but the total overflows, haha. I'm rerunning it. Let's see tomorrow. – justhalf – 2014-09-10T15:18:56.543

Actually, analyzing from the overflowing average, 13,286.53, combined with the expected result around 220k-240k, with the knowledge that int contains 2^32 distinct numbers, it's actually 228,034.89 = 13,286.53 + n*2^32/10000 with the n that causes the result to be within expected range is 5. So on average it's 228,034.89 =). I guess our result can be regarded to be the same as 228k. – justhalf – 2014-09-10T15:30:16.217

This compiles just fine for me, but trying to execute it yields Error: Could not find or load main class Walker. I saved the source code as Walker.java and executed javac Walker.java; java Walker. Am I doing something wrong? – Dennis – 2014-09-10T21:04:53.757

You need to remove the package declaration on top, or run it with java drunkardAdversary.Walker – justhalf – 2014-09-11T02:14:55.737

Beating me by exactly 100 steps. I suspect that's not a coincidence. I wonder just how much more efficiency I could squeeze out with an even-slightly-better path algorithm. – Sparr – 2014-09-11T15:07:57.393

@Sparr: I think that's really coincidence. In an experiment with such huge uncertainty, 100/200k can be safely regarded as an error. I've updated the dynamic path, though, this one seems to be better, with typical average on 1000 runs being 230k. – justhalf – 2014-09-12T06:52:32.717

2Happy 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 :) – Sparr – 2014-09-15T21:55:14.063

Wah, go ahead! I'm looking forward to your answer :) – justhalf – 2014-09-16T01:11:08.487

10

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.

Sparr

Posted 2014-09-08T20:50:53.963

Reputation: 5 758

Color me impressed. You have my vote sir! – stokastic – 2014-09-10T00:35:57.353

1That's pretty impressive. Remember when we just drew on the drunks' faces? – Dennis – 2014-09-10T00:40:32.613

It's pretty hard to discern your graph, perhaps you can change your graph printing to something similar to mine? – justhalf – 2014-09-10T02:39:33.360

1@justhalf your wish is my command – Sparr – 2014-09-10T03:42:32.287

I finally got your back! =D – justhalf – 2014-09-10T05:23:11.440

That strange looking pseudo-random selection of the path seems to make up for the poor quality of rand(). With more straightforward room selection, I get an average of 257,000 using rand(), but only 227,500 with a high quality RNG. – Dennis – 2014-09-10T20:53:48.810

@Dennis I didn't want to use a data structure for the exits, so I count them and then iterate through them. If there are 3 exits then there's a 1/3 chance to choose the first one. If that fails then there's a 1/2 chance to chose the second one. If that fails then there's a 1/1 (certain) chance to choose the third one. – Sparr – 2014-09-10T21:09:40.733

I get that. What surprises me is that it makes up for the poor quality of rand(), while the algorithm I used gives a bloated result using rand().

– Dennis – 2014-09-10T21:23:09.793

Any progress on your proposed improvements? – justhalf – 2014-09-19T08:52:20.463

1@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? :) – Sparr – 2014-09-19T17:11:35.090

@Sparr I'm curious as what improvement you've planned :) – justhalf – 2014-09-23T10:16:27.100

@justhalf ok, I guess I'm never going to get it done. Here's a sketch, the concept should be pretty self evident. I think it might beat your balanced solution by maybe 2%. The tricky part is the algorithm to distribute the paths. http://i.imgur.com/FluB89n.jpg

– Sparr – 2014-09-23T15:29:14.740

Hmm, I doubt that will be better, because what matters most is the maximum length travelled in the worse case (i.e., the drunkard walks with heavy tendency to one side), because the expected length is somewhat positively correlated with the square of the longest path. In your design, if I understand it correctly, the worse case doesn't use the whole grid like your current solution or my solution do. – justhalf – 2014-09-23T16:41:19.130

@justhalf you're correct about the worst case, but this design does better in cases where the walk along the less used path only takes half-plus-a-few steps along a 'zig' of one path before finishing the other path. – Sparr – 2014-09-23T18:38:38.313

@justhalf we could actually quantify this with a list of potential path lengths. Your balanced approach can never have a short path between (approx) 56 and 74 steps long so in the situation where the short path should be 57 steps long my solution will have a longer long path than yours. Ditto for 8 other smaller ranges. – Sparr – 2014-09-23T18:58:13.397

@Sparr Hmm, actually my solution can produce short path of length 38+4k for any valid k >= 0. However, although your point still stand, like I said previously what matters is the longest path, because the expected number of steps is somewhat proportional to n^2, and so maximizing the maximum path is better than improving some non-maximal path. If we can never have a path with length almost spanning the whole grid, it won't beat the approach that can do so. – justhalf – 2014-09-24T01:24:18.410

@justhalf sorry, I mis-spoke, re the path restrictions. Let me be more thorough in explanation. In your topmost illustration, of the "Perfect" path, consider the case where the walker goes north 19 times, east once, south 18 times. Then he backtracks to the start, and goes east, and follows the east/south path all the way to the exit, making the east/south path the long path and the north/west path the short path. The east/south path will be about 18 steps shorter than its ideal length, because the third column of the maze will already be "claimed" by the north/west path. – Sparr – 2014-09-24T05:57:36.300

Yes, that's 18 steps shorter than its "ideal" length, but I think we can't have ideal length at every case, precisely due to the randomness of the walker. If we were to have an ideal length for every case, then at every point of the walk the shortest path towards home should be the ideal length. This is impossible also in your proposal. In that case, the design which can make the drunkard walks a longer path wins, and so having a possibility to travel (almost) the whole grid before having to traverse back is an advantage. What do you think? – justhalf – 2014-09-24T06:42:55.550

@justhalf in my proposal, the long path will usually be within 2 steps of its ideal length. So, your maze has the longest best-case long path, but my I think my proposal has a longer average length of the long path. – Sparr – 2014-09-24T15:09:34.450

@Sparr, yes, but my point is that having longest best-case is better than having longer path on average case, due to the quadratic nature. Seems like we need to test it out to see =) – justhalf – 2014-09-24T15:55:52.757

6

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

stokastic

Posted 2014-09-08T20:50:53.963

Reputation: 981

I reduced the graph size to 20 by 20 to make run times quicker. I hope it helps. – None – 2014-09-09T18:12:43.630

You are currently winning :) – None – 2014-09-09T19:08:00.967

Is your 20 by 20 score over 1000 runs? – None – 2014-09-09T19:20:21.193

@Lembik yes it is. – stokastic – 2014-09-09T19:28:35.100

I think this is the optimal solution for a random walker. – Dennis – 2014-09-09T21:32:09.080

1@Dennis au contraire :) – Sparr – 2014-09-09T23:30:13.030

It's almost optimal indeed, just need to add the element of dynamic adversary inside. – justhalf – 2014-09-10T05:53:32.227

Btw the exact numerical expected number of steps to reach finish for this approach is (n^2/2-1)^2 + (n^2-1)^2, which equals 198802 for n=20 – justhalf – 2014-09-11T02:42:47.477

6

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);
    }
}

Peter Taylor

Posted 2014-09-08T20:50:53.963

Reputation: 41 901

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? – justhalf – 2014-09-10T17:14:28.243

how is the y-E path length N instead of length N/2? – Sparr – 2014-09-10T19:52:07.450

@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. – Peter Taylor – 2014-09-10T20:51:09.817

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) – Sparr – 2014-09-10T20:59:58.027

@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! – justhalf – 2014-09-11T02:17:28.433

Wait, after some more thoughts, I think the dynamic version of this one will be equivalent to the dynamic two-path solution. In dynamic four-path solution, the drunkard will travel almost N/2 when trying to reach x, then he has two N/2 options to y (the almost N/2 path he has travelled + the remaining path from start to y, or the untravelled N/2). Consider in dynamic two-path solution where the drunkard has travelled almost N/2, it will exactly be the same situation as the drunkard in dynamic four-path solution, having two options of N/2-length, with one of them travelled. – justhalf – 2014-09-11T02:31:48.977

@justhalf 2^N is the number of paths. 2 (our solution), 4 (Peter's), 8, etc.

Regarding the dynamic solution... Static one-path is N^2. Static two-path is N^2+(N/2)^2. Dynamic two-path is approx N^2+(7N/10)^2 better than static two-path, by virtue of lengthening the path that the walker covers twice at the expense of the path he covers once.

Static four-path is N^2+(N/2)^2+(N/4)^2 which is better than static two-path but not as good as dynamic two-path.

(to be continued) – Sparr – 2014-09-11T04:03:44.247

So, if we assume that dynamic 4-path will lengthen the early paths (quarters of the whole path) by the same amount as the first half of dynamic two-path is lengthened, then the outcome for dynamic 4-path is about N^2+(7N/10)^2+(7N/20)^2, which is 20k better than the current best score. the trick is arranging the paths in such a way that each of the four paths can be lengthened as it needs to be. – Sparr – 2014-09-11T04:09:41.690

@Sparr: It's tempting to think that way, but I think it can't be generalized, and dynamic 4-path is actually equivalent to dynamic 2-path. In fact, static 4-path is "partially dynamic 2-path". One important point is that after he travels the first leg (which is N/4 in static 4-path, and almost N/2 in dynamic 4-path), that first leg can't be modified, and so we are now in different condition than the start. In particular, there is 50% chance we can't make the second leg >N/2, since the other leg is already travelled. If you think about it, static 4-path case is covered in dynamic 2-path. – justhalf – 2014-09-11T04:28:55.437

Btw how do you approximate the dynamic 2-path to be (7N/10)^2? I spent a good few hours yesterday and today to find exact number, but I couldn't find a good way to model the expected number of steps required until a cut is made in dynamic 2-path (because after that it's just (n^2-1)^2) – justhalf – 2014-09-11T05:37:19.360

@justhalf, my intuition is that it's probably 2/3 rather than 7/10, but I haven't tried to calculate a value from the scores you two get. If it's 2/3 then the score is 1.444... times that of a single path. If it's 7/10 then 1.49. I've thought about Sparr's suggestion of making the 4-path approach dynamic, and if it's possible to get 2/3 of the outer loop then the maximum score seems to be 1.45 by putting 90% of the length in the outer loop and 10% in the inner loop. If it's 7/10 then the improvement would be from 1.49 to 1.4901 and it needs second-order effects to be taken into account. – Peter Taylor – 2014-09-11T09:00:34.827

I think 2/3 better matches the data. The exact expectation on pure double path is (n^2/2-1)^2 + (n^2-1)^2 = 198,802, while for single path it's (n^2-1)^2 = 159,201. The 2/3 gives 229,957 (159,201*1.44..), which is very close to our result. Btw, I still believe that dynamic 4-path is equivalent to dynamic 2-path =) – justhalf – 2014-09-11T09:29:53.657

@justhalf, it's not quite equivalent: if my assumptions are correct then to first order it's a massive 0.4% better. And I've got the path wrong, and would be better putting 2/3 in the outer loop and 1/3 in the inner loop for the static 4-path. Will experiment. – Peter Taylor – 2014-09-11T09:31:53.223

Well, 0.4% of our current result gives less than 1000. It might not be significant experimentally, unless we run a massive number of runs. So, I think it's a bit unfortunate that we can't really see the difference, although I still stand that those two are the same. – justhalf – 2014-09-11T09:37:02.847

7/10 is based on justhalf's existing score for dynamic 2-path. 2/3 was what I estimated when I first posited the existence of the dynamic solution, but @justhalf has already done better than that. – Sparr – 2014-09-11T13:02:12.263

@Sparr: I think the 237k average was a anomaly (on high end). On 100k runs it's 228k, which is about the 2/3 that you posited! =) – justhalf – 2014-09-11T13:12:31.653

5

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);
    }
}

Peter Taylor

Posted 2014-09-08T20:50:53.963

Reputation: 41 901

Am I right in thinking all the edge deletion happens before the walk starts in your solution? – None – 2014-09-09T10:47:02.713

@Lembik, yes. I thought the comment at the top would make that clear. – Peter Taylor – 2014-09-09T10:57:40.697

It does, thank you. I wonder how much difference you can make by deleting edges during the walk. – None – 2014-09-09T10:58:31.803

Out of curiosity, how long does this take to run (altogether and per run)? – stokastic – 2014-09-09T16:53:48.940

@stokastic, about 3 seconds per run. – Peter Taylor – 2014-09-09T17:28:26.163

3

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;
        }
    }
}

Stretch Maniac

Posted 2014-09-08T20:50:53.963

Reputation: 3 971

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. – justhalf – 2014-09-09T07:34:37.330

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. – Peter Taylor – 2014-09-09T08:16:49.263

@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? – justhalf – 2014-09-09T08:22:09.730

1http://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. – Peter Taylor – 2014-09-09T08:24:45.163

How about now? I added another class. And I added function to check if it's solvable (or canComeHome()) – justhalf – 2014-09-09T08:30:28.797

I'd just use java.awt.Point, but yes, you've fixed the problem. Edit: ah, that isn't comparable. Never mind, then. – Peter Taylor – 2014-09-09T08:42:43.053

@justhalf Thanks! I wrote this half asleep last night, so its no wonder there were bugs that I missed. Good work! – Stretch Maniac – 2014-09-09T20:15:37.063

3

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;
}

Geobits

Posted 2014-09-08T20:50:53.963

Reputation: 19 061

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... – Geobits – 2014-09-09T16:00:22.487

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. – None – 2014-09-09T16:51:43.273

If by "not quite as long" you mean ~1/3 as long as one and ~36x as long the other? :P – Geobits – 2014-09-09T16:53:41.373

3

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:

 _   _   _   _   _   _   _   _   _    
| | | | | | | | | | | | | | | | | | | |
| | | | | | | | | | | | | | | | | | | |
| | | | | | | | | | | | | | | | | | | |
| | | | | | | | | | | | | | | | | | | |
| |_| |_| |_| |_| |_| |_| |_| |_| |_| |
|_   _   _   _   _   _   _   _   _   _|
| | | | | | | | | | | | | | | | | | | |
| | | | | | | | | | | | | | | | | | | |
| | | | | | | | | | | | | | | | | | | |
| | | | | | | | | | | | | | | | | | | |
| | | | | | | | | | | | | | | | | | | |
| |_| |_| |_| |_| |_| |_| |_| |_| |_| |
|  _   _   _   _   _   _   _   _   _  |
| | | | | | | | | | | | | | | | | | | |
| | | | | | | | | | | | | | | | | | | |
| | | | | | | | | | | | | | | | | | | |
| | | | | | | | | | | | | | | | | | | |
| | | | | | | | | | | | | | | | | | | |
|_| |_| |_| |_| |_| |_| |_| |_| |_| | |

James_pic

Posted 2014-09-08T20:50:53.963

Reputation: 3 988

Thanks for this. It seems all the money is in the adaptive trickery now :) – None – 2014-09-10T12:24:31.623

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. – justhalf – 2014-09-10T17:01:33.290

@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. – James_pic – 2014-09-10T18:59:52.223

1The 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.) – justhalf – 2014-09-11T02:13:52.203

@justhalf Nice. I was struggling to come up with a good first-principles argument for why 2 was best, but this nails it. – James_pic – 2014-09-11T08:08:36.110

2

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);

}

Sparr

Posted 2014-09-08T20:50:53.963

Reputation: 5 758

0

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)
'''

DenDenDo

Posted 2014-09-08T20:50:53.963

Reputation: 2 811

+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? – justhalf – 2014-09-17T02:59:58.180

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. – None – 2014-09-17T16:47:33.867