问题描述
给定n个大小不等的圆c1,c2,…,cn,现要将这n个圆排进一个矩形框中,且要求各圆与矩形框的底边相切。圆排列问题要求从n个圆的所有排列中找出有最小长度的圆排列。
例如,当n=3,且所给的3个圆的半径分别为1,1,2时,这3个圆的最小长度的圆排列如图所示。其最小长度为2+4√2。
解析
如图所示,三个圆分别与底线相切排列,但是排列方式不同,排列产生的总长度是不同的。
上面的图的总长度是小于下面的图的总长度。圆排列问题是寻求一个产生最短的长度的圆排列方式。
下面用回溯法的限界思想来解圆排列问题
回溯法介绍参考自 回溯法介绍
先介绍一下回溯法
1、基本概念:
回溯法是一种类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。
2、基本思想
在包含问题的所有解的解空间树中,按照有限搜索的策略,从根节点出发深度探索解空间树。当探索到某一结点时,要先判断该结点是否包含问题的解,如果包含,就从该结点出发继续探索,如果该结点不包含问题的解,则逐层想其祖先结点回溯。(回溯法就是对隐式的深度优先搜索算法)
若用回溯法求解问题的所有解时,要回溯到根,且根节点的所有可行的子树都已经被所有遍才结束。而使用回溯法求解任一个解时,只要搜索到问题的一个解就可以结束。
3、解空间的排列树结构
当所给的问题是确定n个元素满足某种性质的排列时,相应的解空间树称之为排列树。排序树通常有**n!**叶结点。
4、回溯法框架
1 void backtrack (int t)
2 {
3 if (t>n) output(x);
4 else
5 for (int i=t;i<=n;i++) {
6 swap(x[t], x[i]);
7 if (legal(t)) backtrack(t+1);
8 swap(x[t], x[i]);//回溯还原
9 }
10 } //调用函数回溯
在圆排列问题中,我们要做的就是以深度优先方式搜索解空间,并在搜索过程中用剪枝函数避免无效搜索。
实现代码主要分为三部分
1、center:求每个圆的圆心横坐标,如图所示第n个圆的圆心横坐标为与其相切的圆的横坐标+2*√(r[n]*r[n-1])。他可能与前n-1个圆中的任一个圆相切,所以需要遍历前n-1个圆求出第n个圆的圆心横坐标。初始化时,第一个圆的圆心横坐标为0。
2、compute:计算圆排列的长度,变量Min表示最小圆排列的长度,通过比较找出最小的左部坐标和最大的右部坐标,相减后就是该圆排列的长度,然后把每次不同的排列长度相比较,对Min进行更新。
3、backtrack:当i>n时,算法搜索至叶节点,得到新的圆排列方案。此时算法调用Compute计算当前圆排列的长度,适时更新当前最优值。当i<n时,当前扩展节点位于排列树的i-1层。此时算法选择下一个要排列的圆,并计算相应的下界函数。实际上有一些圆在排列时其长度就已经超过了最小长度,因此有这些部分圆排列所演变出的圆排列明显是不符合的,要进行减枝处理。
设计
核心代码
double center(int n) {//计算圆心横坐标
double length = 0;
for (int i = 0; i < n; i++) {//判断圆是否与排在它之前的任一圆相切
double temp = x[i] + 2.0 * sqrt(r[n] * r[i]);//前一圆的圆心横坐标加两圆圆心横间距
if (temp > length)
length = temp;
}
return length;
}
void compute() {//计算当前圆排列长度
double 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;
for (int i = 1; i <= n; ++i)
bestsort[i] = r[i];
}
}
void backtrack(int t) {
if (t == n + 1)//最后一个圆
compute();//计算圆排列长度
else {
for (int i = t; i <= n; ++i) {
swap(r[t], r[i]);//全排列
double centerX = center(t);
if (centerX + r[t] + r[1] < Min) {//先判断是否在范围内,如果是则不断搜索下一层,如果不是直接回溯。
x[t] = centerX;
backtrack(t + 1);
}
swap(r[t], r[i]);//全排列换回
}
}
}
分析
时间复杂度
由回溯法的排列树可知,最坏情况下搜索子结点的时间复杂度是O(n!)次,即全排列的时间复杂度。
Backtrack()函数每次计算圆排列长度需要O(n)计算时间。
所以综上,整个算法的计算时间复杂性为O((n+1)!)。
虽然理论上时间复杂度很大,但实际的消耗实际由于增加了剪枝条件,会比O((n+1)!)小很多。
空间复杂度
O(n)
算法还可以改进的地方
1、1,2,…, n和n,…,2,1这种互为镜像的排列具有相同的圆排列长度,只计算一个就够了,可以减少约一半的计算量。
2、若所有圆的半径均相同,则只需要计算一个就够了,而不用计算n!个完全相同的圆排列。