Знайдіть оптимальний стартовий хід Чомпа


14

Chomp - гра для двох гравців із встановленням прямокутника з шматків. Кожен гравець по черзі знімає будь-який шматок разом з усіма фігурами над ним та праворуч. Хто бере нижній лівий шматок, той втрачає. Досить легко можна довести, що перший гравець завжди має виграшний хід (за винятком прямокутника 1 на 1); знайди це.

  1. Введення - це розміри прямокутника (два числа)
  2. Вихід - це місце переможного ходу (два числа)
  3. Якщо переможний хід більше, ви можете вивести будь-який з них.

Це код гольфу; виграє найкоротший код (будь-якою мовою).

Приклади

Примітка. Вихідними даними є лише два числа; мистецтво ASCII нижче - просто продемонструвати, що означають цифри.

Введення: 5 3 (індекси 1 базуються на лівому нижньому куті)

Вихід: 4 3

XXX--
XXXXX
XXXXX

Вхід: 4 4

Вихід: 2 2

X---
X---
X---
XXXX

Бонус

Відніміть 15 балів з вашої оцінки, якщо ви виведете всі виграшні ходи. Кожна пара чисел повинна бути розділена новим рядком.


У вашому першому прикладі я думаю, що у вас є одна занадто багато тире
kitcar2000

@Kitcar Ти маєш рацію; фіксований.
Ypnypn

Я не можу зрозуміти вихідний формат. Як ці числа відповідають цим позиціям?
підземниймонорельс

@undergroundmonorail - індекс на основі 1 знизу. перший індекс - горизонтальна вісь, а другий - вертикальний.
Мартін Ендер

2
У відповідь на вашу суму: у Шахах у вас є менше 119 можливих рухів у будь-який момент часу (як правило, набагато менше), і жоден суперкомп'ютер до сьогодні не наблизився до вирішення шахів, використовуючи навіть найвідоміші алгоритми. На сітці 10 на 10 Chomp є 100 можливих перших ходів, і кожен з них має 1-99 потенційних секундних рухів. Що змушує вас думати, що грубою силою було б легко? Рекомендую обмежити розмір вашої сітки, якщо ви хочете відповіді на грубі сили. EDIT: Але не робіть цього. Зміна вимог серед конкурсу погана.
Rainbolt

Відповіді:


7

GolfScript, 82 ( 108 97 символів - 15 бонусів)

