2Dダンジョンで宝を見つける


22

あなたは1階のダンジョンにいます。ロックされたドアで保護されている宝物があります。ドアは、対応するキーを見つけることで開くことができます。あなたの目標は、宝物への最短経路を見つけることです。

入力

入力は、ダンジョンの初期レイアウトを表す2次元のグリッドになります。

###########
#$   #   g#
#    # ####
###G##    #
#    ####C#
#c  @     #
###########

これはあなたです:@
これらは壁です:#
これは宝です:$
ロックされたドアは大文字です:A... Z
各ドアには対応する小文字のキーがあります:a...z

  • 常に1つ@あります$ます。
  • ダンジョンは常に長方形になります。
  • ダンジョンの外縁が壁であることは保証されません。これは有効なダンジョンです:

      $ 
    A## 
    @ a
    
  • 宝が届くとは限りません。ダンジョンの中には、解けないものもあります。
  • キーのないドアがあるかもしれませんし、ドアを開かないキーがあるかもしれません。
  • ドアやキーが重複することはありません。

出力

プログラムは、配列を印刷する必要がありRLUD(または4つの他の別個のシンボル)を表すために最短宝への可能なパス。ここで、RLUD右、左、上、下をそれぞれ表します。複数の最短パスがある場合、プログラムはそれらの1つを印刷するだけです。

  • 壁に移動することはできません。
  • ダンジョンの境界の外側に移動することはできません。
  • 鍵を拾わずにドアに移動することはできません。
  • キーに移動してそれを拾います。
  • すべてのドアを開く必要はありません。

有効な一連の移動で宝物に到達できない場合、プログラムは何も印刷せずに終了する必要があります。(末尾の改行が許可されます。)

得点

これはので、バイト数が最小の答えが勝ちです。

テストケース

各テストケースには、最初の行にダンジョンの高さと幅があり、最後の行に最適な移動数で1つの可能なパスがあります。

1 2
@$
R (1)

3 3
  $
#Z#
@ z
RRLUUR (6)

3 5
c#d#$
 #C#D
@    
UUDDRRUUDDRRUU (14)

7 16
c   # b #  ###@ 
###     #       
  A #####  #### 
d #           e 
  B    ## ##### 
###    C   ##   
       # a DE $ 
RDDDDDDL (8)

16 37
#####################################
#    #ijk #a M   ##m##   #    #  R  #
#    #    #  #           #       ####
###J#### ############# ###    #  P b#
#e                       N  h #  ####
##########  ###########  ######     #
#        #  #    $    #  #    #  ####
#  D     H  #         #  #       Q f#
# EcF    #  #####A#####  ######  ####
#  G     #  #####B#####  #          #
#        K  #####C#####  ############
#        #                          #
########### #         #### ##### ####
#     # p   #         # n    #      #
# d         #    @    #     o#   r  #
#################Z###################
UUULLLLLLDDLLLDLLLLLLRRRRRRRRRUUURRRRRRRRRRRRRRRDDLLRRUULLUUUUUUURRRRRUURRRDRRRLLLLULLLLLDDLLLLUULLLUDLLLLLULLLRRRRRDRRRRRRDDLLLLLLLLLLLLDDDLLLLLLLDURRRRRRRRDDDDRRRRRRUUUUU (172)

次のダンジョンで宝物に到達することはできません。これらのテストケースでは、出力はありません。

1 3
@#$

7 11
#a#j#$#i#f#
# #E#F#c#H#
# #K#D#A#G#
#         #
#C#J# #I#B#
#h#d# #L#g#
#l#e#@#b#k#

10 25
#########################
   # fgh  #  # c B b #  #
 $ #      #  #   #   #  #
   ###### #  ##H###E##  #
   #                    #
   #     #########  ##e##
   Z @   D     y #  #   #
   #     #########  F  C#
   #     G          # Ad#
#########################

次のスニペットを使用して、回答を検証できます。

