程序员都会的五大算法之五(分支限界法),恶补恶补恶补!!!
程序员文章站
2024-01-31 23:23:04
...
前言
点击查看算法介绍
五大算法
WX搜素"Java长征记"对这些算法也有详细介绍。
分支限界算法
一、算法概述
分支限界法其实类似于回溯法,也是一种在解空间树中搜索解的算法,区别在于
分支限界算法是搜索满足约束条件的一个解,或是在满足约束条件的解中找出使某一
目标函数值达到极大或极小的解,即在某种意义下的最优解。
界:
对于极大化问题即为当前得到可行解的最大值(极小化问题相反),极大化问题界的初值为 0 (极小化问题为对应最大值)。 得到更好的可行解时界也要随之进行更新。
代价函数(限界函数):
即就是确定问题上下界的一个函数,在搜索每个节点的时候都要进行一次界的确定,对于极大化问题是以该节点为根的子树所有可行解的值的上界 ( 极小化问题则为下界)
【对极大化问题父节点代价值不小于子节点的代价值 (极小化问题相反)】
停止分支回溯父节点的依据
- 不满足约束条件
- 对于极大化问题,代价函数值小于当前界(对于极小化问题是大于界)
界的更新
对于极大化问题,若一个新的可行解的值大于 (极小化问题为小于) 当前的界,则把界更新为该可行解的值
二、分支限界算法思想
- 求解目标:找出满足约束条件的一个解,或是在满足约束条件的解中找出在某种意义下的最优解。
- 搜索方式:分支限界法一般采用广度优先遍历去进行搜索。
- 组织活节点表:在分支限界法中,每个活节点只有一次机会成为扩展结点。活节点一旦成为扩展节点,就一次性产生其所有儿子节点。在这些儿子节点中,导致不可行解或导致非最优解的儿子节点被舍弃(即就是剪枝过程),其余儿子节点被加入活节点表中。之后又从活节点表中取出下一扩展节点,重复进行扩展,直至找到所需解活活节点表为空。
四、常用扩展节点方法
队列式(FIFO)分支限界法
队列式分支限界法将活节点表组织成一个队列,并将队列的先进先出原则选取下一个节点为当前扩展节点。
优先队列式分支限界法
将活节点表组织成为一个优先队列,按照优先队列中规定的节点优先级选取优先级最高的节点成为当前扩展节点。
五、分支限界法VS回溯法
六、分支限界算法的实例
一一最大团问题
最大团问题
问题介绍
无向图G=<V,E>, 求G的最大团.例:给定无向图 G = < V, E >, 其中顶点集
V = { 1, 2, … , n },边集为 E . 求 G 中的最大团.
解释:
- G的子图:G’= <V ‘,E’>, 其中V '⊆V, E’⊆E
- G的补图:=<V, E’>, E’是E关于完全图边集的补集G
- G中的团:G 的完全子图
- G的最大团:顶点数最多的团
问题分析
代价函数:
目前的团可能扩张为极大团的顶点数上界F = Cn+ n − k其中 Cn 为目前团的顶点数(初始为0)k 为结点层数
实例
实例图解分析
解释:
- a: 极大团 {1,2,4},顶点数为 3, 界 B=3;
- b:代价函数值 F=3, 回溯;
- c: 极大团 {1,3,4,5}, 顶点数为 4,修改界 B=4;
- d: F=3,不必搜索; e: F=4, 不必搜索.
程序:
//最大团问题
package com.company;
import java.util.*;
public class BiggestGroup {
private static int v[][]={{0,1,1,1,1},
{1,0,0,1,0},
{1,0,0,1,1},
{1,1,1,0,1},
{1,0,1,1,0}};
private static ArrayList<Integer> a=new ArrayList<>();//记录已选团中顶点
private static ArrayList<Integer> bestA=new ArrayList<>();//记录最大团的顶点
private static int n=v[1].length;//顶点个数
private static int best,f,b;//best为上届,f为代价函数值,b为已选顶点数
private static int x[]=new int [n];//记录顶点状态(不选-0 or 选-1)
public static void main(String[] args){
backTrace(0);
System.out.println("最大团顶点个数为:"+best);
output(bestA);
}
/*输出链表*/
private static void output(ArrayList<? extends Integer> a) {
Iterator<Integer> p= (Iterator<Integer>) a.iterator();
while(p.hasNext()){
System.out.print(p.next()+" ");
}
}
/*广度优先遍历*/
public static void backTrace(int t){
/*到达叶节点*/
if(t>=n)
{
/*更新最优解*/
if(b>best) {
best = b;//更新界
bestA.clear();//清空之前的解
for(int i=0;i<a.size();i++)
bestA.add(a.get(i));
}
return;
}
f=b+n-t;//代价函数
/*进左子树*/
if(f>best && isOk(t)){
x[t]=1;
a.add(t+1);
b++;
backTrace(t+1);
b--;//回溯
a.remove(b);
}
f=b+n-t;
/*进右子树*/
if(f>best){
x[t]=0;
backTrace(t+1);
}
}
/*是否符合约束条件*/
public static boolean isOk(int t){
if(a==null || a.size()==0)
return true;
for(int i=0;i<a.size();i++){
if(v[t][a.get(i)-1]==0)
return false;
}
return true;
}
}
时间复杂度
最坏时间复杂度为O(n2^n)
分支限界和回溯法的时间复杂度:W(n) = ( p(n) f(n) ) 其中 p(n) 为每个节点的
工作量f(n)为节点个数,最坏时间复杂度相对都比较大,但是平均复杂度因为剪枝过
程肯定降低了很多,优于蛮力算法。而且空间代价也较小。
干货分享
想学习更多的关于Java基础、算法、数据结构等编程知识的WX搜索"Java长征记"。上面的这些在里面也有详细介绍。
下一篇: Nginx学习之负载均衡