Как эффективно обнаруживать галстук в начале m, n, k-игры (обобщенный tic-tac-toe)?

141
20

Я реализую m,n,k-game, обобщенную версию tic-tac-toe, где m - количество строк, n - количество столбцов, а k - количество частей, которые игрок должен помещать в выигрышную строку. Я выполнил чек на победу, но я не нашел удовлетворительного способа проверить, до того, как на доске полно кусков, если ни один игрок не может выиграть игру. Другими словами, на доске могут быть пустые слоты, но они не могут быть заполнены так, что один игрок победит.


Мой вопрос в том, как эффективно это проверить? Следующий алгоритм - лучшее, что я могу придумать. Он проверяет два условия:


A. Перейдите по всем позициям доски во всех четырех направлениях (сверху вниз, справа налево и обоих по диагонали). Если говорят k = 5 и 4 (= k-1), то будут найдены последовательные пустые слоты, остановите проверку и сообщите "no tie". Это не учитывает, например, следующую ситуацию:


OX----XO    (Example 1)

где a) есть 4 пустых последовательных слота (-) где-то между двумя X 's, b), затем он O turn, c) на доске меньше четырех других пустых позиций ни один игрок не может выиграть, наложив на них кусочки, и d) невозможно выиграть в любом другом направлении, чем горизонтально в показанных слотах. Теперь мы знаем, что это связь, потому что O в конечном итоге блокирует последнюю возможность выигрыша, но ошибочно пока не сообщается, потому что есть четыре последовательных пустых слота. Это было бы нормально (но не здорово). Проверка этого условия дает хорошее ускорение в начале, когда алгоритм проверки обычно находит такой случай раньше, но он замедляется по мере того, как на доску помещаются больше предметов.


B. Если это условие k-1-next-empty-slots не выполняется, алгоритм будет проверять слоты снова последовательно во всех 4 направлениях. Предположим, что мы сейчас проверяем слева направо. Если в какой-то момент встречается a X, и ему предшествуют O или - (пустой слот) или граница доски, тогда начните подсчитывать количество последовательных X и пустых слотов, считая в этом первом столкнулся с X. Если можно сосчитать до 5, то известно, что выигрыш X возможен, и сообщается о "отсутствии привязки". Если O, которому предшествует X, встречается до 5 последовательных X, то X не может выиграть в этих 5 слотах слева направо, начиная с того, с чего мы начали подсчитывать. Например:


X-XXO    (Example 2)
12345

Здесь мы начали проверять позицию 1, подсчитанную на 4, и столкнулись с O. В этом случае один из них будет продолжен из встреченного O таким же образом, пытаясь найти 5 последовательных O или пустых слотов на этот раз. В другом случае при подсчете X или пустых слотов встречается O, которому предшествует один или несколько пустых слотов, до подсчета до 5. Например:


X-X-O    (Example 3)
12345

В этом случае мы снова продолжим с O в позиции 5, но добавим к новому счетчику (последовательных O или пустых слотов) количество последовательных пустых слотов, которые предшествовали O, здесь 1, так что мы не пропустили бы, например, эту возможную выигрышную позицию:


X-X-O---X    (Example 4)

Таким образом, в худшем случае нужно было бы пройти все позиции 4 раза (4 направления и, конечно, диагонали, длина которых меньше k, можно пропустить), давая время работы O (mn).


Лучший способ, о котором я мог думать, заключался в выполнении этих двух описанных проверок A и B за один проход. Если алгоритм проверки проходит через все позиции во всех направлениях, не сообщая "нет связи", он сообщает о связи.


Зная, что вы можете проверить выигрыш, просто проверив около последней части, которая была добавлена ​​с временем выполнения O(k), мне было интересно, есть ли более быстрые способы ранней проверки галстука. Не должно быть асимптотически быстрее. В настоящее время я держу куски в двумерном массиве. Может быть, структура данных, которая позволила бы эффективно проверять? Один из подходов: каков самый высокий порог ходов, который можно дождаться, пока игроки не смогут выполнить какие-либо проверки на галстук?


