6

これは長い投稿であり、ただの楽しみですので、時間があまりない場合は、より重要な質問で人々を助けてください:)

最近xboxでリリースされた「Tower Bloxx」というゲームがあります。ゲームの一部は、最も価値のあるタワーの数を最大化するために、異なる色のタワーを最適な方法でフィールドに配置することです。最も効率的なタワーの配置を決定するアルゴリズムを作成しましたが、あまり効率的ではなく、考えられるすべての組み合わせを総当たりするだけです。4 つのタワー タイプの 4x4 フィールドの場合、約 1 時間で解決します。5 つのタワー タイプでは約 40 時間かかります。これは長すぎます。

ルールは次のとおり です。フィールドに配置できるタワーは 5 種類あります。フィールドにはいくつかのタイプがあります。最も簡単なものは 4x4 マトリックスで、他のフィールドには作成できない「空白」があります。あなたの目的は、できるだけ多くの最も価値のあるタワーをフィールドに配置して、フィールド上のタワーの合計価値を最大化することです (すべてのタワーが一度に建設されたと仮定して、ターンはありません)。

塔の種類(価値の低いものから順に):

  • 青 - どこにでも配置可能、値 = 10
  • 赤 - 青以外にのみ配置可能、値 = 20
  • 緑 - 赤と青のほかに配置、値 = 30
  • 黄 - 緑、赤、青のほか、値 = 40
  • 白 - 黄、緑、赤、青のほか、値 = 100

つまり、たとえば、緑の塔には、北、南、西、または東の隣接セルに少なくとも 1 つの赤と 1 つの青の塔が必要です (対角線はカウントされません)。白い塔は他のすべての色で囲まれている必要があります。

4x4 フィールド上の 4 つのタワーのアルゴリズムは次のとおりです。

  1. 組み合わせの総数 = 4^16
  2. [1..4^16] をループし、タワーの配置をエンコードするためにすべての数値を base4 文字列に変換します。したがって、4^16 = "3333 3333 3333 3333" がタワーのタイプ (0=青、...、 3=黄)
  3. タワー配置文字列を行列に変換します。
  4. マトリックス内のすべてのタワーについて、隣接するタワーをチェックし、いずれかの要件が満たされない場合、この組み合わせ全体が失敗します。
  5. すべての正しい組み合わせを配列に入れ、この配列を文字列として辞書順に並べ替えて、可能な限り最良の組み合わせを見つけます (最初に文字列内の文字を並べ替える必要があります)。

私が思いついた唯一の最適化は、最も価値のある塔を含まない組み合わせをスキップすることです. 一部の処理をスキップしますが、4^16 の組み合わせすべてをループします。

これをどのように改善できるか考えていますか?コード サンプルは、Java または php の場合に役立ちます。

- - - -アップデート - - - -

さらに違法な状態を追加した後 (黄色は隅に建設できず、白は隅と端に建設できず、フィールドには各タイプの塔が少なくとも 1 つ含まれている必要があります)、4x4 フィールドに建設できる可能性があるのは 1 つの白い塔のみであることに気付きました。 Java コードの最適化により、合計時間が 40 時間から 16 時間に短縮されました。スレッド化すると 10 時間に短縮される可能性がありますが、それはおそらくブルート フォースの限界です。

4

7 に答える 7

5

私はこの質問に興味をそそられ、Haskell を独学で学んでいるので、Haskell でソリューションを実装しようと決心しました。

枝縛りも考えたのですが、解を束ねる良い方法が思いつかなかったので、ルールに違反している板を捨てるなどの剪定を行いました。

私のアルゴリズムは、「空の」ボードから始めることで機能します。タワーの可能な各色を最初の空のスロットに配置し、それぞれの場合 (各色) で再帰的に自分自身を呼び出します。再帰呼び出しは、2 番目のスロットで各色を試行し、ボードがいっぱいになるまで再帰します。

