回溯演算法 --- 例題9.圓排列問題
一.問題描述
給定n個大小不等的圓c1,c2,…,cn, 現要將這n個圓排進一個矩形框中, 且要求各圓與矩形框的底邊相切。
圓排列問題要求從n個圓的所有排列中找出有最小長度的圓排列。
例如, 當n=3, 且所給的3個圓的半徑分別為1, 1, 2時, 這3個圓的最小長度的圓排列如圖所示。其最小長度為2+4√2 .
二.解題思路
圓排列問題的解空間是一棵排列樹。按照回溯法搜尋排列樹的演算法框架, 設開始時a=[r1,r2,……rn]是所給的n個圓的半徑, 則相應的排列樹由a[1:n]的所有排列構成。
解圓排列問題的回溯演算法中, CirclePerm(n,a)返回找到的最小的圓排列長度。初始時, 陣列a是輸入的n個圓的半徑, 計算結束後返回相應於最優解的圓排列。
Center函式計算圓在當前圓排列中的橫座標, 由x^2 = sqrt((r1+r2)2-(r1-r2)
Compoute計算當前圓排列的長度。變數min記錄當前最小圓排列長度。陣列r表示當前圓排列。陣列x則記錄當前圓排列中各圓的圓心橫座標。陣列ID記錄當前圓排列的編號
在遞迴演算法Backtrack中:
當i>n時, 演算法搜尋至葉節點, 得到新的圓排列方案。此時演算法呼叫Compute計算當前圓排列的長度, 適時更新當前最優值.
當i<n時, 當前擴充套件節點位於排列樹的i-1層。此時演算法選擇下一個要排列的圓, 並計算相應的下界函式。
程式碼如下:
// 圓排列問題 #include<bits/stdc++.h> using namespace std; class Circle { friend float CirclePerm(int, float *); private: float Center(int t); //計算圓心所在位置 void Compute(); //計算當前圓排列的長度 void Backtrack(int i); float min, //當前最優值 *x, //當前圓排列圓心橫座標 *r; //當前圓排列 int n; //待排列圓的個數 int *ID; //初始圓陣列的編號 }; float Circle::Center(int i) //計算當前所選擇圓的圓心橫座標(1~i-1層每一個圓都要計算,找出一個最大的橫座標,與之相切,具體情況可以參考之後的圖) { float temp = 0; for(int j=1; j<i; j++) //注意!!! i==1時,也就是選擇第一個圓時,得到的x[1] = 0 { float valuex = x[j] + 2.0*sqrt(r[i]*r[j]); if(valuex > temp) temp = valuex; } return temp; } void Circle::Compute() //計算當前圓排列的長度 { float low = 0, high = 0; for(int i=1; i<=n; i++) { if(x[i]-r[i] < low) //找出一個最左的座標(x[i]表示圓心橫座標,r[i]表示圓的半徑) low = x[i] - r[i]; if(x[i]+r[i] > high) //找出一個最右的座標 high = x[i] + r[i]; } if(high-low < min) min = high - low; //如果找到更優解,那麼更新它 } void Circle::Backtrack(int i) { static int k = 1; if(i > n) { Compute(); cout<<"到達第"<<n+1<<"層,得到第"<<k++<<"個可行解:"; for(int i=1; i<=n; i++) cout<<r[i]<<" "; cout<<endl<<"所得最小圓排列值為:"<<min<<endl; cout<<"圓排列的編號陣列為:"; for(int i=1; i<=n; i++) cout<<ID[i]<<" "; cout<<endl; } else { for(int j=i; j<=n; j++) { swap(r[i], r[j]); swap(ID[i], ID[j]); float centerx = Center(i); cout<<"當前第"<<i<<"層"<<",選擇圓的編號為:"<<ID[i]<<",計算得到圓心橫座標centerx:"<<centerx<<" r[i]:"<<r[i]<<" r[1]:"<<r[1]<<" 總和為:"<<centerx+r[i]+r[1]<<" 當前min值為:"<<min<<endl; if(centerx + r[i] + r[1] < min) //下界約束(centerx+r[i]+r[1]這個值比實際值還要小,因為centerx+r[i] { cout<<"滿足剪枝函式,遞迴深入一層,將到達第"<<i+1<<"層"<<endl; x[i] = centerx; Backtrack(i+1); cout<<"當前第"<<i+1<<"層,遞歸回退一層,將到達第"<<i<<"層"<<endl; } else cout<<"不滿足剪枝函式,對應子樹被剪枝"<<endl; swap(r[i], r[j]); swap(ID[i], ID[j]); if(j==n) cout<<"當前層所有情況遍歷完,回退"<<endl; } } } float CirclePerm(int n, float *r) { Circle X; X.n = n; X.r = r; X.min = 100000; float *x = new float[n+1]; X.x = x; int *ID = new int[n+1]; for(int i=1; i<=n; i++) ID[i] = i; X.ID = ID; X.Backtrack(1); delete[] x; delete[] ID; return X.min; } int main() { cout<<"請輸入圓的個數:"; int n; while(cin>>n && n) { cout<<"請輸入每個圓的半徑:"<<endl; float *r = new float[n+1]; for(int i=1; i<=n; i++) cin>>r[i]; float ans = CirclePerm(n, r); cout<<"最小圓排列長度為:"<<ans<<endl; delete[] r; cout<<"請輸入圓的個數:"; } system("pause"); return 0; }
下面做幾個解釋:
-
Center函式.
Center計算圓在當前圓排列中的橫座標,由x^2 = sqrt((r1+r2)2-(r1-r2)2)推匯出x = 2*sqrt(r1 * r2),但是為什麼我們要用一個for迴圈遍歷1到i-1個已經確立好的圓呢?
這是因為我們很容易會有一個先入為主的思想,那就是後一個圓必然與排在它前一個位置的圓相切,其實排在任意位置的圓與其前或後的任意一個圓都有可能相切的,畫個圖就很清晰了。在這個圖中,可以看到,只要大小合適,目標圓就有可能與排列中的任意一個圓相切,不一定就是和前一個相切.所以我們需要遍歷之前已經確定好的i-1個圓,找出計算後最大的橫座標,來作為下一個圓圓心的橫座標.
如果不這樣做的話會發生什麼呢?很明顯,這樣的話就不滿足題目條件了,試想一下如果我們上面的x3這個圓和x2相切了,那麼勢必x3會與x1這個圓出現相交 -
剪枝函式if(centerx + r[i] + r[1] < min)
這個問題,我想這個條件其實想了好一會.
我認為centerx + r[i] + r[1]這個式子的值是要比真實的圓排列長度要小的,通過計算圓排列長度的函式Compute()我們知道,我們是需要找到一個最左的座標以及一個最右的座標,二者相減從而得到我們需要的長度.
用式子表述就是: min = x[k]+r[k] - (x[m]-r[m]) = x[k]+r[k] + r[m]-x[m] , (其中的k和m分別使得一個座標最大,一個座標最小)這裡的centerx並不一定是最右的座標,所以說centerx + r[i] <= x[k] + r[k], (k使得x[k]+r[k]在所有圓中是最大的一個)
同樣的,0也不是最左的座標(當r[m] - x[m] = r[1]時,就是選定了第一個圓,則x[1]=0).
例如下圖:所以說呢,我們兩邊都縮小了一點,是比實際的圓排列長度要小的,如果說按照這個演算法你都還大於了目前的最小值,那麼按照實際長度來計算豈不是更加大於了!
就好比實際最左邊的橫座標為-4,最右邊的為8, 現在我假設最左邊為0,最右邊為6,如果連(6-0)>min了,那麼(8-(-4))更加大於最優值.
執行結果:
構建出的排列樹如下:(和之前的大同小異)
講完了,應該能夠看懂吧大夥.(●'◡'●)
歡迎大家訪問我的個人部落格 --- 喬治的程式設計小屋,和我一起努力進步吧!