40

I need some help finding a good heuristic for the following problem:

You are given an R-by-C grid and a six-sided die. Let start and end be two distinct cells on this grid. Find a path from start to end such that the sum of the faces of the die looking up, as the die is turning along the path, is minimal.

The starting orientation of the die is the following (the "2" is facing south):

enter image description here

The way I modeled this problem is by considering the value of the die's face as the cost of an edge in a graph. The graph's vertices are of the form (row, col, die) (i.e, a position in the grid and the current state/orientation of the die). The reason a vertex is not simply (row, col) is because you can end up on the same cell with multiple configurations/orientations of the die.

I used A* to find the solution to the problem; the answers given are correct, but it is not efficient enough. I've determined that the problem is the heuristic I'm using. Currently I'm using Manhattan distance, which is obviously admissible. If I multiply the heuristic with a constant, it's no longer admissible: it runs much faster but it doesn't always find the right answer.

I need some help in finding a better heuristic than Manhattan distance.

Paul Manta
  • 30,618
  • 31
  • 128
  • 208
  • how big is the grid? are there any obstacles on the grid? – usamec May 14 '13 at 18:50
  • @usamec There are no obstacles, the grid can have any size, and the start and end cells can be positioned anywhere on the grid. – Paul Manta May 14 '13 at 18:52
  • 2
    I think that you shouldn't use A* but you should try to find some pattern how the shortest path looks. – usamec May 14 '13 at 19:38
  • @usamec What should I be using instead of A*? – Paul Manta May 14 '13 at 19:46
  • 6
    *"If I multiply the heuristic with a constant, it's no longer admissible"* - The best I can come up with right now is `(manhattenDistance/3)*6 + (manhattenDistance%3)`, where `/` is integer division and `%` is mod. This works because in any 3 moves with no back-tracking, all three digits will be unique, so the lowest sum we could have is 1+2+3 = 6 *(The `%3` simply adds any extra, non-multiple-of-three moves)*. I'll think about it more later, I'm sure there is something better. – BlueRaja - Danny Pflughoeft May 14 '13 at 21:43
  • is the die right- or left-handed? – גלעד ברקן May 15 '13 at 00:20
  • @groovy I added an image to show how the die is numbered. – Paul Manta May 15 '13 at 06:20
  • It'd better if you try the dice starting in the middle of the board, and consider the 4 corners as 4 different scenarios for goals; see in each case; does there exist a better semi-zig-zag path than a Manhattan path ? – Khaled.K May 15 '13 at 08:41
  • do we count the last face, that is the face on the `end` cell? – גלעד ברקן May 15 '13 at 13:31
  • @groovy Both the start and end faces are counted. But I don't know how much this pertains to finding a better heuristic. – Paul Manta May 15 '13 at 16:39
  • 1
    It does not matter if the start face is counted, the result is the same either way *(you are adding a constant to every path-length)* – BlueRaja - Danny Pflughoeft May 15 '13 at 19:17
  • 2
    @BlueRaja-DannyPflughoeft You can improve your heuristic very slightly with the last term. If manhattenDistance%3 == 2, the minimum value of these final two moves is 3, rather than the 2 that you are adding. – GrantS May 16 '13 at 03:01
  • 1
    @BlueRaja-DannyPflughoeft: I only see your comment now. My answer seems close to what you had in mind, except with a 1.5 factor where yours has 2. – Fred Foo May 16 '13 at 12:25
  • What would be considered efficient? How fast and what size grid? I'm getting about 22 seconds for `start(1,1)` `end(11,11)` but that's calculating all 184756 paths... – גלעד ברקן May 17 '13 at 02:29
  • @groovy The best that I managed to get (with a heuristic I'll describe sometime tomorrow) is 4.6 seconds in a debug build in Java on a 300x300 grid, starting from `(23, 25)` and ending at `(282, 199)`. This is still too slow, the target time is two seconds. I don't know how many paths it checked, but it visited 555,374 unique vertices (not cells). – Paul Manta May 17 '13 at 02:33
  • @PaulManta may I ask, what was the total sum of faces for the example you calculated? (300x300 grid, starting from (23, 25) and ending at (282, 199)) – גלעד ברקן May 17 '13 at 18:43
  • 1
    Just as an aside, I posted a dynamic programming attempt here: http://stackoverflow.com/questions/16585741/dynamic-programming-memoization-in-haskell which hammar helped memoize. – גלעד ברקן May 17 '13 at 18:52
  • @groovy The minimum sum I got for the 300x300 grid is 1517. Also, thanks for posting http://stackoverflow.com/questions/16585741. – Paul Manta May 17 '13 at 19:03
  • The fact that this grid has no obstacles is a clue that we need to find a better approach than A*, I believe. With no obstacles there's too much structure remaining unexploited by A*. I suspect that, above some threshold distance, every optimal path will consist mostly of a repeated path fragment, which is "tidied up" at the ends. Not sure how to formulate this though. – j_random_hacker May 17 '13 at 19:47
  • @j_random_hacker yes, my intuition was on similar lines: when I first thought about the problem I wondered whether it could be split into sub-problems. As it is, I (hope) I've shown in my post that the optimal (amortized) cost has to be `3.5` x the Manhattan distance. Given this, I believe it would make sense to simply choose a path made out of 4-long horizontal pieces and 4-long vertical pieces, which are optimal. And then make up the remainder with a search or a lookup table. So I don't think A* is required. – TooTone May 18 '13 at 00:37
  • @groovy That means my algorithm isn't even correct... Can you please post the path you found (maybe on Pastebin)? (The row, column and die value of every node you visit.) – Paul Manta May 20 '13 at 06:34
  • 2
    @PaulManta Sure! Here it is: http://pastebin.com/bSeM6WMT You can also use the code in my answer as you wish. – גלעד ברקן May 20 '13 at 14:35
  • 2
    just curious -- does anyone have an example of an A* or other algorithm finding a path with 1515 or lower sum for Paul's example? – גלעד ברקן May 24 '13 at 18:27

5 Answers5

10

Well, I'll add my comment here, since it is more optimal than the current highest-voted answer by @larsmans - but, I am convinced there must be something better (hence the bounty).


If I multiply the heuristic with a constant, it's no longer admissible

The best I can come up with is (manhattenDistance/3)*6 + (manhattenDistance%3), where / is integer division and % is mod. This works because in any 3 moves with no back-tracking, all three digits will be unique, so the lowest sum we could have is 1+2+3 = 6 (The %3 simply adds any extra, non-multiple-of-three moves).


[Edit] As @GrantS pointed out in the comments above, my heuristic can be improved very slightly by adding an additional 1 when manhattenDistance%3 == 2. This is easy to do without a conditional: (manhattenDistance/3)*6 + (manhattenDistance%3)*3/2

BlueRaja - Danny Pflughoeft
  • 84,206
  • 33
  • 197
  • 283
  • 2
    Thanks for offering a bounty! I'm glad you like the question. :) – Paul Manta May 16 '13 at 22:28
  • 1
    I upvoted even though the phrase "more optimal" makes me shudder... Would you say "more best"? "Better" is better! – j_random_hacker May 17 '13 at 21:06
  • It seems to me that if one were going to calculate Manhattan distance, one might as well calculate (or better yet pre-calculate) the minimal path sum for `minimalR` and `minimalC` (see my answer), in which case one needn't settle for an estimate. My answer offers a very simple calculation for an exact minimal path sum. No good? – גלעד ברקן May 23 '13 at 19:38
10

Main Edit 3: Proof that the optimal admissible heuristic should be based on 3.5m

The average cost of travelling along the board has to approach 3.5m over the long run where m is the Manhattan distance. Therefore the best admissible heuristic should be 3.5m plus or minus some small constant.

The reason for this is that whenever you move in a direction, x, say, from face x1, the next move in the same direction, to face x2 has to satisfy x1 + x2 = 7. This is because any moves in the perpendicular direction leave the orientation of face x2 the same. Think about rotating a die left to right -- the front and back faces stay the same no matter how many rotations you do. Conversely if you rotate a die front to back, the left and right faces stay the same.

It's easiest to see this with some examples (all starting in the configuration pictured in the question)

   6
2453
1

here you can see that we start with y1=1, and however many times we move in the x-direction afterwards, the next move in the y-direction has to be y2=6, so y1+y2=7. (Also in the x-direction, there is a simple pairing of 2+5 = 7 and 4+3 = 7).

Another example is

  35
 26
14

In this example we start with x1=1, and however many times we move in the y-direction afterwards, the next move in the x-direction has to be x2=6. (Also, we see pairings of 4+3=7 in the y-direction, 2+5=7 in the x-direction. And we know in this case the next move in the x-direction has to be 4, and the next move in the y-direction has to be 1.)

This all assumes it's never worth backtracking, but hopefully this can be taken as read.

The original post below just fills in some details of how the estimate of 3.5m should be adjusted to take account of the ability for it to be beaten over the short term.

As a side-note, as I just commented on the OP, A* search might not be required at all. It ought to make sense to simply choose a path made out of 4-long horizontal pieces and 4-long vertical pieces, say, which are optimal. And then make up the remainder with a search or a lookup table based on orientation and x-y offset. (But the question asks for an admissible heuristic so I'm going to leave my answer.)

Main Edit 2: summarize original empirical work, taking account of comments below

In the long term, as explained above, your average cost per move is 3.5. This can also can be seen empirically in the exploration of the data below.

This gives a naive estimate of 3.5m where m is the Manhattan distance. However this is an over-estimate, because in the short term it is possible to do better than the average. A good hypothesis for this is to explore how we can avoid using any faces larger than 3.

  • If we start with face 1, we can use faces 2 and 3 on our first 2 moves, going 2 moves better than 3.5m predicts.
  • If we start with face 2, we can use faces 1 and 3 on our first 2 moves, going 3 moves better than 3.5m predicts.
  • If we start with face 3, we can use faces 1 and 2 on our first 2 moves, going 4 moves better than 3.5m predicts.
  • If we start with face 4,5, or 6, we can use faces 1, 2 and 3 on our first 3 moves, going 4.5 moves better than 3.5m predicts.

This hypothesis can be confirmed empirically by simply running the script below for every starting possibility of the die, as suggested by BlueRaja - Danny Pflughoeft. So a simple admissible statistic is 3.5m - k, where k = max(f+1, 4.5), and f is the starting face. But this is a bit klunky, giving negative numbers for small values of m. It's easy to write a programmatic version that takes account of whether you have just 1 or 2 or 3 moves to go, see below

    static double Adm(int x, int y, int face /* start face */, out int m)
    {
        double adm = 0;
        m = Math.Abs(x) + Math.Abs(y);
        if (m >= 1) {
            if (face == 1)
                adm += 2;
            else
                adm += 1;
            m--;
        }
        if (m >= 1) {
            if (face <= 2)
                adm += 3;
            else
                adm += 2;
            m--;
        }
        if (m >= 1 && face >=4) {
            // 4,5,6: we can still use a 3 without backtracking
            adm += 3;
            m--;
        }

        adm += 3.5 * m;

        return adm;
    }

Running this across a search space with |x|,|y| <= 100, this function underestimates the actual cost by between 0 and 6, with a median of 0.5 or 1.5 depending on the start face.

Main Edit 1: original post

My basic thought was that it would be good to explore the data. So I had a go at Dijkstra's algorithm to see what the space of solutions looks like. What I found is supportive of what's been said already. Some factor times the Manhattan distance is appropriate, but there may be some justification for a higher factor than 1.5. This is nicely indicated by the shape of a contour plot of cost against deviation from initial x y position.

cost against deviation from initial x y position

Here's a wire frame plot -- to be honest this is just for eye candy.

enter image description here

What's interesting is that if you add another column to your data for the manhattan distance (man) and regress the cost (v) against the manhattan distance in R, you get the following

Coefficients:
          Estimate Std. Error  t value Pr(>|t|)    
(Intercept) -0.6408087  0.0113650   -56.38   <2e-16
df$man       3.4991861  0.0001047 33421.66   <2e-16

I.e. it's telling you that for every move you make horizontally or vertically, your cost is 3.4991861, or v close to 3.5. That just happens to be the average of 1 to 6, so my intuition is that the data is telling us that on average, it's most efficient to use all the faces of the die equally over a long distance. Over short distances you can be more optimal.

I tried 3.5man - k as an estimate, with k = 2.5. This seemed to work ok. When I subtracted the actual cost from this I got -0.5 as the highest value.

> summary(df$est - df$v)
  Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
 -6.500  -2.500  -2.000  -1.777  -1.000  -0.500 

However A* search has to work for all configurations including those after the start where the die is not in the original configuration, so the constant k can't be as low as 2.5 in general. It either needs to be raised, e.g. to 4, or be dependent on the configuration of the die, as suggested in another answer.

It's quite possible that I've made some horrible mistake in all of this so I've put the code below. Like I said, I think the approach of generating the data and investigating it is sound even if my results aren't.

Here are some lines of the result file first.

17,-100,410

17,-99,406

17,-98,403

17,-97,399

17,-96,396

C# code

class Die
{
    int top;
    int bottom;
    int front;
    int back;
    int left;
    int right;

    public int Top { get { return top; } }
    public int Bottom { get { return bottom; } }
    public int Front { get { return front; } }
    public int Back { get { return back; } }
    public int Left { get { return left; } }
    public int Right { get { return right; } }

    public Die(int top, int bot, int fro, int bac, int lef, int rig)
    {
        this.top = top;
        bottom = bot;
        front = fro;
        back = bac;
        left = lef;
        right = rig;
    }

    public Die RotateLeft()
    {
        return new Die(
               top: right,
               rig: bottom,
               bot: left,
               lef: top,
               fro: front,
               bac: back
            );
    }

    public Die RotateRight()
    {
        return new Die(
               rig: top,
               top: left,
               lef: bottom,
               bot: right,
               fro: front,
               bac: back
            );
    }

    public Die RotateUp()
    {
        return new Die(
                top: front,
                fro: bottom,
                bot: back,
                bac: top,
                lef: left,
                rig: right
             );
    }

    public Die RotateDown()
    {
        return new Die(
                fro: top,
                top: back,
                bac: bottom,
                bot: front,
                lef: left,
                rig: right
            );
    }
}



class DieXY

{

    public Die Die { get; set; }
    public int X { get; set; }
    public int Y { get; set; }

    public DieXY(Die die, int x, int y) { Die = die; X = x; Y = y; }

    public override int GetHashCode() { return Die.Top + Die.Bottom*6 + Die.Front*6^2 + Die.Back*6^3 + Die.Left*6^4 + Die.Right*6^5 + X*6^6 + Y*6^8; }
    public override bool Equals(object obj)
    {
        DieXY die = (DieXY)obj;
        return die != null
            && die.Die.Top == Die.Top && die.Die.Bottom == Die.Bottom
            && die.Die.Front == Die.Front && die.Die.Back == Die.Back
            && die.Die.Left == Die.Left && die.Die.Right == Die.Right 
            && die.X == X && die.Y == Y;
    }
}

class Program
{
    static void Main(string[] args)
    {
        Dictionary<DieXY, int> dict = new Dictionary<DieXY, int>();
        int n = 100;
        int sofar = -1;
        DieXY root = new DieXY(new Die(1, 6, 2, 5, 4, 3), 0, 0);
        Queue<Tuple<DieXY, int>> queue = new Queue<Tuple<DieXY, int>>();
        queue.Enqueue(new Tuple<DieXY,int>(root,0));
        while (queue.Count > 0)
        {
            Tuple<DieXY, int> curr = queue.Dequeue();
            DieXY dieXY = curr.Item1;
            Die die = dieXY.Die;
            int x = dieXY.X;
            int y = dieXY.Y;
            if (Math.Max(x,y) > sofar)
            {
                sofar = Math.Max(x, y);
                Console.WriteLine("{0}", sofar);
            }
            int score = curr.Item2;
            if (Math.Abs(x) <= n && Math.Abs(y) <= n)
            {
                int existingScore = 0;
                if (!dict.TryGetValue(dieXY, out existingScore)
                    || score < existingScore)
                {
                    dict[dieXY] = score;
                    Die newDie = null;
                    newDie = die.RotateLeft();
                    queue.Enqueue(new Tuple<DieXY, int>(new DieXY(newDie, x - 1, y), score + newDie.Top));
                    newDie = die.RotateRight();
                    queue.Enqueue(new Tuple<DieXY, int>(new DieXY(newDie, x + 1, y), score + newDie.Top));
                    newDie = die.RotateUp();
                    queue.Enqueue(new Tuple<DieXY, int>(new DieXY(newDie, x, y + 1), score + newDie.Top));
                    newDie = die.RotateDown();
                    queue.Enqueue(new Tuple<DieXY, int>(new DieXY(newDie, x, y - 1), score + newDie.Top));
                }
            }
        }

        int[,] scores = new int[2*n+1,2*n+1];
        for (int aX = 0; aX < 2 * n + 1; aX++)
            for (int aY = 0; aY < 2 * n + 1; aY++)
                scores[aX, aY] = int.MaxValue;
        foreach (KeyValuePair<DieXY, int> curr in dict)
        {
            int aX = curr.Key.X + n;
            int aY = curr.Key.Y + n;
            if (curr.Value < scores[aX, aY])
            {
                scores[aX, aY] = curr.Value;
            }
        }

        using (System.IO.StreamWriter file = new System.IO.StreamWriter("out.csv"))
        {
            file.WriteLine("x,y,v");
            for (int aX = 0; aX < 2*n+1; aX++)
            {
                int x = aX - n;
                for (int aY = 0; aY < 2 * n + 1; aY++)
                {
                    int y = aY - n;
                    file.WriteLine("{0},{1},{2}", x, y, scores[aX, aY]);
                }
            }
        }

        Console.WriteLine("Written file");
        Console.ReadKey();
    }
}

R code below

library(lattice)
df = read.csv("out.csv")
df=transform(df, man=abs(x)+abs(y))

v50=df[abs(df$x)<=50 & abs(df$y)<=50,]
with(v50, wireframe(v ~ x*y))
with(v50, contourplot(v ~ x*y))

summary(lm(df$v ~ df$man))

df$est = df$man * 3.5 - 2.5
summary(df$est - df$v)
Community
  • 1
  • 1
TooTone
  • 7,129
  • 5
  • 34
  • 60
  • 1
    `3.5man - 2.5` fails for the simple case of being diagonal from the finish - the minimum possible is `1+2 = 3`, but `3.5man - 2.5` gives `4.5` – BlueRaja - Danny Pflughoeft May 17 '13 at 00:30
  • @BlueRaja-DannyPflughoeft I might have misunderstood the problem. I thought that to start with, 1 is looking up. So the minimum you can get is roll forward (+2) and rotate left (+3)? – TooTone May 17 '13 at 00:39
  • The heuristic needs to apply to all spaces (and all die-configurations) on the grid in order to be used by A\* – BlueRaja - Danny Pflughoeft May 17 '13 at 00:49
  • @BlueRaja-DannyPflughoeft Sorry, I can see what you mean now: it's not the start position that's the point; it's that this has to hold for subsequent positions. I got a bit over-excited about the analysis I did and forgot the big picture! If the rest of the analysis I presented is correct (and I have a few doubts), I believe that this would be coped with simply by subtracting a larger constant, or, perhaps as suggested in another answer, making the constant you subtract depend on the configuration. – TooTone May 17 '13 at 01:29
  • You could simply try running your script for every starting possibility of the die (There's only 24 - 6 possibilities for the top, and for each top, 4 possible orientations); then analyze based on the minimum number of moves over all of those. – BlueRaja - Danny Pflughoeft May 17 '13 at 03:09
  • yes, will do, it was in the small hours last night so I'll have a look later on today. From the perspective of the manhattan distance alone, the 4 orientations shouldn't make any difference as that's equivalent to rotating the axes (although the metric may be able to be refined by adding orientation). What I'd be _very_ interested in would be some kind of mathematical proof or argument as to why you use every face equally over a long distance. I can see intuitively why that might be true and there's empirical evidence, but it would be nice to have some theory. – TooTone May 17 '13 at 08:33
  • *"The need to use all faces can be seen intuitively, as rolling the die in the same axis uses 4 faces which add up to 14, and going along the other axis doesn't change this sum"* - this is only true if you move along the same axis four times in a row. This argument doesn't hold when you can alternate arbitrarily between the two axes, since the numbers on each axis change every time you move! – BlueRaja - Danny Pflughoeft May 17 '13 at 19:57
  • @BlueRaja-DannyPflughoeft to be honest as I commented earlier I'm aware that it would be good to have a stronger theoretical justification. I did have a minor success in making the theoretical prediction for the `Adm` function _before_ running empirical tests -- but it would be good to have a proper theoretical justification for the `3.5m` part. Do you have any ideas? – TooTone May 17 '13 at 21:13
  • @BlueRaja-DannyPflughoeft think I've got there at last: see the edit! But do keep pointing out any problems. – TooTone May 18 '13 at 00:17
  • Your "proof" is incorrect - from a given state, you know what (say) the next movement in the y-direction will be *(assuming the die always has to move towards the goal in the y-direction, an assumption which may or may not be true)*, but you don't know what the second move in the y-direction will be. In fact, your examples show this: in the first example, the y-direction moves show 1-2-6, but in the second example, they show 1-2-3. – BlueRaja - Danny Pflughoeft May 21 '13 at 16:33
  • @BlueRaja-DannyPflughoeft My proof is sound -- I just don't think you've quite followed what I'm trying to explain. In the second example you move from **1** to 4 in the x direction, and the next move in the x direction lands on **6**, so 1 pairs with 6 (1+6=7). We move from **4** to 2 in the y direction and the next move in the y direction lands on **3** so 4 pairs with 3 (3+4=7). Finally, 2 pairs with 5. (Also, I did state the assumption that you don't go back on yourself -- I said it should be taken as read that this will result in a lower score and I stand by that.). – TooTone May 21 '13 at 23:21
  • Well, it is not at all obvious that the best path will consist of only two directions; but, between your and TooTone's answer I believe that statement is true. However, more importantly, what you are saying is still incorrect, for the reason I stated above. Every other move in one direction will NOT always add up to 7, as one of your examples points out! – BlueRaja - Danny Pflughoeft May 22 '13 at 03:30
  • @BlueRaja-DannyPflughoeft But my examples show exactly that my argument _does_ hold, and I went into more detail in my last comment to explain it again, showing _which_ pairs you have to choose to get pairs that add up to 7 (these were _different_ from the pairs you chose in you comment that I replied to, did you see that?). The key point -- and this is subtle -- is that pairs **won't always be the same distance apart**. Respectfully, I think we're going to have to disagree on this one. – TooTone May 22 '13 at 07:42
7

If I multiply the heuristic with a constant, it's no longer admissible

It can be if you get rid of some corner cases. Let d be the Manhattan distance, and observe that the die can never have its 1 face up in two subsequent steps of the path. It follows that, if you're not already at the goal:

  • the first step has cost at least 1;
  • if 1 is face up, it's at least 2 (and the same holds for 6);
  • the rest of the path is at least as expensive as a series of 1-2 alternations, which has cost 1.5 × (d - 1).

So an admissible heuristic is

if d == 0 then
    h := 0
else if die == 1 or die == 6 then
    h := 2 + 1.5 × (d - 1)
else
    h := 1 + 1.5 × (d - 1)
Fred Foo
  • 355,277
  • 75
  • 744
  • 836
6

Here's my algorithm applied to Paul's example of a 300x300 grid, starting from (23,25) and ending at (282, 199). It finds the minimum path and sum (1515, which is 2 points less than Paul's result of 1517) in 0.52 seconds. A version with look-up tables instead of calculating the small sections took 0.13 seconds.

Haskell code:

import Data.List (minimumBy)
import Data.Ord (comparing)
import Control.Monad (guard)

rollDie die@[left,right,top,bottom,front,back] move
  | move == "U" = [left,right,front,back,bottom,top]
  | move == "D" = [left,right,back,front,top,bottom]
  | move == "L" = [top,bottom,right,left,front,back]
  | move == "R" = [bottom,top,left,right,front,back]

dieTop die = die!!2

--dieStartingOrientation = [4,3,1,6,2,5] --left,right,top,bottom,front,back

rows = 300
columns = 300

paths (startRow,startColumn) (endRow,endColumn) dieStartingOrientation = 
  solve (dieTop dieStartingOrientation,[]) [(startRow,startColumn)] dieStartingOrientation where
    leftBorder = max 0 (min startColumn endColumn)
    rightBorder = min columns (max startColumn endColumn)
    topBorder = endRow
    bottomBorder = startRow
    solve result@(cost,moves) ((i,j):pathTail) die =
      if (i,j) == (endRow,endColumn)
         then [(result,die)]
         else do
           ((i',j'),move) <- ((i+1,j),"U"):next
           guard (i' <= topBorder && i' >= bottomBorder && j' <= rightBorder && j' >= leftBorder)
           solve (cost + dieTop (rollDie die move),move:moves) ((i',j'):(i,j):pathTail) (rollDie die move) 
        where next | null pathTail            = [((i,j+1),"R"),((i,j-1),"L")]
                   | head pathTail == (i,j-1) = [((i,j+1),"R")]
                   | head pathTail == (i,j+1) = [((i,j-1),"L")]
                   | otherwise                = [((i,j+1),"R"),((i,j-1),"L")]

--300x300 grid starting at (23, 25) and ending at (282,199)

applicationNum = 
  let (r,c) = (282-22, 199-24)
      numRowReductions = floor (r/4) - 1
      numColumnReductions = floor (c/4) - 1
      minimalR = r - 4 * fromInteger numRowReductions
      minimalC = c - 4 * fromInteger numColumnReductions
  in (fst . fst . minimumBy (comparing fst) $ paths (1,1) (minimalR,minimalC) [4,3,1,6,2,5])
     + 14*numRowReductions + 14*numColumnReductions

applicationPath = [firstLeg] ++ secondLeg ++ thirdLeg
               ++ [((0,["R"]),[])] ++ [minimumBy (comparing fst) $ paths (1,1) (2,4) die2] 
    where
      (r,c) = (282-22, 199-24) --(260,175)
      numRowReductions = floor (r/4) - 1
      numColumnReductions = floor (c/4) - 1
      minimalR = r - 4 * fromInteger numRowReductions
      minimalC = c - 4 * fromInteger numColumnReductions
      firstLeg = minimumBy (comparing fst) $ paths (1,1) (minimalR,minimalC) [4,3,1,6,2,5]
      die0 = snd firstLeg
      secondLeg = tail . foldr mfs0 [((0,["R"]),die0)] $ [1..numColumnReductions - 1]
      die1 = snd . last $ secondLeg
      thirdLeg = tail . foldr mfs1  [((0,[]),die1)] $ [1..numRowReductions - 3 * div (numColumnReductions - 1) 4 - 1]
      die2 = rollDie (snd . last $ thirdLeg) "R"
      mfs0 a b = b ++ [((0,["R"]),[])] ++ [minimumBy (comparing fst) $ paths (1,1) (4,4) (rollDie (snd . last $ b) "R")]
      mfs1 a b = b ++ [((0,["U"]),[])] ++ [minimumBy (comparing fst) $ paths (1,1) (4,1) (rollDie (snd . last $ b) "U")]

Output:

*Main> applicationNum
1515

*Main> applicationPath
[((31,["R","R","R","R","U","U","R","U","R"]),[5,2,1,6,4,3])
,((0,["R"]),[]),((25,["R","R","R","U","U","U"]),[3,4,1,6,5,2])
,((0,["R"]),[]),((24,["R","U","R","R","U","U"]),[5,2,1,6,4,3])
................((17,["R","R","R","U"]),[5,2,1,6,4,3])]
(0.52 secs, 32093988 bytes)

List of "R" and "U":

*Main> let listRL = concatMap (\((a,b),c) -> b) applicationPath
*Main> listRL
["R","R","R","R","U","U","R","U","R","R","R","R","R","U","U","U","R","R","U","R"
..."U","R","R","R","R","U"]

Sum of the path using the starting die and list of "R" and "U":

*Main> let sumPath path = foldr (\move (cost,die) -> (cost + dieTop (rollDie die move), rollDie die move)) (1,[4,3,1,6,2,5]) path
*Main> sumPath listRL
(1515,[5,2,1,6,4,3])

Calculation of (r,c) from (1,1) using the list of "R" and "U" (since we start at (1,1,), (r,c) gets adjusted to (282-22, 199-24):

*Main> let rc path = foldr (\move (r,c) -> if move == "R" then (r,c+1) else (r+1,c)) (1,1) path
*Main> rc listRL 
(260,175)


Algorithm/Solution

Continuing the research below, it seems that the minimal face-sum path (MFS) 
can be reduced, mod 4, by either rows or columns like so:

MFS (1,1) (r,c) == MFS (1,1) (r-4,c) + 14, for r > 7
                == MFS (1,1) (r,c-4) + 14, for c > 7

This makes finding the number for the minimal path straightforward:

MFS (1,1) (r,c) = 
  let numRowReductions = floor (r/4) - 1
      numColumnReductions = floor (c/4) - 1
      minimalR = r - 4 * numRowReductions
      minimalC = c - 4 * numColumnReductions
  in MFS (1,1) (minimalR,minimalC) + 14*numRowReductions + 14*numColumnReductions

minimalR and minimalC are always less than eight, which means we can easily
pre-calculate the minimal-face-sums for these and use that table to quickly
output the overall solution.

But how do we find the path?
From my testing, it seems to work out similarly:

MFS (1,1) (1,anything) = trivial
MFS (1,1) (anything,1) = trivial
MFS (1,1) (r,c), for r,c < 5 = calculate solution in your favorite way
MFS (1,1) (r,c), for either or both r,c > 4 = 
  MFS (1,1) (minimalR,minimalC) -> roll -> 
  MFS (1,1) (min 4 r-1, min 4 c-1) -> roll -> 
  ...sections must be arranged so the last one includes 
     four rotations for one axis and at least one for the other.
     keeping one row or column the same till the end seems to work.
     (For Paul's example above, after the initial MFS box, I moved in 
     fours along the x-axis, rolling 4x4 boxes to the right, which 
     means the y-axis advanced in threes and then a section in fours 
     going up, until the last box of 2x4. I suspect, but haven't checked, 
     that the sections must divide at least one axis only in fours for 
     this to work)...
  MFS (1,1) (either (if r > 4 then 4 else min 2 r, 4) 
             or     (4, if c > 4 then 4 else min 2 c))
  => (r,c) is now reached

For example,

  MFS (1,1) (5,13) = MFS (1,1) (1,5) -> roll right -> 
                     MFS (1,1) (1,4) -> roll right -> MFS (1,1) (5,4)

  MFS (1,1) (2,13) = MFS (1,1) (1,5) -> roll right -> 
                     MFS (1,1) (1,4) -> roll right -> MFS (1,1) (2,4)


Properties of Dice Observed in Empirical Testing

For target points farther than (1,1) to (2,3), for example (1,1) to (3,4)
or (1,1) to (4,6), the minimum path top-face-sum (MFS) is equal if you 
reverse the target (r,c). In other words: 

1. MFS (1,1) (r,c) == MFS (1,1) (c,r), for r,c > 2

Not only that.

2. MFS (1,1) (r,c) == MFS (1,1) (r',c'), for r,c,r',c' > 2 and r + c == r' + c'
   e.g., MFS (1,1) (4,5) == MFS (1,1) (5,4) == MFS (1,1) (3,6) == MFS (1,1) (6,3)

But here's were it gets interesting:

The MFS for any target box (meaning from startPoint to endPoint) that
can be reduced to a symmetrical combination of (r,c) (r,c) or (r,c) (c,r), for 
r,c > 2, can be expressed as the sum of the MFS of the two smaller symmetrical 
parts, if the die-roll (the change in orientation) between the two parts is 
accounted for. In other words, if this is true, we can breakdown the calculation
into smaller parts, which is much much faster.

For example: 
 Target-box (1,1) to (7,6) can be expressed as: 
 (1,1) (4,3) -> roll right -> (1,1) (4,3) with a different starting orientation

 Check it, baby: 
 MFS  (1,1) (7,6) = MFS (1,1) (4,3) + MFS (1,1) (4,3) 
 (when accounting for the change in starting orientation, rolling right in 
 between)

 Eq. 2., implies that MFS (1,1) to (7,6) == MFS (1,1) (5,8)
 and MFS (1,1) (5,8) can be expressed as (1,1) (3,4) -> roll right -> (1,1) (3,4)

 Check it again: 
 MFS (1,1) (7,6) = MFS (1,1) (5,8) = MFS (1,1) (3,4) + MFS (1,1) (3,4)
 (when accounting for the change in starting orientation, rolling right in
 between)

Not only that.

The symmetrical parts can apparently be combined in any way:

3. MFS (1,1) (r,c) -> roll-right -> MFS (1,1) (r,c) equals
   MFS (1,1) (r,c) -> roll-right -> MFS (1,1) (c,r) equals
   MFS (1,1) (r,c) -> roll-up -> MFS (1,1) (r,c) equals
   MFS (1,1) (r,c) -> roll-up -> MFS (1,1) (c,r) equals
   MFS (1,1) (2*r-1, 2*c) equals
   MFS (1,1) (2*r, 2*c-1), for r,c > 2
גלעד ברקן
  • 23,602
  • 3
  • 25
  • 61
  • +1 I also arrived at the value 14 by noting that there are only three unique sequences of four consecutive faces when going in a straight line (`1-2-4-6`, `1-2-5-6`, and `2-3-4-5`), which all add up to 14. This seemed relevant since, by looking at the paths my algorithm found, it tended to travel in lines of length at least four. The heuristic you describe here is very similar to what I ended up with. I also started thinking it should be possible to do this without using a search algorithm at all. – Paul Manta May 19 '13 at 16:58
  • @PaulManta I think groovy and I found something very similar, which is that you pay 14 for every 4 moves, or 3.5 on average for every move. This is essentially down to the fact that opposite sides of the die add up to 7. If you keep going in the same direction you inevitably use opposite sides of the die every other move. Even if you change directions (as explained in my post in more detail), it's still guaranteed that you use opposite sides of the die every other move _counting moves only in a single direction_. So I agree that a search shouldn't really be needed at all. – TooTone May 19 '13 at 17:04
  • @PaulManta cool.. I hope you understand that the method I suggest for the actual path is not a search for `(1,1)` to `(r,c)` but a much faster search (tables would be fastest probably): `MFS (1,1) (minimalR,minimalC) + sum of remaining sections MFS (1,1) (min 4 r-1, min 4 c-1) until (r,c) is reached, considering the rolls in between`. – גלעד ברקן May 19 '13 at 17:11
  • 1
    @PaulManta: The fact that four moves in a straight line will always sum to 14 is trivial (since opposite sides of the die sum to 7); the issue is that it's possible to get less by moving in zigzag's (or possibly even moving away from the finish, I'm not sure on that yet). I haven't yet finished reading this answer (or TooTone's answer) to understand how they work around that issue. – BlueRaja - Danny Pflughoeft May 20 '13 at 19:15
  • @TooTone: *"it's still guaranteed that you use opposite sides of the die every other move counting moves only in a single direction"* - that statement is false. Moving along a perpendicular axis doesn't change the first move on this axis, but it does change the second. – BlueRaja - Danny Pflughoeft May 20 '13 at 19:18
  • 1
    @BlueRaja-DannyPflughoeft Just to double check, what I meant by my statement is that with face=`f1`, if you move the die up, say, then move the die left or right any number of times, then move the die up again, with the die ending up on `f2`, then `f1+f2=7`. E.g., with the die in the configuration in the OP, `f1=1`. If you move up, then the face=`2`. Then let's say you move right 3 times, to get face `4`, then `5`, then `3`. Then you move up again, and `f2=6`, `f1+f2=1+6=7`. The key point in this example is that no matter how many times you move left or right, `6` is invariant. – TooTone May 20 '13 at 20:06
  • @TooTone but you can move up, right, down, right, up, up and avoid the six. – flup May 21 '13 at 23:06
  • 1
    @flup yes you can, but you've got an up and a down movement in there and going back on yourself is going to cost you more than you gain (I did state this assumption in my post and I think it's reasonable but demonstrating the rationale for the 3.5 per move took a lot of time and I have run out of time to go into a lot more detail on this, interesting as it is!). – TooTone May 21 '13 at 23:24
  • Unfortunately I don't have time to go into this in the detail it deserves, but there are some good ideas here. One idea I had, which is related to your idea of breaking paths down into components, was that (i) there are only 24 distinct orientations for the die, so any sequence of 25 or more moves must contain some orientation more than once; and (ii) if any orientation appears in some path 3 or more times, say at move indices i < j < k, then it must be that the move sequences [i..j-1] and [j..k-1] can be swapped without altering the total length. It may be possible to exploit this for search. – j_random_hacker May 24 '13 at 10:03
  • @j_random_hacker I'm not sure if you're following this..I don't understand why you see a reason for search - the minimal path sum is a given, I tested it empirically. You only need to calculate (or pre-calculate, which I also did in a straightforward manner) the minumal sum/path for `minimalR` and `minimalC` - that's always lower than (1,1) to (8,8). After that the minimal sum/path is a given, increased by 14 per four columns or four rows. – גלעד ברקן May 24 '13 at 13:16
0

Idea:

If you have to move in a straight line, the best you can do is to end your moves with 1 and 2, for all other moves you can't do better than 3.5*distance.

Heuristic:

With ManhattanDistance = x + y the following heuristic could be used:

Heuristic = xH + yH;

where

xH = calculateStraightLineHeuristic(x)
yH = calculateStraightLineHeuristic(y)

and the function calculateStraightLineHeuristic(z) is defined as follows:

calculateStraightLineHeuristic(z)
    if (z = 1)
        return zH = 1
    elseif (z = 2)
        return zH = 2+1
    else
        return zH = (z-2)*3.5+2+1
end
user1884905
  • 3,137
  • 1
  • 22
  • 22