各タワーが配置されると、配置されたばかりのタワーとそのすべての隣人をチェックして、ルールに従っていることを確認し、空の隣人をワイルド カードとして扱います。したがって、白い塔に空の隣人が 4 人いる場合、それは有効であると考えます。配置が無効な場合、その配置を再帰的に実行せず、その下にある可能性のツリー全体を効果的に刈り込みます。

コードの書き方では、考えられるすべてのソリューションのリストを生成し、リストを調べて最適なソリューションを見つけます。実際には、Haskell の遅延評価のおかげで、リスト要素は検索機能が必要とするときに生成され、再度参照されることはないため、すぐにガベージ コレクションに使用できるようになるため、5x5 ボードでもメモリ使用量はかなり少なくなります。 (2MB)。

パフォーマンスはかなり良いです。私の 2.1 GHz ラップトップでは、プログラムのコンパイル済みバージョンは、1 つのコアを使用して 4x4 のケースを最大 50 秒で解決します。現在、5x5 の例を実行して、どれくらい時間がかかるかを確認しています。関数型コードは並列化が非常に簡単なので、並列処理も試してみます。複数のコアだけでなく、複数のマシンにも作業を分散させる並列化された Haskell コンパイラがあり、これは非常に並列化可能な問題です。

これまでの私のコードは次のとおりです。Java または PHP を指定したことは理解していますが、Haskell はまったく異なります。試してみたい場合は、一番下の変数「bnd」の定義を変更して、ボードのサイズを設定できます。((1,1),(x, y)) に設定するだけです。ここで、x と y はそれぞれ列と行の数です。

import Array
import Data.List

-- Enumeration of Tower types.  "Empty" isn't really a tower color,
-- but it allows boards to have empty cells
data Tower = Empty | Blue | Red | Green | Yellow | White
             deriving(Eq, Ord, Enum, Show)

type Location = (Int, Int)
type Board = Array Location Tower

-- towerScore omputes the score of a single tower
towerScore :: Tower -> Int
towerScore White = 100
towerScore t     = (fromEnum t) * 10

-- towerUpper computes the upper bound for a single tower
towerUpper :: Tower -> Int
towerUpper Empty = 100
towerUpper t = towerScore t

-- boardScore computes the score of a board
boardScore :: Board -> Int
boardScore b = sum [ towerScore (b!loc) | loc <- range (bounds b) ]

-- boardUpper computes the upper bound of the score of a board
boardUpper :: Board -> Int
boardUpper b = sum [ bestScore loc | loc <- range (bounds b) ]
    where
      bestScore l | tower == Empty = 
                      towerScore (head [ t | t <- colors, canPlace b l t ])
                  | otherwise = towerScore tower
                  where 
                    tower = b!l
                    colors = reverse (enumFromTo Empty White)

-- Compute the neighbor locations of the specified location
neighborLoc :: ((Int,Int),(Int,Int)) -> (Int,Int) -> [(Int,Int)]
neighborLoc bounds (col, row) = filter valid neighborLoc'
    where
      valid loc = inRange bounds loc
      neighborLoc' = [(col-1,row),(col+1,row),(col,row-1),(col,row+1)]

-- Array to store all of the neighbors of each location, so we don't
-- have to recalculate them repeatedly.
neighborArr = array bnd [(loc, neighborLoc bnd loc) | loc <- range bnd]

-- Get the contents of neighboring cells
neighborTowers :: Board -> Location -> [Tower]
neighborTowers board loc = [ board!l | l <- (neighborArr!loc) ]

-- The tower placement rule.  Yields a list of tower colors that must
-- be adjacent to a tower of the specified color.
requiredTowers :: Tower -> [Tower]
requiredTowers Empty  = []
requiredTowers Blue   = []
requiredTowers Red    = [Blue]
requiredTowers Green  = [Red, Blue]
requiredTowers Yellow = [Green, Red, Blue]
requiredTowers White  = [Yellow, Green, Red, Blue]

-- cellValid determines if a cell satisfies the rule.
cellValid :: Board -> Location -> Bool
cellValid board loc = null required ||
                      null needed   ||
                      (length needed <= length empties)
    where
      neighbors = neighborTowers board loc
      required  = requiredTowers (board!loc)
      needed    = required \\ neighbors
      empties   = filter (==Empty) neighbors

