Линейное решение неграммы

25

Задний план

Nonogram , также известный как Picross или Griddlers, представляет собой головоломку, цель которой состоит в том, чтобы определить, должна ли каждая ячейка на двумерной сетке быть цветной или оставлена ​​пустой, используя количество последовательных цветных ячеек в каждой строке.

Ниже приведен пример головоломки Nonogram с решением.

Проблема в том, что некоторые коммерческие игры / мобильные приложения Nonogram имеют загадки, которые не могут быть решены вручную (например, имеют несколько решений или требуют глубокого возврата). Тем не менее, они также предлагают несколько жизней игроку, где одна жизнь теряется, когда вы пытаетесь закрасить ячейку, правильный ответ которой пуст . Так что теперь пришло время перебрать эти неприятные «загадки»!

Чтобы упростить задачу, представьте только одну строку с ее подсказкой и ничего больше:

3 7 | _ _ _ _ _  _ _ _ _ _  _ _ _ _ _

[3,7]Являются ключами, а длина линии 15 сот. Поскольку у него есть несколько возможных решений, нам нужно рискнуть несколькими жизнями, чтобы полностью решить эту линию (т.е. определить все цветные ячейки).

Вызов

Учитывая строку с подсказками (список положительных целых чисел) и длину линии, найдите максимальное количество жизней, которые вы потеряете, предполагая, что вы перебираете линию с оптимальной стратегией.

Обратите внимание, что вы всегда угадываете цветные клетки . В реальных играх угадывание пустых ячеек (правильных или неправильных) не влияет на вашу жизнь, поэтому вы не можете «решить» головоломку таким образом.

Кроме того, вы можете предположить, что входные данные всегда представляют допустимую строку Nonogram, поэтому вам не нужно беспокоиться о чем-то подобном [6], 5.

объяснение

Давайте сначала посмотрим на несколько простых примеров.

[1,2], 5

Для этой строки есть ровно три варианта ( Oцветная ячейка, .пустая):

O . O O .
O . . O O
. O . O O

Если мы попробуем закрасить ячейку 0 (индекс на основе 0 слева), произойдет одно из следующего:

  • Ячейка правильно окрашена. Теперь у нас есть две возможности, и мы можем выбирать между ячейкой 2 и ячейкой 4, чтобы полностью решить линию. В любом случае мы потеряем одну жизнь в худшем случае.
  • Клетка пуста, и мы теряем жизнь. В этом случае мы уже определили уникальное решение для этой линии, поэтому мы закончили с 1 потерянной жизнью.

Следовательно, ответ для [1,2], 51.

[5], 10

Бинарный поиск? Нет.

Самый очевидный первый выбор - 4 или 5, который покажет одну возможность, если он пуст (по цене 1 жизни). Допустим, мы выбрали 4 в первую очередь. Если ячейка 4 действительно окрашена, мы расширяем ее влево, то есть пробуем 3, 2, 1 и 0 до тех пор, пока жизнь не будет потеряна (или ячейка 0 окрашена, и в итоге мы не тратим вообще никаких жизней). Всякий раз, когда жизнь теряется, мы можем однозначно определить решение, например, если мы увидим что-то вроде этого:

_ _ X O O _ _ _ _ _

тогда мы уже знаем ответ таков:

. . . O O O O O . .

Таким образом, ответ для [5], 10также 1.

[3,7], 15

Начните с ячейки 11, которая, если она пуста, сразу покажет следующее решение.

O O O . O O O O O O O X . . .

Затем попробуйте 12, которое, если оно пустое, дает две возможности, которые могут быть решены ценой 1 дополнительной жизни.

O O O . . O O O O O O O X . .
. O O O . O O O O O O O X . .

Теперь попробуйте 2. Если пусто, это приводит к трем возможностям, которые могут быть решены аналогично [1,2], 5примеру.

. . X O O O . O O O O O O O .
. . X O O O . . O O O O O O O
. . X . O O O . O O O O O O O

