파이썬 3, 67 토큰
import sys
import time
class Bunny():
def __init__(self):
self.direction = [0, 1]
self.coords = [-1, -1]
def setCoords(self, x, y):
self.coords = [x, y]
def rotate(self, dir):
directions = [[1, 0], [0, 1], [-1, 0], [0, -1]]
if dir == 'L':
self.direction = directions[(directions.index(self.direction) + 1) % 4]
if dir == 'R':
self.direction = directions[(directions.index(self.direction) - 1) % 4]
def hop(self):
self.coords = self.nextTile()
# Returns where the bunny is about to jump to
def nextTile(self):
return [self.coords[0] + self.direction[0], self.coords[1] + self.direction[1]]
class BoardState():
def __init__(self, map):
self.unvisited = 0
self.map = []
self.bunny = Bunny()
self.hopsLeft = 0
for x, row in enumerate(map):
newRow = []
for y, char in enumerate(row):
if char == '#':
newRow.append(1)
self.unvisited += 1
elif char == 'S':
newRow.append(2)
if -1 in self.bunny.coords:
self.bunny.setCoords(x, y)
else:
print("Multiple starting points found", file=sys.stderr)
sys.exit(1)
elif char == ' ':
newRow.append(0)
elif char == 'O':
newRow.append(2)
else:
print("Invalid char in input", file=sys.stderr)
sys.exit(1)
self.map.append(newRow)
if -1 in self.bunny.coords:
print("No starting point defined", file=sys.stderr)
sys.exit(1)
def finished(self):
return self.unvisited == 0
def validCoords(self, x, y):
return -1 < x < len(self.map) and -1 < y < len(self.map[0])
def runCom(self, com):
if self.finished():
return
if self.hopsLeft < self.unvisited:
return
if com == 'F':
x, y = self.bunny.nextTile()
if self.validCoords(x, y) and self.map[x][y] != 0:
self.bunny.hop()
self.hopsLeft -= 1
if (self.map[x][y] == 1):
self.unvisited -= 1
self.map[x][y] = 2
else:
self.bunny.rotate(com)
class loop():
def __init__(self, loops, commands):
self.loops = loops
self.commands = [*commands]
def __str__(self):
return "loop({}, {})".format(self.loops, list(self.commands))
def __repr__(self):
return str(self)
def rejectRedundantCode(code):
if isSnippetRedundant(code):
return False
if type(code[-1]) is str:
if code[-1] in "LR":
return False
else:
if len(code[-1].commands) == 1:
print(code)
if code[-1].commands[-1] in "LR":
return False
return True
def isSnippetRedundant(code):
joined = "".join(str(com) for com in code)
if any(redCode in joined for redCode in ["FFF", "RL", "LR", "RRR", "LLL"]):
return True
for com in code:
if type(com) is not str:
if len(com.commands) == 1:
if com.loops == 2:
return True
if type(com.commands[0]) is not str:
return True
if com.commands[0] in "LR":
return True
if len(com.commands) > 1 and len(set(com.commands)) == 1:
return True
if isSnippetRedundant(com.commands):
return True
for i in range(len(code)):
if type(code[i]) is not str and len(code[i].commands) == 1:
if i > 0 and code[i].commands[0] == code[i-1]:
return True
if i < len(code) - 1 and code[i].commands[0] == code[i+1]:
return True
if type(code[i]) is not str:
if i > 0 and type(code[i-1]) is not str and code[i].commands == code[i-1].commands:
return True
if i < len(code) - 1 and type(code[i+1]) is not str and code[i].commands == code[i+1].commands:
return True
if len(code[i].commands) > 3 and all(type(com) is str for com in code[i].commands):
return True
return False
def flatten(code):
flat = ""
for com in code:
if type(com) is str:
flat += com
else:
flat += flatten(com.commands) * com.loops
return flat
def newGen(n, topLevel = True):
maxLoops = 9
minLoops = 2
if n < 1:
yield []
if n == 1:
yield from [["F"], ["L"], ["R"]]
elif n == 2:
yield from [["F", "F"], ["F", "L"], ["F", "R"], ["L", "F"], ["R", "F"]]
elif n == 3:
for innerCode in newGen(n - 1, False):
for loops in range(minLoops, maxLoops):
if len(innerCode) != 1 and 0 < innerCode.count('F') < 2:
yield [loop(loops, innerCode)]
for com in "FLR":
for suffix in newGen(n - 2, False):
for loops in range(minLoops, maxLoops):
if com not in suffix:
yield [loop(loops, [com])] + suffix
else:
for innerCode in newGen(n - 1, False):
if topLevel:
yield [loop(17, innerCode)]
else:
for loops in range(minLoops, maxLoops):
if len(innerCode) > 1:
yield [loop(loops, innerCode)]
for com in "FLR":
for innerCode in newGen(n - 2, False):
for loops in range(minLoops, maxLoops):
yield [loop(loops, innerCode)] + [com]
yield [com] + [loop(loops, innerCode)]
def codeLen(code):
l = 0
for com in code:
l += 1
if type(com) is not str:
l += codeLen(com.commands)
return l
def test(code, board):
state = BoardState(board)
state.hopsLeft = flatten(code).count('F')
for com in code:
state.runCom(com)
return state.finished()
def testAll():
score = 0
for i, board in enumerate(boards):
print("\n\nTesting board {}:".format(i + 1))
#print('\n'.join(board),'\n')
start = time.time()
found = False
tested = set()
for maxLen in range(1, 12):
lenCount = 0
for code in filter(rejectRedundantCode, newGen(maxLen)):
testCode = flatten(code)
if testCode in tested:
continue
tested.add(testCode)
lenCount += 1
if test(testCode, board):
found = True
stop = time.time()
print("{} token solution found in {} seconds".format(maxLen, stop - start))
print(code)
score += maxLen
break
if found:
break
print("Final Score: {}".format(score))
def testOne(board):
start = time.time()
found = False
tested = set()
dupes = 0
for maxLen in range(1, 12):
lenCount = 0
for code in filter(rejectRedundantCode, newGen(maxLen)):
testCode = flatten(code)
if testCode in tested:
dupes += 1
continue
tested.add(testCode)
lenCount += 1
if test(testCode, board):
found = True
print(code)
print("{} dupes found".format(dupes))
break
if found:
break
print("Length:\t{}\t\tCombinations:\t{}".format(maxLen, lenCount))
stop = time.time()
print(stop - start)
#testAll()
testOne(input().split('\n'))
이 프로그램은 단일 입력 보드를 테스트하지만 테스트 이 테스트 드라이버가 더 유용하다는. 모든 단일 보드를 동시에 테스트하고 해당 솔루션을 찾는 데 걸린 시간을 인쇄합니다. 내 컴퓨터에서 해당 코드를 실행하면 (Intel i7-7700K 쿼드 코어 CPU @ 4.20GHz, 16.0GB RAM) 다음과 같은 결과가 나타납니다.
Testing board 1:
2 token solution found in 0.0 seconds
['F', 'F']
Testing board 2:
4 token solution found in 0.0025103092193603516 seconds
[loop(17, [loop(3, ['F']), 'R'])]
Testing board 3:
4 token solution found in 0.0010025501251220703 seconds
[loop(17, [loop(3, ['F']), 'L'])]
Testing board 4:
5 token solution found in 0.012532949447631836 seconds
[loop(17, ['F', loop(7, ['F', 'L'])])]
Testing board 5:
5 token solution found in 0.011022329330444336 seconds
[loop(17, ['F', loop(5, ['F', 'L'])])]
Testing board 6:
4 token solution found in 0.0015044212341308594 seconds
[loop(17, [loop(3, ['F']), 'L'])]
Testing board 7:
8 token solution found in 29.32585096359253 seconds
[loop(17, [loop(4, [loop(5, [loop(6, ['F']), 'L']), 'L']), 'F'])]
Testing board 8:
8 token solution found in 17.202533721923828 seconds
[loop(17, ['F', loop(7, [loop(5, [loop(4, ['F']), 'L']), 'F'])])]
Testing board 9:
6 token solution found in 0.10585856437683105 seconds
[loop(17, [loop(7, [loop(4, ['F']), 'L']), 'F'])]
Testing board 10:
6 token solution found in 0.12129759788513184 seconds
[loop(17, [loop(7, [loop(5, ['F']), 'L']), 'F'])]
Testing board 11:
7 token solution found in 4.331984758377075 seconds
[loop(17, [loop(8, ['F', loop(5, ['F', 'L'])]), 'L'])]
Testing board 12:
8 token solution found in 58.620323181152344 seconds
[loop(17, [loop(3, ['F', loop(4, [loop(3, ['F']), 'R'])]), 'L'])]
Final Score: 67
이 마지막 테스트는 미세한 제한을받지 않습니다.
배경
이것은 내가 대답 한 가장 재미있는 도전 중 하나였습니다! 나는 폭발 패턴 사냥을하고 물건을 줄일 휴리스틱을 찾고있었습니다.
일반적으로 여기 PPCG에서는 비교적 쉬운 질문에 대답하는 경향이 있습니다. 문자열 태그는 일반적으로 내 언어에 매우 적합하기 때문에 특히 좋아 합니다. 어느 날, 약 2 주 전에 배지를 살펴보면서 부흥 배지를 받지 못했다는 것을 깨달았습니다 . 그래서 나는 답을 찾지 못했습니다어떤 것이 내 눈에 띄는 지 확인하기 위해이 질문을 찾았습니다. 나는 비용에 관계없이 대답하겠다고 결정했다. 내가 생각했던 것보다 조금 더 어려워졌지만 마침내 내가 자랑스러워 할 수있는 무차별 대항 답변을 얻었습니다. 그러나 나는 보통 한 번의 답변으로 한 시간 이상을 소비하지 않기 때문에이 도전은 완전히 표준에서 벗어났습니다. 이 대답은 조심스럽게 진행하지는 않았지만 2 주가 조금 넘었고 마침내이 단계에 도달하는 데 10+ 이상의 작업이 필요했습니다.
첫 번째 반복은 순수한 무차별 대입 솔루션이었습니다. 다음 코드를 사용하여 길이 N 까지의 모든 스 니펫을 생성했습니다 .
def generateCodeLenN(n, maxLoopComs, maxLoops, allowRedundant = False):
if n < 1:
return []
if n == 1:
return [["F"], ["L"], ["R"]]
results = []
if 1:
for com in "FLR":
for suffix in generateCodeLenN(n - 1, maxLoopComs, maxLoops, allowRedundant):
if allowRedundant or not isSnippetRedundant([com] + suffix):
results.append([com] + suffix)
for loopCount in range(2, maxLoopComs):
for loopComs in range(1, n):
for innerCode in generateCodeLenN(loopComs, maxLoopComs, maxLoops - 1, allowRedundant):
if not allowRedundant and isSnippetRedundant([loop(loopCount, innerCode)]):
continue
for suffix in generateCodeLenN(n - loopComs - 1, maxLoopComs, maxLoops - 1, allowRedundant):
if not allowRedundant and isSnippetRedundant([loop(loopCount, innerCode)] + suffix):
continue
results.append([loop(loopCount, innerCode)] + suffix)
if loopComs == n - 1:
results.append([loop(loopCount, innerCode)])
return results
이 시점에서 가능한 모든 단일 답변을 테스트하는 것이 너무 느릴 것이라고 확신했기 때문에 isSnippetRedundant
더 짧은 스 니펫으로 작성할 수있는 스 니펫을 필터링했습니다. 예를 들어, ["F", "F", "F"]
동일한 효과를 너무 느리게 달성 할 수 있기 때문에 스 니펫 생성을 거부합니다 . 테스트 사례 12는이 방법을 사용하여 3,000 초 이상이 걸렸습니다. 이것은 분명히 너무 느립니다. 그러나이 정보와 많은 컴퓨터주기를 사용하여 모든 보드에 짧은 솔루션을 강제로 적용하면 새로운 패턴을 찾을 수 있습니다. 발견 된 거의 모든 솔루션은 일반적으로 다음과 같습니다.[Loop(3, ["F"])
길이 3 스 니펫을 테스트하는 지점에 도달하면 길이 3 스 니펫이 현재 보드를 해결할 수 없다는 것을 알고 있습니다. 이것은 좋은 니모닉을 많이 사용했지만 궁극적으로 waaaay였습니다.
[<com> loop(n, []) <com>]
여러 층으로 중첩되어 있으며 각 측면의 단일 통신은 선택 사항입니다. 이는 다음과 같은 솔루션을 의미합니다.
["F", "F", "R", "F", "F", "L", "R", "F", "L"]
절대 나타나지 않을 것입니다. 실제로, 3 개 이상의 비 루프 토큰 시퀀스는 없었습니다. 이를 활용하는 한 가지 방법은 이들을 모두 걸러 내고 테스트하는 데 귀찮게하지 않는 것입니다. 그러나 그것들을 생성하는 것은 여전히 무시할 수없는 시간을 소비했으며, 이와 같은 수백만 개의 스 니펫을 필터링하면 시간이 거의 단축되지 않습니다. 대신이 패턴에 따라 스 니펫 만 생성하도록 코드 생성기를 대폭 재 작성했습니다. 의사 코드에서 새 생성기는 다음과 같은 일반적인 패턴을 따릅니다.
def codeGen(n):
if n == 1:
yield each [<com>]
if n == 2:
yield each [<com>, <com>]
if n == 3:
yield each [loop(n, <com length 2>]
yield each [loop(n, <com>), <com>]
else:
yield each [loop(n, <com length n-1>)]
yield each [loop(n, <com length n-2>), <com>]
yield each [<com>, loop(n, <com length n-2>)]
# Removed later
# yield each [<com>, loop(n, <com length n-3>), <com>]
# yield each [<com>, <com>, loop(n, <com length n-3>)]
# yield each [loop(n, <com length n-3>), <com>, <com>]
가장 긴 테스트 케이스를 140 초로 줄였습니다. 그러나 여기서부터 개선해야 할 부분이 여전히있었습니다. 중복 / 무가치 코드를보다 적극적으로 필터링하고 코드가 이전에 테스트되었는지 확인하기 시작했습니다. 이것은 그것을 더 줄 였지만 충분하지 않았습니다. 결국, 누락 된 마지막 부분은 루프 카운터였습니다. 내 고급 알고리즘 (읽기 : 임의 시행 착오 )을 통해 루프를 실행할 수있는 최적의 범위가 [3-8]이라고 결정했습니다. 그러나 거기에 큰 개선이 있습니다. 우리가 그것을 알고 있다면 3-7에서 루프 카운트로 해결할 수 있습니다. 따라서 3 ~ 8의 모든 루프 크기를 반복하는 대신 외부 루프의 루프 수를 최대로 설정합니다. 결과적으로 검색 공간을[loop(8, [loop(8, ['F', loop(5, ['F', 'L'])]), 'L'])]
우리가 보드를 해결할 수 없다는 , 그 방법은 절대 없습니다.[loop(3, [loop(8, ['F', loop(5, ['F', 'L'])]), 'L'])]
maxLoop - minLoop
또는이 경우 6입니다.
이것은 많은 도움이되었지만 점수가 부풀려졌습니다. 무차별 대입으로 내가 찾은 특정 솔루션 (예 : 보드 4 및 6)을 실행하려면 더 많은 루프 번호가 필요합니다. 따라서 외부 루프 카운트를 8로 설정하는 대신 외부 루프 카운트를 17로 설정했습니다.이 수치는 고급 알고리즘에 의해 계산 된 마법의 숫자이기도합니다. 가장 바깥 쪽 루프의 루프 수를 늘려도 솔루션의 유효성에 영향을 미치지 않기 때문에이 작업을 수행 할 수 있습니다. 이 단계는 실제로 우리의 최종 점수를 13만큼 줄였습니다.