From 3453217e628c040d453508ba4ebbb9ebfbe54ef4 Mon Sep 17 00:00:00 2001 From: th-kim0823 Date: Sat, 25 Apr 2026 19:08:56 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20=EB=B6=80=EC=84=9C=EB=B3=84=20ceil-?= =?UTF-8?q?aware=20=EA=B7=A0=EB=93=B1=20=EB=B6=84=EB=B0=B0=20=EC=95=8C?= =?UTF-8?q?=EA=B3=A0=EB=A6=AC=EC=A6=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기존 round-robin은 운에 따라 부서 분포 max-min=2 발생 (예: HPC 2,1,0,1,1,2,1). ceil 슬롯을 ceil_count 적은 팀에 우선 배정하여 모든 부서 max-min ≤ 1 보장. 결과: - EffTech [1,2,2,2,2,2,1] - System [2,1,1,1,1,1,1] - HPC [1,1,1,1,1,1,2] - Data [1,1,1,1,0,1,1] - Platform [0,0,0,0,1,0,0] (1명) Co-Authored-By: Claude Opus 4.7 (1M context) --- assign_teams.py | 89 +++++++++++++++++++++++++++++++---------------- participants.json | 56 ++++++++++++++--------------- 2 files changed, 87 insertions(+), 58 deletions(-) diff --git a/assign_teams.py b/assign_teams.py index a2ea78f..0f1924b 100644 --- a/assign_teams.py +++ b/assign_teams.py @@ -5,7 +5,7 @@ """ import json import random -from collections import Counter +from collections import Counter, defaultdict from pathlib import Path PEOPLE = [ @@ -48,38 +48,55 @@ PEOPLE = [ NUM_TEAMS = 7 TEAM_SIZE = 5 -MAX_SAME_DEPT = 2 SEED = 20260428 # 행사일 시드 (재현 가능) -# EffTech 신규 합류 → 모든 팀에 ≥1명 필수 (화합 목표) -NEW_DEPT = "AI Efficiency Tech" -MIN_NEW_PER_TEAM = 1 - - -def is_valid(teams): - for team in teams: - depts = [d for _, d in team] - # 같은 부서 ≤ 2명 - if max(Counter(depts).values()) > MAX_SAME_DEPT: - return False - # 신규 부서 ≥ 1명 - if depts.count(NEW_DEPT) < MIN_NEW_PER_TEAM: - return False - return True - def assign(seed): + """ + 부서별 균등 분배 + 팀 사이즈 5 보장. + + 각 부서 N명 → q명 모든 팀 + r명 추가 (q, r = divmod(N, NUM_TEAMS)). + 추가 r명 받을 팀은 "지금까지 추가 적게 받은 팀" 우선 → 모든 팀 정확히 같은 횟수만큼 ceil 받음. + + 효과: + - 부서별 분포: max - min ≤ 1 (균등) + - 모든 팀 정확히 5명 + - EffTech 12명: 5팀이 2명, 2팀이 1명 (모든 팀 ≥1) + """ rng = random.Random(seed) - for attempt in range(20000): - shuffled = PEOPLE[:] - rng.shuffle(shuffled) - teams = [ - shuffled[i * TEAM_SIZE : (i + 1) * TEAM_SIZE] - for i in range(NUM_TEAMS) - ] - if is_valid(teams): - return teams, attempt + 1 - raise RuntimeError("제약 만족하는 배정 20000회 시도에도 실패") + by_dept = defaultdict(list) + for name, dept in PEOPLE: + by_dept[dept].append(name) + + for d in by_dept: + rng.shuffle(by_dept[d]) + + depts_sorted = sorted(by_dept.keys(), key=lambda d: -len(by_dept[d])) + teams = [[] for _ in range(NUM_TEAMS)] + ceil_count = [0] * NUM_TEAMS # 각 팀이 받은 ceil 횟수 + + for dept in depts_sorted: + members = by_dept[dept] + n = len(members) + q, r = divmod(n, NUM_TEAMS) + + if r > 0: + # ceil_count 적은 팀 우선, tie는 random + order = sorted(range(NUM_TEAMS), key=lambda i: (ceil_count[i], rng.random())) + ceil_teams = set(order[:r]) + else: + ceil_teams = set() + + idx = 0 + for ti in range(NUM_TEAMS): + count = q + (1 if ti in ceil_teams else 0) + for _ in range(count): + teams[ti].append((members[idx], dept)) + idx += 1 + if ti in ceil_teams: + ceil_count[ti] += 1 + + return teams def main(): @@ -87,8 +104,20 @@ def main(): f"인원수 불일치: {len(PEOPLE)}명, 기대 {NUM_TEAMS * TEAM_SIZE}명" ) - teams, attempts = assign(SEED) - print(f"# 시드 {SEED}, 시도 {attempts}회 만에 배정 완료\n") + teams = assign(SEED) + print(f"# 시드 {SEED} (round-robin 결정적 배정)\n") + + # 부서별 분포 검증 + dept_dist = defaultdict(list) + for team in teams: + team_counts = Counter(d for _, d in team) + for dept in {d for _, d in PEOPLE}: + dept_dist[dept].append(team_counts.get(dept, 0)) + + print("# 부서별 팀 분배 (max - min ≤ 1 = 균등)") + for dept, dist in sorted(dept_dist.items(), key=lambda x: -sum(x[1])): + print(f" {dept}: {dist} (총 {sum(dist)}명)") + print() mapping = {} for i, team in enumerate(teams, 1): diff --git a/participants.json b/participants.json index 86994a4..f4d82bf 100644 --- a/participants.json +++ b/participants.json @@ -1,37 +1,37 @@ { - "서한배": "팀1", - "서희": "팀1", - "김호승": "팀1", - "손현준": "팀1", + "심성환": "팀1", + "오근현": "팀1", + "박영훈": "팀1", + "이지환": "팀1", "김동국": "팀1", - "장혁진": "팀2", "이준형": "팀2", - "김재현": "팀2", - "이정태": "팀2", - "장다현": "팀2", - "김영관": "팀3", + "조민정": "팀2", + "최호진": "팀2", + "서희": "팀2", + "김태현": "팀2", "정현준": "팀3", - "이재광": "팀3", - "최호진": "팀3", - "한지승": "팀3", - "이성재": "팀4", + "김민섭": "팀3", + "김영관": "팀3", + "김병훈": "팀3", + "변수민": "팀3", + "유준희": "팀4", "유지원": "팀4", - "정채윤": "팀4", + "김재현": "팀4", + "장혁진": "팀4", "박재호": "팀4", - "강승형": "팀4", - "변수민": "팀5", - "김민섭": "팀5", - "이지환": "팀5", - "오근현": "팀5", - "조민정": "팀5", - "길주현": "팀6", - "심성환": "팀6", - "김병훈": "팀6", - "전효준": "팀6", + "이재광": "팀5", + "서한배": "팀5", + "이준석": "팀5", + "장다현": "팀5", + "한지승": "팀5", + "이정태": "팀6", + "김호승": "팀6", "유용혁": "팀6", + "길주현": "팀6", + "강승형": "팀6", + "이성재": "팀7", + "전효준": "팀7", "김정명": "팀7", - "이준석": "팀7", - "박영훈": "팀7", - "김태현": "팀7", - "유준희": "팀7" + "정채윤": "팀7", + "손현준": "팀7" } \ No newline at end of file