~),1/{{:F$0=),{F\+}/}%}@(*(0*\{1${1$\{\(@<},=},{1$\{\(@>},+(-!},:Y!{.,/+0}*;}/;Y{.-1=.@?)' '@)n}/

Оскільки я не знав жодної евристики, це рішення виконує вичерпний пошук у просторі рішення. Ви можете спробувати код в Інтернеті . Хоча реалізація є дуже ефективною, пошуковий простір зростає дуже швидко із збільшенням введення.

Приклади:

> 5 3
4 3

> 5 4
3 3

> 6 6
2 2

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

Представлення однієї дошки розміром w * h задається переліком w- чисел у діапазоні від 0 до h . Кожне число дає кількість штук у відповідному стовпчику. Таким чином, допустима конфігурація - це список, де номери не збільшуються від початку до кінця (будь-яким переміщенням ви гарантуєте, що всі стовпці праворуч максимально вищі, ніж обрані).

~                   # Evaluate the input (stack is now w h)

# BUILDING THE COMPLETE STATE SPACE
# Iteratively builds the states starting with 1xh board, then 2xh board, ...

),1/                # Generate the array [[0] [1] ... [h]] which is the space for 1xh
{                   # This loop is now ran w-1 times and each run adds all states for the 
                    # board with one additional column
  {                 # The {}/] block simply runs for each of the existing states
    :F$0=           #   Take the smallest entry (which has to be the last one)
    ),              #   For the last column all values 0..x are possible
    {F\+}/          #     Append each of these values to the smaller state
  }%
}@(*

# The order ensures that the less occupied boards are first in the list.
# Thus each game runs from the end of the list (where [h h ... h] is) to 
# the start (where [0 0 ... 0] is located).

# RUN THROUGH THE SEARCH SPACE
# The search algorithm therefore starts with the empty board and works through all
# possible states by simply looping over this list. It builds a list of those states
# which are known as non-winning states, i.e. those states where a player should 
# aim to end after the move

(                   # Skips the empty board (which is a winning configuration)
0*\                 # and makes an empty list out of it (which will be the list of
                    # known non-winning states (initially empty))
{                   # Loop over all possible states
  1$                #   Copy of the list of non-winning states
  {                 #   Filter those which are not reachable from the current state,
                    #   because at least one column has more pieces that the current
                    #   board has
    1$\{\(@<},=
  },
  {                 #   Filter those which are not reachable from the current state,
                    #   because no valid move exists
    1$\{\(@>},+     #     Filter those columns which are different between start and
                    #     end state
    (-!             #     If those columns are all of same height it is possible to move
  },
  :Y                #   Assign the result (list of all non-winning states which are
                    #   reachable from the current configuration within one move)
                    #   to variable Y

  !{                #   If Y is non-empty this one is a winning move, otherwise 
                    #   add it to the list
    .,/+
    0               #     Push dummy value
  }*;
}/
;                   # Discard the list (interesting data was saved to variable Y)

# OUTPUT LOOP
# Since the states were ordered the last one was the starting state. The list of 
# non-winning states were saved to variable Y each time, thus the winning moves 
# from the initial configuration is contained in this variable.

Y{                  # For each item in Y
  .-1=.@?)          #   Get the index (1-based) of the first non-h value
  ' '               #   Append a space
  @)                #   Get the non-h value itself (plus one)
  n                 #   Append a newline
}/

+1 Що стосується самого рішення та дуже добре прокоментованого коду
Xuntar

Динамічне програмування знизу вгору, а не зверху вниз. Приємно. Я вважав, що це зробити, але генерування станів у дійсному порядку для обходу знизу вгору було більш робочим і більш заплутаним, ніж рекурсивний пошук. Я здивований, що код закінчився так довго; Я очікував, що така непроста мова, як Гольфскрипт, могла б створити набагато коротше рішення.
user2357112 підтримує Моніку

Дуже приємно і добре продумано
Mouq

8

Пітон 2 3, 141-15 = 126

def win(x,y):w([y]*x)
w=lambda b,f=print:not[f(r+1,c+1)for r,p in enumerate(b)for c in range(p)if(r+c)*w(b[:r]+[min(i,c)for i in b[r:]],max)]

Мінімакс пошуку грубої сили. За кожним можливим рухом ми рекурсивно бачимо, чи може перемогти суперник після того, як ми зробимо цей крок. Досить слабо гольф; хтось інший повинен вміти робити набагато краще. Це відчувається як робота для APL.

  • winє загальнодоступним інтерфейсом. Він приймає розміри плати, перетворює її на представлення дошки та передає її в w.
  • w- алгоритм minimax. Він займає стан дошки, пробує всі рухи, створює список, елементи якого відповідають виграшним ходам, і повертає True, якщо список порожній. За замовчуванням f=printскладання списку має побічний ефект надрукування виграшних кроків. Назва функції використовувалась для отримання більшого сенсу, коли вона повертала список виграшних рухів, але потім я перемістила notперед списком, щоб зберегти пробіл.
  • for r,p in enumerate(b)for c in xrange(p) if(r+c): Повторіть всі можливі рухи. 1 1трактується як не законний крок, трохи спрощуючи базовий випадок.
  • b[:r]+[min(i,c)for i in b[r:]]: Побудувати стан дошки після руху, представленого координатами rта c.
  • w(b[:r]+[min(i,c)for i in b[r:]],max): Повторіть, чи є новий стан програшним. maxце найкоротша функція, яку я міг би знайти, яка б взяла два цілі аргументи і не скаржилася.
  • f(r+1,c+1): Якщо fдрукується, друкує хід. Що б там fне було, воно створює значення для зменшення довжини списку.
  • not [...]: notповертає Trueдля порожніх списків і Falseдля порожніх .

Оригінальний код Python 2, повністю не виконаний, включаючи пам'ять для обробки значно більших входів:

def win(x, y):
    for row, column in _win(Board([y]*x)):
        print row+1, column+1

class MemoDict(dict):
    def __init__(self, func):
        self.memofunc = func
    def __missing__(self, key):
        self[key] = retval = self.memofunc(key)
        return retval

def memoize(func):
    return MemoDict(func).__getitem__

def _normalize(state):
    state = tuple(state)
    if 0 in state:
        state = state[:state.index(0)]
    return state

class Board(object):
    def __init__(self, state):
        self.state = _normalize(state)
    def __eq__(self, other):
        if not isinstance(other, Board):
            return NotImplemented
        return self.state == other.state
    def __hash__(self):
        return hash(self.state)
    def after(self, move):
        row, column = move
        newstate = list(self.state)
        for i in xrange(row, len(newstate)):
            newstate[i] = min(newstate[i], column)
        return Board(newstate)
    def moves(self):
        for row, pieces in enumerate(self.state):
            for column in xrange(pieces):
                if (row, column) != (0, 0):
                    yield row, column
    def lost(self):
        return self.state == (1,)

@memoize
def _win(board):
    return [move for move in board.moves() if not _win(board.after(move))]

Демонстрація:

>>> for i in xrange(7, 11):
...     for j in xrange(7, 11):
...         print 'Dimensions: {} by {}'.format(i, j)
...         win(i, j)
...
Dimensions: 7 by 7
2 2
Dimensions: 7 by 8
3 3
Dimensions: 7 by 9
3 4
Dimensions: 7 by 10
2 3
Dimensions: 8 by 7
3 3
Dimensions: 8 by 8
2 2
Dimensions: 8 by 9
6 7
Dimensions: 8 by 10
4 9
5 6
Dimensions: 9 by 7
4 3
Dimensions: 9 by 8
7 6
Dimensions: 9 by 9
2 2
Dimensions: 9 by 10
7 8
9 5
Dimensions: 10 by 7
3 2
Dimensions: 10 by 8
6 5
9 4
Dimensions: 10 by 9
5 9
8 7
Dimensions: 10 by 10
2 2

Для 13x13зльоту 2x2і ви виграєте.
davidsbro

@davidsbro: Так, це виграшний хід для будь-якої квадратної дошки, що перевищує 1х1, але мій код ще не прорахував її.
user2357112 підтримує Моніку

2

Perl 6: 113 108 символів - 15 = 93 бали

Цей був жорсткий! Ось некешована версія, яка технічно правильна, але для нетривіальних входів знадобиться дуже багато часу.

sub win(*@b){map ->\i,\j{$(i+1,j+1) if @b[i][j]&&!win @b[^i],@b[i..*].map({[.[^j]]})},(^@b X ^@b[0])[1..*]}

Він працює так само, як реалізація Python @ user2357112 (підкресли його / її, я не міг би зрозуміти це без його / її роботи!), За винятком того, що win () приймає дошку Chomp (масив) замість ширини та довжини. Його можна використовувати так:

loop {
    my ($y, $x) = get.words;
    .say for @(win [1 xx $x] xx $y)
}

Версія з запам'ятовуванням, яка насправді може працювати з гідними введеннями (хоча не оптимізована для читабельності):

my %cache;
sub win (*@b) {
    %cache{
        join 2, map {($^e[$_]??1!!0 for ^@b[0]).join}, @b
    } //= map ->\i,\j{
        $(i+1,j+1) if @b[i][j] and not win
            @b[^i], @b[i..*].map({[.[^(* min j)]]}).grep: +*;
    },(^@b X ^@b[0])[1..*]
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.