问题描述
农夫需要把狼、羊、菜和自己运到河对岸去,只有农夫能够划船,而且船比较小,除农夫之外每次只能运一种东西,还有一个棘手问题,就是如果没有农夫看着,羊会偷吃菜,狼会吃羊。请考虑一种方法,让农夫能够安全地安排这些东西和他自己过河。
分析
问题很简单,但如何用计算机求解呢。
农夫渡河从本质上是一种状态的改变。
有农夫、狼、羊、菜四个个体,任何时刻每个个体的状态只有一种,每个个体有两种状态(没有过河、已经过河)。
依次用4位分别代表农夫、狼、羊、菜,0表示未过河,1表示已过河。则起始状态为0000,目标状态为1111。
共有8种过河动作(状态转换运算)
- 农夫单独过河
- 农夫带狼过河
- 农夫带羊过河
- 农夫带菜过河
- 农夫单独返回
- 农夫带狼返回
- 农夫带羊返回
- 农夫带菜返回
优先级:
农夫过河时,优先带货物;回返时优先不带货物。
有限种状态:
可能有16(2^4)种状态,但因为狼吃羊,羊吃菜的限制,部分状态是无法成立的。
实现
状态空间树(回溯法)
是以0000为根的一颗状态树,当某个叶子节点是状态1111,则表示从根到这个叶子节点之间的状态序列是本问题的一个解,需要避免出现重复状态导致死循环。
方法1: 每个状态有8种可选动作,转换为8个新状态,但在特定状态下某些动作是无效的。
定义8种状态转换运算,对当前节点遍历执行这8种运算,找到所有子节点
方法2: 依据当前状态,判别它所有可选的动作(最多4种)。
class Program
{
static void Main(string[] args)
{
var original = new State();
var path = new List<State>();
path.Add(original);
int count = 0;
Search(path, ref count);
Console.ReadKey();
}
private static void Search(List<State> path, ref int count)
{
var cur = path[path.Count - 1];
if (cur.Man && cur.Wolf && cur.Vegetable && cur.Sheep)
{
count++;
Console.WriteLine($"解{count}:");
path.ForEach((a) => { Console.WriteLine(a.Action); });
return;
}
if (cur.Man)
{
Switch(path, ref count, cur, "返回");
}
else
{
Switch(path, ref count, cur, "过河");
}
}
private static void Switch(List<State> path, ref int count, State cur, string action)
{
var newState = cur.Copy();
newState.Man = !newState.Man;
newState.Action = "独自" + action;
Action(path, ref count, newState);
if (cur.Sheep == cur.Man)
{
newState.Sheep = !newState.Sheep;
newState.Action = "带着羊" + action;
Action(path, ref count, newState);
newState.Sheep = !newState.Sheep;
}
if (cur.Wolf == cur.Man)
{
newState.Wolf = !newState.Wolf;
newState.Action = "带着狼" + action;
Action(path, ref count, newState);
newState.Wolf = !newState.Wolf;
}
if (cur.Vegetable == cur.Man)
{
newState.Vegetable = !newState.Vegetable;
newState.Action = "带着菜" + action;
Action(path, ref count, newState);
newState.Vegetable = !newState.Vegetable;
}
}
private static void Action(List<State> path, ref int count, State newState)
{
if (newState.IsOk)
{
foreach (var item in path)
{
if (item.Equals(newState))
{
return;
}
}
path.Add(newState);
Search(path, ref count);
path.RemoveAt(path.Count - 1);
}
}
//false 表示未过河, true表示已过河
private class State
{
public bool Man { get; set; }
public bool Wolf { get; set; }
public bool Sheep { get; set; }
public bool Vegetable { get; set; }
public string Action { get; set; }
public bool IsOk
{
get
{
if (Wolf == Sheep && Wolf != Man)
{
return false;
}
if (Sheep == Vegetable && Sheep != Man)
{
return false;
}
return true;
}
}
public State Copy()
{
return new State
{
Man = this.Man,
Wolf = this.Wolf,
Sheep = this.Sheep,
Vegetable = this.Vegetable
};
}
public bool Equals(State newState)
{
return (this.Man == newState.Man
&& this.Wolf == newState.Wolf
&& this.Sheep == newState.Sheep
&& this.Vegetable == newState.Vegetable);
}
}
}
状态空间图
所有状态作为图的节点
遍历图,找出所有从0000到1111的路径
连接状态的条件
- 农夫的状态要不一样 (只有农夫可以划船,每次过河,不能缺农夫)
- 最多只有一个其他个体的状态不一样(一次只能带一个过河),且这个个体的状态要与农夫一致。
避免重复
一个状态只能经过一次。
class Program
{
static void Main(string[] args)
{
//找到所有的状态
var states = new List<Vertex>();
for (int i = 0; i < 16; i++)
{
var temp = i >> 1;
if (temp == 0b011 || temp == 0b100)
{
continue;
}
var temp2 = i & 0b1011;
if (temp2 == 0b1000 || temp2 == 0b0011)
{
continue;
}
states.Add(new Vertex { State = i });
}
var steps = new List<Step>();
Search(states[0], states, steps);
Console.ReadKey();
}
private static void Search(Vertex cur, List<Vertex> states, List<Step> steps)
{
if(cur.State == 0b1111)
{
Console.WriteLine();
steps.ForEach((a)=> { Console.WriteLine(a.Description); });
return;
}
cur.HasVisited = true;
foreach (var item in states)
{
if (!item.HasVisited && CanBeNext(cur.State,item.State))
{
steps.Add(new Step { From = cur.State,To = item.State });
Search(item, states, steps);
steps.RemoveAt(steps.Count - 1);
}
}
cur.HasVisited = false;
}
private static bool CanBeNext(int a,int b)
{
if (b == 0)
{
return false;
}
if((a^b)>>3 == 0)
{
return false;
}
var man = a >> 3;
var temp = (a & 0b0111)^(b & 0b0111);
if(temp == 0 || temp == 0b100 && (a & 0b0100)>>2 == man || temp == 0b010 && (a & 0b0010)>>1 == man || temp == 0b001 && (a & 0b0001) == man)
{
return true;
}
return false;
}
private class Vertex
{
public int State { get; set; }
public bool HasVisited { get; set; }
}
private class Step
{
public int From { get; set; }
public int To { get; set; }
public string Description
{
get
{
var action = From > 7 ? "返回" : "过河";
var temp = (From & 0b0111) ^ (To & 0b0111);
if(temp == 0)
{
action = "独自" + action;
}
if (temp == 0b100)
{
action = "带着狼" + action;
}
if (temp == 0b010)
{
action = "带着羊" + action;
}
if (temp == 0b001)
{
action = "带着菜" + action;
}
return $"{PrintState(From)}--{action} --> {PrintState(To)}";
}
}
private string PrintState(int a)
{
return $"{a / 8}{a % 8 / 4}{a % 4 / 2}{ a % 2}";
}
}
}