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

📅  最后修改于: 2021-09-24 05:04:02             🧑  作者: Mango

先决条件:Grundy Numbers/Nimbers 和 Mex
我们已经在 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. 然后对于每个子游戏,计算该位置的 Grundy Number。
  3. 然后计算所有计算出的 Grundy Number 的 XOR。
  4. 如果 XOR 值不为零,那么将要进行回合的玩家(第一个玩家)将获胜,否则无论如何他注定要失败。

示例游戏:游戏开始时有 3 堆,分别有 3、4 和 5 颗石头,要移动的玩家可以从任何堆中取出最多 3 颗的任意正数的石头[前提是该堆有那么多的石头]。最后移动的玩家获胜。假设两个玩家都玩得最好,哪个玩家赢得了比赛?

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

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

斯普拉格 - 格兰迪定理

我们已经在上一篇文章中看到了如何计算这个游戏的 Grundy Numbers。
第三步: 3, 0, 1 = 2 的异或
第四步:由于 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://en.wikipedia.org/wiki/Sprague%E2%80%93Grundy_theorem

给读者的练习:考虑下面的游戏。
“一场游戏由两个玩家玩 N 个整数 A1、A2、..、AN。轮到他/她时,玩家选择一个整数,将其除以 2、3 或 6,然后进入场地。如果整数变为 0,则将其删除。最后移动的玩家获胜。如果两个球员都发挥最佳,哪个球员会赢得比赛?”
提示:请参阅上一篇文章的示例 3。

如果您希望与专家一起参加现场课程,请参阅DSA 现场工作专业课程学生竞争性编程现场课程