2

最近我了解到了 nim 游戏和 grundy number 我被困在一个问题上。请给我一些想法

问题: A和B用一堆石头玩游戏。A开始比赛,他们交替移动。在每一步中,玩家必须从堆中移除至少一个且不超过 sqrt 的数字石头。因此,例如,如果一堆包含 10 块石头,那么玩家可以从这堆中取出 1、2、3 块石头。A和B都玩得很完美。无法进行有效移动的玩家输了。现在你得到了石头的数量,你必须找到如果双方都打得最好的将获胜的球员。例子

n=3 一场胜利,

n=5 B 赢

n<=10^12

我可以通过使用 Grundy 数https://www.topcoder.com/community/data-science/data-science-tutorials/algorithm-games/用少量石头解决这个问题?

grundy 函数是 g(x),其中 x 是剩余的石头。调用 F(s) 是我们可以从 x 石头中获得的剩余石头数量的集合。如果 s 是终端位置,则 g(s)=0

如果 s 不是终端位置,令 X = {g(t)| t 在 F(s)} 中。那么,grundy number of s 是 X 中不存在的大于等于 0 的最小整数。

例如 x=10 所以 F(x)={9,8,7} 取 1,2 或 3 个石头。(sqrt(10)<=3)

如果 g(n)>0 => 第一个玩家获胜

g(0)=0

g(1)=1

g(2)=0

g(3)=1

g(4)=2 ....

但是这个算法很慢。

提前致谢。

4

5 回答 5

3

我要添加第二个答案,因为我的第一个答案提供了没有优化的背景理论。但是由于 OP 显然正在寻找一些优化和一个没有大量递归的非常快速的解决方案,我接受了我自己的建议:

当然,真正快速的方法是做更多的数学运算并找出 n 的一些简单属性,您可以检查这些属性将确定它是赢家还是输家。

我将使用我在那里定义的术语,所以如果这没有意义,请阅读该答案!具体来说,n是堆大小,k是要移除的棋子数量,如果玩家 A 从大小为n的堆开始有获胜策略,则n为赢家,否则为输家。让我们从以下关键见解开始:

大多数数字都是赢家。

为什么这是真的?对于小数字来说,这并不明显:0 是输家,1 是赢家,2 是输家,3 是赢家,4 也是如此,但 5 又是输家。但是对于更大的数字,它变得更加明显。

如果某个整数p很大并且是失败者,那么p+1, p+2, ... p+k_m都是一些k_m的获胜者,k_m的大小大约为 sqrt(p)。这是因为一旦我找到了一个失败者,对于任何不比这大太多的堆,我可以移除一些石头让我的对手留下那个失败者。关键是确定k的最大有效值是多少,因为k是根据起始堆大小n而不是最终堆大小p定义的。

所以问题变成了,给定一些整数p ,对于k <= sqrt(n) where n = p+k , k的哪个值是真的。换句话说,给定p,什么样的起始堆大小n允许我删除k并让我的对手留下p。好吧,既然n = p+k并且值都是非负的,我们必须有

k <= sqrt(n) = sqrt(p+k) ==> k^2 <= p + k ==> k^2 - k - p <= 0。

对于p的任何固定值,这是k中的二次方程。可以使用二次公式找到解决方案集的端点:

k = (1 +- sqrt(1 + 4p))/2

因此,对于(1-sqrt(1+4p))/2 和 (1+sqrt(1+4p))/2 之间的k值,不等式得到满足。当然,sqrt(1+4p) 至少是 sqrt(5) > 2,所以左端点为负数。那么k_m = floor((1+sqrt(1+4p))/2)

更重要的是,我声称p之后的下一个失败者是数字L = p + k_m + 1。让我们尝试证明这一点:

定理:如果p是失败者,则L = p + k_m + 1也是失败者,并且每个整数p < n < L都是获胜者。

证明:我们已经证明了区间[p+1, p+k_m]中的每个整数n都是赢家,所以我们只需要证明L是输家即可。

相反,假设L是赢家。然后存在一些1 <= k <= sqrt(L)使得L - k是失败者(根据定义)。因为我们已经证明整数p+1, ..., p+k_m是赢家,所以我们必须有L - k <= p,因为没有小于L和大于p的数字可以成为输家。但这意味着L <= p + kk满足方程k <= sqrt(L) <= sqrt(p + k)。我们已经证明方程k <= sqrt(p + k)的解不大于(1+sqrt(1+4p))/2,所以任何整数解必须满足k <= k_m。但随后L - k = p + k_m + 1 - k >= p + k_m + 1 - k_m = p + 1。这是一个矛盾,因为p < L - k < L并且我们已经证明没有大于p和小于L的失败者。

量子点

