問題描述
給定n個大小不等的圓c1,c2,…,cn,先要將這n個圓排進一個矩形框中,且要求各圓與矩形框的底邊相切。圓排列問題要求從n個圓的所有排列中找出有最小長度的圓排列。例如,當n=3時,且所給的3個圓的半徑分別爲1、1、2時,這3個圓的最小長度的圓排列如圖所示,其最小長度爲2+4*√2.
算法設計
這個問題的解空間應該是一棵排列樹。因爲圓就是按照一定的順序排在矩形框中的,這裏面我們將圓的半徑進行排序,從而代表圓排序。其中a=[r1,r2,…,rn]就是我們的序列。
CirclePerm(n,a)返回找到的最小圓排列長度。初始時,數組a是輸入的n個圓的半徑,計算結束後返回相應於最優解的圓排列。Center計算當前所選擇的圓在當前圓排列中圓心的橫座標,Compute計算當前圓排列的長度,變量min記錄當前最小圓排列的長度,數組r表示當前圓排列,數組x則記錄當前圓排列中各圓的圓心橫座標。算法中約定在當前圓排列中排在第一個的圓的圓心橫座標爲0.
在遞歸算法中,當i>n時,算法搜索至葉結點,得到新的圓排列方案。此時算法調用Compute計算當前圓排列的長度,適時更新當前最優值。
當i<n時,當前擴展結點位於排列樹的第i-1層。此時算法選擇下一個要排列的圓,並計算相應的下界函數。在滿足下界約束的結點處,以深度優先的方式遞歸地對相應子樹搜索(此結點爲擴展結點)。對於不滿足下界約束的結點,剪去相應的子樹。
至於下界約束如何計算呢?我們用一個圖來說明:
代碼
#include <math.h>
#include <iostream>
using namespace std;
class Circle
{
friend float CirclePerm(int,float *);
private:
float Center(int t);//計算當前所選擇圓的圓心橫座標
void Compute(void);
void Backtrack(int t);
float min,//當前最優值
*x,//當前圓排列圓心橫座標
*r;//當前圓排列(可理解爲半徑排列)
int n;//待排列圓的個數
float Circle::Center(int t)
{
float valuex,temp = 0;
//之所以從1-t判斷是因爲防止第t個圓和第t-1個圓不相切
for(int j = 1;j < t;j++)
{
valuex = x[j] + 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[t] < 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)
{
//到達葉子節點,我們計算high與low的差距
Compute();
}
else
{
//排列樹解空間
for(int j = 1;j <= t;j++)
{
//圓的排列其實就是就是半徑的排列,因爲相同半徑的圓是相同的
//交換半徑順序,可以進一步優化,如果半徑相等不交換
//鏡像序列只算一次,例如1,2,3和3,2,1
swap(r[t],r[j]);
if(Center(t)+r[1]+r[t] < min)//下界約束,我們取第一個圓的圓心爲原點,所以計算距離的時候要加上r[1]和r[t]
{
x[t] = Center(t);
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;
}
};
這裏我想說明的一點是,我們之所以一直從頭尋找一個圓來更新當前圓的圓心位置,是因爲相鄰的圓與當前圓t不一定相切,我們應該找到一個相切的。選出最大值就對應了當前圓中心座標
總結
Backtrack需要O(n!)的計算時間(排列樹),計算當前圓排列長度需要從頭遍歷到尾,需要O(n)計算時間,從而整個算法的時間複雜度爲O((n+1)!)
優化:1.排序存在鏡像,我們算一次即可
2.不交換相同半徑圓的位置