問題描述
農夫需要把狼、羊、菜和自己運到河對岸去,只有農夫能夠划船,而且船比較小,除農夫之外每次只能運一種東西,還有一個棘手問題,就是如果沒有農夫看着,羊會偷吃菜,狼會吃羊。請考慮一種方法,讓農夫能夠安全地安排這些東西和他自己過河。
分析
問題很簡單,但如何用計算機求解呢。
農夫渡河從本質上是一種狀態的改變。
有農夫、狼、羊、菜四個個體,任何時刻每個個體的狀態只有一種,每個個體有兩種狀態(沒有過河、已經過河)。
依次用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}";
}
}
}