-- canPlace determines if 'tower' can be placed in 'cell' without
-- violating the rule.
canPlace :: Board -> Location -> Tower -> Bool
canPlace board loc tower =
    let b' = board // [(loc,tower)]
    in cellValid b' loc && and [ cellValid b' l | l <- neighborArr!loc ]

-- Generate a board full of empty cells       
cleanBoard :: Array Location Tower
cleanBoard = listArray bnd (replicate 80 Empty)

-- The heart of the algorithm, this function takes a partial board
-- (and a list of empty locations, just to avoid having to search for
-- them) and a score and returns the best board obtainable by filling
-- in the partial board
solutions :: Board -> [Location] -> Int -> Board
solutions b empties best | null empties = b
solutions b empties best = 
    fst (foldl' f (cleanBoard, best) [ b // [(l,t)] | t <- colors, canPlace b l t ])
    where
      f :: (Board, Int) -> Board -> (Board, Int)
      f (b1, best) b2  | boardUpper b2 <= best = (b1, best)
                       | otherwise = if newScore > lstScore
                                     then (new, max newScore best)
                                     else (b1, best)
                       where
                         lstScore = boardScore b1
                         new      = solutions b2 e' best
                         newScore = boardScore new
      l = head empties
      e' = tail empties

colors = reverse (enumFromTo Blue White)

-- showBoard converts a board to a printable string representation
showBoard :: Board -> String
showBoard board = unlines [ printRow row | row <- [minrow..maxrow] ]
    where
      ((mincol, minrow), (maxcol, maxrow)) = bounds board
      printRow row = unwords [ printCell col row | col <- [mincol..maxcol] ]
      printCell col row = take 1 (show (board!(col,row)))

-- Set 'bnd' to the size of the desired board.                          
bnd = ((1,1),(4,4))

-- Main function generates the solutions, finds the best and prints
-- it out, along with its score
main = do putStrLn (showBoard best); putStrLn (show (boardScore best))
       where
         s = solutions cleanBoard (range (bounds cleanBoard)) 0
         best = s

また、これが私の最初の重要な Haskell プログラムであることを覚えておいてください。もっとエレガントで簡潔にできると確信しています。

更新: 5 色で 5x5 を実行するのは依然として非常に時間がかかるため (12 時間待っても完了していませんでした)、バウンディングを使用して検索ツリーをさらに剪定する方法をもう一度調べました。

私の最初のアプローチは、すべての空のセルが白い塔で満たされていると仮定して、部分的に満たされたボードの上限を推定することでした。次に、'solution' 関数を変更して、見られる最高のスコアを追跡し、上限がその最高のスコアよりも小さいボードを無視するようにしました。

これにより、4x4x5 ボードの時間が 23 秒から 15 秒に短縮されました。さらに改善するために、既存の空でないセルの内容と一致して、各 Empty が可能な限り最高のタワーで満たされていると仮定するように上限関数を変更しました。これは大いに役立ち、4x4x5 の時間が 2 秒に短縮されました。

5x5x5 で実行すると 2600 秒かかり、次のボードが得られました。

G B G R B
R B W Y G
Y G R B R
B W Y G Y
G R B R B

スコアは 730 です。

別の変更を加えて、1 つだけではなく、スコアが最大のボードをすべて検出するようにすることもできます。

于 2009-10-29T07:27:15.023 に答える
4

A* を実行したくない場合は、分岐限定アプローチを使用してください。値関数が適切に定義されているため、問題は比較的簡単にコーディングできるはずです。検索スペースの巨大なセクションを比較的簡単に取り除くことができるはずだと思います。ただし、検索スペースがかなり大きいため、まだ時間がかかる場合があります。見つける唯一の方法:)

ウィキの記事は世界最高ではありません。Google は、アプローチをさらに説明するために、たくさんの素晴らしい例やツリーなどを見つけることができます。

