40

次の問題に対する適切なヒューリスティックを見つける手助けが必要です。

グリッドと 6 面ダイスRが与えられます。Cこのグリッド上の 2 つの異なるセルをstartと と します。サイコロがパスに沿って回転しているときに、サイコロが上を向いている面の合計が最小になるように、からまでendのパスを見つけます。startend

サイコロの最初の向きは次のとおりです (「2」は南向きです)。

ここに画像の説明を入力

私がこの問題をモデル化した方法は、サイコロの面の値をグラフのエッジのコストと見なすことです。グラフの頂点の形式は(row, col, die)(つまり、グリッド内の位置とダイの現在の状態/方向) です。頂点が単純ではない理由(row, col)は、ダイの複数の構成/方向で同じセルになる可能性があるためです。

問題の解決策を見つけるために A* を使用しました。与えられた答えは正しいですが、十分に効率的ではありません。問題は使用しているヒューリスティックにあると判断しました。現在、私は明らかに許容できるマンハッタン距離を使用しています。ヒューリスティックに定数を掛けると、許容できなくなります。はるかに高速に実行されますが、常に正しい答えが見つかるとは限りません。

マンハッタン距離よりも優れたヒューリスティックを見つけるための助けが必要です.

4

5 に答える 5

10

@larsmansによる現在の最高投票数の回答よりも最適であるため、ここにコメントを追加しますが、もっと良いものがあるに違いないと確信しています(したがって、賞金)。


ヒューリスティックに定数を掛けると、もはや許容されません

私が思いつくことができる最高のものは(manhattenDistance/3)*6 + (manhattenDistance%3)、 です。ここで、/は整数除算で、%mod です。これが機能するのは、バックトラッキングのない 3 つの移動では、3 桁すべてが一意であるため、可能な最小の合計は 1+2+3 = 6です (%3 は単に余分な非倍数を追加するだけです)。三手)


[編集] @GrantS が上記のコメントで指摘したように、追加の1whenを追加することで、私のヒューリスティックをわずかに改善できますmanhattenDistance%3 == 2。これは、条件なしで簡単に実行できます。 (manhattenDistance/3)*6 + (manhattenDistance%3)*3/2

于 2013-05-16T22:10:14.897 に答える
10

主な編集 3: 最適な許容ヒューリスティックが基づくべきであることの証明3.5m

ボードに沿って移動する平均コストは3.5m、長い目で見ればmマンハッタン距離に近づく必要があります。したがって、許容できる最良のヒューリスティックは、3.5mプラスまたはマイナスの小さな定数でなければなりません。

この理由は、たとえば face から x 方向に移動するとx1、次の同じ方向の face への移動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=64+3=7(また、 y 方向、 x 方向に のペアリングが見られ2+5=7ます。この場合、x 方向の次の動きは でなければならず4、y 方向の次の動きは でなければならないことがわかっています1。 )

これはすべて、バックトラックする価値がないことを前提としていますが、うまくいけば、これは読んだものと見なすことができます.

以下の元の投稿は3.5m、短期間で打ち負かされる可能性を考慮して、の見積もりをどのように調整する必要があるかについて、いくつかの詳細を埋めているだけです.

補足として、OP についてコメントしたばかりなので、A* 検索はまったく必要ない場合があります。たとえば、最適な 4 つの長い水平ピースと 4 つの長い垂直ピースで構成されるパスを単純に選択することは理にかなっているはずです。そして、向きと XY オフセットに基づいて、検索またはルックアップ テーブルで残りを補います。(しかし、質問は許容できるヒューリスティックを求めているので、答えを残しておきます。)

メイン編集 2: 以下のコメントを考慮して、元の経験的作業を要約します。

上で説明したように、長期的には、移動あたりの平均コストは 3.5 です。これは、以下のデータの調査でも経験的に見ることができます。

これにより、マンハッタン距離の単純な推定値が得られ3.5mます。mただし、これは過大評価です。短期的に平均よりもうまくいく可能性があるからです。これに対する良い仮説は、3 より大きい面の使用を避ける方法を探ることです。

  • 面1から開始すると、最初の 2 つの移動で面 2 と 3 を使用でき、予測よりも2移動うまくいきます。3.5m
  • 面2から始めると、最初の 2 つの手で面 1 と 3 を使用でき、予測よりも3 つの手がうまくいきます。3.5m
  • 面3から開始すると 、最初の 2 つの移動で面 1 と 2 を使用でき、予測よりも4 つの移動がうまくいきます。3.5m
  • 4、5、または 6で開始すると、最初の 3 つの移動で面 1、2、および 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 よりも大きい係数を正当化する理由があるかもしれません。これは、最初の xy 位置からの偏差に対するコストの等高線図の形状によって適切に示されます。

初期 xy 位置からの偏差に対するコスト

これはワイヤー フレーム プロットです。正直なところ、これは単なる見栄えです。

ここに画像の説明を入力

興味深いのは、マンハッタン距離 (man) のデータに別の列を追加し、R のマンハッタン距離に対してコスト (v) を回帰すると、次のようになることです。

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 の平均であるため、私の直感では、このデータは平均して、サイコロのすべての面を長距離にわたって均等に使用することが最も効率的であることを示しています。短距離では、より最適になることができます。

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 

ただし、A* 検索は、ダイが元の構成にない開始後の構成を含むすべての構成に対して機能する必要があるため、一般に定数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)
于 2013-05-17T00:06:43.327 に答える
6

(23,25) から始まり (282, 199) で終わる、300x300 グリッドの Paul の例に適用された私のアルゴリズムを次に示します。0.52 秒で最小パスと合計 (1515、ポールの結果の 1517 よりも 2 ポイント少ない) を見つけます。小さなセクションを計算する代わりにルックアップ テーブルを使用したバージョンでは、0.13 秒かかりました。

Haskell コード:

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」と「U」のリストを使用した(r,c)fromの計算( から開始するため、に調整されます。(1,1)(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
于 2013-05-18T22:54:24.360 に答える
0

考え:

直線で移動する必要がある場合は、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
于 2013-05-21T13:47:16.547 に答える