Гугл Зайчик Google


16

4 грудня 2017 року Google Doodle була графічною грою на програмування із зображенням зайчика . Пізніші рівні були добре нетривіальними і вони здавалися чудовим кандидатом на виклик .

Деталі

Гра

  • Є чотири доступні рухи: стрибок вперед, поворот ліворуч, поворот праворуч і петля. Кожен з цих рухів - це один маркер , що відповідає тому, що вони є кожною плиткою у грі.
  • Зайчик може стикатися з чотирма ортогональними напрямками (тобто північ, південь, схід, захід).
  • Зайчик може стрибати вперед (рухатись одним квадратом у напрямку, що знаходиться перед ним) і повертати вліво або вправо.
  • Петлі можуть мати будь-яку кількість інших рухів всередині них, включаючи інші петлі, а їх кількість ітерацій - це будь-яке додатне ціле число (хоча гра технічно допускає кількість ітерацій 0).
  • Дошка - це набір сітчастих квадратів, і зайчик може стрибати між сусідніми квадратами.
  • Зайчик не може стрибнути в порожнечу. Це означає, що спроба зіскочити з дошки нічого не робить. (Це, очевидно, для деяких людей було несподіванкою, а для інших - розчаруванням.)
  • Квадрати або позначені, або немарки. Коли зайчик знаходиться на площі, він стає помітним.
  • Рівень закінчений, коли всі квадрати позначені.
  • Ви можете припустити, що рішення існує.

Ваш код

  • Мета: задавши дошку, знайти одне або кілька найкоротших рішень.
  • Введення - це список квадратних місць, що утворюють дошку (виокремлюючи позначені та немарковані квадрати), а вихід - список ходів. Формат вводу та виводу взагалі не має значення, якщо вони читаються і зрозумілі для людини.
  • Критерій виграшу: сума кількості рухів найкоротших рішень, знайдених протягом однієї хвилини для кожної дошки. Якщо ваша програма не знайде рішення для будь-якої конкретної дошки, ваш рахунок для цієї дошки (5 * кількість квадратів).
  • Будь-ласка, не жорстко кодуйте рішення. Ваш код повинен мати можливість приймати будь-яку дошку як вхідну інформацію, а не лише ті, що наведені нижче.

Приклади

Рішення ховаються у спойлерах, щоб дати вам можливість пограти в гру спочатку та спробувати деякі з них самостійно. Також, для кожного подано лише одне рішення.

S- початкова площа зайчика (звернена на схід), #- немаркований квадрат і Oє позначений квадрат. Для рухів моє позначення F= хоп вперед, L= повернути вліво, R= повернути праворуч і LOOP(<num>){<moves>}позначає цикл, який повторюється <num>раз і робить <moves>кожен раз. Якщо цикл може запускатися будь-яку кількість разів понад деяке мінімальне число, <num>його можна опустити (тобто працює нескінченність).

Рівень 1:

S##

ФФ

Рівень 2:

S##
  #
  #

LOOP (2) {FFR}

3 рівень:

S##
# #
###

LOOP {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

Відповіді:


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 при 4,20 ГГц, 16,0 ГБ оперативної пам’яті), я отримую такий вихід:

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для фільтрування фрагментів, які можна було записати коротшим фрагментом. Наприклад, я б відмовився давати фрагмент, ["F", "F", "F"]тому що можна досягти таких самих ефектів [Loop(3, ["F"]), тож якщо ми дістанемося до того, коли ми перевіримо фрагменти довжини-3, ми знаємо, що жоден фрагмент довжиною 3 не міг вирішити поточну плату. Це використовувало багато хороших мнемонік, але в кінцевому підсумку було waaaayзанадто повільно. Випробування 12 займало трохи більше 3000 секунд, використовуючи цей підхід. Це явно значно надто повільно. Але використовуючи цю інформацію та купу комп'ютерних циклів, щоб грубо застосовувати короткі рішення до кожної дошки, я міг знайти новий зразок. Я помітив, що майже кожне знайдене рішення, як правило, виглядає приблизно так:

[<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]. Але в цьому є величезне вдосконалення: якщо ми знаємо, що [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. Тож не тривіальний крок.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.