В Qaru есть много связанных вопросов, например , но все обсуждения, которые я мог найти, либо указывали только на очевидное условие привязки, где число ходов (или они проверены, если плата заполнена), или обрабатывается только особый случай, когда плата является квадратной: m = n. Например этот ответутверждает, что делает проверку на связь в постоянное время, но работает только при m = n = k. Мне интересно сообщать об этом как можно раньше и для общих m, n и k. Также, если алгоритм работает более чем для двух игроков, это будет аккуратно.

спросил(а) 2021-01-25T22:30:46+03:00 5 месяцев назад
1
Решение
99

Я бы уменьшил проблему определения привязки к более простой суб-проблеме:
Может ли игрок X по-прежнему побеждать?


Если ответ "нет" для всех игроков, это галстук.


Чтобы узнать, победит ли игрок X:


    заполнить все пробелы виртуальными "X'-частями"
    есть ли k 'X'-куски подряд подряд?
      если нет → Игрок X не может выиграть. return false.
      если есть, найдите ряд k камней с наименьшим количеством виртуальных кусков. Подсчитайте количество виртуальных фрагментов в нем.
      подсчитайте количество перемещений игрока X, чередующихся со всеми другими игроками, до тех пор, пока плата не будет полностью заполнена.
      Если количество ходов меньше количества виртуальных предметов, необходимых для победы, игрок X не может выиграть. return false.
      в противном случае игрок X все равно может выиграть. return true.

(Этот алгоритм сообщит о возможной победе для игрока X даже в тех случаях, когда только выигрышные ходы для X должны были бы выиграть другой игрок, но это нормально, так как это не было бы ничностью)


Если, как вы сказали, вы можете проверить выигрыш, просто проверив его рядом с последней частью, добавленной с временем выполнения O(k), тогда я думаю, что вы можете запустить описанный выше алгоритм в O(k * Number_of_empty_spots): Добавить все виртуальный X-Piece, обратите внимание на любые выигрышные комбинации поблизости от добавленных частей.


Количество пустых слотов может быть большим, но до тех пор, пока существует хотя бы одна пустая строка размера k, а игрок X все еще перемещается налево до тех пор, пока доска не будет заполнена, вы можете быть уверены, что игрок X все еще может выиграть, поэтому вам не нужно выполнять полную проверку.


Это должно работать с любым количеством игроков.

ответил(а) 2021-01-25T22:30:46+03:00 5 месяцев назад
63

Если пространство не является проблемой, у меня возникла эта идея:


Для каждого игрока поддерживается размер структуры (2mn + (1 - k) (m + n) + 2 (m - k + 1) (n - k + 1) + 2 (сумма 1 - (m - k) )), где каждое значение представляет собой, если один из других игроков движется, находится в одном отдельном интервале k-размера. Например, для игры 8-8-4 один элемент в структуре может представлять строку 1, ячейку от 0 до 3; другая строка 1, ячейка 1 - 4; и др.


Кроме того, одна переменная на игрока будет представлять, сколько элементов в их структуре все еще не установлено. Для установки элемента требуется только один шаг, показывающий, что этот k-интервал больше нельзя использовать для победы.


Для каждого перемещения требуется обновление между O (k) и O (4k) раз на одного игрока. Связь определяется, когда количество игроков превышает количество разных элементов.


Используя биты, количество байтов, необходимых для каждой структуры игрока, будет размером структуры, деленным на 8. Обратите внимание, что когда k = m = n, размер структуры равен 4 * k и время O (4) обновления. Менее половины мегабайта на игрока потребуется для игры 1000,1000,5.


Ниже приведен пример JavaScript.


var m = 1000, n = 1000, k = 5, numberOfPlayers = 2

