问题
给定n个大小不等的圆c1,c2,…,cn,现要将这n个圆排进一个矩形框中,且要求各圆与矩形框的底边相切。圆排列问题要求从n个圆的所有排列中找出有最小长度的圆排列。
例如,当n=3,且所给的3个圆的半径分别为1,1,2时,这3个圆的最小长度的圆排列如图所示。其最小长度为2+4√2。
如图:
解析
圆排列问题的主要思路是排列问题,通过建立排列树,再进行回溯剪枝,得出最优排列。
每次修改一个圆的排列位置,若修改后的排列长度变小,则在当前排列的前提下继续排列,否则回溯。
每次排列后,相切情况下圆的X坐标计算公式:x2 = (r+ri)2 + (r-ri)2,
推出x = 2*sqrt(r+ri);r为自身半径,ri为相切圆半径。
考虑最坏情况,即x最大时,x+r0+r为当前最小圆排列长度。
当遍历完所有排列后,留下的就是最优圆排列。
排列共有ABC,ACB,BAC,BCA,CAB,CBA共六种。
算法
class Circle {
private:
//圆总数
int N;
//最小圆排列长度
double minlen = 100000;
//各圆心横坐标
double *x;
//各圆半径
double *r;
//最小圆排列的半径顺序
double *bestr;
//计算最小圆排列长度
void compute()
{
double low = 0, high = 0;
for (int i = 0; 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 < minlen)
{
minlen = high - low;
for (int i = 0; i < N; ++i)
bestr[i] = r[i];
}
}
double center(int t)//得到每个圆的圆心坐标
{
double temp = 0;
double xvalue;
for (int i = 0; i < t; ++i)
{
//随意取一圆相切,计算圆X坐标
xvalue = x[i] + 2.0*sqrt(r[t] * r[i]);
if (xvalue > temp) {
temp = xvalue;
}
}
return temp;
}
void backtrack(int t)
{
if (t == N)
{
compute();
}
else
{
//计算当前最优排列长度
for (int i = t; i < N; ++i)
{
swap(r[t], r[i]);
double centerx = center(t);
//剪枝条件
if (centerx + r[t] + r[0] < minlen)
{
x[t] = centerx;
backtrack(t + 1);
}
swap(r[t], r[i]);//回溯,开始下一种排列
}
}
}
public:
Circle() {};
Circle(int _N, double *_r) {
this->N = _N;
this->x = (double *)malloc(sizeof(double)*_N);
this->r = _r;
this->bestr = (double *)malloc(sizeof(double)*_N);
}
void calculate() {
backtrack(0);
cout << "最小圆排列长度为:" << minlen << endl;
cout << "最优圆排列的顺序对应的半径分别为:";
for (int i = 0; i < N; ++i) {
cout << bestr[i];
if (i==N-1) {
cout << endl;
}
else {
cout << " ";
}
}
}
};
计算结果
分析
如果不考虑计算当前园排列问题中各园圆的圆心横坐标和计算当前园排列长度所需的计算时间,则算法 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!个完全相同的圆排列,只计算一个就够了。