Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
207 changes: 207 additions & 0 deletions algorithm/deploy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
"""
μ‚¬κ³Όκ²Œμž„ 배포 λͺ¨λΈ (best) β€” worst-이웃 Simulated Annealing + 병렬 max.

μ„±λŠ₯ (μ‹€μΈ‘):
- 랜덀 λ³΄λ“œ 평균 ~135 (max-of-10, 2800 iter, νŒλ‹Ή ~70초)
- νŒλ³„λ‘œ near-졜적 (μΆ”μ • 천μž₯ λŒ€λΉ„ -2 정도)
- μ—¬λŸ¬ 독립 방법(λΉ”Β·MCTSΒ·λΆ€λΆ„ 브루트포슀) 쀑 졜고

ꡬ쑰:
1) ν•΄ = ν•œ 판의 μ•‘μ…˜ μ‹œν€€μŠ€ (μ•‘μ…˜ = ν•©10 μ΅œμ†Œ μ‚¬κ°ν˜• (r1,c1,r2,c2))
2) 이웃 = 'worst' 지점(μž‘μ€ 수λ₯Ό λ‚­λΉ„ν•œ 수)을 골라 λ‹€λ₯Έ 유효수둜 κ°ˆμ•„νƒ€ β†’ λ’€λ₯Ό μž¬ν”Œλ ˆμ΄
3) rollout μ •μ±… = 적게 μ§€μš°κΈ°(λ”± λ§žλŠ” 짝 μš°μ„ )
4) 수락 = Metropolis (μ˜¨λ„ μ„ ν˜• 냉각)
5) 병렬 = 같은 λ³΄λ“œλ₯Ό μ—¬λŸ¬ μΈμŠ€ν„΄μŠ€(rng만 닀름) 돌렀 졜고 선택

μ‚¬μš©:
from algorithm.deploy import make_board, deploy
board = make_board(1234)
score, sequence = deploy(board, iters=2800, instances=10)

# λ˜λŠ” CLI
python -m algorithm.deploy --seed 1234 --iters 2800 --instances 10
"""
from __future__ import annotations
import math
import random
import argparse
import multiprocessing as mp
from concurrent.futures import ProcessPoolExecutor

import numpy as np

ROWS, COLS = 9, 18
TOTAL = ROWS * COLS


# ────────────────────────── λ³΄λ“œ μœ ν‹Έ ──────────────────────────
def make_board(seed: int) -> np.ndarray:
"""μ‹œλ“œλ‘œ 9Γ—18 λ³΄λ“œ 생성 (κ°’ 1-9)."""
return np.random.default_rng(seed).integers(1, 10, size=(ROWS, COLS), dtype=np.int8)


def _prefix(grid: np.ndarray) -> np.ndarray:
"""2D λˆ„μ ν•© (μž„μ˜ μ‚¬κ°ν˜• 합을 O(1)에)."""
P = np.zeros((ROWS + 1, COLS + 1), dtype=np.int32)
P[1:, 1:] = np.cumsum(np.cumsum(grid, axis=0), axis=1)
return P


def valid_actions(grid: np.ndarray) -> list[tuple[int, int, int, int]]:
"""ν•©=10인 'μ΅œμ†Œ μ‚¬κ°ν˜•' 유효 수 λͺ©λ‘ (ν…Œλ‘λ¦¬ 4변이 λΉ„μ§€ μ•Šμ€ 것)."""
P = _prefix(grid)

def area(r1, c1, r2, c2):
return int(P[r2 + 1, c2 + 1] - P[r1, c2 + 1] - P[r2 + 1, c1] + P[r1, c1])

res = []
for r1 in range(ROWS):
for r2 in range(r1, ROWS):
for c1 in range(COLS):
for c2 in range(c1, COLS):
s = area(r1, c1, r2, c2)
if s == 10:
if area(r1, c1, r1, c2) == 0: continue # μœ„ λ³€
if area(r1, c2, r2, c2) == 0: continue # 였λ₯Έ λ³€
if area(r2, c1, r2, c2) == 0: continue # μ•„λž˜ λ³€
if area(r1, c1, r2, c1) == 0: continue # μ™Ό λ³€
res.append((r1, c1, r2, c2))
elif s > 10:
break # c2 늘리면 ν•©λ§Œ 컀짐 β†’ κ°€μ§€μΉ˜κΈ°
return res


def apply_move(grid: np.ndarray, mv) -> int:
"""mv μ‚¬κ°ν˜•μ˜ 남은 사과 제거 (in-place). 제거 μΉΈ 수 λ°˜ν™˜."""
r1, c1, r2, c2 = mv
region = grid[r1:r2 + 1, c1:c2 + 1]
cleared = int(np.count_nonzero(region))
region[:] = 0
return cleared


# ────────────────────────── rollout μ •μ±… ──────────────────────────
def _fewest_cells(grid: np.ndarray, actions):
"""적게 μ§€μš°λŠ”(λ”± λ§žλŠ” 짝) 수 선택 β€” κ²€μ¦λœ μ΅œμ„ μ˜ rollout μ •μ±…."""
best, best_cells = actions[0], 1 << 30
for a in actions:
r1, c1, r2, c2 = a
cells = int(np.count_nonzero(grid[r1:r2 + 1, c1:c2 + 1]))
if cells < best_cells:
best_cells, best = cells, a
return best


