1. 程式人生 > >【人工智慧】傳教士和野人問題(M-C問題)

【人工智慧】傳教士和野人問題(M-C問題)

摘要

本題需要解決的是一般情況下的傳教士和野人問題(M-C問題)。通過對問題的一般化,我們用一個三元組定義了問題的狀態空間,並根據約束條件制定了一系列的操作規則,最後通過兩個啟發式函式,來優化搜尋過程,並通過討論,探究兩個函式是否能夠求解到最優解。

導言

有N個傳教士和N個野人來到河邊渡河,河岸有一條船,每次至多可供k人乘渡。問傳教士為了安全起見,應如何規劃擺渡方案,使得任何時刻,河兩岸以及船上的野人數目總是不超過傳教士的數目(否則不安全,傳教士有可能被野人吃掉)。即求解傳教士和野人從左岸全部擺渡到右岸的過程中,任何時刻滿足M(傳教士數)≥C(野人數)和M+C≤k的擺渡方案。

實驗過程

狀態空間

我們用一個三元組(m,c,b)來表示河岸上的狀態,其中m、c分別代表某一岸上傳教士與野人的數目,b=1表示船在這一岸,b=0則表示船不在。 約束條件是: 兩岸上M≥C, 船上M+C≤2。 由於傳教士與野人的總數目是一常數,所以只要表示出河的某一岸上的情況就可以了,為方便起見,我們選擇傳教士與野人開始所在的岸為所要表示的岸,並稱其為左岸,另一岸稱為右岸。顯然僅用描述左岸的三元組就足以表示出整個情況了。 綜上,我們的狀態空間可表示為:(ML,CL,BL),其中0≤ML,CL≤N,BL∈{0, 1}。 狀態空間的總狀態數為(N+1)×(N+1)×2,問題的初始狀態是(N,N,1),目標狀態是(0,0,0)。

操作規則

該問題主要有兩種操作:從左岸划向右岸和從右岸划向左岸,以及每次擺渡的傳教士和野人個數。 我們可以使用一個2元組(BM,BC)來表示每次擺渡的傳教士和野人個數,我們用i代表每次過河的總人數,i = 1~k,則每次有BM個傳教士和BC=i-BM個野人過河,其中BM= 0~i,而且當BM!=0時需要滿足BM>=BC。則從左到右的操作為:(ML-BM,CL-BC,B = 1),從右到左的操作為:(ML+BM,CL+BC,B = 0)。 例如當N=3,K=2時,滿足條件的(BM,BC)有: (0,1)、(0,2)、(0,3)、(1,0)、(1,1)、(2,0)、(2,1)、(2,2)、(3,0)、(3,1)、(3,2)、(3,3)。 由於從左到右與從右到左是對稱的,所以此時一共有24種操作。

搜尋策略

  1. 為了避免重複,我們將搜尋過的狀態記錄下來,之後避開搜尋這個狀態。
  2. 我們把滿足條件的狀態稱為安全狀態,首先要定義出安全狀態,通過對問題的分析,不難得出只有滿足以下條件之一的狀態才是安全的(以左岸為例): 1)傳教士與野人的數目相等; 2)傳教士都在左岸; 3)傳教士都不在左岸。 我們只對安全的狀態進行深度優先搜尋,直至找到一個合法的解。
  3. 由於每一次擺渡都有多種操作可以選擇,因此我們定義以下啟發式函式: F1(x) = ML + CL F2(x) = ML + CL – 2B 其中F1(x)滿足A演算法條件的,F2(x)滿足A*演算法條件。 在每次的擺渡中,優先選擇F(x)大的操作進行搜尋。

結果分析

1.擺渡方案結果示例

樣例1:
請輸入N:3
請輸入k:2
找到的解為:
0個傳教士和2個野人從左岸乘船至右岸
左岸有3個傳教士和1個野人
右岸有0個傳教士和2個野人

0個傳教士和1個野人從右岸乘船至左岸
左岸有3個傳教士和2個野人
右岸有0個傳教士和1個野人

0個傳教士和2個野人從左岸乘船至右岸
左岸有3個傳教士和0個野人
右岸有0個傳教士和3個野人

0個傳教士和1個野人從右岸乘船至左岸
左岸有3個傳教士和1個野人
右岸有0個傳教士和2個野人

2個傳教士和0個野人從左岸乘船至右岸
左岸有1個傳教士和1個野人
右岸有2個傳教士和2個野人

1個傳教士和1個野人從右岸乘船至左岸
左岸有2個傳教士和2個野人
右岸有1個傳教士和1個野人

2個傳教士和0個野人從左岸乘船至右岸
左岸有0個傳教士和2個野人
右岸有3個傳教士和1個野人

0個傳教士和1個野人從右岸乘船至左岸
左岸有0個傳教士和3個野人
右岸有3個傳教士和0個野人

0個傳教士和2個野人從左岸乘船至右岸
左岸有0個傳教士和1個野人
右岸有3個傳教士和2個野人

0個傳教士和1個野人從右岸乘船至左岸
左岸有0個傳教士和2個野人
右岸有3個傳教士和1個野人

0個傳教士和2個野人從左岸乘船至右岸
左岸有0個傳教士和0個野人
右岸有3個傳教士和3個野人

樣例2:
請輸入N:5
請輸入k:3
找到的解為:
0個傳教士和2個野人從左岸乘船至右岸
左岸有5個傳教士和3個野人
右岸有0個傳教士和2個野人

0個傳教士和1個野人從右岸乘船至左岸
左岸有5個傳教士和4個野人
右岸有0個傳教士和1個野人

0個傳教士和2個野人從左岸乘船至右岸
左岸有5個傳教士和2個野人
右岸有0個傳教士和3個野人

0個傳教士和1個野人從右岸乘船至左岸
左岸有5個傳教士和3個野人
右岸有0個傳教士和2個野人

0個傳教士和2個野人從左岸乘船至右岸
左岸有5個傳教士和1個野人
右岸有0個傳教士和4個野人

0個傳教士和1個野人從右岸乘船至左岸
左岸有5個傳教士和2個野人
右岸有0個傳教士和3個野人

3個傳教士和0個野人從左岸乘船至右岸
左岸有2個傳教士和2個野人
右岸有3個傳教士和3個野人

1個傳教士和1個野人從右岸乘船至左岸
左岸有3個傳教士和3個野人
右岸有2個傳教士和2個野人

3個傳教士和0個野人從左岸乘船至右岸
左岸有0個傳教士和3個野人
右岸有5個傳教士和2個野人

0個傳教士和2個野人從右岸乘船至左岸
左岸有0個傳教士和5個野人
右岸有5個傳教士和0個野人

0個傳教士和3個野人從左岸乘船至右岸
左岸有0個傳教士和2個野人
右岸有5個傳教士和3個野人

0個傳教士和2個野人從右岸乘船至左岸
左岸有0個傳教士和4個野人
右岸有5個傳教士和1個野人

0個傳教士和3個野人從左岸乘船至右岸
左岸有0個傳教士和1個野人
右岸有5個傳教士和4個野人

0個傳教士和2個野人從右岸乘船至左岸
左岸有0個傳教士和3個野人
右岸有5個傳教士和2個野人

0個傳教士和3個野人從左岸乘船至右岸
左岸有0個傳教士和0個野人
右岸有5個傳教士和5個野人

2.使用啟發式函式所花費的實際費用示例:

N,k 使用F1(x)花費的實際費用 使用F2(x)花費的實際費用
N=3,k=2 11 11
N=5,k=3 15 15
N=25,k=5 95 95
N=50,k=5 195 195
N=100,k=10 409 409

下面我們來討論兩個啟發式函式求解該問題時能否得到最優解:

