1. 程式人生 > 其它 >回溯演算法 --- 例題9.圓排列問題

回溯演算法 --- 例題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)

2)推匯出x = 2*sqrt(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))更加大於最優值.

執行結果:

構建出的排列樹如下:(和之前的大同小異)

講完了,應該能夠看懂吧大夥.(●'◡'●)

歡迎大家訪問我的個人部落格 --- 喬治的程式設計小屋,和我一起努力進步吧!