回溯法求解圆排序问题

1.问题描述

问题描述

给定n个大小不等的圆 C1,C2,C3,...Cn, 现要将这n个圆排进一个矩形框中,且要求各圆与矩形框的底边相切。圆排列问题要求从n个圆的所有排序中找出最小长度的圆排序。`

问题分析------举例
例如,当n=3,且所给的3个圆的半径分别为1,1,2时,这三个圆有两种可能:

CASE1.png

CASE2.png

显然第二种情况的长度要小于第一种情况下的长度


2. 算法分析

回溯法最重要的就是求出界限函数,按问题性质,画出子集树或者排列树
在圆排列问题中,它的解空间是一棵排列树。设开始时数组a是所给n个圆的半径,则相应排列树由 a[1:n] 的所有排列构成,如下:


permutation tree.png

使用回溯遍历所有情况,在满足下界情况(即当不满足下界条件时,要剪枝),且遍历到叶子结点时,计算当前情况的圆排列长度,并更新当前最优值。


3.算法模型

数组r[ ] -起始表示是输入的n个圆的半径 ;计算结束后返回的数组a是相应于最优解的圆排列。
数组x[ ] -则记录当前圆排列中各圆的圆心横坐标。

backtrack(t) -回溯算法,t 表示选择第t个圆,当 t=N 时返回找到的最小的圆排列长度。
center(t) -计算圆在当前圆排列中的横坐标
compout() -计算当前圆排列的长度。
变量minlen -记录当前最小圆排列长度。

一一解释三个函数 :

center(t) 函数

example1.png

--计算第 t 个圆圆心横坐标。由 例1.png易知d[k]^2= sqrt((r[k]+r[k-1])2-(r[k]-r[k-1])2),推导出d[k] = 2sqrt(r[k] r[k-1]) 从而得x = 2*sqrt(r1*r2)。如图例2.png所示,在计算第三个圆 R[3] 的圆心横坐标时,不能确定是 R[1] 与它相切,还是 R[2] 与它相切,故求解第 t 个圆的圆心坐标的思想是遍历在t之前的所有圆,并计算当每一个圆与 R[t] 相切时的圆心坐标,最大值即为 R[t] 实际的圆心坐标
(伪码)

def center(t):
    temp = 0
    for i in range(1, t):
        xvalue = x[i] + 2.0 * math.sqrt(r[t] * r[i])   #temp暂时存储最大值即第 t 个圆可能圆心坐标
        if xvalue > temp:
            temp = xvalue
    return temp


compute() 函数

example2.png

--计算当前圆排列长度。用变量lenmin记录当前最小圆排列长度;数组 r[] 存储所有圆的半径。数组 x[] 则记录当前圆排列中各圆的圆心横坐标。用 low 和 high 指针分别指向圆排列的最左端和最右端,如例1.png 所示,bestr[] 数组记录当前排列方式(即排列树的一条路径)。

def compute():
low, high = 0, 0
    for i in range(1, N):
        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    #用变量lenmin记录当前最小圆排列长度
        for i in range(1, N):   #bestr[] 数组记录当前排列方式(即排列树的一条路径)
            bestr[i] = r[i]


backtrack(t) 函数

排列树.png

--当t=n时,表示n个圆选择完毕,利用compute()函数得到最小圆排列长度,并保存此时的圆序列;
当t<n时,在r[t+1],r[t+2] .. r[n] 中选择第t个圆r[j],swap r[j],r[t],则此时的r[0]到r[t]为第t个圆为r[j]的圆排序;
下一步,对其进行是否符合剪枝条件的判断,若符合则舍去,并回溯到上一层,即恢复现场,swap r[j],r[t] ;若不符合,则,计算第t个圆的圆心坐标,递归选择下一个圆。
剪枝条件:r[0]到r[t]的圆心距加r[0],r[t]的半径要小于当前最小值

如排列树.png为例,当在第一棵树在第一层搜索第二层结点时即backtrack(2)时,可选择的点为a2,a3;选择a2为第二层结点时,判断a1a2的圆心距+a1的半径+a2的半径是否小于minlen,若小于则递归选择第三层;若不符合退回第一层,选择a3,以此类推。

def backtrack(t):
    if t == N:
        compute()
    else:
        # r[t]-r[N]为未被选择的圆
        for j in range(t, N):
            # 将第j个圆选入排序中
            r[t], r[j] = r[j], r[t]
            if  center(t) + r[t] + r[1] < minlen:
                x[t] = centerx
                backtrack(t + 1)
            # 恢复现场
            r[t], r[j] = r[j], r[t]

4.代码实现

# 回溯法

import math
N = 4
# N = int(input('n='))
minlen = 999
bestr = [0 for i in range(0, N)]
r = [0 for i in range(0, N)]
x = [0 for i in range(0, N)]

r[1], r[2], r[3] =1,1,2
# for i in range(1, N):
#     r[i] = int(input('r='))


# 计算当前所选择圆的圆心横坐标
def center(t):
    temp = 0
    for i in range(1, t):
        xvalue = x[i] + 2.0 * math.sqrt(r[t] * r[i])
        if xvalue > temp:
            temp = xvalue
    return temp




# 计算当前圆排列的长度
# 变量lenmin记录当前最小圆排列长度。
# 数组r存储所有圆的半径。数组x则记录当前圆排列中各圆的圆心横坐标
def compute():
    global minlen
    low, high = 0, 0
    for i in range(1, N):
        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 i in range(1, N):
            bestr[i] = r[i]


# 回溯 选择第t个圆
def backtrack(t):
    if t == N:
        compute()
    else:
        # r[t]-r[N]为未被选择的圆
        for j in range(t, N):
            # 将第j个圆选入排序中
            r[t], r[j] = r[j], r[t]
            centerx = center(t)
            # 剪枝-当末端3个半径逐一递增或递减时不可能为最优解,舍去
        
            if centerx + r[t] + r[1] < minlen:
                x[t] = centerx
                backtrack(t + 1)
            # 恢复现场
            r[t], r[j] = r[j], r[t]

backtrack(1)
print('最小圆排列长度为 ' + repr(minlen))
print('最优圆排列的顺序对应半径分别为',end=';')
for i in range(1, N):
    print(repr(bestr[i]), end=' ')

4.优化与改进

  1. 例如,像1,2,…,n-1,n和n,n-1, …,2,1这种互为镜像的排列显然具有相同的圆排列长度,通过算法优化,可减少约一半的计算量。
  2. 如果所给的n个圆中有k个圆有相同的半径,则这k个圆产生的 k! 个完全相同的圆排列也显然相同。
  3. 剪枝优化:当后三个圆的半径为递增或者递减时,肯定不是最小长度,剪枝。

代码实现

第一部分

# z-奇数为0 偶数为1
z= (N-1)% 2
# 剪掉一半的子树-偶数则搜索2/N个子树,奇数则搜索2/N+1个子树
for i in range(1,int((N-1)/2)+z+1):
    x[1]=0
    r[1],r[i]=r[i],r[1]
    backtrack(2)

第三部分

def backtrack(t):
    if t == N:
        compute()
    else:
        # r[t]-r[N]为未被选择的圆
        for j in range(t, N):
            # 将第j个圆选入排序中
            r[t], r[j] = r[j], r[t]
            # 剪枝优化
            if t>4:
                if (r[t]>r[t-1] and r[t-1]>r[t-2] )or(r[t]<r[t-1]and r[t-1]<r[t-2]):
                    return
            if center(t) + r[t] + r[1] < minlen:
                x[t] = center(t)
                backtrack(t + 1)
            # 恢复现场
            r[t], r[j] = r[j], r[t]

代码优化部分 待续~

5.时间复杂度和空间复杂度

时间复杂度:由回溯法的排列树可知,搜索子结点的时间复杂度是O(n!)次,即全排列的时间复杂度,而Backtrack()函数每次计算需要O(n)计算时间,从而整个算法的计算时间复杂性为O((n+1)!)。
虽然理论上时间复杂度很大,但实际的消耗实际由于增加了剪枝条件,会比O((n+1)!)小很多。
空间复杂度:由于r[]数组,x[]数组的大小都为n,故空间复杂度为O(n)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容