首先,F1(x)=M+C不滿足A*條件,比如狀態(1, 1, 1),F1(x)=M+C=1+1=2,而實際上只要一次擺渡就可以達到目標狀態,其最優路徑的耗散值為1,所以不滿足A*的條件。   而F2(x)=M+C-2B是滿足A*條件的,證明如下:   先考慮船在左岸的情況。如果不考慮限制條件,也就是說,船一次可以將k個人從左岸運到右岸,然後再有一個人將船送回來。這樣,船一個來回可以運過河k-1人,而船仍然在左岸。而最後剩下的k個人,則可以一次將他們全部從左岸運到右岸。所以,在不考慮限制條件的情況下,也至少需要擺渡ceil((2*N-k)/(k-1))*2+1次。其中分子上的”-k”表示剩下k個留待最後一次運過去。除以”k-1”是因為一個來回可以運過去k-1人,需要(2*N-k)/(k-1)個來回,而”來回”數不能是小數,需要向上取整。而乘以”2”是因為一個來回相當於兩次擺渡,所以要乘以2。而最後的”+1”,則表示將剩下的k個運過去,需要一次擺渡。   再考慮船在右岸的情況。同樣不考慮限制條件。船在右岸,需要一個人將船運到左岸。因此對於狀態(M,C,0)來說,其所需要的最少擺渡數,相當於船在左岸時狀態(M+1,C,1)或(M,C+1,1)所需要的最少擺渡數,再加上第一次將船從右岸送到左岸的一次擺渡數。因此所需要的最少擺渡數為:(M+C+1)-2+1 。其中(M+C+1)的”+1”表示送船回到左岸的那個人,而最後邊的”+1”,表示送船到左岸時的一次擺渡。 綜合船在左岸和船在右岸兩種情況下,所需要的最少擺渡次數用一個式子表示為:M+C-2B。其中B=1表示船在左岸,B=0表示船在右岸。由於該擺渡次數是在不考慮限制條件下,推出的最少所需要的擺渡次數。因此,當有限制條件時,最優的擺渡次數只能大於等於該擺渡次數。所以啟發函式F2(x)是滿足A*條件的。 因此,在有解的情況下,F2(x)在求解本問題時總能找到最優解。對於F1(x),當從左向右擺渡時, F1(x)=F2(x)=M+C,當從右向左擺渡時,F1(x)=M+C,F2(x)=M+C-2,即F1(x)=F2(x)+2,由於我們優先搜尋F(x)較大的狀態空間,而通過兩個函式的關係我們可以知道,他們狀態空間的轉移是完全一致的。故雖然F1(x)不滿足A*條件,但是在本問題中,它也是總能找到最優解。

C++程式碼

#include <iostream> 
#include <vector>
#include <cmath>

using namespace std;

int X, Y;
int k;

struct node
{
    int q[3];
};

vector<node> s;
int q[500][3];
//用於存放搜尋結點,q[][0]是左岸傳教士人數
//q[][1]是左岸野蠻人人數,q[][2]是左岸船的數目
//q[][3]用於搜尋中的父親結點序號。
int ans=0;

int op_num = 0;
int go[500][2];
int fx[500][500];

//安全狀態:左岸中,傳教士都在or都不在or傳教士人數等於野人人數 
int is_safe(int state[3])
{
    if ((state[0]==0||state[0]==X||state[0]==state[1])&&(state[1]>=0)&&(state[1]<=Y))
    {
        return 1;
    }
    return 0;
}

//是否到達目標狀態 
int is_success(int state[3])
{
    if (state[0]==0&&state[1]==0)
        return 1;
    return 0;
}

//該狀態是否已經訪問過 
int vis(int state[3])
{
    for (vector<node>::iterator it = s.begin(); it != s.end(); it++)
        if ((*it).q[0] == state[0] && (*it).q[1] == state[1] && (*it).q[2] == state[2])
            return 1;
    return 0;
}

int f1(int state[3])
{
    return state[0]+state[1];
}

int f2(int state[3])
{
    return state[0]+state[1]-2*state[2];    
} 

int find_max(int cur)
{
    int max = -1;
    int op = -1;
    for (int j = 0; j < op_num; j++)//分別考慮可能的動作
    {
        if (fx[cur+1][j] > max)
        {
            max = fx[cur+1][j];
            op = j;
        }           
    }
    if (max == -1)
        op = -1;
    return op;
}

