1"""
2๊ฒ์ ์ด๋ก (Game Theory)
3Game Theory Algorithms
4
5์กฐํฉ ๊ฒ์ ์ด๋ก ๊ณผ ์ต์ ์ ๋ต์ ๋ค๋ฃจ๋ ์๊ณ ๋ฆฌ์ฆ์
๋๋ค.
6"""
7
8from typing import List, Dict, Tuple, Set, Optional
9from functools import lru_cache
10
11
12# =============================================================================
13# 1. ๋ ๊ฒ์ (Nim Game)
14# =============================================================================
15
16def nim_xor(piles: List[int]) -> int:
17 """
18 ๋ ๊ฒ์์ XOR ๊ฐ (Nim-sum)
19 XOR != 0์ด๋ฉด ์ ๊ณต ์น๋ฆฌ, XOR == 0์ด๋ฉด ํ๊ณต ์น๋ฆฌ
20 """
21 result = 0
22 for pile in piles:
23 result ^= pile
24 return result
25
26
27def nim_winning_move(piles: List[int]) -> Optional[Tuple[int, int]]:
28 """
29 ๋ ๊ฒ์์์ ์น๋ฆฌํ๋ ์ ์ฐพ๊ธฐ
30 ๋ฐํ: (๋๋ฏธ ์ธ๋ฑ์ค, ๋จ๊ธธ ๊ฐ์) ๋๋ None
31 """
32 xor = nim_xor(piles)
33
34 if xor == 0:
35 return None # ํจ๋ฐฐ ์ํ, ์น๋ฆฌ ์ ์์
36
37 for i, pile in enumerate(piles):
38 target = pile ^ xor
39 if target < pile:
40 return (i, target) # pile์์ target๊ฐ๋ง ๋จ๊ธฐ๊ธฐ
41
42 return None
43
44
45def nim_game_simulation(piles: List[int], verbose: bool = False) -> int:
46 """
47 ๋ ๊ฒ์ ์๋ฎฌ๋ ์ด์
48 ๋ฐํ: ์น์ (0: ์ ๊ณต, 1: ํ๊ณต)
49 """
50 current = 0 # ํ์ฌ ํ๋ ์ด์ด
51
52 while max(piles) > 0:
53 move = nim_winning_move(piles)
54
55 if move is None:
56 # ํจ๋ฐฐ ์ํ: ์๋ฌด ์๋
57 for i, pile in enumerate(piles):
58 if pile > 0:
59 move = (i, pile - 1)
60 break
61
62 pile_idx, new_count = move
63 if verbose:
64 print(f" Player {current}: ๋๋ฏธ {pile_idx}์์ {piles[pile_idx]}โ{new_count}")
65
66 piles[pile_idx] = new_count
67 current = 1 - current
68
69 return 1 - current # ๋ง์ง๋ง์ ๊ฐ์ ธ๊ฐ ์ฌ๋์ด ์น๋ฆฌ
70
71
72# =============================================================================
73# 2. ์คํ๋ผ๊ทธ-๊ทธ๋ฐ๋ ์ ๋ฆฌ (Sprague-Grundy Theorem)
74# =============================================================================
75
76def mex(s: Set[int]) -> int:
77 """
78 Minimum Excludant
79 ์งํฉ์ ์๋ ๊ฐ์ฅ ์์ ์์ด ์๋ ์ ์
80 """
81 i = 0
82 while i in s:
83 i += 1
84 return i
85
86
87def calculate_grundy(position: int, moves: List[int], memo: Dict[int, int] = None) -> int:
88 """
89 ์คํ๋ผ๊ทธ-๊ทธ๋ฐ๋ ์ ๊ณ์ฐ
90 position: ํ์ฌ ์ํ (์: ๋์ ๊ฐ์)
91 moves: ๊ฐ๋ฅํ ์ด๋๋ ๋ฆฌ์คํธ
92 """
93 if memo is None:
94 memo = {}
95
96 if position in memo:
97 return memo[position]
98
99 if position == 0:
100 memo[position] = 0
101 return 0
102
103 reachable = set()
104 for move in moves:
105 if position >= move:
106 reachable.add(calculate_grundy(position - move, moves, memo))
107
108 result = mex(reachable)
109 memo[position] = result
110 return result
111
112
113def multi_pile_grundy(piles: List[int], moves: List[int]) -> int:
114 """
115 ์ฌ๋ฌ ๋๋ฏธ ๊ฒ์์ ์ ์ฒด ๊ทธ๋ฐ๋ ์
116 ๊ฐ ๋๋ฏธ์ ๊ทธ๋ฐ๋ ์๋ฅผ XOR
117 """
118 memo = {}
119 total_grundy = 0
120
121 for pile in piles:
122 grundy = calculate_grundy(pile, moves, memo)
123 total_grundy ^= grundy
124
125 return total_grundy
126
127
128# =============================================================================
129# 3. ๋ณํ ๋ ๊ฒ์
130# =============================================================================
131
132def staircase_nim(stairs: List[int]) -> int:
133 """
134 ๊ณ๋จ ๋ (Staircase Nim)
135 ํ์ ๋ฒ์งธ ๊ณ๋จ์ XOR = ๊ทธ๋ฐ๋ ์
136 """
137 xor = 0
138 for i in range(0, len(stairs), 2): # ํ์ ์ธ๋ฑ์ค (0-indexed์ ์ง์)
139 xor ^= stairs[i]
140 return xor
141
142
143def misere_nim(piles: List[int]) -> bool:
144 """
145 ๋ฏธ์ ๋ฅด ๋ (๋ง์ง๋ง ๊ฐ์ ธ๊ฐ๋ ์ฌ๋์ด ํจ๋ฐฐ)
146 ๋ฐํ: True๋ฉด ์ ๊ณต ์น๋ฆฌ
147 """
148 xor = nim_xor(piles)
149 all_one_or_zero = all(p <= 1 for p in piles)
150
151 if all_one_or_zero:
152 # 1์ธ ๋๋ฏธ์ ๊ฐ์๊ฐ ํ์๋ฉด ํ๊ณต ์น๋ฆฌ
153 ones = sum(1 for p in piles if p == 1)
154 return ones % 2 == 0
155 else:
156 return xor != 0
157
158
159def poker_nim(piles: List[int], k: int) -> bool:
160 """
161 ํฌ์ปค ๋: ๋๋ฏธ์ ๋์ ์ถ๊ฐํ ์๋ ์์ (์ต๋ k๊ฐ)
162 ๋ฐํ: True๋ฉด ์ ๊ณต ์น๋ฆฌ
163 ๊ท์น: ์ผ๋ฐ ๋๊ณผ ๋์ผ (XOR != 0์ด๋ฉด ์ ๊ณต ์น๋ฆฌ)
164 """
165 return nim_xor(piles) != 0
166
167
168# =============================================================================
169# 4. ๋ฏธ๋๋งฅ์ค ์๊ณ ๋ฆฌ์ฆ (Minimax)
170# =============================================================================
171
172def minimax(position, depth: int, is_maximizing: bool,
173 evaluate, get_moves, is_terminal) -> int:
174 """
175 ๋ฏธ๋๋งฅ์ค ์๊ณ ๋ฆฌ์ฆ
176 position: ํ์ฌ ๊ฒ์ ์ํ
177 depth: ํ์ ๊น์ด
178 is_maximizing: ์ต๋ํ ํ๋ ์ด์ด์ ํด์ธ์ง
179 evaluate: ์ํ ํ๊ฐ ํจ์
180 get_moves: ๊ฐ๋ฅํ ์ ๋ฐํ ํจ์
181 is_terminal: ์ข
๋ฃ ์ํ ํ์ธ ํจ์
182 """
183 if depth == 0 or is_terminal(position):
184 return evaluate(position)
185
186 moves = get_moves(position)
187
188 if is_maximizing:
189 max_eval = float('-inf')
190 for move in moves:
191 new_position = apply_move(position, move)
192 eval_score = minimax(new_position, depth - 1, False,
193 evaluate, get_moves, is_terminal)
194 max_eval = max(max_eval, eval_score)
195 return max_eval
196 else:
197 min_eval = float('inf')
198 for move in moves:
199 new_position = apply_move(position, move)
200 eval_score = minimax(new_position, depth - 1, True,
201 evaluate, get_moves, is_terminal)
202 min_eval = min(min_eval, eval_score)
203 return min_eval
204
205
206def apply_move(position, move):
207 """์๋ฅผ ์ ์ฉํ ์ ์ํ ๋ฐํ (์ถ์ ํจ์)"""
208 # ๊ตฌํ์ ๊ฒ์์ ๋ฐ๋ผ ๋ค๋ฆ
209 pass
210
211
212# =============================================================================
213# 5. ์ํ-๋ฒ ํ ๊ฐ์ง์น๊ธฐ (Alpha-Beta Pruning)
214# =============================================================================
215
216def alpha_beta(position, depth: int, alpha: float, beta: float,
217 is_maximizing: bool, evaluate, get_moves, is_terminal) -> int:
218 """
219 ์ํ-๋ฒ ํ ๊ฐ์ง์น๊ธฐ
220 alpha: ์ต๋ํ ํ๋ ์ด์ด์ ์ต์ ์ ๋ณด์ฅ๊ฐ
221 beta: ์ต์ํ ํ๋ ์ด์ด์ ์ต์ ์ ๋ณด์ฅ๊ฐ
222 """
223 if depth == 0 or is_terminal(position):
224 return evaluate(position)
225
226 moves = get_moves(position)
227
228 if is_maximizing:
229 max_eval = float('-inf')
230 for move in moves:
231 new_position = apply_move(position, move)
232 eval_score = alpha_beta(new_position, depth - 1, alpha, beta,
233 False, evaluate, get_moves, is_terminal)
234 max_eval = max(max_eval, eval_score)
235 alpha = max(alpha, eval_score)
236 if beta <= alpha:
237 break # ๊ฐ์ง์น๊ธฐ
238 return max_eval
239 else:
240 min_eval = float('inf')
241 for move in moves:
242 new_position = apply_move(position, move)
243 eval_score = alpha_beta(new_position, depth - 1, alpha, beta,
244 True, evaluate, get_moves, is_terminal)
245 min_eval = min(min_eval, eval_score)
246 beta = min(beta, eval_score)
247 if beta <= alpha:
248 break # ๊ฐ์ง์น๊ธฐ
249 return min_eval
250
251
252# =============================================================================
253# 6. ํฑํํ (Tic-Tac-Toe) ๊ตฌํ
254# =============================================================================
255
256class TicTacToe:
257 """ํฑํํ ๊ฒ์"""
258
259 def __init__(self):
260 self.board = [[' '] * 3 for _ in range(3)]
261 self.current_player = 'X'
262
263 def get_moves(self) -> List[Tuple[int, int]]:
264 """๊ฐ๋ฅํ ์ ๋ฐํ"""
265 moves = []
266 for i in range(3):
267 for j in range(3):
268 if self.board[i][j] == ' ':
269 moves.append((i, j))
270 return moves
271
272 def make_move(self, row: int, col: int) -> bool:
273 """์ ๋๊ธฐ"""
274 if self.board[row][col] != ' ':
275 return False
276 self.board[row][col] = self.current_player
277 self.current_player = 'O' if self.current_player == 'X' else 'X'
278 return True
279
280 def undo_move(self, row: int, col: int):
281 """์ ๋๋๋ฆฌ๊ธฐ"""
282 self.board[row][col] = ' '
283 self.current_player = 'O' if self.current_player == 'X' else 'X'
284
285 def check_winner(self) -> Optional[str]:
286 """์น์ ํ์ธ"""
287 # ํ
288 for row in self.board:
289 if row[0] == row[1] == row[2] != ' ':
290 return row[0]
291 # ์ด
292 for col in range(3):
293 if self.board[0][col] == self.board[1][col] == self.board[2][col] != ' ':
294 return self.board[0][col]
295 # ๋๊ฐ์
296 if self.board[0][0] == self.board[1][1] == self.board[2][2] != ' ':
297 return self.board[0][0]
298 if self.board[0][2] == self.board[1][1] == self.board[2][0] != ' ':
299 return self.board[0][2]
300 return None
301
302 def is_terminal(self) -> bool:
303 """๊ฒ์ ์ข
๋ฃ ํ์ธ"""
304 return self.check_winner() is not None or len(self.get_moves()) == 0
305
306 def evaluate(self) -> int:
307 """์ํ ํ๊ฐ (X ๊ด์ )"""
308 winner = self.check_winner()
309 if winner == 'X':
310 return 10
311 elif winner == 'O':
312 return -10
313 return 0
314
315 def minimax(self, is_maximizing: bool) -> int:
316 """๋ฏธ๋๋งฅ์ค"""
317 if self.is_terminal():
318 return self.evaluate()
319
320 if is_maximizing:
321 max_eval = float('-inf')
322 for row, col in self.get_moves():
323 self.make_move(row, col)
324 eval_score = self.minimax(False)
325 self.undo_move(row, col)
326 max_eval = max(max_eval, eval_score)
327 return max_eval
328 else:
329 min_eval = float('inf')
330 for row, col in self.get_moves():
331 self.make_move(row, col)
332 eval_score = self.minimax(True)
333 self.undo_move(row, col)
334 min_eval = min(min_eval, eval_score)
335 return min_eval
336
337 def best_move(self) -> Tuple[int, int]:
338 """์ต์ ์ ์ ์ฐพ๊ธฐ"""
339 best_score = float('-inf') if self.current_player == 'X' else float('inf')
340 best_move = None
341
342 for row, col in self.get_moves():
343 self.make_move(row, col)
344 score = self.minimax(self.current_player == 'X')
345 self.undo_move(row, col)
346
347 if self.current_player == 'X':
348 if score > best_score:
349 best_score = score
350 best_move = (row, col)
351 else:
352 if score < best_score:
353 best_score = score
354 best_move = (row, col)
355
356 return best_move
357
358 def display(self):
359 """๋ณด๋ ์ถ๋ ฅ"""
360 for i, row in enumerate(self.board):
361 print(" " + " | ".join(row))
362 if i < 2:
363 print(" " + "-" * 9)
364
365
366# =============================================================================
367# 7. ๋ ๊ฒ์ (Stone Game)
368# =============================================================================
369
370@lru_cache(maxsize=None)
371def stone_game_dp(piles: Tuple[int, ...], left: int, right: int) -> int:
372 """
373 ๋ ๊ฒ์: ์ ๋์์๋ง ๊ฐ์ ธ๊ฐ ์ ์์
374 ์ ๊ณต์ด ์ป์ ์ ์๋ ์ต๋ ์ ์ ์ฐจ์ด ๋ฐํ
375 """
376 if left > right:
377 return 0
378
379 # ์ ๊ณต์ด ์ผ์ชฝ ์ ํ
380 pick_left = piles[left] - stone_game_dp(piles, left + 1, right)
381 # ์ ๊ณต์ด ์ค๋ฅธ์ชฝ ์ ํ
382 pick_right = piles[right] - stone_game_dp(piles, left, right - 1)
383
384 return max(pick_left, pick_right)
385
386
387def stone_game(piles: List[int]) -> bool:
388 """
389 ๋ ๊ฒ์: ์ ๊ณต์ด ์ด๊ธฐ๋ฉด True
390 """
391 n = len(piles)
392 diff = stone_game_dp(tuple(piles), 0, n - 1)
393 return diff > 0
394
395
396# =============================================================================
397# 8. ๋ฐ์ ๊ฒ์ (Bash Game)
398# =============================================================================
399
400def bash_game(n: int, k: int) -> bool:
401 """
402 ๋ฐ์ ๊ฒ์: n๊ฐ ๋์์ ์ต๋ k๊ฐ์ฉ ๊ฐ์ ธ๊ฐ
403 ๋ง์ง๋ง ๋์ ๊ฐ์ ธ๊ฐ๋ ์ฌ๋์ด ์น๋ฆฌ
404 ๋ฐํ: ์ ๊ณต์ด ์ด๊ธฐ๋ฉด True
405 """
406 return n % (k + 1) != 0
407
408
409def bash_game_optimal_move(n: int, k: int) -> int:
410 """๋ฐ์ ๊ฒ์์์ ์ต์ ์ ์ (๊ฐ์ ธ๊ฐ ๋์ ๊ฐ์)"""
411 if n % (k + 1) == 0:
412 return 1 # ํจ๋ฐฐ ์ํ, ์๋ฌด ์๋
413 return n % (k + 1)
414
415
416# =============================================================================
417# 9. ์๋ํ ๊ฒ์ (Wythoff's Game)
418# =============================================================================
419
420def wythoff_game(a: int, b: int) -> bool:
421 """
422 ์๋ํ ๊ฒ์: ๋ ๋๋ฏธ์์ ๊ฐ์ ๊ฐ์ ๋๋ ํ ๋๋ฏธ์์ ์์ ๊ฐ์
423 ๋ง์ง๋ง ๋์ ๊ฐ์ ธ๊ฐ๋ ์ฌ๋์ด ์น๋ฆฌ
424 ๋ฐํ: ์ ๊ณต์ด ์ด๊ธฐ๋ฉด True
425 """
426 phi = (1 + 5 ** 0.5) / 2 # ํฉ๊ธ๋น
427
428 if a > b:
429 a, b = b, a
430
431 k = b - a
432 ak = int(k * phi)
433
434 return a != ak
435
436
437# =============================================================================
438# 10. ์ ํด๋ฆฌ๋ ๊ฒ์ (Euclid's Game)
439# =============================================================================
440
441def euclid_game(a: int, b: int) -> bool:
442 """
443 ์ ํด๋ฆฌ๋ ๊ฒ์: ํฐ ์์์ ์์ ์์ ๋ฐฐ์๋ฅผ ๋บ
444 0์ ๋ง๋๋ ์ฌ๋์ด ์น๋ฆฌ
445 ๋ฐํ: ์ ๊ณต์ด ์ด๊ธฐ๋ฉด True
446 """
447 if a < b:
448 a, b = b, a
449
450 if b == 0:
451 return False # ์ด๋ฏธ ๋๋จ
452
453 # ์ฌ๊ท์ ๋ถ์
454 turn = True # True: ์ ๊ณต์ ํด
455 while b > 0:
456 if a >= 2 * b or a == b:
457 return turn
458 a, b = b, a - b
459 turn = not turn
460
461 return not turn
462
463
464# =============================================================================
465# ํ
์คํธ
466# =============================================================================
467
468def main():
469 print("=" * 60)
470 print("๊ฒ์ ์ด๋ก (Game Theory) ์์ ")
471 print("=" * 60)
472
473 # 1. ๋ ๊ฒ์
474 print("\n[1] ๋ ๊ฒ์ (Nim Game)")
475 piles = [3, 4, 5]
476 xor = nim_xor(piles)
477 move = nim_winning_move(piles)
478 print(f" ๋๋ฏธ: {piles}")
479 print(f" XOR: {xor} ({'์ ๊ณต ์น๋ฆฌ' if xor != 0 else 'ํ๊ณต ์น๋ฆฌ'})")
480 if move:
481 print(f" ์น๋ฆฌ ์: ๋๋ฏธ {move[0]}์์ {move[1]}๊ฐ๋ก")
482
483 # ์๋ฎฌ๋ ์ด์
484 print("\n ๊ฒ์ ์๋ฎฌ๋ ์ด์
:")
485 piles_copy = [3, 4, 5]
486 winner = nim_game_simulation(piles_copy, verbose=True)
487 print(f" ์น์: Player {winner}")
488
489 # 2. ์คํ๋ผ๊ทธ-๊ทธ๋ฐ๋
490 print("\n[2] ์คํ๋ผ๊ทธ-๊ทธ๋ฐ๋ ์ ๋ฆฌ")
491 moves = [1, 3, 4] # ํ ๋ฒ์ 1, 3, 4๊ฐ ๊ฐ์ ธ๊ฐ ์ ์์
492 memo = {}
493 for n in range(10):
494 g = calculate_grundy(n, moves, memo)
495 print(f" G({n}) = {g}", end=" ")
496 print()
497
498 # ์ฌ๋ฌ ๋๋ฏธ
499 piles = [7, 5]
500 total_g = multi_pile_grundy(piles, moves)
501 print(f" ๋๋ฏธ {piles}, ์ด๋ {moves}")
502 print(f" ์ ์ฒด ๊ทธ๋ฐ๋: {total_g} ({'์ ๊ณต ์น๋ฆฌ' if total_g != 0 else 'ํ๊ณต ์น๋ฆฌ'})")
503
504 # 3. ๋ณํ ๋
505 print("\n[3] ๋ณํ ๋ ๊ฒ์")
506 # ๋ฏธ์ ๋ฅด ๋
507 piles_misere = [1, 2, 3]
508 print(f" ๋ฏธ์ ๋ฅด ๋ {piles_misere}: ์ ๊ณต {'์น๋ฆฌ' if misere_nim(piles_misere) else 'ํจ๋ฐฐ'}")
509
510 # ๊ณ๋จ ๋
511 stairs = [3, 1, 2, 4] # ๊ณ๋จ 1, 2, 3, 4
512 print(f" ๊ณ๋จ ๋ {stairs}: ๊ทธ๋ฐ๋ = {staircase_nim(stairs)}")
513
514 # 4. ํฑํํ
515 print("\n[4] ํฑํํ ๋ฏธ๋๋งฅ์ค")
516 game = TicTacToe()
517 game.board = [['X', 'O', 'X'],
518 [' ', 'O', ' '],
519 [' ', ' ', ' ']]
520 game.current_player = 'X'
521 print(" ํ์ฌ ์ํ:")
522 game.display()
523 best = game.best_move()
524 print(f" X์ ์ต์ ์ ์: {best}")
525
526 # 5. ๋ ๊ฒ์
527 print("\n[5] ๋ ๊ฒ์")
528 piles = [5, 3, 4, 5]
529 print(f" ๋๋ฏธ: {piles}")
530 result = stone_game(piles)
531 diff = stone_game_dp(tuple(piles), 0, len(piles) - 1)
532 print(f" ์ ๊ณต {'์น๋ฆฌ' if result else 'ํจ๋ฐฐ'} (์ ์ ์ฐจ์ด: {diff})")
533
534 # 6. ๋ฐ์ ๊ฒ์
535 print("\n[6] ๋ฐ์ ๊ฒ์")
536 n, k = 10, 3
537 print(f" n={n}, k={k}")
538 print(f" ์ ๊ณต {'์น๋ฆฌ' if bash_game(n, k) else 'ํจ๋ฐฐ'}")
539 if bash_game(n, k):
540 print(f" ์ต์ ์ ์: {bash_game_optimal_move(n, k)}๊ฐ ๊ฐ์ ธ๊ฐ๊ธฐ")
541
542 # 7. ์๋ํ ๊ฒ์
543 print("\n[7] ์๋ํ ๊ฒ์")
544 test_cases = [(1, 2), (3, 5), (4, 7), (5, 8)]
545 for a, b in test_cases:
546 result = wythoff_game(a, b)
547 print(f" ({a}, {b}): ์ ๊ณต {'์น๋ฆฌ' if result else 'ํจ๋ฐฐ'}")
548
549 # 8. ์ ํด๋ฆฌ๋ ๊ฒ์
550 print("\n[8] ์ ํด๋ฆฌ๋ ๊ฒ์")
551 test_cases = [(25, 7), (24, 10), (100, 45)]
552 for a, b in test_cases:
553 result = euclid_game(a, b)
554 print(f" ({a}, {b}): ์ ๊ณต {'์น๋ฆฌ' if result else 'ํจ๋ฐฐ'}")
555
556 # 9. ์๊ณ ๋ฆฌ์ฆ ์์ฝ
557 print("\n[9] ๊ฒ์ ์ด๋ก ์๊ณ ๋ฆฌ์ฆ ์์ฝ")
558 print(" | ๊ฒ์ | ์น๋ฆฌ ์กฐ๊ฑด |")
559 print(" |----------------|------------------------------|")
560 print(" | ๋ ๊ฒ์ | XOR != 0 |")
561 print(" | ๋ฏธ์ ๋ฅด ๋ | ๋ณต์กํ ์กฐ๊ฑด |")
562 print(" | ๋ฐ์ ๊ฒ์ | n % (k+1) != 0 |")
563 print(" | ์๋ํ ๊ฒ์ | ํฉ๊ธ๋น ๊ธฐ๋ฐ ํจ๋ฐฐ ์์น |")
564 print(" | ์ผ๋ฐ ๊ฒ์ | ์คํ๋ผ๊ทธ-๊ทธ๋ฐ๋ ์ ๋ฆฌ |")
565
566 print("\n" + "=" * 60)
567
568
569if __name__ == "__main__":
570 main()