题目:
L2-3 人狼羊菜过河(*)
一个人要将一匹狼、一只羊、一筐菜运到河对岸,但是他的船太小了,一次只能带一样过河。当他不在时,狼要吃羊、羊要吃菜。他怎样才能安全地将狼、羊、菜都运过河呢?
请编写程序,找出过河的全部方案。
用 4 个字母分别表示人、狼、羊、菜的状态:
- 用 M 表示人在,用 . 表示人不在
- 用 W 表示狼在,用 . 表示狼不在
- 用 G 表示羊在,用 . 表示羊不在
- 用 C 表示菜在,用 . 表示菜不在
- 用 -> 表示船将从左岸去右岸,用 <- 表示船将从右岸去左岸。
举例说明:
若人狼羊菜全在左岸,则状态表示为
MWGC -> ....
若人将羊运到右岸,左岸只剩狼和菜,则状态表示为
.W.C <- M.G.
输入格式
初始状态
终止状态
输出格式
若有解,则输出所有解法。
每一个解法由若干行组成,每行表示一个状态。
不同的解法之间空一行。
若无解,则输出 None。
要求:输出的解法中不允许出现两行相同的状态。
输入样例1
MWGC -> ....
.... <- MWGC
输出样例1
MWGC -> ....
.W.C <- M.G.
MW.C -> ..G.
...C <- MWG.
M.GC -> .W..
..G. <- MW.C
M.G. -> .W.C
.... <- MWGC
MWGC -> ....
.W.C <- M.G.
MW.C -> ..G.
.W.. <- M.GC
MWG. -> ...C
..G. <- MW.C
M.G. -> .W.C
.... <- MWGC
输入样例2
.W.. <- M.GC
M.GC -> .W..
输出样例2
.W.. <- M.GC
MWG. -> ...C
..G. <- MW.C
M.GC -> .W..
.W.. <- M.GC
MW.C -> ..G.
...C <- MWG.
M.GC -> .W..
输入样例3
MWG. -> ...C
MWG. -> ...C
输出样例3
None
输入样例4
MWG. -> ...C
.WG. -> M..C
输出样例4
None
注:为使输出的解法排列顺序一致,要求人每次渡河时都按以下顺序进行探索:
- 直接过河(不带任何东西)
- 带狼过河
- 带羊过河
- 带菜过河
题意:
在人不在时,狼会吃羊,羊会吃菜;题目给出初始和最终的河岸两边(人狼羊菜)的分布情况S1,S2,问在安全的情况下,输出S1到S2的全部解法(输出全部解法时要求人每次渡河时都按以上题目中规定顺序输出)。
思路:
该题目我们可以将其转化为:可能出现的每一种状态记作结点,存在某两个状态可转移关系(安全情况下)记作边,建图,求S1结点到S2结点的所有简单路径问题。
首先我们以左岸边为参考系,人与船可视为一体,当某一物种在左岸记为1,反之即为0;如此即可将所有分布情况通过四位二进制数XXXX(X=0,1)表示当前状态(结点)。当然还需对每一种状态进行检查,判断该状态是否合法,如果合法则push到结点集e中;
int check1(int i){
// 人在的一岸安全,故检查另一岸是否合理;
if((i&0b1000) != 0) i ^= 0b1111; // 取反;
if((i & (i<<1)) != 0) return 0; // 狼羊 || 羊菜;
return 1;
}
第二步就是遍历结点集中每个结点i(结点编号即为二进制数值),编写函数判断结点i与其他结点是否有转移关系,例如:状态1111与1101和0101与1101都存在转移关系,前者是人带着羊到河对岸,后者是人单独从对岸过来;如果存在两结点i,j有转移关系,即可将边<i, j>push到边集中。
// 判断状态i,j是否可互相转移;
int check2(int i, int j, int &temp){ // temp为边权;
if(i == j) return 0;
if(i < j) swap(i, j);
// 状态转移,人从一岸转移到另一岸;
if(((i>>3) ^ (j>>3)) == 0) return 0; // 当i,j状态都为1xxx,1xxx则不存在转移关系;
// 人最多只能带一个东西;
i %= 8; j %= 8; // 1xxx -> 0xxx;
if(i == j){ temp = 3; return 1;} // 人单独过河;
if(__builtin_popcount(i^j) == 1){ // 人带一个物种过河(__builtin_popcount内置函数,可以求二进制数中有多少个1);
if((i^j) == 0b100) temp = 2; // 带狼过河;
if((i^j) == 0b10) temp = 1; // 带羊过河;
if((i^j) == 0b1) temp = 0; // 带菜过河;
return 1;
}
return 0;
}
最后就是建图求两点所有简单路径了,建图方式这里采用链式前向星的数据结构,题目中特意强调了探索顺序是人单独过河、带狼、带羊、带菜的探索顺序优先输出,所以每次从单独过河的边开始遍历以此类推,故在边集中给每个边设置边权,再根据边权从小到大排序每个边,将边权较小的边先加入到图中,如此通过这个方法来控制遍历结点的边的顺序(感觉这个方法有点呆)。建完图后通过dfs深度优先遍历去寻找所有的S1到S2的简单路径并按相应的格式输出即可。
vector<int> temp_path;
vector<int> vis;
void dfs(int u, int v, vector<vector<int> > &path){
temp_path[++index] = u;
// 当u到达v保存路径;
if(u == v){
temp_path[0] = index--; // temp_path[0]来保存该条路径有多少结点;
path.push_back(temp_path);
return;
}
vis[u] = 1; // 标记经过的路径;
for(int i = head[u];i;i = edge[i].next){
int to = edge[i].v;
if(vis[to]) continue;
dfs(to, v, path);
}
vis[u] = 0; index--;
}
大致思路就是这样,本人萌新小白一枚૧(●´৺`●)૭,解题思路和代码有不足的地方还请各位大佬指点,最后献上源码。
AC源码:
#include <bits/stdc++.h>
using namespace std;
#define INF 0x3f3f3f3f
#define int long long
#define endl '\n'
typedef struct node{
int u, v, w;
bool operator < (const node &n) const{
return w < n.w;
};
}Node;
typedef struct MP {
int num = 0, index = 0;
int head[20];
vector<int> temp_path;
vector<int> vis;
struct edge{
int next, v;
}edge[4000];
void Init(){
temp_path = vector<int>(20, 0);
vis = vector<int>(20, 0);
memset(head, 0, sizeof(head));
}
void add(int u, int v){
edge[++num].next = head[u];
edge[num].v = v;
head[u] = num;
}
void dfs(int u, int v, vector<vector<int> > &path){
temp_path[++index] = u;
// 当u到达v保存路径;
if(u == v){
temp_path[0] = index--;
path.push_back(temp_path);
return;
}
vis[u] = 1; // 标记经过的路径;
for(int i = head[u];i;i = edge[i].next){
int to = edge[i].v;
if(vis[to]) continue;
dfs(to, v, path);
}
vis[u] = 0; index--;
}
}MP;
MP mp;
int cnt = 0;
int sta[20]; // 左岸为参考系,在左岸为1,反之为0,对应的合法状态;
// 判断状态i是否合法;
int check1(int i){
// 人在的一岸安全,故检查另一岸是否合理;
if((i&0b1000) != 0) i ^= 0b1111;
if((i & (i<<1)) != 0) return 0; // 狼羊 || 羊菜;
return 1;
}
// 判断状态i,j是否可互相转移;
int check2(int i, int j, int &temp){
if(i == j) return 0;
if(i < j) swap(i, j);
// 状态转移,人从一岸转移到另一岸;
if(((i>>3) ^ (j>>3)) == 0) return 0;
// 人最多只能带一个东西;
i %= 8; j %= 8;
if(i == j){ temp = 3; return 1;}
if(__builtin_popcount(i^j) == 1){
if((i^j) == 0b100) temp = 2;
if((i^j) == 0b10) temp = 1;
if((i^j) == 0b1) temp = 0;
return 1;
}
return 0;
}
void Init(){
// 获取所有合法状态;
for(int i = 0b1111;i;i = (i-1)&0b1111)
if(check1(i)) sta[++cnt] = i;
sta[++cnt] = 0; // 状态0000合法;
// 获取所有合法状态中,可互相转移的状态对<s1, s2>;
vector<Node> v;
for(int i = 1;i <= cnt;i++){
for(int j = 1;j <= cnt;j++){
int temp = 0;
if(!check2(sta[i], sta[j], temp)) continue;
v.push_back({sta[i], sta[j], temp});
}
}
// 以状态为结点,可转移关系为边建图;
sort(v.begin(), v.end()); mp.Init();
for(auto e : v) mp.add(e.u, e.v);
}
// 打印i状态信息;
char name[4] = {'M', 'W', 'G', 'C'};
string print(int i){
string ret = "";
int temp = i;
for(int j = 3;j >= 0;j--){
if(temp & 1) ret = name[j] + ret;
else ret = '.' + ret;
temp >>= 1;
}
if(i & 0b1000) ret += " -> ";
else ret += " <- ";
temp = (i ^ 0b1111); string ret0 = "";
for(int j = 3;j >= 0;j--){
if(temp & 1) ret0 = name[j] + ret0;
else ret0 = '.' + ret0;
temp >>= 1;
}
return ret + ret0;
}
void solve(){
int u = 0, v = 0;
string temp1, temp2;
getline(cin, temp1); getline(cin, temp2);
for(int i = 0;i < 4;i++){ // 获取初始和终止状态S1,S2
u <<= 1; v <<= 1;
u += temp1[i] == '.' ? 0 : 1;
v += temp2[i] == '.' ? 0 : 1;
}
vector<vector<int> > path; // S1到S2所有的简单路径;
mp.dfs(u, v, path);
if(path.size() == 0 || u == v){ // S1和S2状态不能相同;
cout << "None" << endl;
return;
}
for(int i = 0;i < path.size();i++){
for(int j = 1;j <= path[i][0];j++){
cout << print(path[i][j]) << endl;
}
if(i != path.size()-1) cout << endl;
}
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
Init();
solve();
return 0;
}