📜  组合博弈论|第四套(Sprague – Grundy定理)

📅  最后修改于: 2021-06-25 17:00:31             🧑  作者: Mango

先决条件:肮脏的数字/木材和墨西哥
我们已经在Set 2(https://www.geeksforgeeks.org/combinatorial-game-theory-set-2-game-nim/)中看到,我们可以找到谁在Nim游戏中获胜而无需实际玩游戏。
假设我们稍微改变了经典的Nim游戏。这次,每个玩家只能移除1、2或3块宝石(而不是像Nim的经典游戏一样移除任何数量的宝石)。我们可以预测谁会赢吗?
是的,我们可以使用Sprague-Grundy定理预测获胜者。

什么是Sprague-Grundy定理?
假设有一个由N个子游戏和两个玩家A和B组成的复合游戏(一个以上的子游戏)。然后Sprague-Grundy定理说,如果A和B都玩得最优(即,他们没有(如果有任何错误),那么,如果在游戏开始时每个子游戏中排名靠前的位置的异或数为非零,则保证先赢的玩家获胜。否则,如果XOR评估为零,那么玩家A必定会输,无论如何。

如何应用Sprague Grundy定理?
我们可以将Sprague-Grundy定理应用到任何公正的游戏中并加以解决。基本步骤如下:

  1. 将复合游戏分为子游戏。
  2. 然后,对于每个子游戏,计算该位置的脏数。
  3. 然后,计算所有计算出的格伦迪数的XOR。
  4. 如果XOR值不为零,那么将要转牌的玩家(第一个玩家)将获胜,否则他注定会输,无论如何。

游戏示例:游戏开始时有3堆,分别有3、4和5块石头,并且移动的玩家可以从任意一个堆中取出正数的石头,最多只能取3块(前提是该堆中有那么多的石头)。最后一个移动的玩家获胜。假设两个玩家都发挥最佳状态,那么哪个玩家会赢得比赛?

如何通过应用Sprague-Grundy定理判断谁将获胜?
如我们所见,该游戏本身是由几个子游戏组成的。
第一步:子游戏可以看作是每堆游戏。
第二步:我们从下表中看到

Grundy(3) = 3
Grundy(4) = 0 
Grundy(5) = 1 

斯普拉格-格伦迪定理

在上一篇文章中,我们已经看到了如何计算该游戏的Grundy数。
第三步: 3、0、1 = 2的XOR
第四步:由于XOR是一个非零数字,所以我们可以说第一个玩家将获胜。

下面是实现以上4个步骤的程序。

C++
/*  Game Description-
 "A game is played between two players and there are N piles
 of stones such that each pile has certain number of stones.
 On his/her turn, a player selects a pile and can take any
 non-zero number of stones upto 3 (i.e- 1,2,3)
 The player who cannot move is considered to lose the game
 (i.e., one who take the last stone is the winner).
 Can you find which player wins the game if both players play
 optimally (they don't make any mistake)? "
 
 A Dynamic Programming approach to calculate Grundy Number
 and Mex and find the Winner using Sprague - Grundy Theorem. */
 
#include
using namespace std;
 
/* piles[] -> Array having the initial count of stones/coins
            in each piles before the game has started.
   n       -> Number of piles
 
   Grundy[] -> Array having the Grundy Number corresponding to
             the initial position of each piles in the game
 
   The piles[] and Grundy[] are having 0-based indexing*/
 
#define PLAYER1 1
#define PLAYER2 2
 
// A Function to calculate Mex of all the values in that set
int calculateMex(unordered_set Set)
{
    int Mex = 0;
 
    while (Set.find(Mex) != Set.end())
        Mex++;
 
    return (Mex);
}
 
// A function to Compute Grundy Number of 'n'
int calculateGrundy(int n, int Grundy[])
{
    Grundy[0] = 0;
    Grundy[1] = 1;
    Grundy[2] = 2;
    Grundy[3] = 3;
 
    if (Grundy[n] != -1)
        return (Grundy[n]);
 
    unordered_set Set; // A Hash Table
 
    for (int i=1; i<=3; i++)
            Set.insert (calculateGrundy (n-i, Grundy));
 
    // Store the result
    Grundy[n] = calculateMex (Set);
 
    return (Grundy[n]);
}
 
// A function to declare the winner of the game
void declareWinner(int whoseTurn, int piles[],
                    int Grundy[], int n)
{
    int xorValue = Grundy[piles[0]];
 
    for (int i=1; i<=n-1; i++)
        xorValue = xorValue ^ Grundy[piles[i]];
 
    if (xorValue != 0)
    {
        if (whoseTurn == PLAYER1)
            printf("Player 1 will win\n");
        else
            printf("Player 2 will win\n");
    }
    else
    {
        if (whoseTurn == PLAYER1)
            printf("Player 2 will win\n");
        else
            printf("Player 1 will win\n");
    }
 
    return;
}
 
 
// Driver program to test above functions
int main()
{
    // Test Case 1
    int piles[] = {3, 4, 5};
    int n = sizeof(piles)/sizeof(piles[0]);
 
    // Find the maximum element
    int maximum = *max_element(piles, piles + n);
 
    // An array to cache the sub-problems so that
    // re-computation of same sub-problems is avoided
    int Grundy[maximum + 1];
    memset(Grundy, -1, sizeof (Grundy));
 
    // Calculate Grundy Value of piles[i] and store it
    for (int i=0; i<=n-1; i++)
        calculateGrundy(piles[i], Grundy);
 
    declareWinner(PLAYER1, piles, Grundy, n);
 
    /* Test Case 2
    int piles[] = {3, 8, 2};
    int n = sizeof(piles)/sizeof(piles[0]);
 
 
    int maximum = *max_element (piles, piles + n);
 
    // An array to cache the sub-problems so that
    // re-computation of same sub-problems is avoided
    int Grundy [maximum + 1];
    memset(Grundy, -1, sizeof (Grundy));
 
    // Calculate Grundy Value of piles[i] and store it
    for (int i=0; i<=n-1; i++)
        calculateGrundy(piles[i], Grundy);
 
    declareWinner(PLAYER2, piles, Grundy, n);   */
 
    return (0);
}


Java
import java.util.*;
 
/* Game Description-
"A game is played between two players and there are N piles
of stones such that each pile has certain number of stones.
On his/her turn, a player selects a pile and can take any
non-zero number of stones upto 3 (i.e- 1,2,3)
The player who cannot move is considered to lose the game
(i.e., one who take the last stone is the winner).
Can you find which player wins the game if both players play
optimally (they don't make any mistake)? "
 
A Dynamic Programming approach to calculate Grundy Number
and Mex and find the Winner using Sprague - Grundy Theorem. */
 
class GFG {
     
 
/* piles[] -> Array having the initial count of stones/coins
            in each piles before the game has started.
n     -> Number of piles
 
Grundy[] -> Array having the Grundy Number corresponding to
            the initial position of each piles in the game
 
The piles[] and Grundy[] are having 0-based indexing*/
 
static int PLAYER1 = 1;
static int PLAYER2 = 2;
 
// A Function to calculate Mex of all the values in that set
static int calculateMex(HashSet Set)
{
    int Mex = 0;
 
    while (Set.contains(Mex))
        Mex++;
 
    return (Mex);
}
 
// A function to Compute Grundy Number of 'n'
static int calculateGrundy(int n, int Grundy[])
{
    Grundy[0] = 0;
    Grundy[1] = 1;
    Grundy[2] = 2;
    Grundy[3] = 3;
 
    if (Grundy[n] != -1)
        return (Grundy[n]);
 
    // A Hash Table
    HashSet Set = new HashSet();
 
    for (int i = 1; i <= 3; i++)
            Set.add(calculateGrundy (n - i, Grundy));
 
    // Store the result
    Grundy[n] = calculateMex (Set);
 
    return (Grundy[n]);
}
 
// A function to declare the winner of the game
static void declareWinner(int whoseTurn, int piles[],
                    int Grundy[], int n)
{
    int xorValue = Grundy[piles[0]];
 
    for (int i = 1; i <= n - 1; i++)
        xorValue = xorValue ^ Grundy[piles[i]];
 
    if (xorValue != 0)
    {
        if (whoseTurn == PLAYER1)
            System.out.printf("Player 1 will win\n");
        else
            System.out.printf("Player 2 will win\n");
    }
    else
    {
        if (whoseTurn == PLAYER1)
            System.out.printf("Player 2 will win\n");
        else
            System.out.printf("Player 1 will win\n");
    }
 
    return;
}
 
 
// Driver code
public static void main(String[] args)
{
     
    // Test Case 1
    int piles[] = {3, 4, 5};
    int n = piles.length;
 
    // Find the maximum element
    int maximum = Arrays.stream(piles).max().getAsInt();
 
    // An array to cache the sub-problems so that
    // re-computation of same sub-problems is avoided
    int Grundy[] = new int[maximum + 1];
    Arrays.fill(Grundy, -1);
 
    // Calculate Grundy Value of piles[i] and store it
    for (int i = 0; i <= n - 1; i++)
        calculateGrundy(piles[i], Grundy);
 
    declareWinner(PLAYER1, piles, Grundy, n);
 
    /* Test Case 2
    int piles[] = {3, 8, 2};
    int n = sizeof(piles)/sizeof(piles[0]);
 
 
    int maximum = *max_element (piles, piles + n);
 
    // An array to cache the sub-problems so that
    // re-computation of same sub-problems is avoided
    int Grundy [maximum + 1];
    memset(Grundy, -1, sizeof (Grundy));
 
    // Calculate Grundy Value of piles[i] and store it
    for (int i=0; i<=n-1; i++)
        calculateGrundy(piles[i], Grundy);
 
    declareWinner(PLAYER2, piles, Grundy, n); */
 
    }
}
 
// This code is contributed by PrinciRaj1992


Python3
'''  Game Description-
 "A game is played between two players and there are N piles
 of stones such that each pile has certain number of stones.
 On his/her turn, a player selects a pile and can take any
 non-zero number of stones upto 3 (i.e- 1,2,3)
 The player who cannot move is considered to lose the game
 (i.e., one who take the last stone is the winner).
 Can you find which player wins the game if both players play
 optimally (they don't make any mistake)? "
   
 A Dynamic Programming approach to calculate Grundy Number
 and Mex and find the Winner using Sprague - Grundy Theorem.
  
     piles[] -> Array having the initial count of stones/coins
            in each piles before the game has started.
   n       -> Number of piles
   
   Grundy[] -> Array having the Grundy Number corresponding to
             the initial position of each piles in the game
   
   The piles[] and Grundy[] are having 0-based indexing'''
 
PLAYER1 = 1
PLAYER2 = 2  
 
# A Function to calculate Mex of all
# the values in that set
def calculateMex(Set):
  
    Mex = 0;
   
    while (Mex in Set):
        Mex += 1
   
    return (Mex)
 
# A function to Compute Grundy Number of 'n'
def calculateGrundy(n, Grundy):
 
    Grundy[0] = 0
    Grundy[1] = 1
    Grundy[2] = 2
    Grundy[3] = 3
   
    if (Grundy[n] != -1):
        return (Grundy[n])
     
    # A Hash Table
    Set = set()
   
    for i in range(1, 4):
        Set.add(calculateGrundy(n - i,
                                Grundy))
     
    # Store the result
    Grundy[n] = calculateMex(Set)
   
    return (Grundy[n])
  
# A function to declare the winner of the game
def declareWinner(whoseTurn, piles, Grundy, n):
 
    xorValue = Grundy[piles[0]];
   
    for i in range(1, n):
        xorValue = (xorValue ^
                    Grundy[piles[i]])
   
    if (xorValue != 0):
     
        if (whoseTurn == PLAYER1):
            print("Player 1 will win\n");
        else:
            print("Player 2 will win\n");
    else:
     
        if (whoseTurn == PLAYER1):
            print("Player 2 will win\n");
        else:
            print("Player 1 will win\n");
     
# Driver code
if __name__=="__main__":
     
    # Test Case 1
    piles = [ 3, 4, 5 ]
    n = len(piles)
   
    # Find the maximum element
    maximum = max(piles)
   
    # An array to cache the sub-problems so that
    # re-computation of same sub-problems is avoided
    Grundy = [-1 for i in range(maximum + 1)];
   
    # Calculate Grundy Value of piles[i] and store it
    for i in range(n):
        calculateGrundy(piles[i], Grundy);
   
    declareWinner(PLAYER1, piles, Grundy, n);
   
    ''' Test Case 2
    int piles[] = {3, 8, 2};
    int n = sizeof(piles)/sizeof(piles[0]);
   
   
    int maximum = *max_element (piles, piles + n);
   
    // An array to cache the sub-problems so that
    // re-computation of same sub-problems is avoided
    int Grundy [maximum + 1];
    memset(Grundy, -1, sizeof (Grundy));
   
    // Calculate Grundy Value of piles[i] and store it
    for (int i=0; i<=n-1; i++)
        calculateGrundy(piles[i], Grundy);
   
    declareWinner(PLAYER2, piles, Grundy, n);   '''
 
# This code is contributed by rutvik_56


C#
using System;
using System.Linq;
using System.Collections.Generic;
 
/* Game Description-
"A game is played between two players and there are N piles
of stones such that each pile has certain number of stones.
On his/her turn, a player selects a pile and can take any
non-zero number of stones upto 3 (i.e- 1,2,3)
The player who cannot move is considered to lose the game
(i.e., one who take the last stone is the winner).
Can you find which player wins the game if both players play
optimally (they don't make any mistake)? "
 
A Dynamic Programming approach to calculate Grundy Number
and Mex and find the Winner using Sprague - Grundy Theorem. */
 
class GFG
{
     
 
/* piles[] -> Array having the initial count of stones/coins
            in each piles before the game has started.
n -> Number of piles
 
Grundy[] -> Array having the Grundy Number corresponding to
            the initial position of each piles in the game
 
The piles[] and Grundy[] are having 0-based indexing*/
 
static int PLAYER1 = 1;
//static int PLAYER2 = 2;
 
// A Function to calculate Mex of all the values in that set
static int calculateMex(HashSet Set)
{
    int Mex = 0;
 
    while (Set.Contains(Mex))
        Mex++;
 
    return (Mex);
}
 
// A function to Compute Grundy Number of 'n'
static int calculateGrundy(int n, int []Grundy)
{
    Grundy[0] = 0;
    Grundy[1] = 1;
    Grundy[2] = 2;
    Grundy[3] = 3;
 
    if (Grundy[n] != -1)
        return (Grundy[n]);
 
    // A Hash Table
    HashSet Set = new HashSet();
 
    for (int i = 1; i <= 3; i++)
            Set.Add(calculateGrundy (n - i, Grundy));
 
    // Store the result
    Grundy[n] = calculateMex (Set);
 
    return (Grundy[n]);
}
 
// A function to declare the winner of the game
static void declareWinner(int whoseTurn, int []piles,
                    int []Grundy, int n)
{
    int xorValue = Grundy[piles[0]];
 
    for (int i = 1; i <= n - 1; i++)
        xorValue = xorValue ^ Grundy[piles[i]];
 
    if (xorValue != 0)
    {
        if (whoseTurn == PLAYER1)
            Console.Write("Player 1 will win\n");
        else
            Console.Write("Player 2 will win\n");
    }
    else
    {
        if (whoseTurn == PLAYER1)
            Console.Write("Player 2 will win\n");
        else
            Console.Write("Player 1 will win\n");
    }
 
    return;
}
 
 
// Driver code
static void Main()
{
     
    // Test Case 1
    int []piles = {3, 4, 5};
    int n = piles.Length;
 
    // Find the maximum element
    int maximum = piles.Max();
 
    // An array to cache the sub-problems so that
    // re-computation of same sub-problems is avoided
    int []Grundy = new int[maximum + 1];
    Array.Fill(Grundy, -1);
 
    // Calculate Grundy Value of piles[i] and store it
    for (int i = 0; i <= n - 1; i++)
        calculateGrundy(piles[i], Grundy);
 
    declareWinner(PLAYER1, piles, Grundy, n);
     
    /* Test Case 2
    int piles[] = {3, 8, 2};
    int n = sizeof(piles)/sizeof(piles[0]);
 
 
    int maximum = *max_element (piles, piles + n);
 
    // An array to cache the sub-problems so that
    // re-computation of same sub-problems is avoided
    int Grundy [maximum + 1];
    memset(Grundy, -1, sizeof (Grundy));
 
    // Calculate Grundy Value of piles[i] and store it
    for (int i=0; i<=n-1; i++)
        calculateGrundy(piles[i], Grundy);
 
    declareWinner(PLAYER2, piles, Grundy, n); */
 
    }
}
 
// This code is contributed by mits


输出 :

Player 1 will win

参考 :
https://zh.wikipedia.org/wiki/Sprague%E2%80%93Grundy_theorem

读者练习:考虑以下游戏。
“一个游戏是由两个具有N个整数A1,A2,..,AN的玩家进行的。在回合中,玩家选择一个整数,将其除以2、3或6,然后发言。如果整数变为0,则将其删除。最后一个移动的玩家获胜。如果两个玩家都发挥最佳状态,哪个玩家会赢得比赛?”
提示:请参阅上一篇文章的示例3。