开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 7 天,点击查看活动详情
背景
本文是笔者学习博弈算法的沉淀,先介绍一个经典的博弈问题:
给定一个整型数组 arr,代表数值不同的纸牌排成一条线, 玩家A 和 玩家B 依次拿走每张纸牌。
规定:
1. 玩家A先拿,玩家B后拿 2. 每个玩家每次只能拿走最左或最右的纸牌 3. 玩家A和玩家B都绝顶聪明请返回最后获胜者的分数。
博弈问题中,数组给定,先后手给定以后,谁赢谁输已经有结果了,任你再怎么聪明,也无济于事。
举个例子:给定数组 [2, 100, 6],虽然先手绝顶聪明,但是ta不管选2或6,后手只要选走100就赢了(后手也聪明,不会选那个少的...),先手就只能选最后剩下的数字,也就是说,先手拿到的分数永远只能是 2+6=8,而后手可以拿到100。
笔者觉得这个场景很像三国演义的诸葛亮(卧龙)与司马懿(冢虎)之争:
两人都是绝顶聪明,摆在眼前的三国局势就是给定的这个数组,诸葛亮是先手,司马懿是后手。
局势一定,胜负已分,这场争斗一开始就注定了卧龙之败,天意难违。
好了,接下来分享思路🍺。
博弈算法的思路
笔者的思路是,先准备 2 个函数,一个是先手函数,一个是后手函数。
它们都是接收一个数组,返回自己的最好分数(体现绝顶聪明)。由于算法流程两者需要相互调用,因此需要 base case 以触底返回,这里笔者设计的 base case 是 L === R,也就是只剩一张牌的时刻。
先手函数
🔥功能:在 arr[L..R]上先手,返回先手获得的最好分数。
比如:
- 传入
[100], 先手选走100, 返回100。 - 传入
[100,3], 先手选走100, 后手选走3, 返回100。 - 传入
[2,100,6], 先手选走2, 后手选走100, 先手选走6,返回2+6=8。
后手函数
🔥功能:在arr[L..R]上后手, 获得的最好分数,也就是说,先手留给你一个情况,你被动接受,然后全力以赴地去算,得到你的最好分数。
这里能得出两个信息🍺:
- 先手先选,那么
[L..R]跟你没有关系,先手选完的[L+1,R]或[L,R-1]才跟你有关系。 - 先手绝顶聪明, 那么你能赢的唯一情况就是: 先手怎么选都输。
比如:
- 传入
[100], 先手选走100, 后手没有得到数, 返回0. - 传入
[100,3], 先手选走100, 后手选走3, 返回3. - 传入
[2,100,6], 先手选走2, 后手选走100, 先手选走6,返回100.
实现
设计好这两个函数的功能,就可以开始实现了,实现过程把对方看成黑盒即可,只有到达 base case才会返回:
// 先手函数
function f(arr, L, R) {
// 你是先手,现在是你选。
if (L === R) {
// 如果只剩一张牌,先手直接拿走
return arr[L];
}
// 不止一张牌
// 先手的第一种选择:`拿走最左侧的牌`, 最终分数=左牌分数+我在[L+1,R]上后手获得的最好分数 (我在[L+1,R]上后手,意味着现在[L+1,R]上对方先选)
// 左牌分数+后续分数
// 把g看成普通的函数,本体只有先手一个人,只是在g里它不能主动,只能被迫接收对方留下的结果。
const p1 = arr[L] + g(arr, L + 1, R);
// 先手的第二种选择:`拿走最右侧的牌`, 最终分数=右牌分数+我在[L,R-1]上后手获得的最好分数 (我在[L,R-1]上后手,意味着现在[L,R-1]上对方先选)
// 右牌分数+后续分数
const p2 = arr[R] + g(arr, L, R - 1);
// 选最大的
return Math.max(p1, p2);
}
function g(arr, L, R) {
// 注意⚠️⚠️ 你是后手,现在不是你选!! 主动权在先手的手上!
if (L === R) {
// 先手选走唯一的一张牌,后手没有得到数,返回0。
return 0;
}
// 不止一张牌
// 你是后手,现在你不能选。先手可以选,他有两种选择:
// 1.先手选走 arr[L],让后手 (就是我) 只能在 [L+1,R] 上`先手`得到最好分数
const p1 = f(arr, L + 1, R);
// 2.先手选走 arr[R],让后手 (就是我) 只能在 [L,R-1] 上`先手`得到最好分数
const p2 = f(arr, L, R - 1);
// 重点来了: 先手绝顶聪明,他会算出: 选哪边使得`你在剩下的范围得到最优分`比较小,给你留一个小的,所以返回两者的 min。
// 🔥感受到被动了吗?
return Math.min(p1, p2);
// 那是不是后手一定输?
// 不是!!! 如果这两种可能性都大于先手获得的分,后手就赢了!!
// 比如: 我在[2,100,6]上后手,先手的两种选择是: `给我留 [2,100]` 或 `给我留[100,6]`,给我留的这两种情况我都能拿到 100, 我作为后手, 赢了。
// 也就是说,后手能赢的情况是: 先手选哪边都会输。
}
最后,实现博弈算法的入口:
function game(arr) {
if (!arr || arr.length === 0) {
return 0;
}
const first = f(arr, 0, arr.length - 1);
const second = g(arr, 0, arr.length - 1);
console.log('=================🔥结果==================');
console.log(first > second ? `先手赢了🎉` : `后手赢了🎉`);
return Math.max(first, second);
}
总结
本文介绍了笔者对博弈算法的思考,分享了解题思路,代码有点绕,掘友们可以从简单的 arr(只包含1/2/3个数字这种) 开始 debug,慢慢找到感觉就理解了。
曾国藩说:「有恒,则断无不成之事」,保持学习,主动思考,坚持输出,我是前端涤生,希望这篇文章能帮到您🍺🍺