我相信人都有自尊,而尊重别人的自尊是一种及其高尚的精神。
社会中有很多想不到的惨绝人寰的事情,有时候发生到了自己身上、有时候发生在自己最亲密的人身上、有时候发生在自己周围人的身上等。这些无不露出一些类似于心理变态vs心灵脆弱。
“不尊重别人”在社会中出现了太多太多了。习以为常的人笑而不谈;不习以为常的人又觉得太恐怖了。把人当“玩物”,就是一种典型的不尊重别人的例子。
在这个浮夸、嘈杂的社会中如何去尊重别人是全社会乃至全世界的一门大学问,无论男女、贵贱、老幼,都要有一颗尊重的心!
尊重别人就是尊重自己。
在前两篇介绍了回溯法的内容,我们能看到、能想到,回溯法是一个解决难度较大的组合优化问题的算法。当然回溯法的时间复杂度又是极高的。基本都是指数级、阶层级的时间复杂度。那么如何在这个时间复杂度上尽量少去搜索一些根本不需要搜索的点,从而减少搜索次数,降低运行时间呢?这个就是回溯法的限界思想。
本篇主要介绍回溯法的限界思想与回溯法限界思想的应用:圆排列问题。
1、回溯法的限界思想
我们知道回溯法的结构是一棵树,而优化这棵树的方法,就是进行剪枝操作。就是把不必要搜索的结点以及子结点统统裁掉。就好比拿了把剪子把多余的树枝剪掉了,这样一剪,我们就不必再去搜索这些剪掉的结点了。回溯法的限界思想就是这样的原理。我们希望能找到一个方法能够帮助我们剪掉一些结点,这个方法就是代价函数与界的判断。
什么是代价函数
代价函数是在搜索树的结点过程中计算的函数。
如果是极大化问题,代价函数的值就是以该点为根的子树所有可行解的值的上界;如果是极小化问题,代价函数的值就是以该点为根的子树所有可行解的值的下界。
所以对于极大化问题,父结点的代价>=子结点的代价;对于极小化问题,父结点的代价<=子结点的代价。
我们看到这个图,对于极大化问题,被圈中的点作为父结点,会通过分支搜索产生子树。在这些子树里,有些结点可能会达到可行解。这些可行解都有值,这些值的大小可能会不一样,所以我们让这些值的上界充当代价函数的估值。
正如图中所看到那样,我们在父结点计算代价函数值时,父结点产生的子树中(大红圈画的范围),可行解的值的上界来作为父结点的代价函数值F。在子树中有一个可行解X,我们有F>=v(X)(v(x):表示X的值)。
自然地,如果这棵子树有多个可行解,那么代价函数要>=所有这些可行解的值。
下面介绍一下界的含义
什么是界
如果是极大化问题,界就是当前得到可行解的目标函数的最大值;如果是极小化问题,界就是当前得到可行解的目标函数的最小值。
说白了就是,你在走某一分支的时候,到达了叶结点,得到了一个可行解。比如上篇介绍的最优装载问题,到达叶结点之后,得到了可装载的重量的一个值,这个值就是当前得到的一个最大值,这个在此时的得到的最大值就是界。当然你再走别的分支的时候,这个界是可以被更新的。
我们还是看这个图,当你进行深度搜索的时候,当黄色的那个结点上得到了一个可行解X的值v(X)(v(x):表示X的值)。那么我们就把这个值作为当前的界B,如果在后续的搜索中,如果得到了更好的界,那就替换这个界,界就得到了更新。我们在上面说了,在极大化问题中,代价函数要>=可行解的值。故F>=B。
限界思想
我们寻求一个能够停止分支,回溯到父结点的一个方法。
这个方法是这样的:
1、判断是否满足约束条件,不满足约束条件的则停止分支回溯,回溯到父结点
2、对于极大化问题,代价函数值F小于当前界B,则停止分支,回溯到父结点;而对于极小化问题,代价函数值F大于当前界B,则停止分支,回溯到父结点。
在极大化问题中,计算以父结点形成的根的代价函数值小于当前界,那就意味着,如果搜索以这个结点为根的子树,得到的可行解不会比当前界的值更好。所以从父结点直接回溯就可以了,就不用再搜索了。
下面我们我们看一个圆排列问题的例子
2、圆排列问题
什么是圆排列问题
给定n个圆的半径序列,各圆与底线相切排列,假定每个圆占大于1的长度,求具有最小长度ln的圆的排列顺序,注意各圆之间不能相交。
如图所示,三个圆分别与底线相切排列,但是排列方式不同,排列产生的总长度是不同的。上面的图的总长度是小于下面的图的总长度。圆排列问题是寻求一个产生最短的长度的圆排列方式。
下面用回溯法的限界思想来解圆排列问题
回溯法的限界思想解圆排列问题
我们对问题进行建模:
输入:<r1,r2,r3,…,rn>为1,2,3,…,n的圆的半径
输出:<i1,i2,i3,…,in>为1,2,3,…,n的排列
而对于第一个圆排好,那么对剩下的圆再进行排序。由此可见,圆排列问题是一棵排列树的结构。
由于第n+1个圆是要在未排列的圆的集合中寻找,所以约束条件是ik+1∈{1,2,…,n}-B
得到最小圆排列的长度可以当作界
如图所示,令第一个圆的圆心的横坐标坐标是0的位置,各圆与x轴这条底线相切。
我们假设前k个圆已经排好了,我们看到第k个圆的圆心的横坐标坐标是xk。那么第一个圆到第k个圆的长度是怎么计算的呢?
由图中可知是l=r1+xk+rk,但是xk怎么求?
下面介绍下xk的求法
我们看到第k个圆和第k-1个圆,它们的圆心横坐标之差是dk,那么第k-1个圆与第k-2个圆的横坐标之差是dk-1。像这样的d1+d2+…dk=xk。所以求xk,就是求dk。
我们把其中两个相切的圆抽出来
如图所示,我们看到可以把最近的两个圆拿了出来,而求解dk是可以放进这个直角三角形里面。
我们看到斜边是rk+rk-1,直角边(非dk)是两个半径之差rk-rk-1。根据勾股定理就能得出dk。
我们也可以通过上边那个图看到,如果两个圆相离。那么斜边的长度增加,而直角边(非dk)没有变化,因此dk就会增加。dk增加了总长度就会增加。所以说为什么两圆要相切排列,就是因为这个原因。
但是,我说的是两个圆相切,并没有说是两个序号相近的圆相切,就是第k个圆不一定是与第k-1个圆相切,但是和第k-2个圆相切。大家想过这样的情况吗
再考虑这个情况之前,先说明一种情况。假设所有圆彼此相切,那么我们令x[i]:记录第i个圆的圆心的横坐标,而计算第k个圆的圆心的横坐标是不是第k个圆与第k-1个圆的圆心之间的距离+x[k-1]呢,大家可以想一想是不是。
那么如上图所示那样,如果第k个圆与第k-2个圆的圆心之间的距离+x[k-2]是不是等于第k个圆与第k-1个圆的圆心之间的距离+x[k-1]呢
如上图所示,下边的是第k个圆与第k-1个圆的圆心之间的距离+x[k-1];上边的是第k个圆与第k-2个圆的圆心之间的距离+x[k-2]。它们计算出来的距离都是一样长的。这是圆彼此相切的情况。那么,我们看下存在两个相近的圆彼此不相切的情况。
如图所示
我们看到两个圆,第k个圆是和第k-2个圆相切的,而与第k个圆不相切。如果我们还是依照那种直角三角形勾股定理计算dk,势必会将得出的dk小于实际的dk。那么再去计量第k个圆的圆心的横坐标,势必会将第k个圆的圆心横坐标算的比正常情况的小。大家可以自行画图就能明白。
所以,彼此不相切的圆经过计算得到的圆心的横坐标会比实际的小。因此我们得出了一个方法,将第k个圆与前边排好的圆计算出圆心的横坐标后,将最大的值拿出来,最大表示了这两个是圆是一种相切关系的。这个最大的值就是第k个圆的圆心的横坐标。
你会去想,有没有可能第k个圆已经和相切的圆计算出了正确的圆心的横坐标,会不会再与这个相切的圆之前的圆得到一个更大的横坐标的值呢。答案是不会,因为我们已经在上边说过了,如果彼此相切的圆,它们计算出来的距离都是一样长的。也就是说不会得到一个更大的值,要么得到一个比这个值小的,要么得到一个和这个值一样的。小是因为,前面的圆也会有彼此不相切的情况。所以这个方法是正确的。
下面说一下如何确定第t个位置用哪个圆排。代码中是做交换。比如说我们已经第一个圆和第二个圆的排列关系确定了,第3个位置我们已经尝试了第3个圆,第4个圆,第5个圆。下面打算尝试第6个圆,我们可以把第三个圆与第六个圆进行交换(半径做交换就可以了)。这样既可以算第六个圆与前面排好的两个圆的dk,从而得到第三个圆的圆心的横坐标;也可以将没用的圆都扔到后边去。
如果第六个圆的这样的排列,你感觉很满意。那么我们就把这个圆的半径存入到x[3]中这样就确定了第t个位置用哪个圆排列。
下面又牵扯出了一个问题什么是感觉满意?这又是什么东西
这就是我们要说的界与代价函数的关系了
大家还记得界就是得到最小圆排列的长度。不妨我们把代价函数设置成总长度,就是F=r1+xk+rk,让F和界进行比较。因为这是个极小化问题,所以F<界(等于也可以,不过小于的话更能加速剪枝效果)(不过这里要说一声对于分支限界法,有时候是按按深度优先机械进行,这个等于要看情况加不加,有时候会因为限制效果太重,使得优先队列为空而无法做取出操作,导致程序报错)
最后说说,如何求解,当到达叶子结点的时候。我们可以去计算整个长度的横坐标最小值和横坐标最大值。最大值-最小值不就是整个长度的值了吗。最小值:圆心横坐标-半径,即x[k]-r[k],最大值:圆心横坐标+半径,即x[k]+r[k]。当然最小和最大是经过不断比较得出来的,这个看代码就行。
算法说到这里,代码也已经分析差不多了,最后说说一个逻辑编写。在上一篇已经提到了,子集树的递归是将值还回去之后去递归到右子树去,因为子集树是0,1分叉的,不是0就是1,不进左子树就进右子树;而排列树是选择了第一个就不再去选第一个,而去选剩下的。所以是for循环一遍一遍的尝试所有情况,第1个选完了,再递归到第2个,走不通,就跳出递归,循环选择下一个再尝试,然后再从剩下的选。为了更好的计算和选择,排列树一定是将选的位置与选择的第几个进行交换。
下面我们看下代码
public static double minLength=0x3f3f3f3f; //圆排列问题的最优解
public static void main(String[] args) {
// TODO Auto-generated method stub
double r[]=new double[] {1,1,2,2,3,5};
double x[]=new double[r.length]; //存储圆心横坐标的值
double bestx[]=new double[r.length]; //存储最优情况下的半径
ReBackCirclePermutation(r, x, bestx, 0);
System.out.print("最优解的半径排列是:");
for(int i=0;i<bestx.length;i++)
System.out.print(bestx[i]+" ");
System.out.println("最优解的长度是:"+minLength);
}
//回溯法与限界思想解圆排列问题
public static void ReBackCirclePermutation(double r[],double x[],double bestx[],int t) {
//到达叶结点
if(t==r.length) {
//如果此时的最大坐标-最小坐标<minLength,则记录此时的最优解
double min=0x3f3f3f3f,max=-0x3f3f3f3f;
for(int i=0;i<r.length;i++) {
if(x[i]-r[i]<min)
min=x[i]-r[i]; //这是圆心坐标-半径找到其中的最小左边(最左部的坐标)
if(x[i]+r[i]>max)
max=x[i]+r[i]; //这是最右部的坐标
}
if(max-min<minLength) {
for(int j=0;j<bestx.length;j++)
bestx[j]=r[j];
minLength=max-min;
}
return;
}
//没有到达叶结点
for(int i=t;i<r.length;i++) {
swap(r, t, i);
double xPos=CenterxPos(t, x, r);
//剪枝
//代价函数:第一个圆的半径+第t个圆的半径+第t个圆的圆心横坐标值<minLength
if(r[0]+r[t]+xPos<minLength) {
//记录此时第t个圆的圆心坐标,便于计算下一个圆时使用,然后进入左子树
x[t]=xPos;
ReBackCirclePermutation(r, x, bestx, t+1);
}
swap(r, t, i);
}
}
//交换函数
public static void swap(double x[],int i,int j) {
double temp;
temp=x[i];
x[i]=x[j];
x[j]=temp;
}
//计算圆心横坐标
public static double CenterxPos(int t,double x[],double r[]) {
//因为目标圆有可能与排在它之前的任一圆相切,故需一一判断
double temp=-0x3f3f3f3f,xPos;
for(int i=0;i<t;i++) {
xPos=x[i]+2.0*Math.sqrt(r[t]*r[i]);
if(xPos>temp)
temp=xPos;
}
return temp;
}
圆排列问题分析
因为圆排列问题是一棵排列树,那么它的时间复杂度就是O(n!),看似和蛮力法一样,但是我们经过了限界操作后,裁掉了不少情况,所以比蛮力法搜索要快。