본문으로 건너뛰기
김신건의 로그

스프라그-그런디 정리 (Sprague-Grundy)

· 수정 · 📖 약 3분 · 946자/단어 #algorithm #game #sprague-grundy
sprague-grundy, 스프라그-그런디, SG theorem, Grundy number, nimber, sprague-grundy

정의

스프라그-그런디 정리 (Sprague-Grundy) 는 모든 impartial combinatorial game (두 플레이어가 같은 행동 가능, 무작위성 없음, 완전 정보) 이 Nim-heap 하나와 동등함을 증명하는 정리. 각 게임 상태는 Grundy number (nimber) 로 표현되며, Grundy number 가 0 이면 패배 상태, 0 이 아니면 승리 상태.

문제 상황과 동기

여러 impartial game 이 동시에 진행될 때 (e.g., Nim 의 여러 더미), 각각을 독립 분석 후 XOR 로 합성.

  • naive: 전체 상태 공간의 game tree 그리기. 상태 폭발.
  • SG 이론: 각 부분 게임의 Grundy 수를 XOR. 모든 상태를 Nim 으로 환원.

핵심 통찰: 모든 impartial game 의 상태는 자연수 (Grundy number) 로 완전히 추상화되며, 게임의 합성은 XOR.

시각화

핵심 아이디어

Grundy Number 정의

G(v) = mex({ G(u) | v -> u })

  • mex: Minimum EXcluded non-negative integer.
  • v -> u: 상태 v 에서 한 번의 행동으로 도달 가능한 상태 u.
  • G(v) = 0: losing state (P-position).
  • G(v) != 0: winning state (N-position).

게임 합성 (Nim-Sum)

여러 독립 게임 G1, G2, ..., Gk 가 동시에 진행될 때:

G_total = G1 xor G2 xor ... xor Gk

G_total = 0 이면 후공 승, != 0 이면 선공 승.

계산 예시 (돌 게임, take {1, 3, 4})

G(0) = 0                  (terminal, no moves)
G(1) = mex({G(0)}) = mex({0}) = 1
G(2) = mex({G(1)}) = mex({1}) = 0
G(3) = mex({G(2), G(0)}) = mex({0, 0}) = 1
G(4) = mex({G(3), G(1), G(0)}) = mex({1, 1, 0}) = 2
G(5) = mex({G(4), G(2), G(1)}) = mex({2, 0, 1}) = 3

알고리즘

compute_grundy(max_state, moves):
    G[0..max_state] = 0
    for s = 1..max_state:
        reachable = empty set
        for m in moves:
            if s >= m:
                reachable.add(G[s - m])
        G[s] = mex(reachable)
    return G

mex(S):
    g = 0
    while g in S: g++
    return g

# 게임 합성
xor_sum = 0
for g in grundy_numbers_of_each_game:
    xor_sum ^= g
winner = "First" if xor_sum != 0 else "Second"

구현

// Grundy number DP + 게임 합성
#include <bits/stdc++.h>
using namespace std;

int mex(const vector<bool>& seen) {
  int g = 0;
  while (g < (int)seen.size() && seen[g]) g++;
  return g;
}

int main() {
  const int MAX = 1000;
  vector<int> moves = {1, 3, 4};
  vector<int> G(MAX + 1, 0);

  for (int s = 1; s <= MAX; s++) {
      vector<bool> seen(MAX + 1, false);
      for (int m : moves)
          if (s >= m) seen[G[s - m]] = true;
      G[s] = mex(seen);
  }

  int N; cin >> N;
  if (G[N])
      cout << "G(" << N << ") = " << G[N]
           << " -> First wins\n";
  else
      cout << "G(" << N << ") = 0 -> Second wins\n";

  // 여러 게임 합성
  int k; cin >> k;
  int xsum = 0;
  for (int i = 0; i < k; i++) {
      int p; cin >> p;
      // Nim: Grundy = pile size
      xsum ^= p;
  }
  cout << "Nim XOR = " << xsum << " -> "
       << (xsum ? "First" : "Second") << "\n";
  return 0;
}
stdin
5
3
1 2 3
결과
G(5) = 3 -> First wins
Nim XOR = 0 -> Second

복잡도

항목
Grundy DP 시간O(S x M) (S: 상태 수, M: 행동 수)
게임 합성 시간O(K) (K: 게임 수)
공간O(S) 또는 O(1)

변형 / 활용

응용 분야

유형특징
Nim 계열Grundy = pile size
Take-away 계열행동 집합에 따라 Grundy 주기적
게임 분할한 행동이 게임을 두 개로 분할
Green Hackenbush트리 절단, Grundy = tree height XOR

SG + DP

  • 다각형 게임 (BOJ 13034): N-각형 대각선 긋기. 분할 영역의 Grundy XOR.
  • 격자 게임: 2차원 격자 말 이동. 각 행/열 독립 후 XOR.

함정

1. mex 계산 실수

mex 는 집합에 없는 가장 작은 0 이상 정수. 0부터 순차 확인.

2. 게임 합성 착각

독립적일 때만 XOR 합성 성립. 상태가 연동되면 각각 SG 따로 계산 불가.

3. 큰 상태 공간

MAX 10^6 이상이면 DP 불가. 주기성 찾거나 수식 도출.

BOJ 연습 문제

번호제목정답률링크
BOJ 11868님 게임 280.3%kokoa-lab
BOJ 9660돌 게임 653.5%kokoa-lab
BOJ 13034다각형 게임63.4%kokoa-lab
BOJ 16895님 게임 367.7%kokoa-lab

참고

이 개념을 다룬 위키 페이지 (2)

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기