一个有意思的小问题

138 阅读3分钟

有一个朋友找我,帮忙解决一个随机抽取问题:

“15人,每次取12人,共抽取 n 次,要求每个人最后被抽取的次数一样,应该怎么实现?”

对于这个问题,需要对 n 有一个强制的前提。也就是 12 * n 可以被 15 整除。例如 n = 8 时,有 12 * 8 = 96 次抽取机会,无法被 15 人平分,因此无解。

对于这个问题,很容易想到,本质来说是需要构建一个 n * 12 的结果矩阵。这个矩阵的行表示一次抽取结果,列则表示本次抽取抽中的人。

这样问题就变得简单了:

假设,一共抽取 10 次,则每人抽取到的次数为 8 次。(12 * 10 = 15 * 8,有解)

  1. 对于每一个人 p,循环 8 次
  2. 每次选择一个随机行
  3. 如果该行已经包含 p,则切换下一行直到不包含p
  4. 再随机抽取一列
  5. 如果该列已经被占,则切换下一列直到空位

这个思路很简单,最终必然包含一个 10 * 12 的结果数组,且每个人被选择 8 次。

但是代码写出来后,发现一个问题:在后期,比如第 14 个人,选择随机行的时候,满足条件“不包含p的行”已经被占满。因此会陷入持续寻找下一列的死循环。

对于这个问题,经过思考,可以得出以下条件:如果我们期望最后一位一定恰好可以在不同行插入,则需要保证不同行最后剩余的空位都是 1。

因此,我们在运行过程中选择行的时候,就不可以完全随机选择(行会被提前占满)。而是优先填充空位较多的行。因为,算法思路调整为:

  1. 对于每一个人 p,循环 8 次
  2. 对当前结果数组运算,每一行的行下标数组,按空位量排序 (suggestLines)
  3. 从 suggestLines[0] 中开始取第一位的行
  4. 如果该行存在 p,则继续获取下一个 suggestLines[i],直到该行不存在 p
  5. 再随机抽取一列
  6. 如果该列已经被占,则切换下一列直到空位

这样,我们就可以保证,最后一定会有 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);