refactor: 부서별 ceil-aware 균등 분배 알고리즘
기존 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) <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,7 @@
|
|||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
import random
|
import random
|
||||||
from collections import Counter
|
from collections import Counter, defaultdict
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
PEOPLE = [
|
PEOPLE = [
|
||||||
@@ -48,38 +48,55 @@ PEOPLE = [
|
|||||||
|
|
||||||
NUM_TEAMS = 7
|
NUM_TEAMS = 7
|
||||||
TEAM_SIZE = 5
|
TEAM_SIZE = 5
|
||||||
MAX_SAME_DEPT = 2
|
|
||||||
SEED = 20260428 # 행사일 시드 (재현 가능)
|
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):
|
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)
|
rng = random.Random(seed)
|
||||||
for attempt in range(20000):
|
by_dept = defaultdict(list)
|
||||||
shuffled = PEOPLE[:]
|
for name, dept in PEOPLE:
|
||||||
rng.shuffle(shuffled)
|
by_dept[dept].append(name)
|
||||||
teams = [
|
|
||||||
shuffled[i * TEAM_SIZE : (i + 1) * TEAM_SIZE]
|
for d in by_dept:
|
||||||
for i in range(NUM_TEAMS)
|
rng.shuffle(by_dept[d])
|
||||||
]
|
|
||||||
if is_valid(teams):
|
depts_sorted = sorted(by_dept.keys(), key=lambda d: -len(by_dept[d]))
|
||||||
return teams, attempt + 1
|
teams = [[] for _ in range(NUM_TEAMS)]
|
||||||
raise RuntimeError("제약 만족하는 배정 20000회 시도에도 실패")
|
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():
|
def main():
|
||||||
@@ -87,8 +104,20 @@ def main():
|
|||||||
f"인원수 불일치: {len(PEOPLE)}명, 기대 {NUM_TEAMS * TEAM_SIZE}명"
|
f"인원수 불일치: {len(PEOPLE)}명, 기대 {NUM_TEAMS * TEAM_SIZE}명"
|
||||||
)
|
)
|
||||||
|
|
||||||
teams, attempts = assign(SEED)
|
teams = assign(SEED)
|
||||||
print(f"# 시드 {SEED}, 시도 {attempts}회 만에 배정 완료\n")
|
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 = {}
|
mapping = {}
|
||||||
for i, team in enumerate(teams, 1):
|
for i, team in enumerate(teams, 1):
|
||||||
|
|||||||
@@ -1,37 +1,37 @@
|
|||||||
{
|
{
|
||||||
"서한배": "팀1",
|
"심성환": "팀1",
|
||||||
"서희": "팀1",
|
"오근현": "팀1",
|
||||||
"김호승": "팀1",
|
"박영훈": "팀1",
|
||||||
"손현준": "팀1",
|
"이지환": "팀1",
|
||||||
"김동국": "팀1",
|
"김동국": "팀1",
|
||||||
"장혁진": "팀2",
|
|
||||||
"이준형": "팀2",
|
"이준형": "팀2",
|
||||||
"김재현": "팀2",
|
"조민정": "팀2",
|
||||||
"이정태": "팀2",
|
"최호진": "팀2",
|
||||||
"장다현": "팀2",
|
"서희": "팀2",
|
||||||
"김영관": "팀3",
|
"김태현": "팀2",
|
||||||
"정현준": "팀3",
|
"정현준": "팀3",
|
||||||
"이재광": "팀3",
|
"김민섭": "팀3",
|
||||||
"최호진": "팀3",
|
"김영관": "팀3",
|
||||||
"한지승": "팀3",
|
"김병훈": "팀3",
|
||||||
"이성재": "팀4",
|
"변수민": "팀3",
|
||||||
|
"유준희": "팀4",
|
||||||
"유지원": "팀4",
|
"유지원": "팀4",
|
||||||
"정채윤": "팀4",
|
"김재현": "팀4",
|
||||||
|
"장혁진": "팀4",
|
||||||
"박재호": "팀4",
|
"박재호": "팀4",
|
||||||
"강승형": "팀4",
|
"이재광": "팀5",
|
||||||
"변수민": "팀5",
|
"서한배": "팀5",
|
||||||
"김민섭": "팀5",
|
"이준석": "팀5",
|
||||||
"이지환": "팀5",
|
"장다현": "팀5",
|
||||||
"오근현": "팀5",
|
"한지승": "팀5",
|
||||||
"조민정": "팀5",
|
"이정태": "팀6",
|
||||||
"길주현": "팀6",
|
"김호승": "팀6",
|
||||||
"심성환": "팀6",
|
|
||||||
"김병훈": "팀6",
|
|
||||||
"전효준": "팀6",
|
|
||||||
"유용혁": "팀6",
|
"유용혁": "팀6",
|
||||||
|
"길주현": "팀6",
|
||||||
|
"강승형": "팀6",
|
||||||
|
"이성재": "팀7",
|
||||||
|
"전효준": "팀7",
|
||||||
"김정명": "팀7",
|
"김정명": "팀7",
|
||||||
"이준석": "팀7",
|
"정채윤": "팀7",
|
||||||
"박영훈": "팀7",
|
"손현준": "팀7"
|
||||||
"김태현": "팀7",
|
|
||||||
"유준희": "팀7"
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user