1.问题的提出
3名商人各带1个随从乘船渡河,一只小船只能容纳2人,由他们自己划行。在河的任何一岸当随从的人数多于商人数时,商人就会有危险。但是如何乘船渡河的大权掌握在商人们手中,商人们怎样才能安全渡河呢?
这就是著名的商仆渡河问题,对于这类智力游戏经过一番逻辑思索是可以找出解决办法的。这里要求将问题推广至商仆对数任意、船只容量也任意的一般情况下,建立数学模型,并编程求解。
2.模型建立与符号说明
记商仆对数为m,船容量为c人。
记第k次渡河前此岸的商人数为xk,随从数为yk,k=1,2,…,xk,yk=0,1,2,…,m,
将二维向量sk=(xk,yk)定义为状态,安全渡河条件下的状态集合定义为允许状态空间,记做S
不难验证,S对此岸和彼岸都是安全的。
记第k次渡船上的商人数为uk,随从数为vk,将二维向量dk=(uk,vk)定义为决策dk,允许决策集合记作D,由小船的容量可知
因为k为奇数时船从此岸驶向彼岸,k为偶数时船由彼岸驶回此岸,所以状态sk随决策dk变化的规律称状态转移律。
这样,制定安全渡河方案归结为如下的多步决策模型:
求决策
使状态,按照转移律(3),由初始状态
经过有限步n到达终止状态
详细问题描述见下图:
这里采用BFS算法(广度优先搜索算法)编程求解商仆过河问题。BFS算法是最经典的图搜索算法之一,在本题中可以保证找到的解为最短路径,即所求方案过河总步数最少,其算法描述如下:
建立一个状态队列q,这是一个先进先出的队列。
(1)将起始状态即(n,n)点加入队列q,标记(n,n)点为已访问。
(2)将q的首节点出队,再将所有该节点可达的未被访问的允许状态加入队列q
(3)将所有新加入的状态标记为已访问,如果其中有终止状态(0,0),则问题有解,算法结束。
(4)如果队列q为空,则问题无解,算法结束。
(5)转至(2)执行
分析:
观察BFS算法的执行过程,算法首先将距离起始状态为1次状态转移的所有状态加入队列,如果其中没有终止状态,则继续将所有距离起始状态为2次状态转移的状态加入队列……
由于队列是先进先出的,可以始终保证所需状态转移步数最少的状态排在队列的最前部,并首先由他们扩展下一层状态。
所以加入队列的每个状态都是以最短路径(最少的状态转移步数)到达的,因为如果有更短的路径存在,则此路径上的状态一定会被更早扩展,更早加入状态队列(所需步数少的状态排在状态队列的前部)。
当终止状态加入队列时,到达终止状态的路径也是最短路径,即求得一个所需状态转移步数最少的过河方案。
若某时刻状态队列为空且始终未到达终止状态,则说明所有自起始状态可达的节点均已被访问,且其中没有终止状态,即终止状态不可达,问题无解。
该问题商仆对数和船容量之间的关系与问题是否有解的分析:
商仆对数 |
小船容量 |
1、2、3 |
≥2 |
4、5 |
≥3 |
≥6 |
≥4 |
代码:
#include <bits/stdc++.h> #include "windows.h" #define MAX_SIZE 1010 using namespace std; struct CNode { int x;//x坐标 int y;//y坐标 int flag;//是否可以行走的点 int dir;//标记行船方向 CNode *p;//父节点指针 }; CNode G[MAX_SIZE][MAX_SIZE][2];//状态空间 坐标,访问 int V[MAX_SIZE][MAX_SIZE][2]; //访问标记 deque <CNode> q;//搜索队列 int num;//商仆对数 int cap;//船容量 bool solve;//解标记 int steps;//步数 void Init();//初始化 void BFS();//BFS搜索 void Output(CNode *p);//输出 int main(){ Init();//进行初始化 while(!q.empty()&&!solve){//BFS搜索 BFS(); } if(!solve) cout<<" 问题无解 "; else{ cout<<" 过河方案: "; Output(&G[0][0][1]);//回朔法输出 cout<<"最少需要"<<steps<<"步 "; } return 0; } void Init(){ cout<<"共有商仆对数:"; cin>>num; cout<<"船容量:"; cin>>cap; int i,j; for(int i = 0 ; i <= num ; i++){ for(int j = 0 ; j <= num ; j++){//初始化状态空间 G[i][j][0].x = G[i][j][1].x = i;//坐标x G[i][j][0].y = G[i][j][1].y = j;//坐标y G[i][j][0].flag = G[i][j][1].flag = 0;//均初始化为不可行点为0 G[i][j][0].p = G[i][j][1].p = NULL;//结点 G[i][j][0].dir = -1;//行船方向左或者下 G[i][j][1].dir = 1;//行船方向右或者上 V[i][j][0] = V[i][j][1] = 0;//未访问 } } for( i = 0 ; i <= num ; i++){//将可行点标记为1 G[0][i][0].flag = G[num][i][0].flag = G[i][i][0].flag = 1; G[0][i][1].flag = G[num][i][1].flag = G[i][i][1].flag = 1; } G[num][num][0].flag = G[num][num][1].flag = 0 ;//右上角为初始状态设置为0 solve = false;//标记问题有解与否 q.push_back(G[num][num][0]);//初始点进人队列 steps = 0;//记录下最少的渡河次数 } void BFS(){ int x,y;//队首所在坐标 int dx,dy;//变化坐标 int nx,ny;//行船后坐标 int dir;//行船方向 if(q.empty()||solve)//搜索队列为空或者有解,退出搜索 return; x=q.front().x;//取出队首坐标 y=q.front().y; dir=q.front().dir; q.pop_front();//队首出队 for(dx = 0 ; dx <= cap ; dx++){ for(dy = 0 ; dy <= cap - dx; dy++){//枚举所有可能的状态 nx = x + dx * dir; ny = y + dy * dir; if(nx < 0 || nx > num || ny < 0 || ny > num )//坐标越界 continue; if(G[nx][ny][0].flag == 0)//达到不可行点 continue; if(dx == 0 && dy == 0)//坐标没变化 continue; if(dir > 0 && V[nx][ny][1] == 1)//该点被访问过 continue; if(dir < 0 && V[nx][ny][0] == 1)//该点被访问过 continue; if(dir>0){//放入队列 G[nx][ny][0].p = &G[x][y][1]; q.push_back(G[nx][ny][0]); } else{//放入队列 G[nx][ny][1].p = &G[x][y][0]; q.push_back(G[nx][ny][1]); } if(dir>0)//标记被访问 V[nx][ny][1] = 1; else V[nx][ny][0] = 1; if(nx == 0 && ny == 0){//达到终点 solve = true; return; } } } } void Output(CNode *p){//回溯法输出遍历结果 if(p -> p == NULL){ cout<<"("<<p->x<<","<<p->y<<") "; return; } Output(p->p); cout<<"("<<p->x<<","<<p->y<<") "; steps++; } /* - - - - - - - - - - - - - - - - - - 3对 * * ** * * * 4对 * * * * * ** * * * 5对 * * * ** * * * ** * * * 6对 * * * ** * * * * * * ** * * * n对 图形为 N */
运行截图: