알고리즘

스프라그-그런디 정리

yujaa 2021. 11. 5. 12:30

다음 조건을 만족하는 게임에 대해 논의할 것이다.
이는 backward induction을 위한 조건으로 볼 수 있다.

Combinatorial game
  1. 두 명의 플레이어가 번갈아가면서 게임을 진행한다.
  2. 플레이어는 게임의 상태를 모두 알 수 있다.
  3. 행동에 따른 게임 상태의 변화가 운에 의해 결정되지 않고 확정적이다.
  4. 게임은 반드시 종료된다. (무한히 진행되지 않는다.)
  5. 게임의 승패가 반드시 결정된다. (무승부가 존재하지 않는다.)

Impartial game
취할 수 있는 행동은 오직 게임 상태에 의해 결정된다.
플레이어에 따라 취할 수 있는 행동이 달라지지 않는다.
ex) Nim, 31 게임   cf. 체스

Normal game: 마지막에 움직이는 플레이어가 승리한다.
Misére game: 마지막에 움직이는 플레이어가 패배한다.



스프라그-그런디 함수
$$ g(x) = \begin{cases}
0   & \text{if x is a terminal position} \\
mex\{g(y) : y \in F(x)\}   & \text{else}
\end{cases} $$ $X$ :  가능한 모든 게임 상태들의 집합
$F(x)$ : $x \in X$에서 행동을 취했을 때 얻을 수 있는 모든 게임 상태들의 집합. $F(x) = \emptyset$이면 $x$는 terminal position이다. (더 이상 진행이 불가능한 게임 상태)

terminal position을 base case로 두면 backward induction을 통해 모든 $g(x)$ 값을 구할 수 있다.

그런디 함수의 값은 어떤 의미를 가질까? (Normal game 기준)
  1) $g(x) = 0$   $\rightarrow$   $x$ is a losing position
  2) $g(x) \ne 0$   $\rightarrow$   $x$ is a winning position

증명)
유한한 차례 안에 반드시 terminal position에 도달한다.
terminal position의 $g$ 값은 $0$이다.
$g(x) = 0$이면 다음 상태의 $g$ 값은 항상 $0$이 아니다.
$g(x) \ne 0$이면 다음 상태의 $g$ 값을 $0$으로 만들 수 있다.

예시) Nim
게임 상태 = 더미 안에 남아있는 돌의 개수
$g(0) = 0$   // base case
$g(1) = mex\{g(0)\} = 1$
$g(2) = mex\{g(0), g(1)\} = 2$
$g(3) = mex\{g(0), g(1), g(2) \} = 3$
$\cdots$
$\therefore g(n) = n$



스프라그-그런디 정리
$$g((x_1, x_2, \dots, x_n)) = g(x_1) \oplus g(x_2) \oplus \cdots \oplus g(x_n)$$여러 게임을 동시에 진행하는 경우의 그런디 넘버는 각 게임판의 그런디 넘버를 xor한 값과 같다.

증명) 
$ x := (x_1, x_2, \dots, x_n) $
$ b := g(x_1) \oplus g(x_2) \oplus \cdots \oplus g(x_n)$

(1) $\forall a \in [0, b)$ $\exists y \in F(x)$  $g(y) = a$
$d := a \oplus b$
$k := $ a positive integer such that $2^{k-1} \le d \lt 2^k$

정의를 풀어쓰면, $a$와 $b$의 비트 표현이 달라지기 시작하는 지점이 (오른쪽으로부터) $k$-th 비트라는 뜻이다.
이때 $a \lt b$이므로 $a$의 $k$-th 비트는 $0$, $b$의 $k$-th 비트는 $1$이다.
$b$의 정의에 의해 $g(x_i)$의 $k$-th 비트가 $1$인 $i$가 반드시 존재한다. (WLOG, $i = 1$)
$d \oplus g(x_1) < g(x_1)$이므로 $g(x_1') = d \oplus g(x_1)$를 만족하는 $x_i' \in F(x_1)$가 반드시 존재한다.
$$
\begin{equation}
\begin{aligned}
g((x_1', x_2, \dots , x_n)) &= g(x_1') \oplus g(x_2) \oplus \cdots \oplus g(x_n) \\
&= d \oplus g(x_1) \oplus \cdots \oplus g(x_n) \\
&= d \oplus b \\
&= a
\end{aligned}
\end{equation}
$$
(2) $\nexists y \in F(x)$  $g(y) = b$
(귀류법) $\exists y \in F(x)$  $g(y) = b$라고 가정하자. $$
\begin{align}
g(x_1') \oplus g(x_2) \oplus \cdots \oplus g(x_n) &= g(x_1) \oplus g(x_2) \oplus \cdots \oplus g(x_n) \\
\therefore g(x_1') &= g(x_1)
\end{align}
$$ 이는 그런디 함수 정의에 모순이다.



문제 풀이
11868번 - 님 게임 2
게임 상태 = 더미 안에 남아있는 돌의 개수
$g(n) = n$

$x :=$ 각 더미 안에 있는 돌의 개수를 xor한 값
$x \ne 0$이면 선공이 이기고, $x = 0$이면 후공이 이긴다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <bits/stdc++.h>
using namespace std;
 
int main(void) {
    ios::sync_with_stdio(0);
    cin.tie(0); cout.tie(0);
 
    int n; cin >> n;
    int x = 0;
    while (n--) {
        int a; cin >> a;
        x ^= a;
    }
    if (x) cout << "koosaga";
    else cout << "cubelover";
    return 0;
}
cs



16895번 - 님 게임 3
님 게임 2에서는 누가 이기는 지를 물었고, 이 문제는 어떻게 이기는 지를 묻는다.
다음 상태의 그런디 넘버가 $0$이 되도록 만들면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <bits/stdc++.h>
using namespace std;
 
int main(void) {
    ios::sync_with_stdio(0);
    cin.tie(0); cout.tie(0);
 
    int n; cin >> n;
    vector<int> v(n);
 
    int x = 0;
    for (int i = 0; i < n; i++) {
        cin >> v[i];
        x ^= v[i];
    }
 
    if (x) {
        int ans = 0;
        for (int i: v) {
            if ((x^i) < i) ans++;
        }
        cout << ans;
    }
    else cout << 0;
    return 0;
}
cs



16877번 - 핌버
일반 님 게임과 규칙이 다르다.
그런디 넘버를 새로 구해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <bits/stdc++.h>
using namespace std;
 
const int MAX = 3e6;
const int MAX2 = 32;
int dp[MAX+1];
int fibo[MAX2] = {12};
vector<bool> visited(MAX2);
 
int main(void) {
    ios::sync_with_stdio(0);
    cin.tie(0); cout.tie(0);
 
    for (int i = 2; i < MAX2; i++) {
        fibo[i] = fibo[i-1+ fibo[i-2];
    }
 
    for (int i = 1; i <= MAX; i++) {
        visited.assign(MAX2, false);
        for (int j = 0; j < MAX2 && i - fibo[j] >= 0; j++) {
            visited[dp[i - fibo[j]]] = true;
        }
        for (int j = 0; j < MAX2; j++) {
            if (!visited[j]) {
                dp[i] = j;
                break;
            }
        }
    }
 
    int n; cin >> n;
    int x = 0;
    while (n--) {
        int a; cin >> a;
        x ^= dp[a];
    }
    cout << (x ? "koosaga" : "cubelover");
    return 0;
}
cs



11871번 - 님 게임 홀짝
이 문제 역시 그런디 넘버를 새로 구해야 한다.
$g(0) = 0$   // base case
$g(1) = mex\{g(0)\} = 1$
$g(2) = 0$   // base case
$g(3) = mex\{g(0), g(1)\} = 2$
$g(4) = mex\{g(2)\} = 1$
$g(5) = mex\{g(0), g(1), g(3)\} = 3$
$g(6) = mex\{g(2), g(4)\} = 2$
$\cdots$
\(\therefore g(n) = 
\begin{cases}
0   & n = 0, 2 \\
\frac{n+1}{2}   & \text{if $n$ is odd} \\
\frac{n-2}{2}   & \text{if $n$ is even and $n \gt 2$}
\end{cases}
\)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <bits/stdc++.h>
using namespace std;
 
int main(void) {
    ios::sync_with_stdio(0);
    cin.tie(0); cout.tie(0);
 
    int n; cin >> n;
    int x = 0;
    while (n--) {
        int a; cin >> a;
        if (a & 1) x ^= (a + 1 >> 1);
        else x ^= (a - 1 >> 1);
    }
    cout << (x ? "koosaga" : "cubelover");
    return 0;
}
cs



13034번 - 다각형 게임
선분을 그으면 선분을 기준으로 게임이 분할된다.

게임 상태 = 꼭짓점의 개수
$g(0) = g(1) = 0$   // base case
$g(2) = 1$   // base case
$g(3) = mex\{g(0) \oplus g(1)\} = 0$
$g(4) = mex\{g(0) \oplus g(2), g(1) \oplus g(1)\} = 2$
$g(5) = mex\{g(0) \oplus g(3), g(1) \oplus g(2)\} = 2$
$g(6) = mex\{g(0) \oplus g(4), g(1) \oplus g(3), g(2) \oplus g(2)\} = 1$
$\cdots$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <bits/stdc++.h>
using namespace std;
 
int main(void) {
    ios::sync_with_stdio(0);
    cin.tie(0); cout.tie(0);
 
    int n; cin >> n;
    vector<int> dp(n+1);
    vector<bool> visited(16);
 
    dp[2= 1;
    for (int i = 3; i <= n; i++) {
        visited.assign(16false);
        for (int j = 0; j <= i - 2 - j; j++) {
            visited[dp[j] ^ dp[i - 2 - j]] = true;
        }
        for (int j = 0; j < 16; j++) {
            if (!visited[j]) {
                dp[i] = j;
                break;
            }
        }
    }
    cout << (dp[n] ? 1 : 2);
    return 0;
}
cs



3596번 - 크로스와 크로스
이미 'x' 마크가 있는 칸을 중심으로 두 칸 이하 떨어진 곳에 마크하면 패배한다.
이를 게임이 분할되는 것으로 볼 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <bits/stdc++.h>
using namespace std;
 
int main(void) {
    ios::sync_with_stdio(0);
    cin.tie(0); cout.tie(0);
 
    vector<int> dp(2001);
    vector<bool> visited(100);
    dp[1= dp[2= dp[3= 1;
    
    for (int i = 4; i <= 2000; i++) {
        visited.assign(100false);
        for (int j = 1; j <= (i + 1 >> 1); j++) {
            int tmp = dp[i-j-2] ^ (j - 3 > 0 ? dp[j-3] : 0);
            visited[tmp] = true;
        }
        for (int j = 0; j < 100; j++) {
            if (!visited[j]) {
                dp[i] = j;
                break;
            }
        }
    }
    int n; cin >> n;
    cout << (dp[n] ? 1 : 2);
    return 0;
}
cs



11717번 - Wall Making Game
어떤 cell에 마크하면 게임판이 해당 cell 기준 top-left, top-right, bottom-left, bottom-right으로 나뉜다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include <bits/stdc++.h>
using namespace std;
 
int h, w;
const int MAX = 23;
const int MAX2 = 100;
string board[MAX];
int dp[MAX][MAX][MAX][MAX];
 
inline bool outsideBoard(int y, int x) {
    return y < 0 || y >= h || x < 0 || x >= w;
}
 
int f(int i, int j, int p, int q) {
    if (outsideBoard(i, j) || outsideBoard(p, q) || i > p || j > q) return 0;
    if (dp[i][j][p][q] != -1return dp[i][j][p][q];
 
    vector<bool> visited(MAX2);
    for (int a = i; a <= p; a++) {
        for (int b = j; b <= q; b++) {
            if (board[a][b] == 'X'continue;
            int tmp = 0;
            tmp ^= f(i, j, a - 1, b - 1);
            tmp ^= f(i, b + 1, a - 1, q);
            tmp ^= f(a + 1, j, p, b - 1);
            tmp ^= f(a + 1, b + 1, p, q);
            visited[tmp] = true;
        }
    }
    for (int t = 0; t < MAX2; t++) {
        if (!visited[t]) {
            return dp[i][j][p][q] = t;
        }
    }
    return 0;
}
 
int main(void) {
    ios::sync_with_stdio(0);
    cin.tie(0); cout.tie(0);
 
    cin >> h >> w;
    for (int i = 0; i < h; i++) {
        cin >> board[i];
    }
 
    memset(dp, -1sizeof(dp));
    for (int i = 0; i < h; i++) {
        for (int j = 0; j < w; j++) {
            dp[i][j][i][j] = (board[i][j] == '.');
        }
    }
    cout << (f(00, h - 1, w - 1) ? "First" : "Second");
    return 0;
}
cs



18937번 - 왕들의 외나무다리 돌게임
이 게임은 impartial game이 아니고, 유한한 차례 안에 반드시 종료되지도 않는다.
그러나 잘 생각해보면 Nim과 동일함을 알 수 있다.
상대방이 뒤로 물러설 경우 같은 거리만큼 이동하여 따라가면 된다.

게임 상태 = Whiteking과 Blackking 사이 빈 칸의 개수
$g(n) = n$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <bits/stdc++.h>
using namespace std;
 
int main(void) {
    ios::sync_with_stdio(0);
    cin.tie(0); cout.tie(0);
 
    int n; cin >> n;
    int a, x = 0;
    while (n--) {
        cin >> a;
        x ^= a - 2;
    }
    string s; cin >> s;
    cout << ((s[0!= 'W'== !x ? "Whiteking" : "Blackking");
    return 0;
}
cs



11694번 - 님 게임
지금까지 다룬 게임들은 모두 Normal game이다.
Misére game은 Normal game과 비슷하지만 살짝 다르다.
// 풀이 추가 예정

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <bits/stdc++.h>
using namespace std;
 
int main(void) {
    ios::sync_with_stdio(0);
    cin.tie(0); cout.tie(0);
 
    int n; cin >> n;
    int x = 0;
    int flag = 0;
    while (n--) {
        int a; cin >> a;
        if (a > 1) flag++;
        x ^= a;
    }
 
    if (flag && !|| !flag && x == 1)
        cout << "cubelover";
    else
        cout << "koosaga";
    return 0;
}
cs



11062번 - 카드 게임
그런디 없이 게임 이론으로 푸는 문제이다.
$dp[i][j]$: $[i, j]$ 구간에서 얻을 수 있는 최대 점수
\(
dp[i][j] =
\begin{cases}
v[i] & \text{if $i = j$} \\
max(v[i]+psum[i+1 \dots j] - dp[i+1][j], \\
\quad \quad \, v[j]+psum[i \dots j-1] - dp[i][j-1]) & \text{else}
\end{cases}
\)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <bits/stdc++.h>
using namespace std;
 
int main(void) {
    cin.tie(NULL);
    ios::sync_with_stdio(false);
 
    int t; cin >> t;
    while (t--) {
        int n; cin >> n;
        vector<int> v(n), psum(n+1);
        for (int i = 0; i < n; i++) {
            cin >> v[i];
            psum[i+1= psum[i] + v[i];
        }
 
        vector<vector<int> > dp(n, vector<int>(n, -1));
        for (int i = n-1; i >= 0; i--) {
            for (int j = i; j < n; j++) {
                if (i == j) {
                    dp[i][j] = v[i];
                }
                else { 
                    dp[i][j] = v[i] + psum[j+1- psum[i+1- dp[i+1][j];
                    dp[i][j] = max(dp[i][j], v[j] + psum[j] - psum[i] - dp[i][j-1]);
                }
            }
        }
        cout << dp[0][n-1<< '\n';
    }
    return 0;
}
cs




참고 자료
https://web.mit.edu/sp.268/www/nim.pdf
https://sites.oxy.edu/ron/math/400/09/The%20Game%20Of%20Nim.pptx

'알고리즘' 카테고리의 다른 글

Equivalence Relation and Partition  (0) 2022.07.06
이분 탐색  (0) 2021.07.15