def _rollout(grid, first_action, rng, greedy_prob):
"""first_action 두고 λκΉŒμ§€ ν”Œλ ˆμ΄. (μ‹œν€€μŠ€, 총 제거) λ°˜ν™˜. grid μ†Œλͺ¨λ¨."""
seq, total = [], 0
a = first_action
while a is not None:
total += apply_move(grid, a)
seq.append(a)
acts = valid_actions(grid)
if not acts:
break
a = acts[rng.randrange(len(acts))] if rng.random() >= greedy_prob else _fewest_cells(grid, acts)
return seq, total


def _build_prefix(board, actions):
"""각 μ‹œμ  (λ³΄λ“œ μŠ€λƒ…μƒ·, λˆ„μ  제거) 캐싱."""
grid = board.copy()
pgrid, pclear, acc = [grid.copy()], [0], 0
for a in actions:
acc += apply_move(grid, a)
pgrid.append(grid.copy())
pclear.append(acc)
return pgrid, pclear


def _pick_worst(n, pc, rng):
"""worst 이웃 지점: λ”± λ§žλŠ” 짝(2μΉΈ) 초과둜 'λ‚­λΉ„'ν•œ 수일수둝 μš°μ„  μˆ˜μ •."""
w = [(pc[i + 1] - pc[i] - 2) ** 2 + 0.1 for i in range(n)]
tot = sum(w)
r = rng.random() * tot
acc = 0.0
for i, wi in enumerate(w):
acc += wi
if acc >= r:
return i
return n - 1


# ────────────────────────── 어닐링 ──────────────────────────
def anneal_once(board, iters=2800, rng_seed=0, T0=3.0, greedy_prob=0.8):
"""단일 어닐링 μ‹€ν–‰. (best_score, best_sequence) λ°˜ν™˜."""
rng = random.Random(rng_seed)
acts = valid_actions(board)
if not acts:
return 0, []
cur_seq, cur_score = _rollout(board.copy(), _fewest_cells(board, acts), rng, 1.0)
best_seq, best_score = cur_seq, cur_score
pg, pc = _build_prefix(board, cur_seq)

for it in range(iters):
T = T0 * (1 - it / iters) + 1e-6
if len(cur_seq) < 2:
break
t = _pick_worst(len(cur_seq), pc, rng)
base_g = pg[t]
acts = valid_actions(base_g)
if len(acts) < 2:
continue
cur_a = cur_seq[t]
alt = [a for a in acts if a != cur_a]
if not alt:
continue
nf = alt[rng.randrange(len(alt))]
tail_seq, tail_total = _rollout(base_g.copy(), nf, rng, greedy_prob)
new_score = pc[t] + tail_total
delta = new_score - cur_score
if delta >= 0 or rng.random() < math.exp(delta / T):
cur_seq = cur_seq[:t] + tail_seq
cur_score = new_score
pg, pc = _build_prefix(board, cur_seq)
if cur_score > best_score:
best_seq, best_score = cur_seq, cur_score
return best_score, best_seq


def _worker(arg):
board, iters, rng_seed, T0, gp = arg
return anneal_once(board, iters, rng_seed, T0, gp)


def deploy(board, iters=2800, instances=10, T0=3.0, greedy_prob=0.8):
"""배포 λͺ¨λΈ: instances개 어닐링(같은 λ³΄λ“œ, rng만 닀름) 병렬 β†’ 졜고 선택.
(score, sequence) λ°˜ν™˜. 병렬은 spawn μ»¨ν…μŠ€νŠΈλ‘œ macOS fork+numpy λ°λ“œλ½ νšŒν”Ό."""
args = [(board, iters, i, T0, greedy_prob) for i in range(instances)]
if instances == 1:
return _worker(args[0])
ctx = mp.get_context("spawn")
with ProcessPoolExecutor(max_workers=instances, mp_context=ctx) as ex:
results = list(ex.map(_worker, args))
return max(results, key=lambda r: r[0])


# ────────────────────────── CLI ──────────────────────────
def main():
p = argparse.ArgumentParser(description="μ‚¬κ³Όκ²Œμž„ 배포 λͺ¨λΈ (worst-SA + 병렬 max)")
p.add_argument("--seed", type=int, default=1234, help="λ³΄λ“œ μ‹œλ“œ")
p.add_argument("--iters", type=int, default=2800)
p.add_argument("--instances", type=int, default=10)
p.add_argument("--show-seq", action="store_true", help="수순 좜λ ₯")
args = p.parse_args()

board = make_board(args.seed)
score, seq = deploy(board, iters=args.iters, instances=args.instances)
left = TOTAL - score
print(f"seed {args.seed}: {score}/{TOTAL} ({len(seq)}수, 남은 {left}μΉΈ) "
f"[합계 {int(board.sum())}, 이둠상 μ΅œλŒ€ {162 if board.sum() % 10 == 0 else 161}]")
if args.show_seq:
for i, a in enumerate(seq, 1):
print(f" {i:2d}. {tuple(int(x) for x in a)}")


if __name__ == "__main__":
main()