1. 程式人生 > 實用技巧 >回溯法之圓排列問題

回溯法之圓排列問題

問題描述

給定n個大小不等的圓c1,c2,…,cn,現要將這n個圓排進一個矩形框中,且要求各圓與矩形框的底邊相切。圓排列問題要求從n個圓的所有排列中找出有最小長度的圓排列。例如,當n=3,且所給的3個圓的半徑分別為1,1,2時,這3個圓的最小長度的圓排列如圖所示。其最小長度為。


問題分析

圓排列問題的解空間是一棵排列樹。按照回溯法搜尋排列樹的演算法框架,設開始時a=[r1,r2,……rn]是所給的n個元的半徑,則相應的排列樹由a[1:n]的所有排列構成。

解圓排列問題的回溯演算法中,CirclePerm(n,a)返回找到的最小的圓排列長度。初始時,陣列a是輸入的n個圓的半徑,計算結束後返回相應於最優解的圓排列。center計算圓在當前圓排列中的橫座標,由x^2 = sqrt((r1+r2)^2-(r1-r2)^2)推匯出x = 2*sqrt(r1*r2)。

Compoute計算當前圓排列的長度。變數min記錄當前最小圓排列長度。陣列r表示當前圓排列。陣列x則記錄當前圓排列中各圓的圓心橫座標。

在遞迴演算法Backtrack中,當i>n時,演算法搜尋至葉節點,得到新的圓排列方案。此時演算法呼叫Compute計算當前圓排列的長度,適時更新當前最優值。

當i<n時,當前擴充套件節點位於排列樹的i-1層。此時演算法選擇下一個要排列的圓,並計算相應的下界函式。

演算法具體程式碼如下:

//圓排列問題 回溯法求解
#include "stdafx.h"
#include <iostream>
#include <cmath>
using
namespace std; float CirclePerm(int n,float *a); template <class Type> inline void Swap(Type &a, Type &b); int main() { float *a = new float[4]; a[1] = 1,a[2] = 1,a[3] = 2; cout<<"圓排列中各圓的半徑分別為:"<<endl; for(int i=1; i<4; i++) { cout<<a[i]<<"
"; } cout<<endl; cout<<"最小圓排列長度為:"; cout<<CirclePerm(3,a)<<endl; return 0; } class Circle { friend float CirclePerm(int,float *); private: float Center(int t);//計算當前所選擇的圓在當前圓排列中圓心的橫座標 void Compute();//計算當前圓排列的長度 void Backtrack(int t); float min, //當前最優值 *x, //當前圓排列圓心橫座標 *r; //當前圓排列 int n; //圓排列中圓的個數 }; // 計算當前所選擇圓的圓心橫座標 float Circle::Center(int t) { float temp=0; for (int j=1;j<t;j++) { //由x^2 = sqrt((r1+r2)^2-(r1-r2)^2)推導而來 float valuex=x[j]+2.0*sqrt(r[t]*r[j]); if (valuex>temp) { temp=valuex; } } return temp; } // 計算當前圓排列的長度 void Circle::Compute(void) { float low=0,high=0; for (int i=1;i<=n;i++) { if (x[i]-r[i]<low) { 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 t) { if (t>n) { Compute(); } else { for (int j = t; j <= n; j++) { Swap(r[t], r[j]); float centerx=Center(t); if (centerx+r[t]+r[1]<min)//下界約束 { x[t]=centerx; Backtrack(t+1); } Swap(r[t], r[j]); } } } float CirclePerm(int n,float *a) { Circle X; X.n = n; X.r = a; X.min = 100000; float *x = new float[n+1]; X.x = x; X.Backtrack(1); delete []x; return X.min; } template <class Type> inline void Swap(Type &a, Type &b) { Type temp=a; a=b; b=temp; }
View Code

執行結果

演算法效率

如果不考慮計算當前圓排列中各圓的圓心橫座標和計算當前圓排列長度所需的計算時間按,則 Backtrack需要O(n!)計算時間。由於演算法Backtrack在最壞情況下需要計算O(n!)次圓排列長度,每次計算需要O(n)計算時間,從而整個演算法的計算時間複雜性為O((n+1)!)

上述演算法尚有許多改進的餘地。例如,像1,2,…,n-1,n和n,n-1, …,2,1這種互為映象的排列具有相同的圓排列長度,只計算一個就夠了,可減少約一半的計算量。另一方面,如果所給的n個圓中有k個圓有相同的半徑,則這k個圓產生的k!個完全相同的圓排列,只計算一個就夠了。

參考文獻:王曉東《演算法設計與分析》
https://blog.csdn.net/liufeng_king/article/details/8890603