用回溯法解決圓排列問題
教材是用的王曉東的《計算機演算法設計與實現》第四版,c++版
一下是問題描述:
演算法實現:
/**
*能確定一個正確的想法(即每種情況都能考慮到,然後找一個最簡單而準確的方式表達出來)
*
**/ #include<iostream>//此問題沒有要求相鄰的圓必須相切
#include<math.h>
using namespace std;
class Circle{
friend float CirclePerm(int,float*);//找到最優排列
private:
float Center(int t);//計算橫座標
void Compute();//計算當前圓排列的長度
void Backtrack(int t);
float min,//最優值
*x,//當前圓排列圓心橫座標
*r; //當前元的半徑
int n;//當前待排園的個數
};
template <class T>
void Swap(T a,T b)
{
T temp;
temp = a;
a = b;
b = temp;
}
//此問題沒有要求相鄰的圓必須相切 ——對於計算橫座標,肯定選最大值
float Circle::Center(int t)//t代表當前選的第t個圓
{
//計算當前所選園的圓心橫座標
float temp = 0.0;//第一個圓預設為0
//j表示第t個圓之前的圓,
//x[j]這個陣列第零個元素無用
for(int j=1;j<t;j++)//為什麼每次都從第一個圓開始比 ——找最大值,防止前一個與之相切的圓特別小 (不是,見下)
{
float valuex = x[j]+2*sqrt(r[t]*r[j]);//疑問① 考慮特殊情況 (如果特大圓放在第二個位置,圓心距與橫座標並沒有錯)——核心問題不是這個,這個兩個之間也是相切的
//另一種特殊情況是第二個圓特別小而第三個圓很大,導致一三相切二三不想切,這就是for迴圈的原因
//疑問二 不考慮特殊情況,為什麼要加x[j]? —懂了,畫圖就明白了,x[j]是之前圓的橫座標
if(valuex>temp)temp = valuex;
}
return temp;
}
//負軸取絕對值最大,正軸取最大 ,n、
void Circle::Compute(void){//計算當前圓排列的長度,我的想法——(1--j)上所有圓的橫座標加上這個的圓心距取最小值(若最後一個圓特別小,這個公式根本不成立);課本上的想法——(負軸取絕對值最大,正軸取最大)
float low = 0,
high= 0;
for(int i = 1;i<=n;i++)//n始終是不變的
{
if(x[i]-r[i]<low)low = x[i]-r[i];//為什麼要積累low——也和特殊情況有關嗎? //這個算的就是第一個的半徑吧,到底為什麼要積累 ——不對,算的是負值的部分,若第一個特別小,第二個很大,則取最大值
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 = 1000000;
float*x = new float[n+1];
X.x = x;
X.Backtrack(1);
delete[] x;
return X.min;
}
int main()
{
int n;
float*a = new float[n+1];
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>a[i];
}
cout<<CirclePerm(n,a)<<endl;
return 0;
}
注意:
演算法部分來源於課本
此演算法中的出來的是最優解
考慮到了相鄰圓之間可能不相切的情況
在看演算法的過程中提了很多智障問題,都在註釋裡面,但也都自圓其說了,望大佬們如果發現有錯誤的,可以不吝賜教。