问题描述
给定n个大小不等的圆 ,现要将这n个圆排进一个矩形框中,且要求各圆与矩形框的底边相切。圆排列问题要求从n个圆的所有排列中找出有最小长度的圆排列。例如,当n=3,且所给的3个圆的半径分别为1,1,2时,这3个圆的最小长度的圆排列如图所示。其最小长度为 。
算法设计
设计一个随机化算法,对于给定的n个圆,计算n个圆的最佳排列方案,使其长度尽可能小。
数据输入
由文件input.txt给出输入数据。第一行有1个正整数n (1≤n≤20)。接下来的1行有n个数,表示n个圆的半径。
解题思路
随机化算法
这里不展开介绍随机化算法,它在我看来只是一种使算法复杂度与输入数据解耦合的思想,并不是某种特定的算法。比如我们都知道快速排序对于不同的输入数据会有不同的时间复杂度,所以我们称其为不稳定的排序算法,而采用随机化的思想就能使其变成稳定的算法,最简单的办法就是对所有输入数据都进行一次随机排列。
在本问题中,解题思想也相同,所谓随机化只是将输入的圆半径给随机打乱罢了。
本题解题思路
本问题我没有采用递归、回溯等算法,想看该算法请移步(图文并茂详尽剖析圆排列问题)。
上面用回溯的算法,时间复杂度达到了O(n!),阶乘时间复杂度可是比指数还吓人,那还不如直接用几层循环了事呢。因此本文采用三层循环设计算法,时间复杂度里面两层肯定是O(n2),最外层时间复杂度不好统计,估摸着最坏也是O(n3)吧。关于性能我才用上文评论区中提供的数据进行测试(如下图),很快就出结果了(不到1秒),因此粗略估计本文算法效率高于上文。
基本思想很简单,定义三层循环,核心在circle_search函数中。第一层while循环控制是否该结束里面两层的循环,里面两层则每次分别交换两个圆的顺序。关于内层循环的j取值从0开始和从i+1开始应该都是没问题的,从0开始能减少最外层循环次数,从i+1开始则是因为反正最外层还有个循环能兜底。内两层循环思想参考冒泡即可。每次交换俩圆顺序,若比最小值更小则将最小值记为这次交换后的圆排列长度,如果交换后长度没有变小,那就得再换回来。至于最开始的将半径随机化,则只是将算法和输入数据解耦罢了。
至于求圆排列长度的方法,大家可以自己琢磨一下,也可以参考下上面提到那篇文章的方法:
完整代码如下(python3版本)
import numpy as np
import random
# 给定一个半径数组,计算其按顺序的长度
def get_len(a, n):
len = 0
if n < 1:
return None
if n == 1:
return a[0]
len += a[0]
for i in range(n - 1):
len += 2 * ((a[i]*a[i + 1]) ** (1/2))
len += a[-1]
return len
# 定义解圆排列问题的随机化算法
def circle_search(a, n):
global min_len
# 将输入数据中的圆的半径随机化,从而让算法性能与输入数据解耦合
a = random.sample(a, n)
# 计算随机化后的长度
min_len = get_len(a, n)
found = True
while(found):
found = False
for i in range(n):
for j in range(n):
a[i], a[j] = a[j], a[i]
if get_len(a, n) < min_len:
found = True
min_len = get_len(a, n)
else:
a[i], a[j] = a[j], a[i]
# 读取数据
with open('./input.txt', 'r') as f:
n = int(f.readline().strip())
a = [float(x) for x in f.readline().strip().split(' ')]
min_len = float('inf')
circle_search(a, n)
with open('output.txt', 'w') as f:
f.write(str(round(min_len, 5)))