Если вы продолжите минимизировать риск таким способом, вы можете достичь любого решения с макс. 2 жизни потрачено.

Тестовые случаи

[1,2] 5 => 1
[2] 5 => 2
[1] 5 => 4
[] 5 => 0
[5] 10 => 1
[2,1,5] 10 => 0
[2,4] 10 => 2
[6] 15 => 2
[5] 15 => 2
[4] 15 => 3
[3,7] 15 => 2
[3,4] 15 => 3
[2,2,4] 15 => 4
[1,1,1,1,1,1,1] 15 => 2

[2,1,1,3,1] 15 => 3
[1,1,1,2,1] 15 => 5

Для последних двух случаев оптимальная стратегия - не проходить через минимальные пробелы, а просто проходить слева направо (или справа налево). Спасибо @crashoz за указание на это.

правила

Применяются стандартные правила . Побеждает самая короткая действительная передача в байтах.

премия

Если кто-то придумает алгоритм с полиномиальным временем (с доказательством правильности), я присуду +100 наград за такое решение.

фонтанчик для питья
источник
Для чего предназначен выход [6], 5?
Утренняя монахиня
Когда вы делаете предположение, нужно ли угадывать, что клетка черная, или вы можете угадать либо черную, либо белую?
feersum
@LeakyNun Это недопустимая строка. Вы можете предположить, что ввод всегда является допустимой линией Nonogram.
Bubbler
@feersum Вы всегда угадываете цветные клетки. В реальных играх угадывание пустой ячейки (правильной или неправильной) не влияет на вашу жизнь, поэтому вы не можете получить никакой обратной связи с ней.
Bubbler
Фантастическая задача
Энрико Борба

Ответы:

19

Рубин , 85 байт

f=->l,n,s=n-l.sum-l.size+1{*a,b=l;b&&s>0?(a[0]?1+f[a,n-b-2,s-1]:(n.to_f/b).ceil-1):0}

Попробуйте онлайн!

объяснение

l=[l1,l2,...,lx]xn

lx
nlx
nlx1+f(l,nlx)
1+f(l~,nlx2)l~l

