Google的跳兔


16

2017年12月4日,Google Doodle是一款具有兔子功能图形编程游戏。后来的级别相当重要,它们似乎是挑战的绝佳候选人。

细节

游戏

  • 有四种可用的动作:向前跳,向左转,向右转和循环。这些动作中的每一个都是一个令牌,对应于它们在游戏中都是一个图块这一事实。
  • 兔子可以面对四个正交方向(即北,南,东,西)。
  • 兔子可以向前跳(在其面对的方向上移动一个正方形)并向左或向右转。
  • 循环内部可能有任意数量的其他移动,包括其他循环,并且它们的迭代计数是任何正整数(尽管从技术上讲,游戏允许迭代计数为0)。
  • 棋盘是一组网格对齐的正方形,并且兔子可以在相邻的正方形之间跳跃。
  • 兔子无法跳入虚空。意味着企图跳板没有任何作用。(这显然使某些人感到惊讶,而另一些人则感到失望。)
  • 正方形已标记或未标记。当兔子在正方形上时,它会被标记。
  • 标记所有正方形后,该级别完成。
  • 您可能会认为存在解决方案。

您的密码

  • 目标:给定董事会,找到一个或多个最短的解决方案。
  • 输入是构成木板的正方形位置的列表(区分标记的正方形和未标记的正方形),输出是移动的列表。输入和输出格式完全无关紧要,只要它们是人类可读和可理解的即可。
  • 获胜标准:每块板在一分钟内找到的最短解决方案的移动总数。如果您的程序找不到任何特定板的解决方案,则该板的分数为(5 *平方数)。
  • 请不要以任何方式对解决方案进行硬编码。您的代码应该能够将任何面板作为输入,而不仅仅是下面示例中给出的面板。

例子

解决方案隐藏在剧透中,使您有机会先玩游戏,然后自己尝试其中的一些。另外,下面仅针对每个提供一种解决方案。

S是兔子的起始正方形(朝东),#是未标记的正方形,并且O是标记的正方形。对于移动,我的表示法是F=向前跳,L=向左转,R=向右转,并LOOP(<num>){<moves>}表示一个循环,每次循环<num>执行<moves>。如果循环可以运行超过最小次数的任何次数,<num>则可以忽略(即无限工作)。

1级:

S##

FF

第2级:

S##
  #
  #

LOOP(2){FFR}

3级:

S##
# #
###

循环{FFR}

第4级:

###
# #
##S##
  # #
  ###

LOOP {F LOOP(7){FL}}(由DJMcMayhem找到)

5级:

#####
# # #
##S##
# # #
#####

LOOP(18){LOOP(10){FR} L}
来源:Reddit

6级:

 ###
#OOO#
#OSO#
#OOO#
 ###

LOOP {LOOP(3){F} L}

大板:(目前尚不清楚最短的解决方案)

12x12:

S###########
############
############
############
############
############
############
############
############
############
############
############

5级,但更大:

#############
# # # # # # #
#############
# # # # # # #
#############
# # # # # # #
######S######
# # # # # # #
#############
# # # # # # #
#############
# # # # # # #
#############

多孔板:

S##########
###########
## ## ## ##
###########
###########
## ## ## ##
###########
###########
## ## ## ##
###########
###########

S#########
##########
##  ##  ##
##  ##  ##
##########
##########
##  ##  ##
##  ##  ##
##########
##########

最后,不对称可能是屁股上的真正痛苦:

#######
# ##  #
#######
###S###
# ##  #
# ##  #
#######

#########
# ##  ###
###S  ###
# #######
###    ##
#####   #
####  ###
#########
#########


“找到一个或多个最短的解决方案”我以为停顿的问题阻止了这一点
Leaky Nun

@Leaky Nun这与停止问题无关。这是一个图形搜索
WhatToDo

但是允许循环播放……
Leaky Nun

4
我认为这并不适用,因为董事会是有限的。对于每个循环,它要么永远运行,要么停止。如果没有迭代次数的参数,则内部没有循环的循环将永远循环。在这种情况下,有限数量的电路板状态可确保循环将开始重复状态,可以对其进行检查。
WhatToDo

Answers:


12

Python 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.20 GHz,16.0 GB 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个多星期的时间,并且至少花了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过滤掉可以用较短代码段编写的代码段。例如,我会拒绝让代码片段太慢。使用这种方法,测试用例12花费了超过3,000秒。这显然太慢了。但是,利用这些信息和大量的计算机周期,强行将简短的解决方案应用于每块电路板,我可以找到一种新的模式。我注意到几乎找到的每个解决方案通常都看起来像以下内容:["F", "F", "F"]因为使用可以实现完全相同的效果[Loop(3, ["F"]),因此,如果达到测试长度为3的代码段的程度,我们知道长度为3的代码段无法解决当前电路板。这用了很多很好的助记符的,但最终是waaaay

[<com> loop(n, []) <com>]

嵌套了几层深,每边的单个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]。但是那里有一个巨大的改进:如果我们知道那[loop(8, [loop(8, ['F', loop(5, ['F', 'L'])]), 'L'])]不能解决我们的董事会,那么绝对没有办法[loop(3, [loop(8, ['F', loop(5, ['F', 'L'])]), 'L'])]或3-7中的任何循环计数都可以解决。因此,我们没有遍历3-8的所有循环大小,而是将外部循环的循环计数设置为最大值。最终将搜索空间缩小了倍maxLoop - minLoop,或在这种情况下为6。

这起到了很大的作用,但最终导致分数提高。我之前通过蛮力发现的某些解决方案需要较大的循环编号才能运行(例如,板4和6)。因此,我们没有将外部循环计数设置为8,而是将外部循环计数设置为17,这也是由我的高级算法计算得出的神奇数字。我们知道我们可以这样做,因为增加最外层循环的循环数不会影响解决方案的有效性。这一步实际上使我们的最终分数降低了13。所以这不是一个微不足道的步骤。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.