问题来源:
组内(13人)要进行1对1,面对面的feedback,每对10分钟。需要在最短内,每个人不重复不间断的进行(人数为奇数时,每个人轮空一次)。
一开始尝试思考算法,手动分组。先试4个人,6个人的排法。到6个人的时候,发现就需要用到回溯了。
回溯算法
回溯算法也叫试探法,它是一种系统地搜索问题的解的方法。
用回溯算法解决问题的一般步骤:
1、 针对所给问题,定义问题的解空间,它至少包含问题的一个(最优)解。
2 、确定易于搜索的解空间结构,使得能用
回溯法方便地搜索整个解空间 。3 、以深度优先的方式搜索解空间,并且在搜索过程中用剪枝函数避免无效搜索。
基本思路:
- 从决策树的一条路开始走,能进则进,不能进则退回来,换一条路试一试。
简单来说👇
回溯法可以理解成为通过选择不同的岔路口,来寻找目的地,一个岔路口一个岔路口的去尝试找到目的地,如果走错了路的话,继续返回到上一个岔路口的另外一条路,直到找到目的地。
剪枝
这里似乎发现,回溯说到底就是「穷举法」,但是如果只是单纯的穷举的话,不剪枝的话,时间复杂度是巨大的,那么如何剪枝呢?
我们将回溯优化的方法可以称之为剪枝,或者是剪枝函数,通过这个函数,我们可以减去一些状态,剪去一些不可能到达(「最终状态」),这里说的最终状态,可以认为是答案状态,这样子的话,就减少了部分空间树节点的生成
算法思路
- 「递归出口」:包括成功条件和剪枝条件
- 「选择列表」:通常而言,可以用数组存储可以满足选择的操作
- 「路径」:记录做出的选择, 做出选择,递归调用,进入下一层,不选择则撤回,回到上一步
解题思路:
['A', 'B', 'C', 'D', 'E', 'F'] 代表6个人
- 生成不重复的两两组合关系
[ [ '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' ]
]
-
确定成功条件,也就是选出来的三组中,没有重复的人,且每个人都参与了。
例如[ [ 'A', 'B' ], [ 'C', 'D' ], [ 'E', 'F' ] ]
-
确定剪枝条件和撤销操作,当有重新重复的人时,就跳出,不继续匹配下一组
例如第一组为[ 'A', 'B' ],循环的下一个是[ 'A', 'C' ],有重复的'A',表示第二组没有找成功,不开始找第三组,而是继续找第二组,直到找到了没有重复的[ 'C', 'D' ],再开始往[ 'C', 'D' ]当前位置开始往后找第三组。
-
把已经分配成功的组从映射关系中剔除,下一次不能再次选择。
-
分配成功之后,需要在剩余的组合里面,从头开始循环找。
-
回溯点,分配第二次的时候,一个组是[ 'A', 'C' ], 找到第二组且满足不重复条件的组是[ 'B', 'D' ],再继续往后循环找,发现循环完了还没有找到第三组,因为能匹配的[ 'E', 'F' ]在上一次已经被选择。这是就需要撤销一步,把第二组选择[ 'B', 'D' ]撤回,从[ 'B', 'D' ]的下一个位置[ 'B', 'E' ]开始找,[ 'B', 'E' ]也满足不重复的条件,则继续找第三组[ 'D', 'F' ]
-
最终结果
[ [ [ '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', '空']