A * допустимая эвристика для прокатки штампа по сетке


Мне нужна помощь в поиске хорошей эвристики для следующей задачи:

Вам дается R-мимо-C сетка и шестигранная плашка. Пусть start и еще end будьте двумя различными ячейками в этой сетке. Найдите путь от start до end такой, что сумма лиц кубика, смотрящих вверх, когда кубик поворачивается вдоль пути, равна минимальный.

Начальная ориентация штампа следующая ("2" обращена Юг):

Введите описание изображения здесь

Способ, которым я смоделировал эту проблему, заключается в рассмотрении значения грани матрицы как стоимости ребра в графе. Вершины графа имеют вид (row, col, die) (то есть положение в сетке и текущее состояние/ориентация матрицы). Причина, по которой вершина не является Просто (row, col), заключается в том, что вы можете оказаться в одной и той же ячейке с несколькими конфигурациями/ориентациями матрицы.

Я использовал A*, чтобы найти решение проблемы; ответы даны правильно, но недостаточно эффективно. Я решил, что проблема заключается в эвристике, которую я использую. В настоящее время я использую манхэттенское расстояние, которое, очевидно, допустимо. Если я умножу эвристику на константу, она больше не будет приемлемой: она работает намного быстрее, но не всегда находит правильный ответ.

Мне нужна помощь в поиске лучшей эвристики, чем манхэттенское расстояние.
5 40

5 ответов:

Вот мой алгоритм, примененный к примеру пола с сеткой 300x300, начиная с (23,25) и заканчивая (282,199). Он находит минимальный путь и сумму (1515, что на 2 пункта меньше результата Павла 1517) за 0,52 секунды. Версия с таблицами поиска вместо вычисления небольших секций заняла 0,13 секунды.

Код Хаскелла:

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")]

Вывод:

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

Список букв " R " и "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"]

Сумма пути с использованием начального умереть и список "R" и "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])

Вычисление (r,c) из (1,1) с использованием списка "R" и " U " (так как мы начинаем с (1,1,), (r,c) настраивается на (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)


Алгоритм / Решение

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.
Но как мы найдем этот путь?
из моего тестирования, похоже, это работает аналогично:
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

Например,

  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)


свойства костей, наблюдаемые при эмпирическом тестировании

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

Не только тот.

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)

Но вот что интересно:

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)

Не только это.

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

Хорошо, я добавлю свой комментарий здесь, так как он более оптимален, чем текущий самый высокооплачиваемый ответ @larsmans-но, я убежден, что должно быть что - то лучше (отсюда и щедрость).


Если я умножу эвристику на константу, это больше не допустимо

Лучшее, что я могу придумать, это (manhattenDistance/3)*6 + (manhattenDistance%3), где / - целочисленное деление, а % - mod. Это работает, потому что в любых 3 ходах без обратного отслеживания все три цифры будут уникальными, поэтому наименьшая сумма мы могли бы это сделать. 1+2+3 = 6 ( %3 просто добавляет любые дополнительные, не кратные трем ходам) .


[Edit] как @GrantS указал в комментариях выше, моя эвристика может быть улучшена очень немного, добавив дополнительный 1 Когда manhattenDistance%3 == 2. Это легко сделать без условного: (manhattenDistance/3)*6 + (manhattenDistance%3)*3/2

основное редактирование 3: доказательство того, что оптимальная допустимая эвристика должна основываться на 3.5m

Средняя стоимость путешествия по доске должна приближаться к 3.5m в долгосрочной перспективе, где m - расстояние Манхэттена. Поэтому наилучшей допустимой эвристикой должна быть 3.5m плюс-минус некоторая малая константа. Причина этого в том, что всякий раз, когда вы двигаетесь в направлении, скажем, x от лица x1, следующее движение в том же направлении, к лицу x2 должен удовлетворять x1 + x2 = 7. Это происходит потому, чтолюбые движения в перпендикулярном направлении оставляют ориентацию грани x2 такой же . Подумайте о вращении кубика слева направо - передняя и задняя грани остаются одинаковыми независимо от того, сколько оборотов вы делаете. И наоборот, если вы поворачиваете кубик спереди назад, левая и правая грани остаются одинаковыми.

Это проще всего увидеть на некоторых примерах (все они начинаются в конфигурации, изображенной в вопросе)

   6
2453
1

Здесь вы можете увидеть что мы начинаем с y1=1, и сколько бы раз мы ни двигались в направлении x после этого, следующее движение в направлении y должно быть y2=6, так что y1+y2=7. (Также в направлении x существует простое сопряжение 2+5 = 7 и 4+3 = 7).

Другой пример -

  35
 26
14
В этом примере мы начинаем с x1=1, и сколько бы раз мы ни двигались в направлении y, следующее движение в направлении x должно быть x2=6. (Кроме того, мы видим пары 4+3=7 в направлении y, 2+5=7 в направлении Y. х-направление. И мы знаем, что в этом случае следующее движение в направлении x должно быть 4, а следующее движение в направлении y должно быть 1.)

Все это предполагает, что никогда не стоит отступать, но, надеюсь, это можно считать прочитанным.

Оригинальное сообщение ниже просто заполняет некоторые детали того, как оценка 3.5m должна быть скорректирована с учетом способности к ее преодолению в краткосрочной перспективе.

