回溯算法解决1对1feedback组合问题

192 阅读4分钟

问题来源:

组内(13人)要进行1对1,面对面的feedback,每对10分钟。需要在最短内,每个人不重复不间断的进行(人数为奇数时,每个人轮空一次)。

一开始尝试思考算法,手动分组。先试4个人,6个人的排法。到6个人的时候,发现就需要用到回溯了。

回溯算法

回溯算法也叫试探法,它是一种系统地搜索问题的解的方法。

用回溯算法解决问题的一般步骤:

1、 针对所给问题,定义问题的解空间,它至少包含问题的一个(最优)解。

2 、确定易于搜索的解空间结构,使得能用回溯法方便地搜索整个解空间 。

3 、以深度优先的方式搜索解空间,并且在搜索过程中用剪枝函数避免无效搜索。

基本思路:

  • 从决策树的一条路开始走,能进则进,不能进则退回来,换一条路试一试。

简单来说👇

回溯法可以理解成为通过选择不同的岔路口,来寻找目的地,一个岔路口一个岔路口的去尝试找到目的地,如果走错了路的话,继续返回到上一个岔路口的另外一条路,直到找到目的地。

剪枝

这里似乎发现,回溯说到底就是「穷举法」,但是如果只是单纯的穷举的话,不剪枝的话,时间复杂度是巨大的,那么如何剪枝呢?

我们将回溯优化的方法可以称之为剪枝,或者是剪枝函数,通过这个函数,我们可以减去一些状态,剪去一些不可能到达(「最终状态」),这里说的最终状态,可以认为是答案状态,这样子的话,就减少了部分空间树节点的生成

算法思路

  1. 「递归出口」:包括成功条件和剪枝条件
  2. 「选择列表」:通常而言,可以用数组存储可以满足选择的操作
  3. 「路径」:记录做出的选择, 做出选择,递归调用,进入下一层,不选择则撤回,回到上一步

解题思路:

['A', 'B', 'C', 'D', 'E', 'F'] 代表6个人

  1. 生成不重复的两两组合关系
[  [ 'A', 'B' ], [ 'A', 'C' ], [ 'A', 'D' ], [ 'A', 'E' ], [ 'A', 'F' ], 
  [ 'B', 'C' ], [ 'B', 'D' ], [ 'B', 'E' ], [ 'B', 'F' ], 
  [ 'C', 'D' ], [ 'C', 'E' ], [ 'C', 'F' ],
  [ 'D', 'E' ], [ 'D', 'F' ],
  [ 'E', 'F' ]
]
  1. 确定成功条件,也就是选出来的三组中,没有重复的人,且每个人都参与了。

    例如[ [ 'A', 'B' ], [ 'C', 'D' ], [ 'E', 'F' ] ]

  2. 确定剪枝条件和撤销操作,当有重新重复的人时,就跳出,不继续匹配下一组

    例如第一组为[ 'A', 'B' ],循环的下一个是[ 'A', 'C' ],有重复的'A',表示第二组没有找成功,不开始找第三组,而是继续找第二组,直到找到了没有重复的[ 'C', 'D' ],再开始往[ 'C', 'D' ]当前位置开始往后找第三组。

  3. 把已经分配成功的组从映射关系中剔除,下一次不能再次选择。

  4. 分配成功之后,需要在剩余的组合里面,从头开始循环找。

  5. 回溯点,分配第二次的时候,一个组是[ 'A', 'C' ], 找到第二组且满足不重复条件的组是[ 'B', 'D' ],再继续往后循环找,发现循环完了还没有找到第三组,因为能匹配的[ 'E', 'F' ]在上一次已经被选择。这是就需要撤销一步,把第二组选择[ 'B', 'D' ]撤回,从[ 'B', 'D' ]的下一个位置[ 'B', 'E' ]开始找,[ 'B', 'E' ]也满足不重复的条件,则继续找第三组[ 'D', 'F' ]

  6. 最终结果

[  [ [ 'A', 'B' ], [ 'C', 'D' ], [ 'E', 'F' ] ],
  [ [ 'A', 'C' ], [ 'B', 'E' ], [ 'D', 'F' ] ],
  [ [ 'A', 'D' ], [ 'B', 'F' ], [ 'C', 'E' ] ],
  [ [ 'A', 'E' ], [ 'B', 'D' ], [ 'C', 'F' ] ],
  [ [ 'A', 'F' ], [ 'B', 'C' ], [ 'D', 'E' ] ]
]

详细代码

const isEqualArray = (arr1, arr2) => {
  return arr2.every(item => arr1.includes(item));
}

const hasRepeatValue = (arr) => {
  return arr.length > new Set(arr).size;
}

const removeSubArr = (arr, subArr) => {
  subArr.forEach(subArrItem => {
    arr.forEach((arrItem, index) => {
      if (isEqualArray(subArrItem, arrItem)) {
        arr.splice(index, 1);
      }
    })
  })
}

// 生成不重复1对1的映射关系
const generateMapping = (list) => {
  const mapping = [];
  list.forEach((left, index) => {
    const laveList = list.slice(index + 1, list.length);
    laveList.forEach((right) => {
      mapping.push([left, right]);
    })
  })
  return mapping;
}

// 标志是否重头开始循环
let flag = false;

// 回溯
const backtracking = (mapping, target, start, solution, results) => {
  if (hasRepeatValue(solution.flat())) {
    return;
  }

  if (isEqualArray(solution.flat(), target)) {
    results.push([...solution])
    removeSubArr(mapping, solution);
    flag = true;
    return;
  }

  for (let i = start; i < mapping.length; i++) {
    if (flag) {
      flag = false;
      backtracking(mapping, names, 0, [], results);
      return
    }
    solution.push(mapping[i])

    backtracking(mapping, names, start + 1, solution, results);

    solution.pop()
  }
}

// 主方法
const grouping = (names) => {
  const results = [];
  const mapping = generateMapping(names);
  backtracking(mapping, names, 0, [], results);

  return results;
}

调用

const names = ['A', 'B', 'C', 'D', 'E', 'F']

const results = grouping(names)

console.log(results);

当人数为奇数时,可以增加一个空占位符 例如const names = ['A', 'B', 'C', 'D', 'E', '空']