//過河操作
int search(int cur)
{
    if (is_success(q[cur]))
    {
        ans = cur;
        return 1;
    }
    int state[3];
    int j;
    //cout<<"第"<<cur<<"層搜尋"<<endl;
    //獲取當前搜尋結點
    //cout<<"展開結點"<<cur<<":"<<q[cur][0]<<' '<<q[cur][1]<<' '<<q[cur][2]<<endl;
    if (q[cur][2])//船在左邊
    {
        for (j = 0; j < op_num; j++)//分別考慮可能的動作
        {
            state[0]=q[cur][0]-go[j][0];
            state[1]=q[cur][1]-go[j][1];
            state[2]=0;//船到了右邊
            fx[cur+1][j]=f2(state);
        }
        j = find_max(cur);
        while (j != -1)
        {
            fx[cur+1][j] = -1;
            state[0]=q[cur][0]-go[j][0];
            state[1]=q[cur][1]-go[j][1];
            state[2]=0;//船到了右邊
            if (is_safe(state)&&!vis(state))//如果是安全狀態//判斷與之前展開結點是否相同
            {
                node nd;
                nd.q[0]=q[cur+1][0]=state[0];
                nd.q[1]=q[cur+1][1]=state[1];
                nd.q[2]=q[cur+1][2]=state[2];
                s.push_back(nd);
                //cout<<"合法結點:"<<state[0]<<' '<<state[1]<<' '<<state[2]<<endl;       
                if (search(cur+1))
                    return 1;
            } 
            j = find_max(cur);       
        }
    }
    else    //船在右邊
    {
        for (j = 0; j < op_num; j++)//分別考慮可能的動作
        {
            state[0]=q[cur][0]+go[j][0];
            state[1]=q[cur][1]+go[j][1];
            state[2]=1;
            fx[cur+1][j]=f2(state);
        }
        j = find_max(cur);
        while (j != -1)
        {
            fx[cur+1][j] = -1;
            state[0]=q[cur][0]+go[j][0];
            state[1]=q[cur][1]+go[j][1];
            state[2]=1; //船回到左邊
            if (is_safe(state)&&!vis(state))//如果是安全狀態且與之間狀態不同
            {
                node nd;
                nd.q[0]=q[cur+1][0]=state[0];
                nd.q[1]=q[cur+1][1]=state[1];
                nd.q[2]=q[cur+1][2]=state[2];
                s.push_back(nd);
                //cout<<"合法結點:"<<state[0]<<' '<<state[1]<<' '<<state[2]<<endl;
                if(search(cur+1))
                    return 1;
            }
            j = find_max(cur);
        }
    }
    return 0;
}

int main()
{
    int n;
    cout<<"請輸入N:";
    cin>>n;
    cout<<"請輸入k:";
    cin>>k;
    X = Y = n;

    int state[3];
    //初始狀態 
    node nd;
    nd.q[0]=state[0]=q[0][0]=X;
    nd.q[1]=state[1]=q[0][1]=Y;
    nd.q[2]=state[2]=q[0][2]=1;

    s.push_back(nd);
    //初始化操作
    cout<<"合法的操作組有:"<<endl;
    for (int i = 1; i <= k; i++)
        for ( int j = 0; j <= i; j++)
        {
            if (j >= i-j || j == 0)
            {
                go[op_num][0] = j;
                go[op_num][1] = i-j;
                cout<<go[op_num][0]<<' '<<go[op_num][1]<<endl;
                op_num++;               
            }           
        } 
    cout<<endl;
    if (!search(0))
    {
        cout<<"無解"<<endl;
        return 0;
    }
    cout<<"找到的解為:"<<endl;
    for (int i = 0; i <= ans; i++)
    {
        //cout<<q[i][0]<<' '<<q[i][1]<<' '<<q[i][2]<<endl;
        if (i > 0)
        {
            cout<<abs(q[i][0]-q[i-1][0])<<"個傳教士和"<<abs(q[i][1]-q[i-1][1])<<"個野人";
            if (q[i][2])
                cout<<"從右岸乘船至左岸"<<endl; 
            else
                cout<<"從左岸乘船至右岸"<<endl; 
            cout<<"左岸有"<<q[i][0]<<"個傳教士和"<<q[i][1]<<"個野人"<<endl; 
            cout<<"右岸有"<<n-q[i][0]<<"個傳教士和"<<n-q[i][1]<<"個野人"<<endl<<endl; 
        }
    }

    cout<<"本次搜尋所花費的費用:"<<ans<<endl;     

    return 0;
}

--------------------- 本文來自 jiange_zh 的CSDN 部落格 ,全文地址請點選:https://blog.csdn.net/jiange_zh/article/details/49313787?utm_source=copy