В качестве примечания, как я только что прокомментировал ОП, A* поиск может вообще не потребоваться. Должно быть, имеет смысл просто выбрать путь, состоящий из 4-х длинных горизонтальных частей и 4-х длинных вертикальных частей, скажем, которые являются оптимальными. А затем составьте остаток с помощью поиска или таблицы подстановки, основанной на ориентации и смещении x-Y. (Но вопрос требует допустимой эвристики, поэтому я оставлю свой ответ.)

основная правка 2: обобщить исходную эмпирическую работу с учетом комментариев ниже

В в долгосрочной перспективе, как было объяснено выше, ваши средние затраты на один ход составляют 3,5. Это также можно увидеть эмпирически в исследовании данных ниже.

Это дает наивную оценку 3.5m, где m - расстояние Манхэттена. Однако это завышенная оценка, потому что в краткосрочной перспективе это возможно сделать лучше, чем в среднем. Хорошая гипотеза для этого состоит в том, чтобы исследовать, как мы можем избежать использования любых граней больше 3.
  • если мы начнем с лица 1, мы может использовать лица 2 и 3 на наших первых 2 ходах, идя 2 движется лучше, чем предсказывает 3.5m.
  • если мы начнем с лица 2, мы можем использовать лица 1 и 3 на наших первых 2 ходах, идя 3 движется лучше, чем предсказывает 3.5m.
  • если мы начнем с лица 3, мы можем использовать лица 1 и 2 на наших первых 2 ходах, идя 4 движется лучше, чем предсказывает 3.5m.
  • если мы начнем с лица 4,5 или 6 , мы можем использовать лица 1, 2 и 3 на нашем первом 3 хода, идет 4.5 движется лучше, чем предсказывает 3.5m.
Эта гипотеза может быть подтверждена эмпирически, просто запустив сценарий ниже для каждой стартовой возможности кубика, как это предлагает BlueRaja-Danny Pflughoeft. Таким образом, простой допустимой статистикой является 3.5m - k, где k = max(f+1, 4.5) и f - начальная грань. Но это немного неуклюже, давая отрицательные числа для малых значений m. Легко написать программную версию, которая учитывает, являетесь ли вы есть только 1 или 2 или 3 хода, чтобы пойти, см. ниже
    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;
    }

Запустив его через пространство поиска с помощью |x|,|y| <= 100, эта функция недооценивает фактическую стоимость от 0 до 6, с медианой 0,5 или 1,5 в зависимости от начальной грани.

Главная правка 1: Оригинальный пост

Моя основная мысль заключалась в том, что было бы неплохо изучить эти данные. Поэтому я попробовал алгоритм Дийкстры , чтобы посмотреть, как выглядит пространство решений. То, что я нашел, это поддерживаю то, что уже было сказано. Некоторый коэффициент, умноженный на манхэттенское расстояние, является подходящим, но может быть некоторое оправдание для более высокого коэффициента, чем 1,5. На это хорошо указывает форма контурного графика стоимости против отклонения от начального положения x Y.

себестоимости на отклонения от начального положения X г

Вот сюжет с проволочной рамкой - честно говоря, это только для глазных конфет.

Введите описание изображения здесь

Что интересно, если вы добавите еще один столбец к вашим данным для Манхэттена расстояние (человек) и регрессия стоимости (v) против расстояния Манхэттена в R, вы получите следующее

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
То есть он говорит вам, что за каждое движение, которое вы делаете по горизонтали или вертикали, ваша стоимость составляет 3,4991861, или v близко к 3,5. Это просто среднее значение от 1 до 6, поэтому моя интуиция подсказывает нам, что в среднем наиболее эффективно использовать все грани кубика одинаково на большом расстоянии. На коротких расстояниях вы можете быть более оптимальными.

I попробовал 3.5man - k в качестве оценки, с k = 2.5. Это, казалось, работало нормально. Когда я вычел фактическую стоимость из этого, я получил -0,5 как самое высокое значение.

> 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 

Однако поиск* должен работать для всех конфигураций, включая те после запуска, где матрица не находится в исходной конфигурации, поэтому константа k не может быть такой низкой, как 2.5 в целом. Он либо должен быть поднят, например до 4, либо зависеть от конфигурации штампа, как это предлагается в другом примере. ответ.

Вполне возможно, что я сделал какую-то ужасную ошибку во всем этом, поэтому я поместил код ниже. Как я уже сказал, Я думаю, что подход к созданию данных и их исследованию является разумным, даже если мои результаты не являются таковыми.

Сначала приведем несколько строк результирующего файла.

17,-100,410

17,-99,406

17,-98,403

17,-97,399

17,-96,396

C# код

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 ниже

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)

Если я умножу эвристику на константу, это больше не допустимо

Это может быть, если вы избавитесь от некоторых угловых случаев. Пусть d - манхэттенское расстояние, и заметьте, что кубик никогда не может иметь свою 1 грань на двух последующих шагах пути. Отсюда следует, что, если вы еще не достигли цели:

  • первый шаг обошелся минимум в 1;
  • если 1 лежит лицевой стороной вверх, то это по крайней мере 2 (и то же самое справедливо для 6);
  • остальная часть пути-это по крайней мере, так же дорого, как серия из 1-2 чередований, которая стоила 1,5 × (d - 1).

Таким образом, допустимой эвристикой является

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)

Идея:

Если вам нужно двигаться по прямой, лучшее, что вы можете сделать, это закончить свои ходы с 1 и 2, для всех остальных ходов вы не можете сделать лучше, чем 3.5*distance.

Эвристика:

С ManhattanDistance = x + y можно использовать следующую эвристику:

Heuristic = xH + yH;

Где

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

И функция calculateStraightLineHeuristic(z) определяется следующим образом:

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