上述定理为我们提供了一个很好的方法,因为我们现在知道获胜者落入由单个失败者分隔的整数区间,并且我们知道如何计算两个失败者之间的间隔。特别是,如果p是失败者,则p + k_m + 1是失败者,其中

k_m = floor((1+sqrt(1+4p))/2)

现在我们可以以纯迭代的方式重写函数,这种方式应该很快并且需要恒定的空间。该方法只是计算失败者的序列,直到我们找到n(在这种情况下它是失败者)或确定n位于两个失败者之间的区间内。

bool is_winner(int n) {
  int p = 0;
  // loop through losers until we find one at least as large as n
  while (p < n) {
    int km = floor((1+sqrt(1+4p))/2);
    p = p + km + 1;
  }

  /* if we skipped n while computing losers, 
   * it is a winner that lies in the interval 
   * between two losers. So n is a winner as 
   * long as it isn't equal to p.*/
  return (p != n);  
}
于 2015-09-13T18:36:40.370 回答
2

你必须从头开始递归地思考这个游戏:显然,要赢,你必须拿下最后一块石头。

  • 1 个石头:第一个玩家获胜。轮到A拿走唯一的石头了。

  • 2个石头:第二个玩家获胜。A 不能拿两块石头,但必须拿一颗。所以 A 被迫拿走一块石头,而另一块留给 B 拿。

  • 3个石头:先手获胜。仍然没有选择。A 必须拿一颗子,然后微笑,因为他们知道 B 拿两颗子是赢不了的。

  • 4个石头:先手获胜。现在A可以选择留下两三颗石头。综上所述,A 知道 B 出 3 颗子就赢,B 出 2 颗就输,所以 A 拿了 2 颗。

  • 5个石头:第二个玩家获胜。即使 A 可以选择留下 3 颗或 4 颗棋子,B 也可以在任意一个数量下获胜。

n正如您所看到的,您可以通过完全了解棋局的结果,1轻松计算出谁将赢得棋局n-1

因此,算法解决方案将创建一个布尔数组winswins[i]如果给定i棋子的玩家将赢得游戏,则该数组为真。wins[0]被初始化为false。然后通过扫描数组的可到达部分以查找错误条目,从一开始就迭代地填充数组的其余部分。如果发现一个假条目,则将当前条目设置为真,因为 A 可以让 B 处于松散状态,否则设置为假。

于 2015-09-13T17:25:38.803 回答
2

我将以 cmaster 的回答为基础,因为它已经非常接近了。问题是如何有效地计算这些值。

答案是:我们不需要整个数组。只有false值是有趣的。我们来分析一下:

如果我们false在数组中有一个值,那么接下来的几个条目将是true因为它们可以移除石头,这样其他玩家就会落在该false值上。true问题是:会有多少条目?

如果我们在falseentry z,那么 entryx将是一个trueentry if x - sqrt(x) <= z。我们可以解决这个问题x并得到:

x <= 1/2 * (1 + 2 * z + sqrt(1 + 4 * z))

这是最后一个true条目。例如z = 2,这返回4。下一个条目将是错误的,因为玩家只能移除棋子,这样对手就会出现在一个true条目中。

知道了这一点,我们的算法就差不多完成了。从已知false值(例如 0)开始。然后,迭代地移动到下一个false值,直到达到n

bool isWinner(long long n)
{
    double loser = 0;
    while(n > loser)
        loser =  floor(0.5 * (1 + 2 * loser + sqrt(1 + 4 * loser))) + 1;
    return n != loser;
}
于 2015-09-13T17:58:51.957 回答
0

像这样的游戏(河内塔是另一个经典例子)旨在说明归纳和递归的数学原理,递归与编程特别相关。

我们想确定一堆n块石头是赢堆还是输堆。直观地说,赢牌堆是这样的,无论对手做出什么样的选择,你总能拿走一定数量的棋子来保证你会赢。同样,失败的牌堆是这样一种牌堆,无论你做出什么选择,你总是给对手留下一个获胜的策略。

显然,n = 0是输桩;你已经输了。并且n = 1是一个胜利堆,因为你拿了一块石头并离开你的对手n=0。那么n=2呢?好吧,你只被允许拿一块石头,此时你已经给了你的对手一个获胜的堆(n=1),所以n=2是一个失败的数字。我们可以使这在数学上更精确。

定义:如果n=0 ,则整数n失败者,或者对于1 和sqrt(n)之间的每个整数knk胜利者。如果在 1 和sqrt(n)之间存在某个整数k使得nk失败者,则整数n获胜者。

在这个定义中,n是堆的大小,k是你选择拿的石头的数量。如果要移除的每一个有效数量的棋子都给你的对手一个获胜的堆,那么一个堆就是一个失败的堆,而一个获胜的堆是某个选择给你的对手一个失败的堆的堆。