function run() {var dungeonText = document.getElementById("dungeon").value;var dungeonLines = dungeonText.split("\n");var height = dungeonLines.length;var width = dungeonLines[0].length;var dungeon = new Array(height);for (i = 0; i < dungeon.length; i++) {var dungeonLine = dungeonLines[i];if (dungeonLine.length != width) {return error("The dungeon is not rectangular");} dungeon[i] = dungeonLines[i].split("");} var movesText = document.getElementById("moves").value;var moves = movesText.trim().split("");var moveCount = moves.length;var rowAt, colAt;for (r = 0; r < dungeon.length; r++) {for (c = 0; c < dungeon[r].length; c++) {if (dungeon[r][c] == '@') {rowAt = r;colAt = c;}}} var treasure = false;while (moves.length > 0) {var move = moves[0];var row = rowAt,col = colAt;switch (move) {case 'R':col++;break;case 'L':col--;break;case 'U':row--;break;case 'D':row++;break;default:return print(dungeon, moves, "Invalid move");} if (row < 0 || col < 0 || row >= height || col >= width) {return print(dungeon, moves, "Out of bounds");} var target = dungeon[row][col];if (target.match(/[A-Z#]/)) {return print(dungeon, moves, "Path blocked");} if (target.match(/[a-z]/)) {var door = target.toUpperCase();for (r = 0; r < dungeon.length; r++) {for (c = 0; c < dungeon[r].length; c++) {if (dungeon[r][c] == door) {dungeon[r][c] = ' ';}}}} if (target == '$') {treasure = true;} dungeon[row][col] = '@';dungeon[rowAt][colAt] = '.';rowAt = row;colAt = col;moves.shift();} if (treasure) {print(dungeon, moves, "Got the treasure in " + moveCount + " moves!");} else {print(dungeon, moves, "Failed to reach treasure");}} function error(message) {var result = document.getElementById("result");result.innerHTML = message;} function print(dungeon, moves, message) {var output = message + "\n";for (r = 0; r < dungeon.length; r++) {for (c = 0; c < dungeon[r].length; c++) {output += dungeon[r][c];} output += "\n";} for (i = 0; i < moves.length; i++) {output += moves[i];} var result = document.getElementById("result");result.innerHTML = output;}
Dungeon:<br/><textarea id="dungeon" name="dungeon" rows="20" cols="40"></textarea><br/>Moves:<br/><textarea id="moves" name="moves" cols="40"></textarea><br/><button id="run" name="run" onclick="run();">Start</button><br/><br/>Result:<br/><textarea id="result" name="result" rows="20" cols="40" disabled></textarea><br/>


4
これを前に言うのを忘れました:PPCGへようこそ!これは非常によく書かれた(興味深い)最初の課題です。良くやった。:)
マーティンエンダー

すごいナイムはこれを見て答えたいと思います
ロナンデジェロ

回答:


5

Perl、157 152 151バイト

+4を含みます-p0(複数の場所-eで使用'するため、単なる拡張としてはカウントできません)

STDINの迷路で実行します。

./keymaze.pl < maze.txt

keymaze.pl

#!/usr/bin/perl -p0
1until$n{map/\n/&&"L1R-1U@+D-@+"=~s%\pL%$t=$1{$_}.$&;pos=$^H=-$'+s'@' '*"@-",s/\G[a-z\$ ]/\@/+s/$&/ /i?/\$/?$1{$_}:$\||=$t:0for"$_"%reg,$_,%1}++.$\}{

交換する\n^H主張しスコアのための彼らのリテラルバージョンで。大きな迷路を解決するには、私のマシンで約1時間と2ギガバイト未満の時間が必要です。


4

Java 8-1282 1277 1268 1259 1257バイト

これはすべてのテストに合格します。ただし、それらのいくつかについては、若干異なる結果が得られます(複数の最適な方法があり、それが問題にならない場合)。

4番目のテストでは、次の結果が得られます。

RDDDDDLD

これの代わりに:

RDDDDDDL

5番目のテストでは、次のようになります。

LLLLUUULLDDLLLLDLLLLLRRRRRRURRRUURRRRRRRRRRRRRRRDDLLRRUULLUUUUUUURRRRRUURRRDRRRLLLLULLLDDLLLLLLUULLLUDLLLLLULLLRRRRRDRRRRRRDDLLLLLLLLLLLLDDDLLLLLLLDURRRRRRRRDDDDRRRRRRUUUUU

これの代わりに:

UUULLLLLLDDLLLDLLLLLLRRRRRRRRRUUURRRRRRRRRRRRRRRDDLLRRUULLUUUUUUURRRRRUURRRDRRRLLLLULLLLLDDLLLLUULLLUDLLLLLULLLRRRRRDRRRRRRDDLLLLLLLLLLLLDDDLLLLLLLDURRRRRRRRDDDDRRRRRRUUUUU

ゴルフバージョン:

import java.util.*;class G{int y,w,h,p;String C="",S,o,v;Map m=new HashMap();String q(int a){return a<1?"":"#"+q(a-1);}public static void main(String[]a)throws Exception{new G(new String(java.nio.file.Files.readAllBytes(new java.io.File(a[0]).toPath())));}G(String a){w=(a+"\n").indexOf(10)+3;String t=q(w)+a.replace("\n","##")+q(w);for(char j=65,k=97;j<91;j++,k++){if(t.indexOf(j)*t.indexOf(k)<0)t=t.replace(j,'#').replace(k,' ');}h=t.length()/--w;S=v=q(w*h);t=g(t);if(t!=v)System.out.print(t);}String g(String t){o=(String)m.get(t);if(o!=null)return o;if(t.indexOf(36)<0){if(S.length()>C.length())S=C;return"";}String d="";int f=t.indexOf(64),M[]=new int[w*h],N[]=new int[w*h];Queue<Integer>s=new ArrayDeque();s.add(f);while(!s.isEmpty()){y=s.poll();int[]P={y+1,y-1,y+w,y-w};for(int v:P){char j=t.replaceAll("[A-Z]","#").charAt(v);if(v!=f&j!=35&(N[v]<1|M[y]+1<M[v])){M[v]=M[y]+1;N[v]=y;s.add(v);if(j>32)d+=j;}}}o=d.chars().distinct().mapToObj(e->{String z="",c=C;for(y=t.indexOf(e);y!=f;y=N[y]){p=y-N[y];z=(p==w?"D":p==-w?"U":p==1?"R":"L")+z;}if(S.length()<=(C+z).length())return v;C+=z;String u=g(t.replace('@',' ').replace((char)e,'@').replace((char)(e-32),' '));C=c;return u==v?v:z+u;}).reduce(v,(a,b)->a.length()<b.length()?a:b);m.put(t,o);return o;}}

ゴルフされていないバージョン

特徴:

  • 有益な変数名;
  • 明確かつ詳細なコメント。
  • 適切な識別。
import java.util.*;

/**
 * @author Victor Stafusa
 */
class TreasureHunt {

    // Note: on normal (non-golfing programming) those variables should have been scoped properly.
    // They are instance variables just for golfing purposes.
    // On the golfed version, nextCellIndex and waypointCellIndex are the same variable. The same also happens to cachedValue and result. This happens is for golfing purposes.

    int nextCellIndex,
            width,
            height,
            waypointCellIndex,
            cellIndexDifference;

    String previousPath = "",
            bestSolutionSoFar,
            cachedValue,
            result,
            failureFlag;

    // This should be Map<String, String>, but the generics were omitted for golfing.
    // It is needed to avoid recomputing long partial dungeons (i.e. dynamic programming).
    Map cachedResults = new HashMap();

    // Returns a lot of hashes. Like aLotOfHashes(7) will return "#######".
    String aLotOfHashes(int howMany) {
        return howMany < 1 ? "" : "#" + aLotOfHashes(howMany - 1);
    }

    // Here is where our program starts.
    public static void main(String[] args) throws Exception {
        // Read all the content of the file from args[0] and put it into a string.
        // Pass that string as a parameter to the constructor.
        // The instance itself is useless - it is just a golfing trick.
        new TreasureHunt(new String(java.nio.file.Files.readAllBytes(new java.io.File(args[0]).toPath())));
    }

    // Pre-processs the source in order to format it in the way that we want:
    // * No separators between rows. It uses the (row * width + column) formula, so no separators are needed.
    // * An extra layer of wall is added in all sides. This naturally fix up problems about walking out of the edges of the board, wrapping-around or acessing invalid array indexes.
    // This is a constructor just for golfing purposes. Its instances are worthless.
    TreasureHunt(String originalSource) {

        // Finds the width by searching the first line-feed.
        // If there is just one line and no line-feed, the [+ "\n"] will ensure that it will not break.
        // The [+ 3] is because we will add a layer of walls around, so it will be widen by one cell in the left and one in the right (which is +2).
        // We still get one more in the width that will be decremented later to use that in the aLotOfHashes method below.
        // 10 == '\n'.
        width = (originalSource + "\n").indexOf(10) + 3;

        // Add a layer of walls in the top and in the bottom (using a lot of hashes for that).
        // Replaces the line-feed by a pair of walls, representing the rightmost wall of a row and the leftmost row of the following row.
        // Since there is no line-feed before the first line nor after the last line, we add more two walls to fill those.
        String newSource = aLotOfHashes(width) + originalSource.replace("\n", "##") + aLotOfHashes(width);

        // Remove the keys without door (replaces them as blank spaces) and the doors without keys (replaces them with walls.
        // This way, the resulting dungeon will always have matching keys and doors.
        // 65 == 'A', 97 == 'a', 91 == 'z'+1
        for (char door = 65, key = 97; door < 91; door++, key++) {

            // Now a little math trick. For each key or door, we find an index. If the key or door exist, it will be a positive number. Otherwise it will be negative.
            // The result will never be zero, because the zeroey position is filled with part of the layer of wall that we added.
            // If only one of the key and the door exist, the multiplication will be the product of two numbers with opposite signals, i.e. a negative number.
            // Otherwise (both exists or both don't), then the product will be positive.
            // So, if the product is negative, we just remove the key and the door (only one of them will be removed of course, but we don't need to care about which one).
            if (newSource.indexOf(door) * newSource.indexOf(key) < 0) {
                newSource = newSource.replace(door, '#').replace(key, ' ');
            }
        }

        // Knowing the source length and the width (which we fix now), we can easily find out the height.
        height = newSource.length() / --width;

        // Creates a special value for signaling a non-existence of a path. Since they are sorted by length, this must be a sufficiently large string to always be unfavoured.
        bestSolutionSoFar = failureFlag = aLotOfHashes(width * height);

        // Now, do the hard work to solve the dungeon...
        // Note: On the golfed version, newSource and solution are the same variable.
        String solution = solvingRound(newSource);

        // If a solution is found, then show it. Otherwise, we just finish without printing anything.
        // Note: It is unsafe and a bad practice to compare strings in java using == or != instead of the equals method. However, this code manages the trickery.
        if (solution != failureFlag) System.out.print(solution);
    }

    // This does the hard work, finding a solution for a specific dungeon. This is recursive, so the solution of a dungeon involves the partial solution of the dungeon partially solved.
    String solvingRound(String dungeon) {
        // To avoid many redundant computations, check if this particular dungeon was already solved before. If it was, return its cached solution.
        cachedValue = (String) cachedResults.get(dungeon);
        if (cachedValue != null) return cachedValue;

        // If there is no treasure in the dungeon (36 == '$'), this should be because the adventurer reached it, so there is no further moves.
        if (dungeon.indexOf(36) < 0) {
            if (bestSolutionSoFar.length() > previousPath.length()) bestSolutionSoFar = previousPath;
            return "";
        }

        String keysOrTreasureFound = ""; // Initially, we didn't found anything useful.
        int adventurerSpot = dungeon.indexOf(64), // 64 == '@', find the cell index of the adventurer.
                cellDistance[] = new int[width * height],
                previousWaypoint[] = new int[width * height];

        // Use a queue to enqueue cell indexes in order to floodfill all the reachable area starting from the adventurer. Again, screw up the proper user of generics.
        Queue<Integer> floodFillQueue = new ArrayDeque();
        floodFillQueue.add(adventurerSpot); // Seed the queue with the adventurer himself.

        // Each cell thies to populate its neighbours to the queue. However no cell will enter the queue more than once if it is not featuring a better path than before.
        // This way, all the reachable cells will be reached eventually.
        while (!floodFillQueue.isEmpty()) {
            nextCellIndex = floodFillQueue.poll();

            // Locate the four neighbours of this cell.
            // We don't need to bother of checking for wrapping-around or walking into an invalid cell indexes because we added a layer of walls in the beggining,
            // and this layer of wall will ensure that there is always something in each direction from any reachable cell.
            int[] neighbourCells = {nextCellIndex + 1, nextCellIndex - 1, nextCellIndex + width, nextCellIndex - width};

            // For each neighbouring cell...
            for (int neighbourCellIndex : neighbourCells) {
                // Find the cell content. Considers doors as walls.
                char neighbourCellContent = dungeon.replaceAll("[A-Z]", "#").charAt(neighbourCellIndex);

                if (neighbourCellIndex != adventurerSpot // If we are not going back to the start ...
                        & neighbourCellContent != 35 // ... nor walking into a wall or a door that can't be opened (35 == '#') ...
                        & (previousWaypoint[neighbourCellIndex] < 1 // ... and the neighbour cell is either unvisited ...
                            | cellDistance[nextCellIndex] + 1 < cellDistance[neighbourCellIndex])) //  ... or it was visited before but now we found a better path ...
                { // ... then:
                    cellDistance[neighbourCellIndex] = cellDistance[nextCellIndex] + 1; // Update the cell distance.
                    previousWaypoint[neighbourCellIndex] = nextCellIndex; // Update the waypoint so we can track the way from this cell back to the adventurer.
                    floodFillQueue.add(neighbourCellIndex); // Enqueue the cell once again.
                    if (neighbourCellContent > 32) keysOrTreasureFound += neighbourCellContent; // If we found something in this cell (32 == space), take a note about that.
                }
            }
        }

        // Brute force solutions chosing each one of the interesting things that we found and recursively solving the problem as going to that interesting thing.
        // Warning: This has an exponential complexity. Also, if we found something interesting by more than one path, it will compute that redundantly.
        result = keysOrTreasureFound.chars().distinct().mapToObj(keyOrTreasure -> {
            String tracingWay = "", savedPreviousPath = previousPath;

            // From our keyOrTreasure, trace back the path until the adventurer is reached, adding (in reverse order) the steps needed to reach it.
            for (waypointCellIndex = dungeon.indexOf(keyOrTreasure); waypointCellIndex != adventurerSpot; waypointCellIndex = previousWaypoint[waypointCellIndex]) {

                // Use the difference in cell indexes to see if it is going up, down, right or left.
                cellIndexDifference = waypointCellIndex - previousWaypoint[waypointCellIndex];
                tracingWay = (cellIndexDifference == width ? "D" : cellIndexDifference == -width ? "U" : cellIndexDifference == 1 ? "R" : "L") + tracingWay;
            }

            // If this path is going to surely be longer than some other path already found before, prune the search and fail this path.
            if (bestSolutionSoFar.length() <= (previousPath + tracingWay).length()) return failureFlag;

            // Prepare for recursion, recording the current path as part of the next level recursion's previous path.
            previousPath += tracingWay;

            // Now that we traced our way from the adventurer to something interesting, we need to continue our jorney through the remaining items.
            // For that, create a copy of the dungeon, delete the door of the key that we found (if it was a key),
            // move the adventurer to the thing that we just found and recursively solve the resulting simpler problem.
            String nextRoundPartialSolution = solvingRound(dungeon
                        .replace('@', ' ') // Remove the adventurer from where he was...
                        .replace((char) keyOrTreasure, '@') // ... and put him in the spot of the key or treasure.
                        .replace((char) (keyOrTreasure - 32), ' ')); // ... and if it was a key, delete the corresponding door ([- 32] converts lowercase to uppercase, won't do anything in the case of the treasure).

            // Recursion finished. Now, get back the previous path of the previous recursion level.
            previousPath = savedPreviousPath;

            // If the subproblem resulted in a failure, then it is unsolvable. Otherwise, concatenates the subproblem solution to the steps that we took.
            return nextRoundPartialSolution == failureFlag ? failureFlag : tracingWay + nextRoundPartialSolution;

        // From all the paths we took, choose the shorter one.
        }).reduce(failureFlag, (a, b) -> a.length() < b.length() ? a : b);

        // Now that we have the result of this recursion level and solved this particular dungeon instance,
        // cache it to avoid recomputing it all again if the same instance of the dungeon is produced again.
        cachedResults.put(dungeon, result);
        return result;
    }
}

入力する

実行するには、これを試してください:

javac G.java
java G ./path/to/file/with/dungeon.txt

あなたがungolfedバージョンを実行している場合は、交換してくださいGとのをTreasureHunt

ファイルにはダンジョンが含まれている必要があります。入力は改行で終わるべきではありません。さらに、\nフォーマット内の行末のみを受け入れます。\r\nまたはでは機能しません\r

また、入力を検証またはサニタイズしません。入力の形式が正しくない場合、動作は未定義です(例外をスローする可能性が高い)。ファイルが見つからない場合、例外がスローされます。

備考

私の最初の実装は1100バイトに近いどこかで、妥当な時間で5番目のテストケースを解決できませんでした。その理由は、私の実装では、アクセス可能な(つまり、アクセスできない部屋にロックされていない)収集可能なアイテム(つまり、キーと宝物)のすべての可能な順列を総当たり攻撃するからです。

最悪の場合、26個すべてのキーと宝物があるため、これは27になります!= 10,888,869,450,418,352,160,768,000,000の異なる順列。

OPは、答えが妥当な時間内に実行されるものであるべきだとは指定しませんでした。ただし、これは悪用したくない抜け穴だと思います。そこで、すべてのテストケースで許容できる時間内に実行することにしました。それを達成するために、私の改訂されたプログラムは、いくつかの既に知っている解決策よりも悪いことが証明されている検索パスの枝刈りを特徴としています。さらに、発生する可能性のある多くの同一のダンジョンの再計算を回避するために、サブソリューション(動的プログラミング)もキャッシュします。それにより、私のコンピューターでわずか1分で5番目のテストケースを解決できます。

ソリューションは再帰的です。アイデアは、最初に冒険者を何らかのアイテム(鍵または宝物)に誘導することです。キーの場合、冒険者がそれに到達すると、キーとドアの両方が削除された新しい類似のダンジョンが生成され、冒険者はキーがあった場所に移動します。これにより、生成されたより単純なダンジョンは、宝物に到達するか、アルゴリズムが到達可能なアイテムがないと結論付けるまで再帰的に解決されます。訪問するアイテムの順序は、上記で説明したように、プルーニングとキャッシングでブルートフォースされています。

冒険者とアイテムの間の経路探索は、フラッドフィルとダイクストラの両方に似たアルゴリズムで行われます。

最後に、この問題はNP完全(おそらく、ドア/キーの数に制限のない一般化バージョン)であると思われます。これが当てはまる場合、ドアとキーのミリアドで非常に大きなダンジョンを適切な時間で最適に解決するソリューションを期待しないでください。準最適なパスが許可されている場合、いくつかのヒューリスティックを使用して簡単に処理できます(可能な場合は宝物に移動し、それ以外の場合は最も近いキーに移動します)。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.