回溯法之圓排列問題

問題描述

給定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.不交換相同半徑圓的位置

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章