问题描述
给定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.不交换相同半径圆的位置