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