于 2009-10-26T18:28:29.020 に答える
3

A* 実装のための優れたヒューリスティックを考え出すのは難しいと思うので、分岐限定アルゴリズムを使用することになると思います (ただし、それは私の直感にすぎません)。

分岐限定実装の擬似コードは次のとおりです。

board = initial board with nothing on it, probably a 2D array

bestBoard = {}

function findBest(board)
  if no more pieces can be added to board then
     if score(board) > score(bestBoard) then
       bestBoard = board
     return
  else
    for each piece P we can legally add to board
      newBoard = board with piece P added
      //loose upper bound, could be improved
      if score(newBoard) + 100*number of blanks in newBoard > score(bestBoard)
        findBestHelper(newBoard)

アイデアは、考えられるすべてのボードを順番に検索するというものですが、これまでに見つかった最高のボードを追跡します (これが限界です)。次に、これまでのところ最高のボードよりも優れているとわかっている部分的なボードを見つけた場合、その部分的なボードで作業するのをやめます。検索ツリーのそのブランチをトリムします。

上記のコードでは、すべての空白が白い部分で埋められると仮定してチェックを行っています。これ以上のことはできないからです。少し考えれば、それよりも厳しい境界を思いつくことができると確信しています。

最適化を試みることができるもう 1 つの場所は、for-each ループの順序です。正しい順番でピースを試してみたい。つまり、最初に見つかった解が最良の解、または少なくともスコアが非常に高い解であることが最適です。

于 2009-10-26T19:18:37.657 に答える
3

ブルート フォース法を改善する簡単な方法の 1 つは、合法的な状態のみを調査することです。たとえば、考えられるすべての状態を試す場合、右上隅が白い塔である多くの状態をテストすることになります。これらの州はすべて違法になります。これらの状態をすべて生成してテストするのは意味がありません。そのため、一度に 1 ブロックずつ状態を生成し、実際に有効な可能性のある状態にあるときにのみ、ツリーの奥に進みます。これにより、検索ツリーが何桁も削減されます。

できることは他にもあるかもしれませんが、これは (できれば) 理解しやすい現在のソリューションの改善です。

于 2009-10-26T18:32:28.793 に答える
1

白いタワーから始めて、要件に基づいてその周りに一連のタワーを構築し、連結タイルとして機能できる最小の色付きの形状セットを見つけようとするのが良いアプローチのようです。

于 2009-10-26T18:06:16.773 に答える
1

アルゴリズムの面で多くの良いアドバイスをいただいたので、追加することはあまりありません。しかし、言語として Java を想定すると、パフォーマンスを改善するためのかなり明白な提案がいくつかあります。

  • その 4^16 ループ内でオブジェクトをインスタンス化していないことを確認してください。新しいオブジェクトを作成するよりも、JVM が既存のオブジェクトを再初期化する方がはるかに安価です。プリミティブの配列を使用する方がさらに安価です。:)
  • できることなら、コレクション クラスから離れてください。おそらく必要のない多くのオーバーヘッドが追加されます。
  • 文字列を連結していないことを確認してください。StringBuilder を使用します。
  • 最後に、すべてを C で書き直すことを検討してください。
于 2009-10-26T21:14:06.487 に答える
1

整数の未知数を持つ線形計画法を提唱したかったのですが、バイナリの場合でも NP 困難であることがわかりました。ただし、多くの有効なソリューションがあり、単に最適なソリューションが必要な場合は、問題を最適化することで大きな成功を収めることができます。

この種の問題に対する線形計画法は、本質的に多くの変数 (たとえば、セル (M, N) に存在する赤い塔の数) と変数間の関係 (たとえば、セル内の白い塔の数) を持つことになります。セル (M, N) は、隣接するすべてのセルの中で、白色以外の色のタワーの数が最も小さいタワーの数以下でなければなりません)。線形プログラムを作成するのはちょっと面倒ですが、数秒で実行されるソリューションが必要な場合は、おそらく最善の策です。

于 2009-10-26T18:28:06.853 に答える