有一个朋友找我,帮忙解决一个随机抽取问题:
“15人,每次取12人,共抽取 n 次,要求每个人最后被抽取的次数一样,应该怎么实现?”
对于这个问题,需要对 n 有一个强制的前提。也就是 12 * n 可以被 15 整除。例如 n = 8 时,有 12 * 8 = 96 次抽取机会,无法被 15 人平分,因此无解。
对于这个问题,很容易想到,本质来说是需要构建一个 n * 12 的结果矩阵。这个矩阵的行表示一次抽取结果,列则表示本次抽取抽中的人。
这样问题就变得简单了:
假设,一共抽取 10 次,则每人抽取到的次数为 8 次。(12 * 10 = 15 * 8,有解)
- 对于每一个人 p,循环 8 次
- 每次选择一个随机行
- 如果该行已经包含 p,则切换下一行直到不包含p
- 再随机抽取一列
- 如果该列已经被占,则切换下一列直到空位
这个思路很简单,最终必然包含一个 10 * 12 的结果数组,且每个人被选择 8 次。
但是代码写出来后,发现一个问题:在后期,比如第 14 个人,选择随机行的时候,满足条件“不包含p的行”已经被占满。因此会陷入持续寻找下一列的死循环。
对于这个问题,经过思考,可以得出以下条件:如果我们期望最后一位一定恰好可以在不同行插入,则需要保证不同行最后剩余的空位都是 1。
因此,我们在运行过程中选择行的时候,就不可以完全随机选择(行会被提前占满)。而是优先填充空位较多的行。因为,算法思路调整为:
- 对于每一个人 p,循环 8 次
- 对当前结果数组运算,每一行的行下标数组,按空位量排序 (suggestLines)
- 从 suggestLines[0] 中开始取第一位的行
- 如果该行存在 p,则继续获取下一个 suggestLines[i],直到该行不存在 p
- 再随机抽取一列
- 如果该列已经被占,则切换下一列直到空位
这样,我们就可以保证,最后一定会有 8 行的空位数量都为 1(因为如果某一行空位为 2,则前一位一定会先占用)。这样,就可以获取这个结果数组了。
代码如下:
let result = [];
for (let i = 0; i < 10; i++) {
result.push([]);
for (let j = 0; j < 12; j++) {
result[i].push(0);
}
}
const persons = [];
for (let i = 0; i < 15; i++) {
persons.push('p' + (i + 1));
}
const nextColumn = (j) => {
if (j === result[0].length - 1) {
return 0;
}
return j + 1;
};
const linesOrderByEmpty = () => {
const lines = result.map((line, index) => ({
empty: line.length - line.filter(Boolean).length,
index,
}));
lines.sort((a, b) => {
return b.empty - a.empty;
});
return lines.map((item) => item.index);
};
for (let i = 0; i < persons.length; i++) {
for (let j = 0; j < 8; j++) {
const suggestLines = linesOrderByEmpty();
let suggestLineIndex = 0;
let randomLine = suggestLines[suggestLineIndex];
while (
result[randomLine].includes(persons[i]) &&
suggestLineIndex < suggestLines.length
) {
suggestLineIndex++;
randomLine = suggestLines[suggestLineIndex];
}
let randomColumn = Math.floor(Math.random() * result[randomLine].length);
while (result[randomLine][randomColumn]) {
randomColumn = nextColumn(randomColumn);
}
result[randomLine][randomColumn] = persons[i];
}
}
// validate
const countMap = new Map();
for (let i = 0; i < result.length; i++) {
const row = result[i];
const rowSet = new Set(row);
if (rowSet.size !== row.length) {
// 一行中重复
console.log('error');
}
for (let j = 0; j < result[i].length; j++) {
if (result[i][j] === 0) {
// 一行中有空位
console.log('error');
}
const key = result[i][j];
if (!countMap.has(key)) {
countMap.set(key, 1);
} else {
countMap.set(key, countMap.get(key) + 1);
}
}
}
for (var [key, value] of countMap) {
if (value !== 8) {
// 一个人出现次数不对
console.log('error');
}
}
console.log(result);