Java 8-1282 1277 1268 1259 1257 바이트
이것은 모든 테스트를 통과합니다. 그러나 그중 일부는 약간 다른 결과를 제공합니다 (두 가지 이상의 최적의 방법이있을 때 문제가되지 않음).
네 번째 테스트의 경우 다음을 제공합니다.
RDDDDDLD
이 대신에 :
RDDDDDDL
다섯 번째 테스트의 경우 다음을 제공합니다.
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는 응답이 합리적인 시간에 실행되도록 지정하지 않았습니다. 그러나 나는 이것이 악용하고 싶지 않은 허점이라고 생각합니다. 그래서 모든 테스트 사례에 대해 적절한 시간 내에 실행하기로 결정했습니다. 이를 위해 내 수정 된 프로그램에는 이미 알려진 솔루션보다 더 나쁜 것으로 밝혀진 검색 경로의 정리 기능이 있습니다. 또한 발생할 수있는 많은 동일한 던전을 재 계산하지 않도록 하위 솔루션 (예 : 동적 프로그래밍)을 캐시합니다. 이를 통해 컴퓨터에서 5 분만에 5 번째 테스트 사례를 해결할 수 있습니다.
솔루션은 재귀 적입니다. 아이디어는 먼저 모험가에게 어떤 물건 (열쇠 또는 보물)을 얻는 것입니다. 열쇠의 경우 모험가가 열쇠에 도달하면 열쇠와 문이 모두 삭제되고 모험가가 열쇠가 있던 곳으로 이동 한 새로운 유사한 던전이 생성됩니다. 이를 통해 생성 된 더 간단한 던전은 보물에 도달하거나 알고리즘이 도달 할 수있는 항목이 없다고 결론을 내릴 때까지 재귀 적으로 해결됩니다. 방문 할 항목의 순서는 위에서 설명한대로 가지 치기 및 캐싱으로 무차별 강제 적용됩니다.
모험가와 아이템 사이의 길 찾기는 범람과 Dijkstra와 유사한 알고리즘으로 이루어집니다.
마지막으로, 나는이 문제가 NP- 완전 (문 / 키 수에 대한 제한없이 일반화 된 버전)이라고 생각합니다. 이것이 사실이라면, 적당한 시간 안에 문과 열쇠의 열쇠로 매우 큰 던전을 최적으로 해결하는 솔루션을 기대하지 마십시오. 차선의 경로가 허용되면 일부 휴리스틱으로 쉽게 다루기 쉽습니다 (가능한 경우 보물로 이동하고, 그렇지 않으면 가장 가까운 키로 이동).