回溯法之圓排列問題


問題描述

     給定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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM