随机队伍生成器

先清洗名单,再用 Fisher-Yates 洗牌、按商余分解切段,保证各队人数差不超过 1、每人恰好出现一次。默认 20 人分 4 队:商 5 余 0,每队正好 5 人。

20 人分 4 队:商 5 余 0,无尾差
r 恒小于队数,两法同解;蛇形只在队数被改大时才显出差别
示例已服务端直出,脚本没跑也有内容;按 Ctrl/Cmd + Enter 触发,Enter 留给换行
名单清洗:原始 20 行 → 去空行 20 → 剥离序号 20 → 去重 20(无重名)
洗牌:Fisher-Yates(Durstenfeld 版),随机源 crypto.getRandomValues + 拒绝采样
分配:20 人 ÷ 4 队 = 商 5 余 0 → 人数差 0,各队 5 人

第 1 队(5 人):示例队名 A
第 2 队(5 人):示例队名 B
第 3 队(5 人):示例队名 C
第 4 队(5 人):示例队名 D

校验:4 队 × 5 人 = 20 = 名单人数 ✓ 最大队 5 − 最小队 5 = 0 ≤ 1 ✓
每人恰好出现一次:20 个名字去重后仍是 20 个 ✓

(分组结果每次都不同;示例仅演示格式,真实分队请以你点按钮得到的结果为准)

人数 → 队数 → 每队人数对照(商余分解)

总人数队数商余各队人数校验
62303 / 36 ✓
72314 / 37 ✓
83223 / 3 / 28 ✓
93303 / 3 / 39 ✓
104223 / 3 / 2 / 210 ✓
124303 / 3 / 3 / 312 ✓
143425 / 5 / 414 ✓
155303 / 3 / 3 / 3 / 315 ✓
164404 / 4 / 4 / 416 ✓
186303 / 3 / 3 / 3 / 3 / 318 ✓
203627 / 7 / 620 ✓
206324 / 4 / 3 / 3 / 3 / 320 ✓
100714215 / 15 / 14 / 14 / 14 / 14 / 14100 ✓
101714315 / 15 / 15 / 14 / 14 / 14 / 14101 ✓
225425 / 5 / 4 / 4 / 422 ✓
236354 / 4 / 4 / 4 / 4 / 323 ✓
135233 / 3 / 3 / 2 / 213 ✓
174415 / 4 / 4 / 417 ✓
2002010010 × 20 队200 ✓
200366267 / 67 / 66200 ✓
200728429 ×4 / 28 ×3200 ✓
1992091910 ×19 / 9 ×1199 ✓
55101 × 5 队5 ✓

使用方法

  1. 把名单粘进「名单」框,一行一人(默认已直出 20 个名字,上限 200 人)。支持空格、逗号、顿号、分号混排;按 Ctrl/Cmd + Enter 触发,Enter 留给换行。
  2. 填「队伍数」(默认 4,整数 1~20)。队数不能超过人数——当前 3 人时填 5 队会被拒绝并告诉你最多 3 队,而不是给你 2 支空队。
  3. 选「分配方式」。默认均匀取商:有余数时前 r 队各多 1 人。选蛇形轮转时余数轮流发给不同队——实测两法在 r < 队数时永远同解,这个选项只在队数被改大时才显出差别。
  4. 点「随机分组」。四张卡给出有效人数、队数、人数差与随机源;结果区逐队列出成员,末尾附两条校验。
  5. 「载入示例」还原 20 人名单与 4 队;对照表固定展示商余分解的 24 组数据,可导出 CSV。

这个随机队伍生成器能解决什么问题

一是让「轮流挑人」这件事变得没有争议。让 supervisor 挨个点名分队,顺序会影响结果——先被点到的人往往来自同一个项目、同一批入职,彼此不认识,分出来的队还是「小圈子」。随机队伍生成器把洗牌放在切段之前,这是唯一能消除顺序影响的顺序。示例名单如果按姓氏笔画排好就直接切 4 段,每一队拿到的必然是笔画相邻的人;洗牌之后这个相关性消失。

二是人数不整除时不靠「凑」。23 人分 6 队,用商余分解得 4/4/4/4/4/3,人数差恰好 1;用「每队轮流多拉一个」的手工办法很容易变成 5/5/5/4/3/1,差 2。本页把不变式写死:各队人数差 ≤ 1 且 Σ = 总人数,两条都由构造保证,不靠事后检查。

三是把名单里的脏数据先清掉。从表格复制的名单常带空行、行首序号(1. 张三)、全角空格、零宽字符、纯符号行,甚至同一个名字出现两次。直接拿去洗牌,结果就是「某队 6 人、某队 4 人」和「张三在一队、二队、三队各出现一次」。本页在洗牌前做五步清洗(剔零宽字符 → trim 去空行 → 按多分隔符拆 → 剥序号 → 去纯符号并去重),并把每一步剔掉几个如实报出来。

