问题描述:
给出一组圆,将他们排列在一条数轴上,每个圆均与数轴相切,同时每个圆之间不能重叠。将圆先后的排列在数轴上,找到在数轴上留下的最短距离。
简单图示:
需要通过圆的不同排列使得x的值最小。
问题关键点分析:
1、不论是首圆还是后面的圆,每个圆的圆心的横坐标都必须是大于等于它本身的半径。因为如果后圆半径远大于首圆,可能会出现后圆超出起始点的情况。
2、前圆不一定与后圆相切,情况可以参考下图所示;
后圆需要考虑与前面每个圆相切时的位置。
3、当两圆相切时,有如下计算公式:
为前圆与相切的后圆之间圆心的横坐标的距离;
为相切后圆的半径;
为前圆的半径;
4、当现有圆排列组合的长度大于已知最小排列长度后,不再继续向后调换排列,回溯向前。
5、使用排列树实现算法回溯与迭代。
以5个圆,半径为1、2、3、4、5为例:
6、使用EasyX来绘制算法过程演示图像。
代码展示:
#include <iostream>
#include <bits/stdc++.h>
#include <graphics.h> // Include the EasyX header file
#include <conio.h>
#include <windows.h>
using namespace std; // Add this line to use std namespace
void circle(int x, int y, int radius);//画无填充的圆。
int n; // 圆的个数
float a[100]; //圆的半径
float a_x[100];//初始圆的横坐标
float x[100];//计算当前圆排列坐标
float minn = 100000;
void Compute() {
float w = 0;
int i = 0;
float temp = 0;
for (i = 0; i < n; i++)
{
temp = x[i] + a[i];
if (w < temp)
{
w = temp;
}
}
//w = x[n-1]+a[n-1];
if (w < minn)
{
minn = w;
}
cout << "min=" << w << endl;
}
void Print(float b[100]) {
Sleep(500);
cleardevice();
setlinestyle(PS_SOLID, 3);
setlinecolor(RED);//线的颜色为红色
float temp = 0;
for (int i = 0; i < n; i++)
{
cout << b[i] << " ";
//temp = (x[i] * 20) + b[0] * 20;
temp = x[i] * 20;
circle((int)b[i] * 20, (int)temp, (int)b[i] * 20);
}
//Sleep(500);
cout << endl;
}
float Center(int t) {//求圆心坐标:假定它跟前面的所有圆相切,求出圆心坐标,值最大的就是符合条件的。
float ans = a[t];
for (int i = 0; i < t; i++) {
float v = x[i] + 2.0*sqrt(a[t] * a[i]);//2.0*sqrt(a*b)=sqrt(pow(a+b,2),pow(a-b,2))
//float v = x[i - 1] + 2.0*sqrt(a[t] * a[i]);//2.0*sqrt(a*b)=sqrt(pow(a+b,2),pow(a-b,2))
if (v > ans) ans = v;
}
return ans;
}
void Swap(float &a, float &b) {
float t = a; a = b; b = t;
}
//排列树
void BackTrack(int t) { //第t个顶点
if (t >= n) {//到达叶结点
Compute();
Print(a);
return;
}
else {
for (int i = t; i < n; i++) {//不断交换剩下的圆的位置。
Swap(a[t], a[i]);
float centerx = Center(t);//求t的圆心坐标
if (t == 0)
{
centerx = a[0];
}
if (centerx + a[t] < minn) {
x[t] = centerx;
BackTrack(t + 1);
}
Swap(a[t], a[i]);
}
}
}
int main()
{
float temp = 0;
memset(a, 0, sizeof(a));
memset(a_x, 0, sizeof(a_x));
memset(x, 0, sizeof(x));
cin >> n;
for (int i = 0; i < n; i++)
{
cin >> a[i];
}
initgraph(960, 960);
setlinestyle(PS_SOLID, 3);
setlinecolor(RED);//线的颜色为红色
for (int i = 0; i < n; i++)
{
a_x[i] = a[i] + temp;
temp = temp + 2 * a[i];
circle((int)(a[i] * 20), (int)(a_x[i] * 20), (int)(a[i] * 20));
x[i] = a_x[i];
}
BackTrack(0);
cout << "minn=" << minn << endl;
_getch();
closegraph();
return 0;
}
生成好的release版exe与程序已打包上传。
推荐演示使用参数:
圆的个数<=7,圆的半径总和<=24。
回溯算法虽然不能将问题的时间复杂度下降到多项式级别,但是对于很多具体的问题还是能够有效的降低消耗时间。例如如果本问题中对7个圆进行排列,如果使用蛮力算法,需要穷举5040种组合可能,如果每种都花费0.5秒进行显示,那么总共需要花费42分钟左右。但是使用回溯法,大约只需要探索数百条左右的组合可能就完成了。如果是把每个长度更新的时候再进行展示,那么大约仅需数十次展示就完成了。这大大节省了相应的时间。