, numberOfHorizontalKIs = m * Math.max(n - k + 1,0)
, numberOfverticalKIs = n * Math.max(m - k + 1,0)
, horizontalVerticalKIArraySize = Math.ceil((numberOfHorizontalKIs + numberOfverticalKIs)/31)
, horizontalAndVerticalKIs = Array(horizontalVerticalKIArraySize)
, numberOfUnsetKIs = horizontalAndVerticalKIs
, upToM = Math.max(0,m - k) // southwest diagonals up to position m
, upToMSum = upToM * (upToM + 1) / 2
, numberOfSouthwestKIs = 2 * upToMSum //sum is multiplied by 2 to account for bottom-right-corner diagonals
+ Math.max(0,n - m + 1) * (m - k + 1)
, diagonalKIArraySize = Math.ceil(2 * numberOfSouthwestKIs/31)
, diagonalKIs = Array(diagonalKIArraySize)
, numberOfUnsetKIs = 2 * numberOfSouthwestKIs + numberOfHorizontalKIs + numberOfverticalKIs

function checkTie(move){
var row = move[0], column = move[1]

//horizontal and vertical
for (var rotate=0; rotate<2; rotate++){
var offset = Math.max(k - n + column, 0)

column -= offset

var index = rotate * numberOfHorizontalKIs + (n - k + 1) * row + column
, count = 0
while (column >= 0 && count < k - offset){
var KIArrayIndex = Math.floor(index / 31)
, bitToSet = 1 << index % 31

if (!(horizontalAndVerticalKIs[KIArrayIndex] & bitToSet)){
horizontalAndVerticalKIs[KIArrayIndex] |= bitToSet
numberOfUnsetKIs--
}
index--
column--
count++
}

//rotate board to log vertical KIs
var mTmp = m
m = n
n = mTmp
row = move[1]
column = move[0]
count = 0
}

//rotate board back
mTmp = m
m = n
n = mTmp

// diagonals
for (var rotate=0; rotate<2; rotate++){
var diagonalTopColumn = column + row

if (diagonalTopColumn < k - 1 || diagonalTopColumn >= n + m - k){
continue
} else {
var offset = Math.max(k - m + row, 0)

row -= offset
column += offset

var dBeforeM = Math.min (diagonalTopColumn - k + 1,m - k)
, dAfterM = n + m - k - diagonalTopColumn
, index = dBeforeM * (dBeforeM + 1) / 2
+ (m - k + 1) * Math.max (Math.min(diagonalTopColumn,n) - m + 1,0)
+ (diagonalTopColumn < n ? 0 : upToMSum - dAfterM * (dAfterM + 1) / 2)
+ (diagonalTopColumn < n ? row : n - 1 - column)
+ rotate * numberOfSouthwestKIs
, count = 0

while (row >= 0 && column < n && count < k - offset){
var KIArrayIndex = Math.floor(index / 31)
, bitToSet = 1 << index % 31

if (!(diagonalKIs[KIArrayIndex] & bitToSet)){
diagonalKIs[KIArrayIndex] |= bitToSet
numberOfUnsetKIs--
}
index--
row--
column++
count++
}
}
//mirror board
column = n - 1 - column
}

if (numberOfUnsetKIs < 1){
return "This player cannot win."
} else {
return "No tie."
}
}

ответил(а) 2021-01-25T22:30:46+03:00 5 месяцев назад
63

На самом деле решение по постоянному времени, на которое вы ссылаетесь, работает только тогда, когда k = m = n. Если k меньше, то я не вижу никакого способа адаптировать решение, чтобы получить постоянное время, в основном потому, что в каждой строке/столбце/диагонали есть несколько местоположений, где может появиться выигрышный последовательный k 0 или 1.


Однако сохранение вспомогательной информации для каждой строки/столбца/диагонали может дать ускорение. Для каждой строки/столбца/диагонали вы можете хранить начальное и конечное местоположения для последовательных вхождений 1 и пробелов как возможные выигрышные позиции для игрока 1 и аналогичным образом хранить начальные и конечные местоположения последовательных вхождений 0 и пробелов как возможные выигрышные позиции для игрока 0. Обратите внимание, что для заданной строки/столбца/диагонали интервалы для игроков 0 и 1 могут перекрываться, если они содержат пробелы. Для каждой строки/столбца/диагонали сохраните интервалы для игрока 1 в отсортированном порядке в самобалансирующемся двоичном дереве (обратите внимание, что вы можете сделать это, потому что интервалы не пересекаются). Аналогичным образом сохраняются интервалы для игрока 0, отсортированного по дереву. Когда игрок совершает движение, найдите строки/столбцы/диагонали, которые содержат местоположение перемещения, и обновите интервалы, содержащие перемещение, в соответствующем столбце строки и диагональных деревьях для игрока, который не сделал ход. Для игрока, который не совершил движение, это разделит интервал (если он существует) на меньшие интервалы, который вы можете заменить старым интервалом, а затем перебалансировать дерево. Если интервал когда-либо достигает длины меньше k, вы можете удалить его. Если дерево когда-либо становится пустым, то для этого игрока невозможно выиграть в этой строке/столбце/диагонали. Вы можете поддерживать счетчик того, сколько строк/столбцов/диагоналей невозможно выиграть для каждого игрока, и если счетчик когда-либо достигнет общего количества строк/столбцов/диагоналей для обоих игроков, то вы знаете, что у вас есть галстук. Общее время работы для этого - O (log (n/k) + log (m/k)), чтобы проверить привязку за ход, с дополнительным пространством O (mn/k).

Аналогичным образом можно поддерживать деревья, которые сохраняют последовательные интервалы в 1 (без пробелов) и обновлять деревья в O (log n + log m) время, когда выполняется перемещение, в основном поиск позиций до и после перемещения в вашем дерево и обновление найденного интервала (ов) и слияние двух интервалов, если найдены два интервала (до и после). Затем вы сообщаете выигрыш, если интервал когда-либо создается/обновляется и получает длину, большую или равную k. Аналогично для игрока 0. Общее время проверки выигрыша - O (log n + log m), которое может быть лучше O (k) в зависимости от того, насколько велико k. Дополнительным пространством является O (mn).

ответил(а) 2021-01-25T22:30:46+03:00 5 месяцев назад
63

Посмотрим на одну строку (или столбец или диагональ, это не имеет значения) и подсчитайте количество выигрышных строк длины k ( "k-line" ), чтобы в каждом месте в строке можно было создать игрок X. Это решение будет отслеживать это число в течение игры, проверяя выполнение условия выигрыша на каждом ходу, а также обнаруживая связь.


1 2 3... k k k k... 3 2 1


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


Затем игра с обнаруженной привязкой - это та, в которой ни одна клетка не имеет счетчик ненулевых k-линий для любого игрока. Это легко проверить, отслеживая индекс первой ненулевой ячейки. Поддержание структуры составляет O (k * players). Количество пустых слотов меньше заполненных, для позиций, которые могут быть привязаны, так что другие ответы хороши для проверки изолированной позиции. Однако, по крайней мере, для достаточно небольшого количества игроков эта проблема тесно связана с проверкой условия выигрыша в первую очередь, что, как минимум, должно быть сделано O (k), на каждом ходу. В зависимости от вашего игрового движка может быть лучшая структура, которая достаточно богата, чтобы находить хорошие ходы, а также обнаруживать связи. Но структура подсчета возможностей имеет приятное свойство, которое вы можете проверить на выигрыш, обновляя его.

ответил(а) 2021-01-25T22:30:46+03:00 5 месяцев назад
Ваш ответ
Введите минимум 50 символов
Чтобы , пожалуйста,
Выберите тему жалобы:

Другая проблема