从魅魔过河题到 JavaScript 的两个技巧

1、看题

最近【2023年11月20日】群里看见了一道算法题,虽然很快看见了答案,但是还是想用程序思维去解一遍,题目如下:

5个人过河 分别是 妈妈 爸爸 哥哥 妹妹 路人:

  • 妈妈是魅魔会单独与男性在一起时对男性进行侵犯
  • 爸爸是鬼父单独与妹妹在一起时回进行侵犯
  • 哥哥是德国骨科单独跟妹妹在一起时会进行性行为
  • 妹妹也是德国骨科单独跟哥哥在一起时候回进行性行为但妹妹身体弱不能开船
  • 路人是集佬单独于哥哥在一起时候回进行侵♂犯

只有一首船 每次只能坐两个人 船不会自己回来 过去就要有人开回来
如何在不发生任何性行为5个人成功到对面?

分析题目,我们可以将场景分为三个,左岸、船上、右岸,在这三个场景之中,都不能发生任何羁绊,即不能出现爸&妈爸&妹妈&哥妈&路哥&妹哥&路的情况,而且从左到右,船上肯定是两个人,以及从右到左,船上肯定是一个人。此时,解题思路就很清晰了:

  • 当船在左边时,挑选两个上船到右边。
  • 当船在右边时,挑选一个去左边。
  • 每次变动进行失败、成功及重复性检测。

2、先上解题代码:

const [,,,,] = [0b00001, 0b00010, 0b00100, 0b01000, 0b10000];
const rule = [,|,|,|,|,|,|];
const stageFirst = { d: [0b11111, 0, 0], boat: 0 };
const toArray = (m) => [...(m + 32).toString(2)].slice(1).map((v, k) => Number('0b' + v) << 4 - k).filter(v => !!v);
const arrPickCases = (arr, size) => {
    const keyCases = (l, n) => {
        if (n === l) return [new Array(l).fill(1).map((_v, k) => k)];
        if (n === 1) return [...new Array(l).fill(1).map((_v, k) => [k])];
        return [...keyCases(l - 1, n), ...keyCases(l - 1, n - 1).map(v => v.concat(l - 1))];
    }
    return keyCases(arr.length, size).map(keys => keys.map(key => arr[key]));
}
const calc = (s, log = [], r = [], cs = [], ns = {}) => {
    if (s.d.some(v => rule.indexOf(v) > -1)) return false;
    if (cs.indexOf(JSON.stringify(s.d)) > -1) return false;
    [cs.push(JSON.stringify(s.d)), log.push([...s.d])];
    if (s.d[2] + s.d[1] === 0b11111) r.push([...log, [0, 0, 31]]);
    else if (s.boat === 0) {
        arrPickCases(toArray(s.d[0] = s.d[0] + s.d[1]), 2).map(vs => {
            [ns = { d: [...s.d], boat: 1 }, ns.d[1] = vs[0] + vs[1], ns.d[0] -= ns.d[1], calc(ns, [...log], r, cs)];
        });
    } else {
        toArray(s.d[2] = s.d[2] + s.d[1])
            .map(v => [ns = { d: [...s.d], boat: 0 }, ns.d[1] = v, ns.d[2] -= v, calc(ns, [...log], r, cs)]);
    }
    return r;
}
const result = calc(stageFirst);
const parseLabel = n => Object.entries({,,,,, __: 0 }).find(v => v[1] === n)[0];
console.log(`我们有${result.length}种正直的结局:\n\n`);
result.map((datas, no) => {
    const render = datas.map(data => data.map((v, k) => [...toArray(v), 0, 0, 0, 0, 0].slice(0, k == 1 ? 2 : 5).map(n => parseLabel(n)).join('')).join('\t\t\t')).join('\n');
    console.log(`[结局 ${no + 1}]:\n\n${render}`)
});
以及输出结果
我们有2种正直的结局:
[结局 1]:

路妹哥妈爸			____			__________
妹哥妈____			路爸			__________
妹哥妈____			爸__			路________
哥爸______			妹妈			路________
哥爸______			路__			妹妈______
哥________			路爸			妹妈______
哥________			爸__			路妹妈____
__________			哥爸			路妹妈____
__________			____			路妹哥妈爸
[结局 2]:

路妹哥妈爸			____			__________
妹哥妈____			路爸			__________
妹哥妈____			爸__			路________
哥爸______			妹妈			路________
哥爸______			路__			妹妈______
路________			哥爸			妹妈______
路________			爸__			妹哥妈____
__________			路爸			妹哥妈____
__________			____			路妹哥妈爸

3、技巧一:遍历数组元素组合

既然要用程序去解这种题目,实现我们就面临了第一个问题:怎么优雅的从一个数组中取出N个元素呢?
我们先定义这个函数为 f (arr, s) arr就是数组本身,s为每个组合的数组尺寸,同时 数组的每一项其实都可以写成 arr[key]所以我们可以把f (arr, s) 问题先转换为f2 (n, s) 的问题,n是什么,n就是 [ 1, 2, 3, ... n]这样的有序数组的最大值。
首先我们先分析一下我们有哪些规律可循:

  • 从n个元素中取出n个元素,那结果就只有一个,即 f(n, n) = [[ 1, 2, 3, ... n]]
  • 从n个元素中取出1个元素,那结果就为n个,即 f(n, 1) = [[1], [2], [3], ... [n]]
  • 从n个元素中取出s (s < n)个元素,可以转换为从n-1个元素取出s个元素的合集,加上从n-1个元素取出s-1个元素的合集每一项加上n本身,即 f(n, s) = f(n-1,s) + f(n-1, s-1).map(v=>v + n)

利用第三点的降级法,我们始终可以让 n 和 s 降级到 n = s, 或者s = 1的情况,那我们就可以写出以下递归函数:

const arrPickCases = (arr, size) => {
    const keyCases = (l, n) => {
        if (n === l) return [new Array(l).fill(1).map((_v, k) => k)];
        if (n === 1) return [...new Array(l).fill(1).map((_v, k) => [k])];
        return [...keyCases(l - 1, n), ...keyCases(l - 1, n - 1).map(v => v.concat(l - 1))];
    }
    return keyCases(arr.length, size).map(keys => keys.map(key => arr[key]));
}

4、技巧二:利用二进制位计算代表boolean数列

在这道题目里,有三个场景,而我们判断成功和失败的条件也是,某些人是否同时都在同一个场景这种,比如 爸.isHere === 1 && 妈.isHere === 1 && others.isHere === 0,那我们就可以定义一个数字中的二进制的每一位的1代表这个人在,0代表这个人不在,比如定义好const [爸, 妈, 哥, 妹, 路] = [0b00001, 0b00010, 0b00100, 0b01000, 0b10000];,即 0b11111代表所有人都在,0b00000代表所有人都不在,0b00010代表只有妹妹一个人,属于不可发生的情况,可以把题目里的规则定义为具体的场景值数组:

const rule = [,|,|,|,|,|,|];

有这种简单的方法,那么我们只需要对比场景值就可以判断是否成功或者是否失败了。同时,也方便去修改人的离开和加入,比如 0b1001 + 0b00100 = 0b101010b1111 1 - 0b00100 = 0b11011简单的加减法就可以完成人物的加入和离去,比修改数组方便很多。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值