当然,这个定义应该让你有点不安,因为我们实际上不知道它是否对我们已经检查过的n=0,1,2以外的任何东西有意义。也许某个数字既符合赢家符合输家的定义,或者两者都不符合。这肯定会令人困惑。这就是归纳的用武之地。

定理:每个非负整数要么是赢家,要么是输家,但不是两者兼而有之。

证明:我们将使用强或完全归纳原理。我们知道n=0是失败者(根据定义),我们已经证明n=1是胜利者,而n=2直接是失败者。这些是我们的基本情况。

现在让我们考虑一些整数n_0 > 2,我们将假设(使用强归纳法)每个小于n_0的非负整数要么是赢家要么是输家,但不是两者兼而有之。让s = floor(sqrt(n_0))并考虑整数集P = {n_0-s, n_0-s+1, ..., n_0 - 1}。(因为{1, 2, ..., s}是要移除的可能选择的棋子的集合,所以P是我可以留给对手的棋子的集合。)通过强归纳,因为P中的每个值都是非负数小于n_0的整数,它们每个都是赢家或输家(但不是两者)。如果P中的任何值是失败者,那么根据定义,n_0是赢家(因为您移除了足够多的石头,让您的对手输掉了一堆)。如果不是,那么P中的每个值都是赢家,那么n_0就是输家(因为无论你拿了多少石头,你的对手仍然有一堆赢家)。因此,n_0要么是赢家,要么是输家,但不是两者兼而有之。

通过强归纳,我们得出结论,每个非负整数要么是赢家,要么是输家,但不是两者兼而有之。

量子点

好的,如果您对归纳感到满意,那将非常简单。但我们所展示的是,我们非常直观的定义实际上是有道理的,你得到的每一堆要么是赢家(如果你打得正确),要么是输家(如果你的对手打得好)。我们如何确定哪个是哪个?

好吧,归纳导致递归。让我们为我们的两个定义编写递归函数:n是赢家还是输家?这是一些没有错误检查的类 C 伪代码。

bool is_winner(int n) {
  // check all valid numbers of stones to remove (k)
  for (int k = 1; k <= sqrt(n); k++) {
    if (is_loser(n-k)) {
      // I can force the loser n-k on my opponent, so n is a winner
      return true;
    }
  }
  // I didn't find a way to force a loser on my opponent, so this must be a loser.
  return false;
}

bool is_loser(int n) {
  if (n == 0) {  // this is our base case
    return true;
  }
  for (int k = 1; k <= sqrt(n); k++) {
    if (!is_winner(n-k)) {
      // we found a way to give them a pile that ISN'T a winner, so this isn't a loser
      return false;
    }
  }
  // Nope: every pile we can give our opponent is a winner, so this pile is a loser
  return true;
}

当然,上面的代码有些多余,因为我们已经证明每个数字要么是赢家,要么是输家。因此,将其实现is_loser为仅返回更有意义,!is_winner反之亦然。也许我们只是is_winner作为一个独立的实现来做。

bool is_winner(int n) {
  if (n < 0) {
    // raise error
  } else if (n == 0) {
    return false; // 0 is a loser
  } else {
    for (int k = 1; k <= sqrt(n); k++) {
      if (!is_winner(n-k)) {
        // we can give opponent a loser, this is a winner
        return true;
      }
    }
    // all choices give our opponent a winner, this is a loser
    return false;
  }
}

用这个函数来回答这个问题,如果游戏开始时有n颗棋子,玩家 A 先走,并且两个玩家都玩得最佳,那么玩家 A 获胜,如果is_winner(n)玩家 B 获胜!is_winner(n)。要弄清楚他们的游戏应该是什么,如果你有一个获胜的牌堆,你应该选择一些有效的k使得nk是失败者(哪个不重要,但最大的值将使游戏以最快的速度结束),如果给你一个失败的堆,你选择什么都没关系——这就是失败者的点,但同样,选择最大的k值会使游戏更快结束。

这些都没有真正考虑性能。由于n可能非常大,因此您可以考虑很多事情。例如,至少在单个递归调用中预先计算您要考虑的n的常见小值,或使用Memoization 。此外,正如我之前所建议的,移除k的最大值会以更少的回合结束游戏。同样,如果您反转循环并首先检查k的最大允许值,您应该能够减少递归调用的数量。

当然,真正快速的方法是做更多的数学运算并找出n的一些简单属性,您可以检查这些属性将确定它是赢家还是输家。

于 2015-09-13T17:26:01.870 回答
-1
public class Solution {
    public boolean canWinNim(int n) {
        if(n % 4 == 0)
        {
            return false;
        }
        else
        {
            return true;
        }
    }
}
于 2015-10-23T07:40:04.323 回答