1. 程式人生 > >程式設計師應該用程式來思維,有空來研究一下狼 羊 草和農夫過河,將演算法轉換為程式碼 轉

程式設計師應該用程式來思維,有空來研究一下狼 羊 草和農夫過河,將演算法轉換為程式碼 轉

               

題目描述:農夫需要把狼、羊、菜和自己運到河對岸去,只有農夫能夠划船,而且船比較小,除農夫之外每次只能運一種東西,還有一個棘手問題,就是如果沒有農夫看著,羊會偷吃菜,狼會吃羊。請考慮一種方法,讓農夫能夠安全地安排這些東西和他自己過河。

        這個題目考察人的快速邏輯運算和短期記憶力。分析一下,在狼-》羊-》菜這個食物鏈條中,“羊”處在關鍵位置,解決問題的指導思想就是將“羊”與“狼”和“菜”始終處於隔離狀態,也就是說“羊”應該總是最後被帶過河的。來看一個答案:

農夫帶羊過河

農夫返回

農夫帶狼過河

農夫帶羊返回

農夫帶菜過河

農夫返回

農夫帶羊過河

<結束>

再看一個答案:

農夫帶羊過河

農夫返回

農夫帶菜過河

農夫帶羊返回

農夫帶狼過河

農夫返回

農夫帶羊過河

<結束>

解決問題都是圍繞著羊進行的。

        上面已經提到了兩個答案,那麼,這個問題到底有多少中答案呢?答案還需要用計算機進行窮舉。用計算機解決這個問題的關鍵還是狀態遍歷,這個和《算法系列――三個水桶均分水問題》一文中提到的狀態遍歷是一個道理,歸根到底就是一個有限狀態機。農夫、狼、羊和菜根據它們的位置關係可以有很多個狀態,但總的狀態數還是有限的,我們的演算法就是在這些有限個狀態之間遍歷,直到找到一條從初始狀態轉換到終止狀態的“路徑”,並且根據題目的要求,這條“路徑”上的每一個狀態都應該是合法的狀態。

        本題無論是狀態建模還是狀態轉換演算法,都比“用三個水桶均分8

升水”要簡單。首先是狀態,農夫、狼、羊和菜做為四個獨立的Item,它們的狀態都很簡單,要麼是過河,要麼是沒有過河,任意時刻每個Item的狀態只有一種。如果用“HERE”表示沒有過河,用“THERE”表示已經過河,用[農夫,狼,羊,菜]四元組表示某個時刻的狀態,則本題的狀態空間就是以[HEREHEREHEREHERE]為根的一棵狀態樹,當這個狀態樹的某個葉子節點是狀態[THERETHERETHERETHERE],則表示從根到這個葉子節點之間的狀態序列就是本問題的一個解。

本題的狀態轉換演算法依然是對狀態空間中所有狀態進行深度優先搜尋,因為狼、羊和菜不會划船,所以狀態轉換演算法也很簡單,不需要象“用三個水桶均分8

升水”問題那樣要用排列組合的方式確定轉換方法(倒水動作),本題一共只有8種固定的狀態轉換運算(過河動作),分別是:

農夫單獨過河;

農夫帶狼過河;

農夫帶羊過河;

農夫帶菜過河;

農夫單獨返回;

農夫帶狼返回;

農夫帶羊返回;

農夫帶菜返回;

        本題的廣度搜索邊界就是這8個動作,依次對這8個動作進行遍歷最多可以轉換為8個新狀態,每個新狀態又最多可以轉化為8個新新狀態,就形成了每個狀態節點有8個(最多8個)子節點的狀態樹(八叉樹)。本題演算法的核心就是對這個狀態樹進行深度優先遍歷,當某個狀態滿足結束狀態時就輸出一組結果。

        需要注意的是,並不是每個動作都可以得到一個新狀態,比如“農夫帶狼過河”這個動作,對於那些狼已經在河對岸的狀態就是無效的,無法得到新狀態,因此這個八叉樹並不是滿樹。除此之外,題目要求的合法性判斷也可以砍掉很多無效的狀態。最後一點需要注意的是,即使是有效的狀態,也會有重複,在一次深度遍歷的過程中如果出現重複的狀態可能會導致無窮死迴圈,因此要對重複出現的狀態進行“剪枝”。

        程式實現首先要描述狀態模型,本演算法的狀態定義為:

33 struct ItemState

34 {

35   ......

43   State  farmer,wolf,sheep,vegetable;

44   Action curAction;

35   ......

45 };