本页的随机队伍生成器不做这些事:不保证「强队的都分开」这类平衡性约束——那需要知道每个人的水平,本页只认名字;不保留原顺序或做能力加权;也不做「连续按几次得到同样结果」的复现,那需要自带 PRNG 而不是用 crypto。如果你要的是不重复点名而不是分队,请用随机转盘;要的是纯乱序,请用列表随机打乱。

原理:Fisher-Yates 洗牌与商余分解

为什么必须是 Fisher-Yates,而不是 sort(() => Math.random() - 0.5)。常用的错误写法把随机性塞进比较器里,问题在于当比较器返回 0 时 V8 的 TimSort 会保留原相对序,结果是「某些输入永远排不出某些结果」,且分布随数组规模偏离。实测数据:4 人分队 200000 次,Fisher-Yates 覆盖全部 4! = 24 种排列,最大计数 8537、最小 8121,卡方 30.8(自由度 23、α = 0.05 的临界值是 35.17,低于它即与均匀分布无显著差异);而 sort(Math.random() - 0.5) 的最大计数冲到 37543、最小 3018,卡方 232304.9,是前者的 7500 倍。第三种错误写法「逐位独立取下标」卡方 5965.5,同样不合格。

本页用的具体算法是 Durstenfeld 版 Fisher-Yates:Fisher 与 Yates 在 1938 年的《Statisticians' Key》表 8 给出原始版本(每次从未选元素中随机挑一个移到末尾,用辅助数组),Durstenfeld 在 1992 年把它改成原地交换:for (i = n - 1; i > 0; i--) { j = randomInt(i + 1); swap(a, i, j) }。循环从后往前,每次把 i+1 个未确定位置里的随机一个换到 i,每个位置只需一次取样、总共 n-1 次,且不需要额外内存。

取样用 crypto 加拒绝采样,消除模偏差。页面调 crypto.getRandomValues(new Uint32Array(1)) 拿一个 32 位无符号整数(值域 0~4294967295),再用 randomInt(n) 映射到下标。直接 % n 有模偏差:2^32 mod 20 = 16,下标 0~15 各多一个映射,相对偏差为 n / 2^32(n = 20 时约 4.66×10⁻⁹)。拒绝采样把上界截到 floor(2^32 / n) × n,超出那段整段丢弃重抽,偏差恒为 0。实测各档 n 的拒绝概率最高是 n = 999 时的 0.000013%,期望重抽次数小于 1.0000001 次。这个量级小到测不出来,但抽签、抽奖这类场景应该走 crypto——V8 的 Math.random() 用 xorshift128+,只有 128 位内部状态、周期 2128−1,不是密码学安全的。旧浏览器或非安全上下文(http 非 localhost)下 crypto 不可用,页面降级并在结果区明确标注随机源。

切段用商余分解,不变量是构造出来的。base = floor(n / k)、r = n mod k,于是 r 恒在 [0, k)。均匀取商给前 r 队各加 1 人;蛇形轮转把第 j 个余数发给第 j mod k 队。实测两法在 r < k 时完全同解——20/3、22/5、23/6、13/5、100/7、17/4 六组数据都是这样。原因是 r = n mod k < k 恒成立,余数不够分一圈,蛇形的「轮转」和「顺序发」落在同一批队上。保留蛇形选项是防御性的:若将来允许「每队至少 1 人后再轮转」这类规则,它才是必需的。

为什么不按原始顺序切段。这是最容易犯的错:20 人按原顺序切 4 段,得到的四队是「1-5 号、6-10 号、11-15 号、16-20 号」,如果名单恰好按姓氏笔画或工号排好,每队的人就高度相关。洗牌是唯一解。反过来,随机是幂等混合的:名单如果已经随机过一次,再洗一次不会让结果「更随机」,所以不需要判断输入是否已随机。

名单清洗的四步,各有针对性。① 按 /[,、;\\s]+/ 切分并 filter(Boolean),让空格、逗号、顿号、分号、换行都能当分隔符——「陈嘉禾,林小满、黄梓萱;吴俊杰」这样混排也能正确切成 4 人;② String.prototype.trim 去首尾空白,它会清掉全角空格 U+3000(实测 ' 孙可欣 '.trim() 得 '孙可欣'),但不会清零宽字符 U+200B 与 U+FEFF,所以要额外一步 replace(/[\\u200B-\\u200D\\uFEFF]/g, '');③ 剥行首序号 /^\\d+[.、)]\\s*/,把「1. 陈嘉禾」还原成「陈嘉禾」;④ 用 Set 按原样去重(姓名不做小写化,中文没有大小写问题)。示例名单 20 行、无重名,清洗前后都是 20 人。

上限 200 人、20 队是两层考虑。200 人时切 20 队每队 10 人,人数差必然为 0;200 人切 3 队是 67/67/66。人数上限不是算力问题(200 次取样是毫秒级),而是超过 200 人后队长已经无法凭记忆核对每个队员的名字。队数上限 20 则来自同一考虑。

完整示例(数值仅作演示)