f(l,n)={0,if 1xli+x1nnlx1if x=11+max{f(l,nlx)f(l~,nlx2),otherwise

Вот пример _неизвестны, Xэто известное пространство, Oэто известная цветная клетка и Lпотеря жизни

[2,2,4] 15                  _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
(1) -> [2,2,4] 11           _ _ _ _ _ _ _ _ _ _ _ L X X X
    (1) -> [2,2,4] 7        _ _ _ _ _ _ _ L X X X L X X X
        0                   X X X L X X X L X X X L X X X
    (2) -> [2,2] 5          _ _ _ _ _ X O O O O L L X X X
        0                   O O X O O X O O O O L L X X X 
(2) -> [2,2] 9              _ _ _ _ _ _ _ _ _ X O O O O L
    (1) -> [2,2] 7          _ _ _ _ _ _ _ L X X O O O O L
        (1) -> [2,2] 5      _ _ _ _ _ L X L X X O O O O L
            0               O O X O O L X L X X O O O O L
        (2) -> [2] 3        _ _ _ X O O L L X X O O O O L
            1               O O L X O O L L X X O O O O L               
    (2) -> [2] 5            _ _ _ _ _ X O O L X O O O O L
        2                   O O L L X X O O L X O O O O L

Это делает бинарное дерево, чтобы получить количество потерянных жизней, нам просто нужно посчитать максимальную высоту дерева. Однако это делает его потому что мы оцениваем все ветви. Мы можем сделать лучше.O(2n)

Давайте определим функцию стоимости , которая поможет нам «выбрать» правильную ветвь на каждом шаге.h

h(l,n)=n1xlix+1

h подсчитывает количество лишних пробелов, которые у нас есть, если мы упаковываем все подсказки с одним пробелом между ними. Таким образом, это, по сути, показатель того, сколько жизней нам понадобится для решения данного случая, чем он выше, тем больше жизней необходимо. Идея состоит в том, чтобы применить этот индикатор к нашей рекурсивной формуле.

По определению имеем: h

h(l,nlx)=nlx1xlix+1=(n1xlix+1)lx=h(l,n)lx

И,

h(l~,nlx2)=nlx21x1li(x1)+1=(n1xlix+1)1=h(l,n)1

Тогда

h(l,n)={0,if 1xli+x1nnlx,if x=1max{h(l,nlx)+lxh(l~,nlx2)+1,otherwise

Мы хотим максимизировать на каждом шаге, чтобы получить худший случай, поэтому давайте проверим разницу между двумя выражениями в повторяемостиh

[h(l,nlx)+lx][h(l~,nlx2)+1]=nlxn1xlix+1+lx[nlx21x1li(x1)+1+1]=2

[h(l,nlx)+lx][h(l~,nlx2)+1]=2[h(l,nlx)+lx][h(l~,nlx2)+1]<0[h(l,nlx)+lx]<[h(l~,nlx2)+1]
Таким образом, 2-е выражение всегда максимально, мы можем удалить первое

h(l,n)={0,if 1xli+x1nnlx,if x=1h(l~,nlx2)+1otherwise

Наконец, это рекурсивное определение показывает нам, что опция (2) в функции всегда является наихудшим случаем (давая максимальное количество возможностей, т.е. максимизируя )hfh

f(l,n)={0,if 1xli+x1nnlx1if x=11+f(l~,nlx2),otherwise

Каждый шаг мы уменьшаем как минимум на 3, и теперь есть один рекурсивный вызов, сложностьO ( n )nO(n)

crashoz
источник
2
Добро пожаловать в PPCG, невероятный первый пост!
Коул
1
@cole Это не первый их пост, но это невероятно! Очень умный подход +1
г-н Xcoder
1
Потрясающая работа. Я назначу награду через 2 дня, если до этого никто не обнаружит серьезного логического недостатка.
Bubbler
2

Python, 303 289 байт

Первый гольф в течение длительного времени, поэтому может быть много лишнего жира. (Спасибо, Джо Кинг, что нашел 14 байтов.)

Функция f генерирует все возможные расположения (хотя всегда с пробелом в качестве первого символа, но это нормально, если мы увеличиваем длину на 1, прежде чем мы ее вызовем). Функция g выбирает позицию с наименьшим количеством пробелов и рекурсов. Функция h объединяет их.

f=lambda l,n:["."*i+"X"*l[0]+c for i in range(1,n-l[0]+1)for c in f(l[1:],n-i-l[0])]if l else["."*n]
def g(q,n):O,X=min([[[p[:i]+p[i+1:]for p in q if p[i]==u]for u in".X"]for i in range(n)],key=lambda x:len(x[0]));return(len(q)>1)*1and max(1+g(O,n-1),g(X,n-1))
h=lambda l,n:g(f(l,n+1),n+1)

Все примеры работают нормально:

>>> h([3,7],15)
2
>>> h([3,4],15)
3
>>> h([1,1,1,2,1],15)
6
Ури Гранта
источник
1
Вы позволили ли вернуться Falseк 0? Если это так, вы можете изменить (len(q)>1)*1andна len(q)>1and. Если вы не можете вернуться Falseк 0, а затем сделать это, но изменение g(f(l,n+1),n+1)в , 1*g(f(l,n+1),n+1)и он будет по- прежнему сохранить один байт
Захари
1
Еще лучше: в этом случае Falseнельзя 0вместо того, g(f(l,n+1),n+1)чтобы 1*g(f(l,n+1),n+1)менять его на+g(f(l,n+1),n+1)
Zacharý
2
Кроме того, вам не нужно подсчитывать количество h=байтов
Zacharý
1
288 байт .
Джонатан Фрех