演算法在窮舉的過程中需要儲存當前搜尋路徑上的所有合法狀態,考慮到是深度優先演算法,用Stack是最佳選擇,但是Stack沒有提供線性遍歷的介面,在輸出結果和判斷是否有重複狀態時都需要線性遍歷儲存的狀態路徑,所以本演算法不用Stack,而是用Deque(雙端佇列)。

        整個演算法的核心就是ProcessState()函式,ProcessState()函式通過對自身的遞迴呼叫實現對狀態樹的遍歷,程式碼如下:

  291 void ProcessState(deque<ItemState>& states)

  292 {

  293     ItemState current= states.back();/*每次都從當前狀態開始*/

  294     if(current.IsFinalState())

  295     {

  296         PrintResult(states);

  297         return;

  298     }

  299 

  300     ItemState next;

  301     for(int i = 0; i < action_count; ++i)

  302     {

  303         if(actMap[i].processFunc(current, next))

  304         {

  305             if(IsCurrentStateValid(next)&& !IsProcessedState(states, next))

  306             {

  307               states.push_back(next);

  308               ProcessState(states);

  309               states.pop_back();

  310             }

  311         }

  312     }

  313 }

引數states是當前搜尋的狀態路徑上的所有狀態列表,所以ProcessState()函式首先判斷這個狀態列表的最後一個狀態是不是最終狀態,如果是則說明這個搜尋路徑可以得到一個解,於是呼叫PrintResult()函式列印結果,隨後的return表示終止設個搜尋路徑上的搜尋。如果還沒有達到最終狀態,則依次從8個固定的過河動作得到新的狀態,並從新的狀態繼續搜尋。為了避免長長的switch…case語句,程式演算法使用了表驅動的方法,將8個固定過河動作的處理函式放在一張對映表中,用簡單的查表代替switch…case語句。對映表內容如下:

  279 ActionProcess actMap[action_count]=

  280 {

  281     { FARMER_GO,                  ProcessFarmerGo               },

  282     { FARMER_GO_TAKE_WOLF,        ProcessFarmerGoTakeWolf       },

  283     { FARMER_GO_TAKE_SHEEP,       ProcessFarmerGoTakeSheep      },

  284     { FARMER_GO_TAKE_VEGETABLE,   ProcessFarmerGoTakeVegetable  },

  285     { FARMER_BACK,                ProcessFarmerBack             },

  286     { FARMER_BACK_TAKE_WOLF,      ProcessFarmerBackTakeWolf     },

  287     { FARMER_BACK_TAKE_SHEEP,     ProcessFarmerBackTakeSheep    },

  288     { FARMER_BACK_TAKE_VEGETABLE, ProcessFarmerBackTakeVegetable}

  289 };

 表中的處理函式非常簡單,就是根據當前狀態以及過河動作,得到一個新狀態,如果過河動作與當前狀態矛盾,則返回失敗,以FARMER_GO_TAKE_WOLF動作對應的處理函式ProcessFarmerGoTakeWolf()為例,看看ProcessFarmerGoTakeWolf()函式的程式碼:

  182 bool ProcessFarmerGoTakeWolf(const ItemState& current, ItemState& next)

  183 {

  184     if((current.farmer!= HERE) || (current.wolf!= HERE))

  185         return false;

  186 

  187     next = current;

  188 

  189     next.farmer   = THERE;

  190     next.wolf     = THERE;

  191     next.curAction= FARMER_GO_TAKE_WOLF;

  192 

  193     return true;

  194 }

        當過河動作對應的處理函式返回成功,表示可以得到一個不矛盾的新狀態時,就要對新狀態進行合法性檢查,首先是檢查是否滿足題目要求,比如狼和羊不能獨處以及羊和菜不能獨處,等等,這個檢查在IsCurrentStateValid()函式中完成。接著是檢查新狀態是否和狀態路徑上已經處理過的狀態有重複,這個檢查由IsProcessedState()函式完成,IsProcessedState()函式的實現也很簡單,就是遍歷states,與新狀態比較是否有相同狀態,程式碼如下:

  131 bool IsProcessedState(deque<ItemState>& states, ItemState& newState)

  132 {

  133     deque<ItemState>::iterator it= find_if( states.begin(), states.end(),

  134                                              bind2nd(ptr_fun(IsSameItemState), newState));

  135 

  136     return (it != states.end());

  137 }

        執行程式,最終得到的結果是:

Find Result 1:

Unknown action, item states is : 0 0 0 0

Farmer take sheep go over river, item states is : 1 0 1 0

Farmer go back, item states is : 0 0 1 0

Farmer take wolf go over river, item states is : 1 1 1 0

Farmer take sheep go back, item states is : 0 1 0 0

Farmer take vegetable go over river, item states is : 1 1 0 1

Farmer go back, item states is : 0 1 0 1

Farmer take sheep go over river, item states is : 1 1 1 1

Find Result 2:

Unknown action, item states is : 0 0 0 0

Farmer take sheep go over river, item states is : 1 0 1 0

Farmer go back, item states is : 0 0 1 0

Farmer take vegetable go over river, item states is : 1 0 1 1

Farmer take sheep go back, item states is : 0 0 0 1

Farmer take wolf go over river, item states is : 1 1 0 1

Farmer go back, item states is : 0 1 0 1

Farmer take sheep go over river, item states is : 1 1 1 1

看來確實是只有兩種結果。