回溯法解決喝酒問題 (轉)
回溯法解決喝酒問題
先介紹一下回溯法的理論:
可用回溯法解決的問題P,通常能夠表達為:
對於已知的、由n元組(x1,x2,……,xn)組成的一個狀態空間 E={(x1,x2,……,xn) | xi ∈Si, i=1,2,..,n},給定關於n元組中的分量的一個集D,要求E中滿足D的全部約束的所有n元組。其中Si是分量xi的定義域且|Si|有限,i=1,2,...n。我們稱E中滿足D的全部約束條件的任一n元組為問題P的一個解。
對於n元組(x1,x2,……,xn)中分量的約束,一般分為兩類,一類是顯約束,它給出對於n元組中分量的顯式限制,比如當i≠j時xi≠xj;另一類是隱約束,它給出對於n元組中分量的隱式限制,比如:f(x1,x2,……,xn)≠ 0,其中f是隱。不過隱式顯式並不絕對,兩者可以相互轉換。
解問題P的最樸素的方法是窮舉法,即對E中的所有n元組,逐一地檢測其是否滿足D的全部約束。全部滿足,才是問題p的解;只要有一個不滿足,就不是問題P的解。顯然,如果記m(i)=|S(i+1)|,i=0,1,...n-1,那麼,窮舉法需要對m=m(0)*m(1)*...*m(n-1)個n元組一個不漏地加以檢測。可想而知,其計算量是非常之大的。
我們發現,對於許多問題,所給定的約束集D具有完備性,即i元組(x1,x2,……,xi)滿足D中僅涉及到x1,x2,……,xi的所有約束意味著j(j
這個發現告訴我們,對於約束集D具有完備性的問題P,一旦檢測斷定某個j元組(x1,x2,……,xj)違反D中僅涉及x1,x2,……,xj的一個約束,就可以肯定,以(x1,x2,……,xj)為字首的任何n元組(x1,x2,……,xj,……,xn)都不會是問題的解,因而就不必去搜尋它們、檢測它們。回溯法正是針對這類問題,利用這類問題的上述性質而提出來的比窮舉法高得多的演算法。
回溯法首先將問題P的n元組的狀態空間E表示成一棵高為n的帶權有序樹T,把在E中求問題P的所有解轉化為在T中搜尋問題P的所有解。樹T類似於檢索樹。它可這樣構造:設Si中的元素可排成x(i,1),x(i,2),……,x(i,m(i-1)),i=1,2,……,n。從根開始,讓T的第i層的每一個結點都有m(i)個兒子。這m(i)個兒子到它們的共同父親的邊,按從左到右的次序分別帶權x(i+1,1),x(i+1,2),……,x(i+1,m(i)),i=0,1,2,……,n-1。照這種構造方式,E中的一個n元組(x1,x2,……,xn)對應於T中的一個葉結點,T的根到這個葉結點的路上依次的n條邊分別以x1,x2,……,xn為其權,反之亦然。另外,對於任意的0≤i≤n-1,E中n元組(x1,x2,……,xn)的一個字首i元組(x1,x2,……,xi)對應於T中的一個非葉結點,T的根到這個非葉結點的路上依次的i條邊分別以了x1,x2,……,xi為其權,反之亦然。特別,E中的任意一個n元組的空字首(),對應於T的根。
因而,在E中尋找問題P的一個解等價於在T中搜尋一個葉結點,要求從T的根到該葉結點的路上依次的n條邊相應帶的n個權x1,x2,……,xn滿足約束集D的全部約束。在T中搜尋所要求的葉結點,很自然的一種方式是從根出發逐步深入,讓路逐步延伸,即依次搜尋滿足約柬條件的字首1元組(xl),字首2元組(xl,x2),字首i元組(x1,x2,……,xi),……,直到i=n為止。注意,在這裡,我們把(x1,x2,……,xi)應該滿足的D中僅涉及x1,x2,……,xi的所有約束當做判斷(x1,x2,……,xi)是問題p的解的必要條件,只有當這個必要條件加上條件i=n才是充要條件。為了區別,我們稱使積累的判別條件成為充要條件的那個條件(如條件i=n)為終結條件。
在回溯法中,上面引入的樹T被稱為問題P的狀態空間樹;樹T上的任意一個結點被稱為問題p的狀態結點;樹T上的任意一個葉結點被稱為問題P的一個解狀態結點;樹T上滿足約束集D的全部約柬的任意一個葉結點被稱為問題P的一個回答狀態結點,簡稱為回答結點或回答狀態,它對應於問題P的一個解。
例如8皇后問題,就是要確定一個8元組(x1,x2,..,x8),xi表示第i行的皇后所在的列,這樣的問題很容易應用上面的搜尋樹模型;然而,有些問題的解無法表示成一個n元組,因為事先無法確定這個n是多少,比如這個喝酒問題,問題的解就是一系列的倒酒喝酒策略,但是事先無法確定究竟需要進行多少步;還有著名的8數碼問題(文曲星上的那個9x9方格中移數字的遊戲),那個問題也是預先不知道需要移動多少步才能達到目標。不過這並不影響回溯法的使用,只要該問題有解,一定可以將解用有限的變元來表示,我們可以假設n就是問題的一個解的變元的個數,這樣就可以繼續利用上面的搜尋樹模型了。事實上,這棵搜尋樹並非預先生成的,而是在搜尋的過程中逐步生成的,所以不知道樹的深度n並不影響在樹中搜尋葉子節點。但是有一點很重要,如果問題根本不存在有限的解,或者問題的狀態空間無窮大,那麼沿著某條道路從根出發搜尋葉節點,可能永遠無法達到葉結點,因為搜尋樹會不斷地擴充套件,然而葉結點也許是確實存在的,只要換一條道路就可以找到,只不過一開始就走錯了路,而這條錯路是永遠無法終止的。為了避免這種情況我們一般都規定狀態空間是有限的,這樣即使搜尋整個狀態空間的每個狀態也可以在有限時間內完成,否則的話回溯法很可能不適用。
搜尋樹的每一個節點表示一個狀態,節點i要生成節點j必須滿足約束集D中的約束條件,我們也可以將這個約束條件稱為“狀態轉移規則”或者“產生規則”(意指從節點i產生節點j的規則,這是從“產生式”理論的角度來解釋回溯法)。因此回溯法的實質是在一個狀態空間中,從起始狀態(搜尋樹的根)搜尋到一條到達目標狀態(搜尋樹的葉結點)的路徑(就和走迷宮差不多,這是從圖論的角度來解釋回溯法)。一般來說,為了防止搜尋的過程中出現迴路,必須記錄已經走過的節點(狀態),在同一條路徑中不能重複走過的節點(狀態),這樣只要狀態空間是有限的,回溯法總是可以終止的。
===========================================================================================
下面我們就根據回溯法來解決這個喝酒問題
(1)狀態的表示
一個狀態用一個7元組表示 X=(x1,x2,x3,x4,x5,x6,x7);,其中x1~x3分別表示a,b,c三個酒瓶中的酒,x4~x7分別表示A,B,C,D四個人已經喝的酒;
(2)約束條件
1。每個人喝的酒不能超過4兩;
2。每個瓶中容納的酒不能超過該瓶的容量;
為了方便設第k個人喝的酒不超過C[k], 第i個酒瓶的容量為C[i], 則
C[1]=C[2]=8, C[3]=3, C[4]=C[5]=C[6]=C[7]=4;
約束條件為
0<= X[i] <= C[i];
(3)狀態的轉移規則(狀態產生規則)
從某個狀態X轉移到另一個狀態Y有以下幾種情況:
1。i瓶中的酒倒入j瓶中,並將j瓶裝滿: Y[i] = X[i] - (C[j]-X[j]) , Y[j] = C[j], i,j∈[1,3]
2。i瓶中的酒倒入j瓶中,並將i瓶倒空: Y[i] = 0 , Y[j] = X[j] + X[i] , i,j∈[1,3]
3。某個人j喝光了i瓶中的酒: Y[i] = 0; Y[j] = X[j] + X[i], i∈[1,3], j∈[4,7]
當然狀態Y必須滿足(2)中的約束條件;
(4)初始狀態
a,b兩個瓶中裝滿酒,c中為空: X0[1]=C[1], X0[2]=C[2], X0[3]=C[3], X0[4]=X0[5]=X0[6]=X0[7]=0;
(5)目標狀態
所有的瓶中的酒為空,每個人都喝飽了酒: Xn[1]=Xn[2]=Xn[3]=0 , Xn[4]=C[4], Xn[5]=C[5], Xn[6]=C[6], Xn[7]=C[7];
下面給出一個通用的回溯法虛擬碼:
void DFS_TRY( s )
{
if (狀態s是目標狀態) {
列印結果;
退出; // 如果要求輸出所有的解,這裡退出函式,如果只要求輸出一組解,這裡退出整個
}
for 從狀態s根據產生規則產生的每個狀態t
if (t不在堆疊中) {
狀態t壓入堆疊;
DFS_TRY(t);
狀態t彈出堆疊;
}
}
主程式為:
初始狀態s0壓入堆疊;
DFS_TRY(s0);
然而,對於這個問題,如果單純地用上面的回溯法解決效率非常的低,幾乎無法忍受。所以要改進一下。我們注意到每個狀態是一個7元組,而且根據約束條件,所有的合法的狀態的個數是8*8*3*4*4*4*4 =49152個,完全可以將所有的狀態記錄下來,即使窮舉所有的狀態也是可以忍受的。所以在上面的DFS_TRY中,我們不是在堆疊中尋找已經搜尋過的狀態,而是在一個狀態表中找已經搜尋過的狀態,如果某個狀態在狀態表中的標誌表明該狀態已經搜尋過了,就沒有必要再搜尋一遍。比如,單純的回溯法搜尋出來的搜尋樹如下所示:
a
/
/
b c
/
/
d
/
/
從a出發,搜尋 a - b - d - ... 然後回溯到a, 又搜尋到 a - c - d - ..., 因為d在搜尋的路徑上並沒有重複,所以在堆疊中是發現不了d節點被重複搜尋的,這樣就重複搜尋了d和它的子樹;如果用一個表格紀錄每個節點是否被搜尋過了,這樣搜尋 a - b - d - ...回溯到a, 又搜尋到 a - c - d ,這時候查表發現d已經搜尋過了,就可以不用再搜尋d和它的子樹了。
這種用一個表格來記錄狀態的搜尋策略叫做“備忘錄法”,是動態規劃的一種變形,關於動態規劃和備忘錄法,請參見:
備忘錄法的虛擬碼:
bool Memoire_TRY( s )
{
if (狀態s是目標狀態) {
記錄狀態s;
return true; // 這裡假設只要求輸出一組解
}
for 從狀態s根據產生規則產生的每個狀態t
if (狀態t沒有被搜尋過) { // 注意這裡的改變
標記狀態t被訪問過;
if (DFS_TRY(t)) {
記錄狀態s;
return true;
}
}
return false;
}
主程式為:
初始化設定狀態表中的所有狀態未被訪問過
初始狀態設為s0;
if (Memoire_TRY(s0))
列印記錄下來的解;
這樣就不需要自己設定堆疊了,但是需要維護一個狀態訪問表。
下面是按照這種思路寫的程式,注意,求出來的不是最優解,但是很容易修改該程式求出最優解。
#include
#include
const int CUP_COUNT = 3; // 酒杯的數目
const int STATE_COUNT = 7; // 狀態變數的維數
typedef int State[STATE_COUNT]; // 記錄狀態的型別
const State CONSTR = {8, 8, 3, 4, 4, 4, 4}; // 約束條件
const State START = {8, 8, 0, 0, 0, 0, 0}; // 初始狀態
const State GOAL = {0, 0, 0, 4, 4, 4, 4}; // 目標狀態
const int MAX_STATE_COUNT = 10*10*10*10*10*10*10; //態空間的狀態數目
const MAX_STEP = 50; // 假設最多需要50步就可以找到目標
const State key = {3, 5, 3, 3, 2, 0, 0};
bool visited[MAX_STATE_COUNT]; // 用來標記訪問過的狀態
State result[MAX_STEP]; // 記錄結果;
int step_count = 0; // 達到目標所用的步數
// 計算狀態s在狀態表中的位置
int pos(const State &s)
{
int p = 0;
for (int i=0; i
}
return p;
}
// 判斷狀態a,b是否相等
bool equal(const State &a, const State &b) {
for (int i=0; i
return true;
}
void printState(const State &s) {
for (int i=0; i
cout << endl;
}
// 備忘錄法搜尋
bool Memoire_TRY(const State &s, int step)
{
if (memcmp(s,GOAL,sizeof(s))==0) { // 如果是目標狀態
step_count = step;
memcpy(result[step-1],s, sizeof(s)); // 記錄狀態s
return true;
}
int i, j;
// 第一種規則,第i個人喝光杯子j中的酒
for (i=CUP_COUNT; i
for (j=0; j
State t;
memcpy(t, s, sizeof(s));
t[i] += t[j]; // 第i個人喝光第j杯的酒
t[j] = 0;
int tmp = pos(t);
if (!visited[pos(t)]) { // 如果狀態t沒有訪問過
visited[pos(t)] =true; // 標記狀態t訪問過了
if (Memoire_TRY(t, step+1)) { // 從狀態t出發搜尋
memcpy(result[step-1],s, sizeof(s)); // 記錄狀態s
return true;
} // end of if (Memoire_TRY(t, step+1))
} // end of if (!visited[pos(t)])
} // end of if (s[i] + s[j] <= CONSTR[i])
// 第二種規則,將杯子i中的酒倒入到杯子j中去
for (i=0; i
int k = (CONSTR[j] - s[j] < s[i] ? CONSTR[j] - s[j] : s[i] ); // 計算出可以從i中倒入j中的酒的數量
if (k > 0) { // 如果可以倒
State t; // 生成新的狀態t
memcpy(t, s, sizeof(s));
t[i] -= k;
t[j] += k;
int tmp = pos(t);
if (!visited[pos(t)]) { // 如果狀態t沒有訪問過
visited[pos(t)] =true; // 標記狀態t訪問過了
if (Memoire_TRY(t, step+1)) { // 從狀態t出發搜尋
memcpy(result[step-1],s, sizeof(s)); // 記錄狀態s
return true;
} // end of if (Memoire_TRY(t, step+1))
} // end of if (!visited[pos(t)])
} // end of if (k > 0)
} // end of if (i != j)
return false;
} // end of Memoire_TRY
void main()
{
memset(visited, false, sizeof(visited));
if (Memoire_TRY(START,1)) {
cout << "find a solution: " << endl;
for (int i=0; i
cout << endl;
}
} else
cout << "no solution." << endl;
}
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752043/viewspace-991971/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 回溯法解決迷宮問題
- 回溯法解決全排列問題總結
- 回溯法(排列樹)解決八(N)皇后問題
- (回溯法)解決一系列組合問題
- 回溯法求迷宮問題
- 八皇后之回溯法解決
- 用棧+回溯+非遞迴解決N皇后問題遞迴
- 【力扣】排列問題(回溯法)(去重)力扣
- 回溯問題
- js使用遞迴回溯法解八皇后問題程式碼分享JS遞迴
- 使用回溯演算法解決N皇后問題以及間隔排列問題演算法
- 橡皮鴨子解決問題法
- leetcode題解(遞迴和回溯法)LeetCode遞迴
- c++求解李白喝酒問題C++
- 解決jequry使用keydown無法跳轉的問題
- 解決五筆輸入法的安裝問題(轉)
- 跨域問題解決辦法跨域
- 解決機器學習問題有通法機器學習
- gmail無法訪問問題解決--FGWAI
- [解決問題] Vagrant nginx 站點配置問題(ThinkPHP HTML 無法調跳轉)NginxPHPHTML
- 解決無法使用VI的問題
- 解決ASM無法啟動問題ASM
- 減治法解決假幣問題
- SaaS無法解決“關鍵”問題
- svn相關問題解決辦法
- 解決「問題」,不要解決問題
- 回溯法
- c++迷宮問題回溯法遞迴演算法C++遞迴演算法
- 解決Spring Boot無法跳轉jsp頁面問題Spring BootJS
- RedHat9的service named stop問題及解決辦法(轉)Redhat
- VC下TabCtrl的控制問題解決辦法( 原創 ) (轉)
- Python 命令跳轉微軟應用商店問題解決辦法Python微軟
- 【轉】css解決相容的問題CSS
- 解決寶塔皮膚無法訪問的問題?
- 解決split無法得到空字串問題字串
- 【Ubuntu】unknown host問題解決辦法Ubuntu
- Parallels Tools 無法安裝問題解決Parallel
- HTC Vive遇到的問題解決辦法