示例条件与页面默认值完全一致:名单 20 人、4 队、均匀取商。逐项核对:

上面每个数字都能在下方对照表里印证结构:第 1 行(6 人 2 队,商 3 余 0)与第 10 行(20 人 3 队,商 6 余 2 → 7/7/6)是两类典型——整除与不整除。「20 人 3 队」这一行是本页默认值 4 队旁边最近的一行,方便你改成 3 队时直接对照。

输入范围与对照表

下面这张表覆盖 12 组整除与 12 组不整除的情况,每行都标了商、余、各队人数与 Σ 校验:

场景总人数队数商余人数差典型分法
整除2045005 / 5 / 5 / 5
余 1723114 / 3
余 21434215 / 5 / 4
余 = 队数 − 12363514 / 4 / 4 / 4 / 4 / 3
余接近队数1352313 / 3 / 3 / 2 / 2
大队数小余数1744115 / 4 / 4 / 4
百人规模1007142115 / 15 / 14 × 5
上限人数2003662167 / 67 / 66
上限人数满队20020100010 × 20
单人一队551001 × 5
队数=人数20201001 × 20
奇数大余19920919110 × 19 / 9 × 1

最后一行值得多看一眼:199 人分 20 队,商 9、余 19,也就是前 19 队各 10 人、最后 1 队只有 9 人。这是「均匀取商」的固有偏好——最后那队永远吃亏 1 人。如果这 1 人让你在意,把分配方式切到蛇形轮转虽然在这个例子结果相同(余数 19 刚好发满一圈),但配合「每队至少 1 人后再轮转」的规则就能把差距摊得更均匀。

边界与精度说明

常见问题

为什么不用「大家轮流点」那种手工作业?

因为顺序会影响结果。轮流点时,先被点到的人往往来自同一项目、同一批入职,彼此熟悉,分出来的队仍是小圈子。必须先洗牌再切段,顺序才被彻底打乱。实测也证明了这个差异的量级:Fisher-Yates 的卡方 30.8(与均匀分布无显著差异),而错误写法 sort(Math.random() - 0.5) 卡方 232304.9。

每次点按钮结果都不一样,能指定种子让它固定吗?

不能,也不建议。取样走 crypto.getRandomValues,它没有种子参数;要固定结果必须换成 Mulberry32 这类自带 PRNG,但那样就不再抗预测了。抽签场景需要的是「别人算不出你要出什么」,所以本页坚持每次都真随机。碰到想要上一次的名单,用「复制结果」先存走。

23 人分 6 队,最后一队只有 3 人,能更平均吗?

不能更好。商余分解给出的 4/4/4/4/4/3 已经是「人数差 ≤ 1」的最优解——要让每队都 ≥ 4 人就需要 24 人。要真正均衡只能改变规则,比如「每队先保底 1 人,剩下的按 round-robin 轮流进」,那会引入「谁先被选」的顺序问题,与随机分队的初衷冲突。

名单里有重名的会怎样?

全部重复行会被剔掉(保留第一次出现的那个),并在结果区报告剔了几行。示例名单 20 人无重名,所以你看到的是「去重 20」。如果你的名单里确实有两个「张伟」而他们确实是不同的人,请先改成「张伟(销售)」「张伟(技术)」再粘进来。

队数填比人数大,为什么直接报错?

因为商余分解会产出空队:3 人分 5 队得到 [1,1,1,0,0],含 2 支空队,1 人分 100 队含 99 支。空队在结果里表现为「(0 人)」的空行,看起来像故障。页面选择提前拒绝并告诉你「当前 3 人最多只能组 3 队」,而不是让你自己从一堆空队里判断发生了什么。

粘进来的名字带序号「1.」和全角空格,能识别吗?

能。清洗分四步:先按空格/逗号/顿号/分号/换行切分,再 trim()(它会清掉全角空格 U+3000),再剥掉行首的 数字 + 点/顿号/右括号 + 空白,最后按原样去重。零宽字符 U+200B、U+200D、U+FEFF trim() 清不掉,页面额外做一次显式剔除。实测「1. 陈嘉禾」会还原成「陈嘉禾」。

「蛇形轮转」和默认的「均匀取商」什么时候才有区别?

在当前规则下永远没有区别。因为余数 r = n mod k 恒小于队数 k,蛇形发的第 j 个余数落到的第 j mod k 队,和顺序发的前 r 队是完全同一批。实测 20/3、22/5、23/6、13/5、100/7、17/4 六组全部同解。保留这个选项是防御性的:将来若改成「每队保底 1 人后再轮转」,它才是必需的。

名单会传到服务器吗?

不会。清洗、洗牌、分配三步全部在当前页面完成,只用到 split、trim、Set、Array.prototype.sort 之外的原生数组操作和 crypto.getRandomValues,没有网络请求,也不写 Cookie 或 localStorage。名单里有真实姓名时可以放心在 Network 面板里自查——面板始终是空的。

相关工具