野人传教士过河问题

797 阅读3分钟

2022-09-09 整点简单的问题看看。

问题描述

河的左岸有 33 个传教士和 33 个野人,有一条能载 22 人的船。要求在过河的过程中任意一个时刻,要求两岸都必须满足以下两个条件中至少一个:

  1. 没有传教士;
  2. 传教士的数量大于等于野人的数量。

在此约束条件下,试设计一个渡河方案,使得 66 个人最终都渡到河的右岸。

形式化表述

状态 SS 是一个五元组,满足 S=(ML,SL,MR,SR,B)S=\left(M_L, S_L, M_R, S_R, B\right),其中 MLM_L 表示左岸的传教士个数,SLS_L 表示左岸的野人个数,MRM_R 表示右岸的传教士个数, SRS_R 表示右岸的野人个数,B{L,R}B\in\{L, R\} 表示现在船在左岸还是在右岸。

那么,初始状态为 SSTART=(3,3,0,0,LEFT)S_{START}=\left(3, 3, 0, 0, LEFT\right),即 66 个人都在左岸且船也在左岸;我们想要达到的目标状态为 SEND=(0,0,3,3,RIGHT)S_{END}=\left(0, 0, 3, 3, RIGHT\right),即 66 个人都在右岸且船也在右岸。

我们对安全状态(即合法状态)的定义为:

SAFE(S)=((S.MLS.SL)(S.ML=0))((S.MRS.SR)(S.MR=0)) \text{SAFE}(S)=\left(\left(S.M_L\geq S.S_L\right) \vee \left(S.M_L=0\right)\right) \wedge \left(\left(S.M_R\geq S.S_R\right) \vee \left(S.M_R=0\right)\right)

我们不妨用一个二元组 A=(M,S)A=(M,S) 表示可以采取的行动,其中 MM 表示过河的传教士的数量,SS 表示过河的野人的数量。由于至少要有一个人划船,因此总共可以采取的行动有五种:

  1. 两个野人过河,即 A1=(0,2)A_1=(0, 2)
  2. 两个传教士过河,即 A2=(2,0)A_2=(2, 0)
  3. 一个野人和一个传教士过河 A3=(1,1)A_3=(1, 1)
  4. 一个野人过河 A4=(0,1)A_4=(0, 1)
  5. 一个传教士过河 A5=(1,0)A_5=(1, 0)

在一个状态 SS 下能够采用某个行动 AA 的条件是人数足够调遣且目标状态不会产生危险,即:

AVAI(S,A)=((A.MS.MS.B)(A.SS.SS.B))SAFE(AIM(S,A))\text{AVAI}(S, A)=((A.M\leq S.M_{S.B}) \wedge (A.S \leq S.S_{S.B}))\wedge \text{SAFE}(\text{AIM}(S, A))

其中目标状态 AIM\text{AIM} 的定义为:

AIM(S,A)={(S.MLA.M,S.SLA.S,S.MR,S.SR,R),if  S.B=L(S.ML,S.SL,S.MRA.M,S.SRA.S,L),if  S.B=R\text{AIM}(S, A)=\left\{ \begin{aligned} (S.M_L - A.M, S.S_L - A.S, S.M_R, S.S_R, R), & if \; S.B=L\\ (S.M_L, S.S_L, S.M_R - A.M, S.S_R - A.S, L), & if \; S.B=R \end{aligned} \right.

这样我们就建立了一个形式化的搜索树模型,考虑到一共只有 4×4×2=324\times4\times2=32 种可能的状态,我们可以将这些状态编码为一个整数 HASH(S)\text{HASH}(S) 以便状态判重。

HASH(S)=2×(S.ML×4+S.SL)+[S.B=R]\text{HASH}(S)=2\times(S.M_L \times4+S.S_L) + [S.B=R]

广度有限搜索 (BFS) 实现

/* 传教士野人过河问题 */
/* 针对 N = 3, K = 2 的解法 */

#include <algorithm>
#include <cassert>
#include <iostream>
#include <queue>
#include <stack>

const int L = 0; // 船在河的左岸 B = 0
const int R = 1; // 船在河的右岸 B = 1

struct Action { // 定义一个动作
    int M, S;
    Action(int _M, int _S) { // 初始化工作
        M = _M; S = _S;
    }
};

struct Status { // 定义一个状态
    int M[2]; // M[L] 表示左岸的传教士个数, M[R] 表示右岸的传教士个数
    int S[2]; // S[L] 表示左岸的野人  个数, S[R] 表示右岸的野人  个数
    int B   ; // B = L 表示船在左岸,B = R 表示船在右岸 
    Status(int M_L, int S_L, int M_R, int S_R, int B_N) { // 初始化工作
        M[L] = M_L; M[R] = M_R;
        S[L] = S_L; S[R] = S_R; B = B_N;
    }
    int HASH() const { // 获取状态的哈希值
        return 2*(M[L] * 4 + S[L]) + B;
    }
    int SAFE() const { // 检查一个状态是否安全
        for(int i = std::min(L, R); i <= std::max(L, R); i ++) { // 检查两岸安全性
            if(M[i] < S[i] && M[i] != 0)
                return false;
        }
        return true;
    }
    Status AIM(const Action& A) const { // 根据当前状态和移动方式,生成下一步状态
        Status nxt = *this;
        nxt.M[ B] -= A.M;
        nxt.S[ B] -= A.S;
        nxt.M[!B] += A.M;
        nxt.S[!B] += A.S; // 将对应数量的野人和传教士渡到对岸
        nxt.B      = !B;  // 将船渡到对岸
        return nxt;
    }
    bool AVAI(const Action& A) const { // 判断当前状态能够施加行动 A
        return M[B] >= A.M && S[B] >= A.S && AIM(A).SAFE();
    }
    Status(int hash) {               // 从哈希值加载一个状态
        B    = hash & 1; hash >>= 1;
        S[L] = hash & 3; hash >>= 2; S[R] = 3 - S[L];
        M[L] = hash & 3; hash >>= 2; M[R] = 3 - M[L];

        /* 检查哈希值是否合法 */
        assert(hash == 0);
    }
    void SHOW() const { // 输出信息到屏幕
        std::cout << "(" << M[L] << "," << S[L] <<  ","
            << M[R] << "," << S[R] << "," << B << ")" << std::endl;
    }
};

bool operator==(const Status& S1, const Status& S2) { // 判断状态相同
    for(int i = std::min(L, R); i <= std::max(L, R); i ++) {
        if(S1.M[i] != S2.M[i] || S1.S[i] != S2.S[i])
            return false;
    }
    return S1.B == S2.B;
}

const Action actions[] = {
    Action(2, 0),
    Action(0, 2),
    Action(1, 1),
    Action(1, 0),
    Action(0, 1)
};

const Status S_BEGIN(3, 3, 0, 0, L);
const Status S_END  (0, 0, 3, 3, R);

const int StatusCnt = 32;
int vis[StatusCnt] = {}; // 记录每个状态是否被访问过
int pre[StatusCnt] = {}; // 记录每个状态的前一状态

int main() {
    /* BFS */
    std::queue<Status> Q;
    Q.push(S_BEGIN); vis[S_BEGIN.HASH()] = true;
    while(!Q.empty()) {
        Status Stmp = Q.front(); Q.pop(); // 弹出一个待拓展状态
        for(const Action& Atmp: actions) {// 枚举一个
            if(Stmp.AVAI(Atmp)) {         // 可以实施当前行为
                Status nxt = Stmp.AIM(Atmp);
                int hash   = nxt.HASH();
                if(!vis[hash]) {          // 更新最短路径
                    vis[hash] = true;
                    pre[hash] = Stmp.HASH();
                    Q.push(nxt);
                    if(nxt == S_END) {    // 找到了答案
                        break;
                    }
                }
            }
        }
    }
    /* 输出答案 */
    int hashNow = S_END.HASH();
    if(!vis[hashNow]) {
        std::cout << "SOLUTION NOT FOUND!" << std::endl;
    }else {
        std::stack<Status> STK;
        while(hashNow != S_BEGIN.HASH()) {
            Status SNow(hashNow);
            STK.push(SNow);
            hashNow = pre[hashNow];
        }
        STK.push(S_BEGIN);
        int cnt = 0;
        while(!STK.empty()) { // 正向输出路径
            std::cout << cnt << "\t";
            STK.top().SHOW(); STK.pop();
            cnt += 1;
        }
    }
    return 0;
}

1024code 链接:1024code.com/codecubes/J…