|
@@ -6,6 +6,7 @@ Required input columns: 班级, 姓名, 性别, 总分
|
|
|
"""
|
|
"""
|
|
|
|
|
|
|
|
import argparse
|
|
import argparse
|
|
|
|
|
+import itertools
|
|
|
import random
|
|
import random
|
|
|
import sys
|
|
import sys
|
|
|
from pathlib import Path
|
|
from pathlib import Path
|
|
@@ -41,8 +42,22 @@ def parse_args():
|
|
|
)
|
|
)
|
|
|
parser.add_argument(
|
|
parser.add_argument(
|
|
|
"-t", "--strategy", type=str, default="good-with-good",
|
|
"-t", "--strategy", type=str, default="good-with-good",
|
|
|
- choices=["good-with-good", "good-with-bad", "random"],
|
|
|
|
|
- help="成绩配对策略:good-with-good(好+好)、good-with-bad(好+差)、random(无关)"
|
|
|
|
|
|
|
+ choices=["good-with-good", "good-with-bad", "random", "good-with-good-advance"],
|
|
|
|
|
+ help="成绩配对策略:good-with-good(好+好)、good-with-bad(好+差)、"
|
|
|
|
|
+ "random(无关)、good-with-good-advance(好+好进阶:好生集中,"
|
|
|
|
|
+ "差生正前方或正后方必有优生,同桌同性)"
|
|
|
|
|
+ )
|
|
|
|
|
+ parser.add_argument(
|
|
|
|
|
+ "--score-column", type=str, default="赋分成绩",
|
|
|
|
|
+ help="排序用的成绩列(默认:赋分成绩,即「总分」组下的赋分子列;找不到则用总分列)"
|
|
|
|
|
+ )
|
|
|
|
|
+ parser.add_argument(
|
|
|
|
|
+ "--good-ratio", type=float, default=1 / 3,
|
|
|
|
|
+ help="优生比例(仅 good-with-good-advance 策略,默认 1/3)"
|
|
|
|
|
+ )
|
|
|
|
|
+ parser.add_argument(
|
|
|
|
|
+ "--bad-ratio", type=float, default=1 / 3,
|
|
|
|
|
+ help="差生比例(仅 good-with-good-advance 策略,默认 1/3)"
|
|
|
)
|
|
)
|
|
|
parser.add_argument(
|
|
parser.add_argument(
|
|
|
"-o", "--output", type=str, default=None,
|
|
"-o", "--output", type=str, default=None,
|
|
@@ -67,7 +82,24 @@ def parse_args():
|
|
|
return parser.parse_args()
|
|
return parser.parse_args()
|
|
|
|
|
|
|
|
|
|
|
|
|
-def read_students(filepath, class_name, sheet_name=None):
|
|
|
|
|
|
|
+def find_score_column(ws, header_row, total_col, score_column):
|
|
|
|
|
+ """在「总分」分组表头下方找到子表头为 score_column 的那一列(1-based)。
|
|
|
|
|
+
|
|
|
|
|
+ 排行榜表格第2行是分组表头(总分/语数外/…),第3行才是子表头
|
|
|
|
|
+ (原始成绩/赋分成绩/班级排名)。直接取「总分」列拿到的是原始成绩,
|
|
|
|
|
+ 这里按子表头选列。返回 (列号, 是否按子表头命中);找不到就用「总分」列。
|
|
|
|
|
+ """
|
|
|
|
|
+ sub_row = header_row + 1
|
|
|
|
|
+ if sub_row <= ws.max_row:
|
|
|
|
|
+ # 只在「总分」组内往后找,避免串到后面的科目分组
|
|
|
|
|
+ for c in range(total_col, min(total_col + 4, ws.max_column + 1)):
|
|
|
|
|
+ val = ws.cell(row=sub_row, column=c).value
|
|
|
|
|
+ if val and str(val).strip() == score_column:
|
|
|
|
|
+ return c, True
|
|
|
|
|
+ return total_col, False
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def read_students(filepath, class_name, sheet_name=None, score_column="赋分成绩"):
|
|
|
"""从Excel读取学生数据,按班级筛选。自动查找表头行。"""
|
|
"""从Excel读取学生数据,按班级筛选。自动查找表头行。"""
|
|
|
from openpyxl import load_workbook
|
|
from openpyxl import load_workbook
|
|
|
|
|
|
|
@@ -108,6 +140,13 @@ def read_students(filepath, class_name, sheet_name=None):
|
|
|
class_name_clean = class_name.rstrip("班")
|
|
class_name_clean = class_name.rstrip("班")
|
|
|
data_start = header_row + 1
|
|
data_start = header_row + 1
|
|
|
|
|
|
|
|
|
|
+ total_col = indices["总分"] + 1
|
|
|
|
|
+ score_col, hit = find_score_column(ws, header_row, total_col, score_column)
|
|
|
|
|
+ if hit:
|
|
|
|
|
+ print(f"排序成绩列:第 {score_col} 列({score_column})")
|
|
|
|
|
+ else:
|
|
|
|
|
+ print(f"排序成绩列:第 {total_col} 列(未找到「{score_column}」子列,改用表头「总分」列)")
|
|
|
|
|
+
|
|
|
students = []
|
|
students = []
|
|
|
for row in range(data_start, ws.max_row + 1):
|
|
for row in range(data_start, ws.max_row + 1):
|
|
|
class_val = str(ws.cell(row=row, column=indices["班级"] + 1).value or "").strip()
|
|
class_val = str(ws.cell(row=row, column=indices["班级"] + 1).value or "").strip()
|
|
@@ -117,7 +156,7 @@ def read_students(filepath, class_name, sheet_name=None):
|
|
|
|
|
|
|
|
name = str(ws.cell(row=row, column=indices["姓名"] + 1).value or "").strip()
|
|
name = str(ws.cell(row=row, column=indices["姓名"] + 1).value or "").strip()
|
|
|
gender = str(ws.cell(row=row, column=indices["性别"] + 1).value or "").strip()
|
|
gender = str(ws.cell(row=row, column=indices["性别"] + 1).value or "").strip()
|
|
|
- score_str = str(ws.cell(row=row, column=indices["总分"] + 1).value or "").strip()
|
|
|
|
|
|
|
+ score_str = str(ws.cell(row=row, column=score_col).value or "").strip()
|
|
|
|
|
|
|
|
if not name:
|
|
if not name:
|
|
|
continue
|
|
continue
|
|
@@ -246,6 +285,314 @@ def allocate_students(students, desk_sizes, strategy, allow_mixed, seed):
|
|
|
return groups
|
|
return groups
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+# ------------------------------------------------------- 分层排布(进阶策略)
|
|
|
|
|
+
|
|
|
|
|
+TIER_GOOD = "优"
|
|
|
|
|
+TIER_MID = "中"
|
|
|
|
|
+TIER_BAD = "差"
|
|
|
|
|
+TIER_CYCLE = (TIER_GOOD, TIER_BAD, TIER_MID) # 排层次循环:优生排紧跟差生排
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def gender_key(student):
|
|
|
|
|
+ """把性别归一成「女」/「男」两池(无法识别的归入男生池)。"""
|
|
|
|
|
+ return "女" if is_female(student["gender"]) else "男"
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def assign_tiers(students, good_ratio, bad_ratio):
|
|
|
|
|
+ """按成绩排名划分层次:前 good_ratio 为优生,后 bad_ratio 为差生,其余为中。"""
|
|
|
|
|
+ ordered = sorted(students, key=lambda s: (-s["score"], s["name"]))
|
|
|
|
|
+ n = len(ordered)
|
|
|
|
|
+ n_good = min(n, max(0, int(round(n * good_ratio))))
|
|
|
|
|
+ n_bad = min(n - n_good, max(0, int(round(n * bad_ratio))))
|
|
|
|
|
+ for i, stu in enumerate(ordered):
|
|
|
|
|
+ if i < n_good:
|
|
|
|
|
+ stu["tier"] = TIER_GOOD
|
|
|
|
|
+ elif i >= n - n_bad:
|
|
|
|
|
+ stu["tier"] = TIER_BAD
|
|
|
|
|
+ else:
|
|
|
|
|
+ stu["tier"] = TIER_MID
|
|
|
|
|
+ return ordered
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _subset_sums(widths):
|
|
|
|
|
+ """一组桌宽能凑出的所有座位数。"""
|
|
|
|
|
+ sums = {0}
|
|
|
|
|
+ for w in widths:
|
|
|
|
|
+ sums |= {s + w for s in sums}
|
|
|
|
|
+ return sorted(sums)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _choose_counts(rows_bands, total, rng):
|
|
|
|
|
+ """给每一排定一个女座数,使合计等于 total,并尽量让女生分散到各排。"""
|
|
|
|
|
+ options = [_subset_sums([w for _, w in rb]) for rb in rows_bands]
|
|
|
|
|
+ caps = [sum(w for _, w in rb) for rb in rows_bands]
|
|
|
|
|
+ total_cap = sum(caps) or 1
|
|
|
|
|
+ best = None
|
|
|
|
|
+
|
|
|
|
|
+ def walk(i, remain, acc):
|
|
|
|
|
+ nonlocal best
|
|
|
|
|
+ if i == len(rows_bands):
|
|
|
|
|
+ if remain != 0:
|
|
|
|
|
+ return
|
|
|
|
|
+ score = sum(abs(acc[j] - total * caps[j] / total_cap) for j in range(len(caps)))
|
|
|
|
|
+ if best is None or score < best[0] - 1e-9 or (
|
|
|
|
|
+ abs(score - best[0]) < 1e-9 and rng.random() < 0.5):
|
|
|
|
|
+ best = (score, list(acc))
|
|
|
|
|
+ return
|
|
|
|
|
+ for v in options[i]:
|
|
|
|
|
+ if v <= remain:
|
|
|
|
|
+ walk(i + 1, remain - v, acc + [v])
|
|
|
|
|
+
|
|
|
|
|
+ walk(0, total, [])
|
|
|
|
|
+ return best[1] if best else None
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _choose_bands(bands, target, rng):
|
|
|
|
|
+ """在一排里挑出若干桌组坐女生,座位数之和等于 target,尽量打散到更多组。"""
|
|
|
|
|
+ for size in range(len(bands), -1, -1):
|
|
|
|
|
+ hits = [combo for combo in itertools.combinations(range(len(bands)), size)
|
|
|
|
|
+ if sum(bands[i][1] for i in combo) == target]
|
|
|
|
|
+ if hits:
|
|
|
|
|
+ return {bands[i][0] for i in rng.choice(hits)}
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _tail_positions(sizes, count):
|
|
|
|
|
+ """从最右侧座位往左数 count 个,返回 [(组, 组内位置), ...]。"""
|
|
|
|
|
+ out = []
|
|
|
|
|
+ for b in range(len(sizes) - 1, -1, -1):
|
|
|
|
|
+ for p in range(sizes[b] - 1, -1, -1):
|
|
|
|
|
+ out.append((b, p))
|
|
|
|
|
+ if len(out) >= count:
|
|
|
|
|
+ return out
|
|
|
|
|
+ return out
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def plan_tiered_layout(students, sizes, good_ratio, bad_ratio, seed):
|
|
|
|
|
+ """分层排布:优生集中,差生正前方或正后方必有优生。
|
|
|
|
|
+
|
|
|
|
|
+ 1. 排层次按「优-差-中」循环,所以每排差生的正前方恰好是优生排;
|
|
|
|
|
+ 每排各桌组同层次、同性别(同桌必定同性)。
|
|
|
|
|
+ 2. 各层次座位容量减去人数得到空位数。优生排的空位必须和它正后方
|
|
|
|
|
+ 差生排的「同组、同位置」一起挖空,否则会出现差生正前方变成空位、
|
|
|
|
|
+ 前后都没有优生的情况。
|
|
|
|
|
+ 3. 按层次把女生的座位数分配到各排,再决定排内哪几个组坐女生。
|
|
|
|
|
+ 4. 每个 (性别, 层次) 池内按成绩降序,从前往后、从左往右填座,
|
|
|
|
|
+ 所以优生里分数最高的坐在最前排。
|
|
|
|
|
+ """
|
|
|
|
|
+ rng = random.Random(seed)
|
|
|
|
|
+ ordered = assign_tiers(students, good_ratio, bad_ratio)
|
|
|
|
|
+ n = len(ordered)
|
|
|
|
|
+ row_capacity = sum(sizes)
|
|
|
|
|
+
|
|
|
|
|
+ unknown = [s["name"] for s in ordered if not is_male(s["gender"]) and not is_female(s["gender"])]
|
|
|
|
|
+ if unknown:
|
|
|
|
|
+ print(f"警告:{len(unknown)} 名学生性别无法识别({'、'.join(unknown[:5])}"
|
|
|
|
|
+ f"{' 等' if len(unknown) > 5 else ''}),已按男生编排。")
|
|
|
|
|
+
|
|
|
|
|
+ if n == 0:
|
|
|
|
|
+ return {
|
|
|
|
|
+ "groups": [[] for _ in range(len(sizes))],
|
|
|
|
|
+ "grid": {}, "voids": set(), "row_roles": [TIER_CYCLE[0]],
|
|
|
|
|
+ "num_rows": 1, "ordered": [],
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ num_rows = max(1, (n + row_capacity - 1) // row_capacity)
|
|
|
|
|
+ row_roles = [TIER_CYCLE[r % len(TIER_CYCLE)] for r in range(num_rows)]
|
|
|
|
|
+
|
|
|
|
|
+ capacity = {t: 0 for t in TIER_CYCLE}
|
|
|
|
|
+ for t in row_roles:
|
|
|
|
|
+ capacity[t] += row_capacity
|
|
|
|
|
+ need = {t: 0 for t in TIER_CYCLE}
|
|
|
|
|
+ for stu in ordered:
|
|
|
|
|
+ need[stu["tier"]] += 1
|
|
|
|
|
+ deficit = {t: capacity[t] - need[t] for t in TIER_CYCLE}
|
|
|
|
|
+
|
|
|
|
|
+ for t in TIER_CYCLE:
|
|
|
|
|
+ if deficit[t] < 0:
|
|
|
|
|
+ print(f"错误:{num_rows} 排座位装不下 {need[t]} 名「{t}」等生(该层次只有 {capacity[t]} 座)。")
|
|
|
|
|
+ print(" 可调整 --good-ratio / --bad-ratio,或换一个座位样式。")
|
|
|
|
|
+ sys.exit(1)
|
|
|
|
|
+ if deficit[TIER_GOOD] != deficit[TIER_BAD]:
|
|
|
|
|
+ print(f"错误:优生空位({deficit[TIER_GOOD]})与差生空位({deficit[TIER_BAD]})不相等,")
|
|
|
|
|
+ print(" 无法保证每个差生的正前或正后方都是优生。请让优生、差生比例保持一致。")
|
|
|
|
|
+ sys.exit(1)
|
|
|
|
|
+
|
|
|
|
|
+ # --- 空位:优生排与其正后方的差生排按同组同位置配对挖空 ---
|
|
|
|
|
+ voids = set()
|
|
|
|
|
+ pair = min(deficit[TIER_GOOD], row_capacity)
|
|
|
|
|
+ if pair:
|
|
|
|
|
+ anchor = None
|
|
|
|
|
+ for r in range(num_rows - 1, -1, -1):
|
|
|
|
|
+ if row_roles[r] == TIER_GOOD and r + 1 < num_rows and row_roles[r + 1] == TIER_BAD:
|
|
|
|
|
+ anchor = r
|
|
|
|
|
+ break
|
|
|
|
|
+ if anchor is None:
|
|
|
|
|
+ print("错误:排数不足以形成「优生排 + 差生排」的配对空位。")
|
|
|
|
|
+ sys.exit(1)
|
|
|
|
|
+ for b, p in _tail_positions(sizes, pair):
|
|
|
|
|
+ voids.add((anchor, b, p))
|
|
|
|
|
+ voids.add((anchor + 1, b, p))
|
|
|
|
|
+
|
|
|
|
|
+ # --- 空位:中等生排从后往前挖 ---
|
|
|
|
|
+ left = deficit[TIER_MID]
|
|
|
|
|
+ for r in range(num_rows - 1, -1, -1):
|
|
|
|
|
+ if left <= 0:
|
|
|
|
|
+ break
|
|
|
|
|
+ if row_roles[r] != TIER_MID:
|
|
|
|
|
+ continue
|
|
|
|
|
+ free = [(b, p) for b in range(len(sizes)) for p in range(sizes[b])
|
|
|
|
|
+ if (r, b, p) not in voids]
|
|
|
|
|
+ for b, p in reversed(free):
|
|
|
|
|
+ if left <= 0:
|
|
|
|
|
+ break
|
|
|
|
|
+ voids.add((r, b, p))
|
|
|
|
|
+ left -= 1
|
|
|
|
|
+ if left > 0:
|
|
|
|
|
+ print(f"错误:还有 {left} 个「{TIER_MID}」等生空位没地方安排。")
|
|
|
|
|
+ sys.exit(1)
|
|
|
|
|
+
|
|
|
|
|
+ # --- 每排实际可用的桌组与座位数 ---
|
|
|
|
|
+ row_bands = {}
|
|
|
|
|
+ for r in range(num_rows):
|
|
|
|
|
+ bands = [(b, sum(1 for p in range(sizes[b]) if (r, b, p) not in voids))
|
|
|
|
|
+ for b in range(len(sizes))]
|
|
|
|
|
+ row_bands[r] = [item for item in bands if item[1] > 0]
|
|
|
|
|
+
|
|
|
|
|
+ # --- 男女分配:先定每排女座数,再定排内哪几个组坐女生 ---
|
|
|
|
|
+ female_need = {t: sum(1 for s in ordered if s["tier"] == t and gender_key(s) == "女")
|
|
|
|
|
+ for t in TIER_CYCLE}
|
|
|
|
|
+ band_gender = {}
|
|
|
|
|
+ for t in TIER_CYCLE:
|
|
|
|
|
+ rows = [r for r in range(num_rows) if row_roles[r] == t]
|
|
|
|
|
+ if not rows:
|
|
|
|
|
+ continue
|
|
|
|
|
+ counts = _choose_counts([row_bands[r] for r in rows], female_need[t], rng)
|
|
|
|
|
+ if counts is None:
|
|
|
|
|
+ print(f"错误:「{t}」等生的男女人数排不进对应的桌组,请调整比例或样式。")
|
|
|
|
|
+ sys.exit(1)
|
|
|
|
|
+ for r, want in zip(rows, counts):
|
|
|
|
|
+ female_bands = _choose_bands(row_bands[r], want, rng)
|
|
|
|
|
+ if female_bands is None:
|
|
|
|
|
+ print(f"错误:第 {r + 1} 排无法排出 {want} 个女生座位。")
|
|
|
|
|
+ sys.exit(1)
|
|
|
|
|
+ for b, _ in row_bands[r]:
|
|
|
|
|
+ band_gender[(r, b)] = "女" if b in female_bands else "男"
|
|
|
|
|
+
|
|
|
|
|
+ # --- 填座 ---
|
|
|
|
|
+ pools = {(g, t): [s for s in ordered if gender_key(s) == g and s["tier"] == t]
|
|
|
|
|
+ for g in ("女", "男") for t in TIER_CYCLE}
|
|
|
|
|
+ idx = {key: 0 for key in pools}
|
|
|
|
|
+
|
|
|
|
|
+ grid = {}
|
|
|
|
|
+ for t in TIER_CYCLE:
|
|
|
|
|
+ for r in range(num_rows):
|
|
|
|
|
+ if row_roles[r] != t:
|
|
|
|
|
+ continue
|
|
|
|
|
+ for b, _ in row_bands[r]:
|
|
|
|
|
+ g = band_gender[(r, b)]
|
|
|
|
|
+ pool = pools[(g, t)]
|
|
|
|
|
+ for p in range(sizes[b]):
|
|
|
|
|
+ if (r, b, p) in voids or idx[(g, t)] >= len(pool):
|
|
|
|
|
+ continue
|
|
|
|
|
+ grid[(r, b, p)] = pool[idx[(g, t)]]
|
|
|
|
|
+ idx[(g, t)] += 1
|
|
|
|
|
+
|
|
|
|
|
+ unseen = {key: len(pools[key]) - idx[key] for key in pools if len(pools[key]) != idx[key]}
|
|
|
|
|
+ if unseen:
|
|
|
|
|
+ print(f"错误:有学生没排下:{unseen}")
|
|
|
|
|
+ sys.exit(1)
|
|
|
|
|
+
|
|
|
|
|
+ groups = []
|
|
|
|
|
+ for r in range(num_rows):
|
|
|
|
|
+ for b in range(len(sizes)):
|
|
|
|
|
+ groups.append([grid[(r, b, p)] for p in range(sizes[b]) if (r, b, p) in grid])
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ "groups": groups,
|
|
|
|
|
+ "grid": grid,
|
|
|
|
|
+ "voids": voids,
|
|
|
|
|
+ "row_roles": row_roles,
|
|
|
|
|
+ "num_rows": num_rows,
|
|
|
|
|
+ "ordered": ordered,
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def verify_tiered_layout(layout, sizes):
|
|
|
|
|
+ """校验硬性约束:同桌同性、差生正前或正后方同列有优生、落座人数。"""
|
|
|
|
|
+ grid = layout["grid"]
|
|
|
|
|
+ num_rows = layout["num_rows"]
|
|
|
|
|
+ problems = []
|
|
|
|
|
+
|
|
|
|
|
+ mixed = 0
|
|
|
|
|
+ for r in range(num_rows):
|
|
|
|
|
+ for b in range(len(sizes)):
|
|
|
|
|
+ genders = {gender_key(grid[(r, b, p)]) for p in range(sizes[b]) if (r, b, p) in grid}
|
|
|
|
|
+ if len(genders) > 1:
|
|
|
|
|
+ mixed += 1
|
|
|
|
|
+ problems.append(f"第 {r + 1} 排第 {b + 1} 组同桌男女混坐")
|
|
|
|
|
+ tiers = {grid[(r, b, p)]["tier"] for p in range(sizes[b]) if (r, b, p) in grid}
|
|
|
|
|
+ if len(tiers) > 1:
|
|
|
|
|
+ problems.append(f"第 {r + 1} 排第 {b + 1} 组层次不统一")
|
|
|
|
|
+
|
|
|
|
|
+ roster = sorted(s["name"] for s in layout["ordered"])
|
|
|
|
|
+ seated = sorted(stu["name"] for stu in grid.values())
|
|
|
|
|
+ if seated != roster:
|
|
|
|
|
+ problems.append(f"落座名单与班级名单不符(落座 {len(seated)} 人,班级 {len(roster)} 人)")
|
|
|
|
|
+
|
|
|
|
|
+ bad_total = bad_ok = 0
|
|
|
|
|
+ for (r, b, p), stu in sorted(grid.items()):
|
|
|
|
|
+ if stu["tier"] != TIER_BAD:
|
|
|
|
|
+ continue
|
|
|
|
|
+ bad_total += 1
|
|
|
|
|
+ if any(grid.get((r + dr, b, p)) and grid[(r + dr, b, p)]["tier"] == TIER_GOOD
|
|
|
|
|
+ for dr in (-1, 1)):
|
|
|
|
|
+ bad_ok += 1
|
|
|
|
|
+ else:
|
|
|
|
|
+ problems.append(f"第 {r + 1} 排第 {b + 1} 组「{stu['name']}」正前/正后方没有优生")
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ "mixed": mixed,
|
|
|
|
|
+ "bad_total": bad_total,
|
|
|
|
|
+ "bad_ok": bad_ok,
|
|
|
|
|
+ "placed": len(grid),
|
|
|
|
|
+ "empty": num_rows * sum(sizes) - len(grid),
|
|
|
|
|
+ "problems": problems,
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def print_tiered_report(layout, stats, sizes):
|
|
|
|
|
+ """打印分层排布的每排构成与约束校验结论。"""
|
|
|
|
|
+ grid = layout["grid"]
|
|
|
|
|
+ num_rows = layout["num_rows"]
|
|
|
|
|
+ print("\n分层排布结果(good-with-good-advance):")
|
|
|
|
|
+ for r in range(num_rows):
|
|
|
|
|
+ counts = {}
|
|
|
|
|
+ for b in range(len(sizes)):
|
|
|
|
|
+ for p in range(sizes[b]):
|
|
|
|
|
+ stu = grid.get((r, b, p))
|
|
|
|
|
+ if stu:
|
|
|
|
|
+ key = (stu["tier"], gender_key(stu))
|
|
|
|
|
+ counts[key] = counts.get(key, 0) + 1
|
|
|
|
|
+ detail = ",".join(f"{t}生{k} {v}人" for (t, k), v in sorted(counts.items()))
|
|
|
|
|
+ empty = sum(1 for b in range(len(sizes)) for p in range(sizes[b])
|
|
|
|
|
+ if (r, b, p) not in grid)
|
|
|
|
|
+ tail = f",空 {empty} 座" if empty else ""
|
|
|
|
|
+ print(f" 第 {r + 1:2d} 排({layout['row_roles'][r]}生排){detail}{tail}")
|
|
|
|
|
+
|
|
|
|
|
+ ok_gender = "通过" if stats["mixed"] == 0 else "不通过"
|
|
|
|
|
+ ok_frontback = "通过" if stats["bad_ok"] == stats["bad_total"] else "不通过"
|
|
|
|
|
+ print("\n约束校验:")
|
|
|
|
|
+ print(f" 同桌同性:{ok_gender}(混坐 {stats['mixed']} 组)")
|
|
|
|
|
+ print(f" 差生正前或正后方有优生:{ok_frontback}({stats['bad_ok']}/{stats['bad_total']} 人)")
|
|
|
|
|
+ print(f" 落座 {stats['placed']} 人,空 {stats['empty']} 座")
|
|
|
|
|
+ if stats["problems"]:
|
|
|
|
|
+ print(" 未通过明细(最多列 20 条):")
|
|
|
|
|
+ for msg in stats["problems"][:20]:
|
|
|
|
|
+ print(f" - {msg}")
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
def generate_excel(groups, style_str, output_path, class_name):
|
|
def generate_excel(groups, style_str, output_path, class_name):
|
|
|
"""生成座位表Excel文件。"""
|
|
"""生成座位表Excel文件。"""
|
|
|
wb = Workbook()
|
|
wb = Workbook()
|
|
@@ -361,7 +708,7 @@ def main():
|
|
|
|
|
|
|
|
# 读取学生(只读一次)
|
|
# 读取学生(只读一次)
|
|
|
print(f"正在读取 {args.excel_file} ...")
|
|
print(f"正在读取 {args.excel_file} ...")
|
|
|
- students = read_students(args.excel_file, args.class_name, args.sheet)
|
|
|
|
|
|
|
+ students = read_students(args.excel_file, args.class_name, args.sheet, args.score_column)
|
|
|
|
|
|
|
|
if not students:
|
|
if not students:
|
|
|
print(f"警告:班级 '{args.class_name}' 中没有找到任何学生,将生成空座位表。")
|
|
print(f"警告:班级 '{args.class_name}' 中没有找到任何学生,将生成空座位表。")
|
|
@@ -380,8 +727,12 @@ def main():
|
|
|
"good-with-good": "好+好",
|
|
"good-with-good": "好+好",
|
|
|
"good-with-bad": "好+差",
|
|
"good-with-bad": "好+差",
|
|
|
"random": "与成绩无关",
|
|
"random": "与成绩无关",
|
|
|
|
|
+ "good-with-good-advance": "好+好进阶(分层排布,差生前后排必有优生)",
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ if args.strategy == "good-with-good-advance" and args.allow_mixed:
|
|
|
|
|
+ print("提示:good-with-good-advance 固定「同桌同性」,-m/--allow-mixed 对它无效。")
|
|
|
|
|
+
|
|
|
# 生成多个座位表
|
|
# 生成多个座位表
|
|
|
for i in range(args.count):
|
|
for i in range(args.count):
|
|
|
seed = base_seed + i
|
|
seed = base_seed + i
|
|
@@ -393,9 +744,18 @@ def main():
|
|
|
else:
|
|
else:
|
|
|
output_path = output_base
|
|
output_path = output_base
|
|
|
|
|
|
|
|
- groups = allocate_students(students, desk_sizes, args.strategy, args.allow_mixed, seed)
|
|
|
|
|
-
|
|
|
|
|
- generate_excel(groups, args.style, output_path, args.class_name)
|
|
|
|
|
|
|
+ if args.strategy == "good-with-good-advance":
|
|
|
|
|
+ sizes = [int(c) for c in args.style]
|
|
|
|
|
+ layout = plan_tiered_layout(
|
|
|
|
|
+ students, sizes, args.good_ratio, args.bad_ratio, seed
|
|
|
|
|
+ )
|
|
|
|
|
+ groups = layout["groups"]
|
|
|
|
|
+ generate_excel(groups, args.style, output_path, args.class_name)
|
|
|
|
|
+ stats = verify_tiered_layout(layout, sizes)
|
|
|
|
|
+ print_tiered_report(layout, stats, sizes)
|
|
|
|
|
+ else:
|
|
|
|
|
+ groups = allocate_students(students, desk_sizes, args.strategy, args.allow_mixed, seed)
|
|
|
|
|
+ generate_excel(groups, args.style, output_path, args.class_name)
|
|
|
|
|
|
|
|
# 摘要
|
|
# 摘要
|
|
|
print(f"\n座位表配置:")
|
|
print(f"\n座位表配置:")
|