圆排列问题

问题描述

给定n个大小不等的圆c1,c2,…,cn,现要将这n个圆排进一个矩形框中,且要求各圆与矩形框的底边相切。圆排列问题要求从n个圆的所有排列中找出有最小长度的圆排列。
例如,当n=3,且所给的3个圆的半径分别为1,1,2时,这3个圆的最小长度的圆排列如图所示。其最小长度为2+4√2。
圆排列问题_第1张图片

解析

如图所示,三个圆分别与底线相切排列,但是排列方式不同,排列产生的总长度是不同的。
圆排列问题_第2张图片
上面的图的总长度是小于下面的图的总长度。圆排列问题是寻求一个产生最短的长度的圆排列方式。
下面用回溯法的限界思想来解圆排列问题

回溯法介绍参考自 回溯法介绍

先介绍一下回溯法
1、基本概念:
回溯法是一种类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。
2、基本思想
在包含问题的所有解的解空间树中,按照有限搜索的策略,从根节点出发深度探索解空间树。当探索到某一结点时,要先判断该结点是否包含问题的解,如果包含,就从该结点出发继续探索,如果该结点不包含问题的解,则逐层想其祖先结点回溯。(回溯法就是对隐式的深度优先搜索算法)
若用回溯法求解问题的所有解时,要回溯到根,且根节点的所有可行的子树都已经被所有遍才结束。而使用回溯法求解任一个解时,只要搜索到问题的一个解就可以结束。
3、解空间的排列树结构
当所给的问题是确定n个元素满足某种性质的排列时,相应的解空间树称之为排列树。排序树通常有**n!**叶结点。
圆排列问题_第3张图片
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。
圆排列问题_第4张图片
2、compute:计算圆排列的长度,变量Min表示最小圆排列的长度,通过比较找出最小的左部坐标和最大的右部坐标,相减后就是该圆排列的长度,然后把每次不同的排列长度相比较,对Min进行更新。
3、backtrack:当i>n时,算法搜索至叶节点,得到新的圆排列方案。此时算法调用Compute计算当前圆排列的长度,适时更新当前最优值。当i

设计

核心代码

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!个完全相同的圆排列。

你可能感兴趣的:(圆排列问题)