问题
圆排列描述的是在给定n个大小不等的圆,C1,C2,C3,…,Cn,现要求将这n个圆排进一个矩形框中,且要求各圆与矩形框的底边相切。圆排列问题要求从n个圆的所有排列中找出有最小长度的圆的排列。
例如,给定3个圆,半径分别为1,2,1,圆排序后结果如下图所示
解析
圆排列问题的解空间是一棵排列树。按照回溯法搜索排列树的算法框架,设开始时a=[r1,r2,……rn]是所给的n个元的半径,则相应的排列树由a[1:n]的所有排列构成。
-
calCircleCenter计算圆在当前圆排列中的横坐标,由x^ 2 = sqrt((r1+r2)^ 2-(r1-r2)^2)推导出x = 2*sqrt(r1 * r2)。
把这个计算放在循环里,是因为后面的圆可能与前面任意一个圆相切,未必是往后面排列。
如下图,如果是直接按照输入的顺序排列圆,
但是也可以这样相切
-
calCircleSortLength计算当前圆排列的长度。变量lenmin记录当前最小圆排列长度。数组r存储所有圆的半径。数组x则记录当前圆排列中各圆的圆心横坐标。
我们可以想象其中任意的一个圆无限大或无限小,无限大的话那其余的圆就可以统统忽略了。因为已知所有圆的x[]和r[],很容易求出每个圆的左右坐标,通过比较找出最小的左部坐标和最大的右部坐标,一减就是该圆排列的长度,然后把每次不同的排列长度相比较,找到更小的minLen就更新。 -
在递归算法findCircleSort中,当i>n时,算法搜索至叶节点,得到新的圆排列方案。此时算法调用calCircleSortLength计算当前圆排列的长度,适时更新当前最优值。当i<n时,当前扩展节点位于排列树的i-1层。此时算法选择下一个要排列的圆,并计算相应的下界函数。
设计
根据上述解析,首先写calCircleCenter函数。
double calCircleCenter(int n){//计算圆心坐标
double maxx = 0;
for(int i = 1;i < n;i++){
int num = x[i] + 2.0 * sqrt(r[i] * r[n]);
if(num > maxx)
maxx = num;
}
return maxx;
}
calCircleSortLength函数
void calCircleSortLength(){//计算圆排列的总长度
double low = 99999,high = 0;
for(int i = 1;i <= CIRCLENUM;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 = 1;i <= CIRCLENUM;i++){
circleSeq[i] = r[i];
}
}
}
findCircleSort函数
void findCircleSort(int n){//查找圆排序
if(n==CIRCLENUM + 1){
calCircleSortLength();
}
for(int i = n;i <= CIRCLENUM;i++){
swap(r[n],r[i]);
double center = calCircleCenter(n);//获取圆当前的横坐标
x[n] = center;
findCircleSort(n + 1);//向下继续搜索
swap(r[n],r[i]);//回溯
}
}
分析
由排列组合可知,生成一个长度为n的序列的全排列的时间复杂度为O(n!)
同时在这个算法中,对于每一个排列中的每一个圆,它有可能和在它之前的任意一个圆相切,为了正确的确定是与那个圆相切需要使用for循环进行遍历,通过遍历找到与之相切的圆,其每次的时间复杂度为O(n)
计算最小圆排列的复杂度为O(n * n!)