我正在实现一个m,n,k-game,一个通用版本的tic-tac-toe,其中 m 是行数, n 是数字列和 k 是玩家需要连续赢得的棋子数。我已经实现了一场胜利的检查,但是如果没有球员可以赢得比赛,我还没有想出一个令人满意的方法来检查董事会是否满满的。换句话说,棋盘上可能有空位,但是不能以一个玩家赢的方式填补。
我的问题是,如何有效地检查?以下算法是我能想到的最好的算法。它检查两个条件:
A。遍历所有4个方向的所有电路板位置(从上到下,从右到左,以及两个对角线方向)。如果说 k = 5,并且找到4(= k -1)个连续的空槽,则停止检查并报告“无关系”。这没有考虑到以下情况:
OX----XO (Example 1)
其中a)在两个-
之间有4个空的连续槽(X
),b)接下来它是O
,c)有少于棋盘上的其他四个空位置,没有玩家可以通过将棋子放到那些位置来获胜,并且d)在所示的位置中不可能在任何其他方向上获胜而不是横向赢得。现在我们知道它是一个平局,因为O
最终会阻止最后一次获胜的可能性,但错误的是它还没有被报告,因为有四个连续的空位。那没关系(但不是很好)。当检查算法通常早期发现这种情况时,检查这种情况会在开始时提供良好的加速,但随着更多的碎片被放在板上,它会变慢。
B。如果不满足此 k -1-continuous-slots-slots条件,算法将在所有4个方向上连续再次检查时隙。假设我们目前正在从左到右进行检查。如果在某个时刻遇到X
并且前面有O
或-
(空插槽)或电路板边框,则开始计算连续X
的数量这是第一次遇到X
的空插槽。如果一个人可以数到5,那么就知道X
有可能获胜,并且报告“没有平局”。如果在连续5个O
之前遇到以X
开头的X
,则X
无法从我们开始计数的位置开始从左到右赢取这5个广告位。例如:
X-XXO (Example 2)
12345
这里我们开始检查位置1,计入4,并遇到O
。在这种情况下,人们将以相同的方式从遇到的O
继续,这次尝试找到5个连续的O
或空槽。在计算X
或空插槽的另一种情况下,在计数到5之前,遇到前面有一个或多个空插槽的O
。例如:
X-X-O (Example 3)
12345
在这种情况下,我们将再次从位置5的O
继续,但是添加到新计数器(连续O
或空位)前面的连续空位数{ {1}},这里1,所以我们不会错过这个可能的获胜位置:
O
这样,在最坏的情况下,必须经过4次所有位置(4个方向,当然还可以跳过长度小于 k 的对角线),时间 O(mn)。
我能想到的最好方法就是一次性完成这两个描述的检查A和B.如果检查算法通过所有方向的所有位置而没有报告“无关系”,则报告平局。
知道你可以通过检查最后一块添加了运行时间X-X-O---X (Example 4)
的附近区域来检查胜利,我想知道是否有更快的方法来提前检查领带。不必渐近渐近。我目前正在将这些碎片保存在二维阵列中。是否有可以进行有效检查的数据结构?一种方法:在运行任何支票检查之前,人们可以等待玩家的最高移动阈值是什么?
Stack Overflow上有很多相关的问题,例如this,但我发现的所有讨论都只指出了明显的关系条件,其中移动的数量等于电路板的大小(或者他们检查电路板是否已满,或仅处理电路板为方形的特殊情况: m = n 。例如,this answer声称会在固定时间内检查并列关系,但仅在 m = n = k 时才有效。我有兴趣尽早报告关系,一般 m,n 和 k 。此外,如果算法适用于两个以上的玩家,那将是整洁的。
答案 0 :(得分:4)
我会减少确定与更容易的子问题相关的问题:
玩家X能否获胜?
如果答案是'否'对于所有球员来说,这是一个平局。
了解玩家X是否可以获胜:
return false
。return false
。return true
。(此算法将报告玩家X的可能胜利,即使在X的唯一获胜动作会让另一位玩家获得第一名的情况下也是如此,但这没关系,因为这也不是一个平局)
如果如你所说,你可以通过检查最后一块添加了运行时间O(k)
的附近区域来检查胜利,那么我认为你可以在{{1}中运行上述算法}:添加所有虚拟X片,记下添加片段附近的任何获胜组合。
空槽的数量可能很大,但只要至少有一个大小为k的空行且玩家X仍然向左移动直到棋盘被填满,你可以确定玩家X仍然可以获胜,所以你不需要进行全面检查。
这适用于任何数量的玩家。
答案 1 :(得分:2)
实际上,您引用的常数时间解决方案仅在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)。
答案 2 :(得分:1)
如果空间不是问题,我有这个想法:
对于每个玩家保持一个结构大小(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."
}
}
答案 3 :(得分:1)
让我们看一行(或列或对角线,无关紧要)并计算长度 k (“ k -line”)的获胜行数)可以在行的每个位置为玩家X
制作。该解决方案将在游戏过程中跟踪该数字,检查每次移动中获胜条件的实现以及检测并列。
1 2 3... k k k k... 3 2 1
有一个 k -line,在最左边的插槽中包含一个X,两个从左边开始包含第二个插槽,依此类推。如果对方玩家O
或其他方式在此行中玩,我们可以减少O( k 中玩家X
的 k - 线可能性计数em>)移动时的时间。 (在执行示例之后,此步骤的逻辑应该是直截了当的,不需要其他数据结构,但任何涉及检查 k 的每个 k 行的方法都可以。从左到右,只需要对计数进行 k 操作。)敌人应该将可能性计数设置为-1。
然后,可检测地绑定的游戏是任何玩家没有单元格具有非零 k 线可能性计数的游戏。通过跟踪第一个非零单元的索引来检查这一点很容易。维持结构相当于每次移动的O(k *玩家)工作。对于可能被绑定的位置,空槽的数量小于填充的槽数,因此其他答案适合于单独检查位置。 但是,至少对于数量相当少的玩家来说,这个问题与首先检查获胜条件密切相关,至少你必须这样做,O( k ),一举一动。根据您的游戏引擎,可能会有一个更好的结构,足以找到好的移动以及检测关系。但计数结构的可能性有一个很好的属性,你可以在更新时检查获胜。