分支限界法總結--例題(01揹包,最大團,單源最短路徑,裝載問題,佈線問題)
阿新 • • 發佈:2019-02-01
目錄
- 分支限界法剪枝搜尋策略(廣度搜索)與演算法框架
- 01揹包問題
- 最大團
- 單源最短路徑
- 裝載問題
- 佈線問題
分支限界法剪枝搜尋策略(廣度搜索)與演算法框架
基本思想
- 分支限界法與回溯法求解目標不同,回溯法的求解目標是找出解空間中滿足約束條件的所有解,而分支限界法的求解目標是找出滿足約束條件的一個解,或者是在滿足約束條件的解中找出使某一個目標函式值達到極大或者極小的解,即某種意義下的**最優解
- 搜尋方式不同,回溯法以深度優先搜尋的方式進行搜尋,而分支限界法使用廣度優先搜尋或者最小耗費優先的方式進行搜尋解空間,其策略是 : 在擴充套件結點處,先生成其所有的兒子結點(分支), 然後從當前的活結點表中選擇下一個擴充套件結點。
- 計算一個函式值(限界) : 為了加速搜尋的過程,在每一個活結點處,計算一個函式值,並根據函式值,從當前活結點表中選擇一個最有利的結點作為擴充套件結點,使得搜尋朝著解空間上最優解的分支進行推進,一遍儘快的找出一個最優解。
- 每個活結點只有一次機會成為擴充套件結點,一旦成為擴充套件結點,就一次性產生所有的兒子結點,在兒子結點中,導致不可行解或者導致非最優解的兒子結點被捨棄,其餘的加入到活結點表中。
- 活結點表有兩種框架:(1) 佇列式分支限界法。(2)優先佇列式分支限界法
(主要是確定優先順序的選取)
01揹包問題
解題思路:
- 對於0-1揹包問題中的每個活結點只有兩個兒子 結點,分別表示對物品i的選取和對物品i的捨去;在判斷兒子結點是否能加入到活結點表中,有兩個 函式需要滿足,第一個稱為約束函式
,判斷能否滿足揹包容量約束,第二個稱為限界函式,判斷是否可能有最優解。- 每次選取下一個活結點成為擴充套件結點的判斷依據是當前情況下最有可能找到最優解的下一個結點。因此,每次選擇擴充套件結點的方法:當前情況下,在活結點表中選擇活結點的上界uprofit(通過限界函式Bound求出)最大的活結點成為當前的擴充套件結點。 這一過程一直持續到找到所需的解或活結點表為空時為止。
- 為了在活結點表中選擇擁有最大的上界uprofit的活結點,在活結點表上實現優先佇列。(堆按照uprofit排序)
- 為了求出0-1揹包問題的最優解,對於每一個在 活結點表中的活結點建立一個樹結點,樹節點需要反映該結點的父節點和是否有左孩子(有左孩子 表示物品i選取了,沒有左孩子表示物品i捨去了)。因此,可以構造一顆子集樹,最優解就是從樹根 到葉子結點的路徑,子集樹的第i層的所有結點就是在不同情況下對物品i的取捨結點。構造最優解的 順序是從葉子結點到根結點的過程。
import java.io.BufferedInputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.Scanner;
/**
* 使用分支界限法解決01揹包問題
* @author 鄭鑫
*/
//子集樹的結構
class BBnode {
BBnode parent; //記錄子集樹的父節點
boolean lChild; // 記錄是左兒子還是右兒子
public BBnode() {
}
public BBnode(BBnode parent, boolean lChild) {
super();
this.parent = parent;
this.lChild = lChild;
}
}
//對揹包按照單位重量價值排序的類
class UnitW implements Comparable<UnitW>{
private int id;
private double d;
public int getId() {
return id;
}
public UnitW(int id, double d) {
super();
this.id = id;
this.d = d;
}
@Override
public int compareTo(UnitW o) { //按照d降序排列
return -(d > o.d ? 1 :( d == o.d ? 0 : -1 ));
}
}
class HeapNode implements Comparable<HeapNode>{
BBnode liveNode; //活結點
int upProfit; //活結點的價值上界
int profit; //結點所相應的價值
int weight; //結點所相應的重量
int level; //結點在子集樹中的層數
public HeapNode(BBnode liveNode, int upProfit, int profit, int weight, int level) {
super();
this.liveNode = liveNode;
this.upProfit = upProfit;
this.profit = profit;
this.weight = weight;
this.level = level;
}
@Override
public int compareTo(HeapNode o) { //按照上界價值降序排列
return -(upProfit > o.upProfit ? 1 : (upProfit == o.upProfit ? 0: -1));
}
}
public class BBknapsack {
private int C;
private int n;
private int[] w;
private int[] p;
private int cw; //當前的重量
private int cp; //當前的價值
private int[] bestx; //記錄最優解
private LinkedList<HeapNode>heap; //使用這個,方便取頭元素和刪除頭元素
//建構函式
public BBknapsack(int C, int n, int[] w, int[] p, int cw, int cp, int[] bestx,
LinkedList<HeapNode> heap) {
super();
this.C = C;
this.n = n;
this.w = w;
this.p = p;
this.cw = cw;
this.cp = cp;
this.bestx = bestx;
this.heap = heap;
}
//計算最優上界
public int bound(int i){
int cLeft = C - cw;
int b = cp; //價值的上界
while(i < n && w[i] <= cLeft ) { //以物品單位重量價值遞減的順序裝填剩餘容量
cLeft -= w[i];
b += p[i];
i++;
}
if(i < n) b += p[i]*1.0/w[i]*cLeft;
return b;
}
//生成一個活結點插入到子集樹和最大堆中
public void addLiveNode(int up,int p,int w,int lev,BBnode par,boolean iCh){
BBnode b = new BBnode(par,iCh);
HeapNode hN = new HeapNode(b, up, p, w, lev); //生成一個堆元素
heap.add(hN); //加入到堆中
Collections.sort(heap); //調整一下堆(按照最大上限價值)
}
//分支限界法求解
public int MaxKnapsack(){
int i = 0;
BBnode endNode = null;
int bestP = 0;
int upProfit = bound(0); //計算一開始的上界
while( i != n ){
if(cw + w[i] <= C){ //進入左子樹
if(cp + p[i] > bestP){
bestP = cp + p[i];
}
addLiveNode(upProfit, cp+p[i], cw + w[i], i+1, endNode, true); //左子樹插入到最大堆中
}
upProfit = bound(i); //計算這一層的上界
if(upProfit >= bestP) { //右子樹可能含有最優解
addLiveNode(upProfit, cp, cw, i+1, endNode, false);
}
HeapNode top = heap.poll(); //把堆頂元素刪掉
endNode = top.liveNode; //記錄父親結點
cw = top.weight;
cp = top.profit;
upProfit = top.upProfit;
i = top.level;
}
//構造最優解
for(int j = n-1; j >= 0; j--){
bestx[j] = (endNode.lChild == true) ? 1 : 0;
endNode = endNode.parent;
}
return cp;
}
public static void main(String[] args) {
Scanner cin = new Scanner(new BufferedInputStream(System.in));
int W = 0, V = 0, n, C;
n = cin.nextInt();
C = cin.nextInt();
int[] w = new int[n + 1];
int[] p = new int[n + 1];
ArrayList<UnitW> unit = new ArrayList<UnitW>();
for (int i = 0; i < n; i++)w[i] = cin.nextInt();
for (int i = 0; i < n; i++)p[i] = cin.nextInt();
for (int i = 0; i < n; i++) {
unit.add(new UnitW(i, p[i] * 1.0 / w[i]));
W += w[i];
V += p[i];
}
if (W <= C) {
System.out.println(V);
System.exit(0);
}
Collections.sort(unit); // 按照單位重量進行降序排序
int[] neww = new int[n + 1];
int[] newp = new int[n + 1];
for (int i = 0; i < n; i++) {
neww[i] = w[unit.get(i).getId()];
newp[i] = p[unit.get(i).getId()];
}
int[] bestx = new int[n+1];
int[] bestxx = new int[n+1];
LinkedList<HeapNode> heap = new LinkedList<HeapNode>();
BBknapsack K = new BBknapsack(C, n, neww, newp, 0, 0, bestx, heap);
int maxP = K.MaxKnapsack(); // 從第0層開始呼叫
System.out.println("---揹包最大價值-----");
System.out.println(maxP);
for(int i = 0; i < n; i++){
bestxx[unit.get(i).getId()] = bestx[i]; //獲取最優解
}
System.out.println("---被選中物品的序號(從1開始)----");
for(int i = 0; i < n ; i++)if(bestxx[i] == 1)System.out.print(i+1 + " " );
System.out.println();
}
}
測試資料
最大團
最大團問題的解空間也是一顆子集樹,結點的解空間樹的結點型別是BNode,活結點優先佇列中元素型別是CliqueHeapNode,cn是表示的該節點相應的團的頂點數,upn表示該結點為根的子樹中最大頂點數的上界,level表示結點在子集空間樹種所處的層次,lChild是左右兒子的標記 ,liveNode表示的是在子集樹中的結點。
這裡注意更新最優解以及右子樹的剪枝:cn + n-i >= bestn。
import java.io.BufferedInputStream;
import java.util.Collections;
import java.util.LinkedList;
import java.util.Scanner;
/**
* 用分支限界法解決最大團問題
* @author 鄭鑫
*/
//子集樹的類
class BNode {
BNode parent; //記錄子集樹的父節點
boolean lChild; // 記錄是左兒子還是右兒子
public BNode() {
}
public BNode(BNode parent, boolean lChild) {
super();
this.parent = parent;
this.lChild = lChild;
}
}
//優先佇列的元素 -->按照
class CliqueHeapNode implements Comparable<CliqueHeapNode>{
BNode liveNode; //在子集樹中的位置
public int upn;
public int cn;
public int level;
public CliqueHeapNode(BNode liveNode, int upn, int cn, int level) {
super();
this.liveNode = liveNode;
this.upn = upn;
this.cn = cn;
this.level = level;
}
@Override
public int compareTo(CliqueHeapNode o) { //按照upn降序排列
return -(upn > o.upn ? 1 : (upn == o.upn ? 0 : -1));
}
}
public class BBMaxClique {
private int[][] map;
private int n; //圖的頂點個數
private LinkedList<CliqueHeapNode>heap;
private int[] bestx;
public BBMaxClique(int[][] map, int n, LinkedList<CliqueHeapNode> heap, int[] bestx) {
super();
this.map = map;
this.n = n;
this.heap = heap;
this.bestx = bestx;
}
public void AddLiveNode(int upn,int cn,int lev,BNode par,boolean iCh){
BNode b = new BNode(par,iCh);
CliqueHeapNode CH = new CliqueHeapNode(b, upn, cn, lev);
heap.add(CH);
Collections.sort(heap);
}
public int MaxClique(){
int i = 0;
BNode E = null;
int bestn = 0,cn = 0;
while( i != n){
boolean flag = true;
BNode B = E;
for(int j = i-1; j >= 0; B = B.parent, j--){ //注意起點是0
if(B.lChild == true && map[i][j] == 0){
flag = false;
break;
}
}
if(flag){
if(cn + 1 > bestn)bestn = cn+1;
AddLiveNode(cn + n-i+1,cn+1,i+1,E,true);//注意這裡的上界是cn+n-i+1
}
if(cn + n-i >= bestn){
AddLiveNode(cn + n-i,cn,i+1,E,false);
}
CliqueHeapNode top = heap.poll();
E = top.liveNode;
cn = top.cn;
i = top.level;
}
for(int j = n-1; j >= 0; j--){
bestx[j] = (E.lChild == true) ? 1 : 0;
E = E.parent;
}
return bestn;
}
public static void main(String[] args) {
Scanner cin = new Scanner(new BufferedInputStream(System.in));
int n,m; //頂點數,邊數
n = cin.nextInt(); //頂點的序號是0~n-1
m = cin.nextInt();
int[] bestx = new int[n+1] ;// 記錄每一個頂點
for(int i = 0; i < n; i++) bestx[i] = 0; //一開始都不在團裡面
int[][] map = new int[n+1][n+1];
for(int i = 0; i < n; i++)for(int j = 0; j < n; j++)map[i][j] = 0;
for(int i = 0; i < m; i++){
int a = cin.nextInt();
int b = cin.nextInt();
map[a-1][b-1] = map[b-1][a-1] = 1;
}
LinkedList<CliqueHeapNode>heap = new LinkedList<CliqueHeapNode>();
BBMaxClique mC = new BBMaxClique(map, n, heap, bestx);
System.out.println("-----最大團中點的個數-----");
System.out.println(mC.MaxClique());
for(int i = 0; i < n; i++)System.out.print(bestx[i]+ " ");
System.out.println();
System.out.println("---最大團中的點(序號從1開始)---");
for(int i = 0; i < n; i++)if(bestx[i] == 1)System.out.print(i+1 + " ");
System.out.println();
cin.close();
}
}
單源最短路徑
活結點佇列中使用極小堆儲存,優先順序是結點所對應的當前路徑長。取出堆中的最小結點後,如果當前結點i到結點j有邊可達,且從源點出發,途經頂點i再到頂點j的所相應的路徑的長度小於當前最優路徑長度,則將該結點加入到佇列中。
然後就是利用控制關係進行剪枝,如果在解空間樹種兩條路徑到達圖中的一個頂點,而解空間樹種A點所經過的長度小於B,則把B以及它的子樹剪去。
import java.io.BufferedInputStream;
import java.util.Collections;
import java.util.LinkedList;
import java.util.Scanner;
/**
* 分支限界法解決單源最短路徑
* @author 鄭鑫
*/
public class BBShortestPath {
//最小堆中的元素類 id表示該活結點所表示的圖中的相應的頂點號,length表示源點到該點的距離
private static class MinHeapNode implements Comparable<MinHeapNode>{
private int id;
private int length;
public MinHeapNode(int id, int length) {
super();
this.id = id;
this.length = length;
}
@Override
public int compareTo(MinHeapNode o) { //升序排列
return length > o.length ? 1: (length == o.length ? 0 : -1);
}
}
private static final int INF = 10000000;
private int n;
private int[][] map;
private int[] dist,pre; //記錄最短距離的陣列,以及儲存前驅頂點的陣列
public BBShortestPath(int n, int[][] map, int[] dist, int[] pre) {
super();
this.n = n;
this.map = map;
this.dist = dist;
this.pre = pre;
}
public void ShortestPath(int s){
LinkedList<MinHeapNode>heap = new LinkedList<MinHeapNode>();
MinHeapNode now = new MinHeapNode(s, 0);
for(int i = 1; i <= n; i++)dist[i] = INF;
dist[s] = 0;
while(true){
for(int j = 1; j <= n; j++){
if(map[now.id][j] != -1 && now.length + map[now.id][j] < dist[j]){
dist[j] = now.length + map[now.id][j];
pre[j] = now.id;
MinHeapNode next = new MinHeapNode(j, dist[j]) ;//加入活結點佇列中
heap.add(next);
Collections.sort(heap);
}
}
if(heap.isEmpty())break;
else now = heap.poll();
}
}
public static void main(String[] args) {
Scanner cin = new Scanner(new BufferedInputStream(System.in));
int n = cin.nextInt();
int[] dist = new int[n+1];
int[] pre = new int[n+1];
int[][] map = new int[n+1][n+1];
for(int i = 1; i <= n; i++){
for(int j = 1; j <= n; j++)map[i][j] = cin.nextInt();
}
BBShortestPath bbp = new BBShortestPath(n, map, dist, pre);
bbp.ShortestPath(1);
for(int i = 2; i <= n; i++){
System.out.println("源點到" + i + "結點的最短距離是 " + dist[i]);
System.out.println("這個結點的前驅結點是" + pre[i]);
}
}
}
執行樣例(dijsktra中的)
執行效果
裝載問題
使用優先佇列式分支限界法解決裝載問題,優先佇列的優先順序是從根節點到當前結點的相應載重量加上剩餘集裝箱的重量之和,,進入左孩子之前,保證輪船的能裝下當前的物品,進入右子樹不需要檢查。
import java.io.BufferedInputStream;
import java.util.Collections;
import java.util.LinkedList;
import java.util.Scanner;
/**
* 分支限界法結局解決最優裝載問題
* @author 鄭鑫
*/
class BMNode{
BMNode parent;
boolean iChild;
public BMNode() {
}
public BMNode(BMNode parent, boolean iChild) {
super();
this.parent = parent;
this.iChild = iChild;
}
}
class BMaxHeapNode implements Comparable<BMaxHeapNode>{
BMNode liveNode;
int upw;
int level;
public BMaxHeapNode(BMNode liveNode, int upw, int level) {
super();
this.liveNode = liveNode;
this.upw = upw;
this.level = level;
}
@Override
public int compareTo(BMaxHeapNode o) { //降序排列
return -(upw > o.upw ? 1 : ( upw == o.upw ? 0 : -1 ));
}
}
public class BBMaxLoading {
private int n,C;
private int[] w,bestx,r;
private LinkedList<BMaxHeapNode>heap;
public BBMaxLoading(int n, int c, int[] w, int[] bestx, int[] r, LinkedList<BMaxHeapNode> heap) {
super();
this.n = n;
C = c;
this.w = w;
this.bestx = bestx;
this.r = r;
this.heap = heap;
}
public void addLiveNode(int upw,int lev,BMNode par,boolean iCh){
BMNode bn = new BMNode(par,iCh);
BMaxHeapNode BH = new BMaxHeapNode(bn, upw, lev);
heap.add(BH);
Collections.sort(heap);
}
public int maxLoading(){ //返回最優值
int i = 1;
int cw = 0;
BMNode E = null;
while(i != n+1){
if(cw + w[i] <= C){
addLiveNode(cw+w[i]+r[i],i+1,E,true); //更新上界
}
addLiveNode(cw+r[i], i+1, E, false);
BMaxHeapNode top = heap.poll();
i = top.level;
E = top.liveNode;
cw = top.upw - r[i-1];
}
for(int j = n; j >= 1; j--){
bestx[j] = (E.iChild == true) ? 1 : 0;
E = E.parent;
}
return cw;
}
public static void main(String[] args) {
Scanner cin = new Scanner(new BufferedInputStream(System.in));
int n = cin.nextInt();
int C = cin.nextInt();
int[] w = new int[n+1];
int[] r = new int[n+1];
int[] bestx = new int[n+1];
for(int i = 1; i <= n; i++)w[i] = cin.nextInt();
for(int i = n-1; i >= 1; i--)r[i] = r[i+1] + w[i]; //定義剩餘重量陣列r
LinkedList<BMaxHeapNode>heap = new LinkedList<BMaxHeapNode>();
BBMaxLoading bbL = new BBMaxLoading(n, C, w, bestx, r, heap);
System.out.println(bbL.maxLoading());
for(int i = 1; i <= n; i++)System.out.print(bestx[i] + " ");
System.out.println("\n" + "---裝入輪船的集裝箱---");
for(int i = 1; i <= n; i++)if(bestx[i] == 1) System.out.print(i + " ");
System.out.println();
}
}
佈線問題
這個問題和BFS尋找最短路沒有很大的區別,注意一下四周填1的操作和記錄路徑的技巧就行。
import java.io.BufferedInputStream;
import java.util.LinkedList;
import java.util.Scanner;
/**
* 利用分支限界法解決佈線問題 -->其實就是BFS加路徑記錄
* @author 鄭鑫
*/
public class BBWireRouter {
private static class Point{
private int row;
private int col;
public Point(int row, int col) {
super();
this.row = row;
this.col = col;
}
}
private static final int[] dx = {0,1,0,-1};
private static final int[] dy = {1,0,-1,0};
private static int[][] map;//地圖
private static int n,m,pathLen;
private static Point start,end;
private static LinkedList<Point> queue;
private static Point[] path; //記錄路徑
public static boolean FindPath(){
if(start.row == end.row && start.col == end.col ){
pathLen = 0;
return true;
}
map[start.row][start.col] = 2;
queue = new LinkedList<Point>();
queue.add(start);
Point now = new Point(start.row,start.col);
Point next = new Point(0,0);
boolean flag = false;
while(!queue.isEmpty()){
now = queue.poll();
if(now.row == end.row && now.col == end.col){
flag = true;
break;
}
for(int i = 0; i < 4; i++){
next = new Point(now.row + dx[i], now.col + dy[i]);
if(map[next.row][next.col] == 0){
map[next.row][next.col] = map[now.row][now.col] + 1;
queue.add(next);
}
}
}
if(!flag)return false;
pathLen = map[end.row][end.col] - 2;
path = new Point[pathLen];
now = end;
for(int j = pathLen-1; j >= 0; j--){
path[j] = now;
for(int i = 0; i < 4; i++){
next = new Point(now.row + dx[i],now.col + dy[i]);
if(map[next.row][next.col] == j+2)break;
}
now = next;
}
return true;
}
public static void main(String[] args) {
Scanner cin = new Scanner(new BufferedInputStream(System.in));
n = cin.nextInt(); m = cin.nextInt();
map = new int[n+2][m+2];
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
map[i][j] = cin.nextInt();
}
}
for(int i = 0; i <= m+1; i++)map[0][i] = map[n+1][i] = 1;
for(int i = 0; i <= n+1; i++)map[i][0] = map[i][m+1] = 1;
start = new Point(0, 0);
end = new Point(0, 0);
start.row = cin.nextInt(); start.col = cin.nextInt();
end.row = cin.nextInt(); end.col = cin.nextInt();
FindPath();
System.out.println(pathLen);
for(int i = 0; i < pathLen-1; i++){
System.out.println("結點"+ (i+1) +"的位置" + (path[i].row) + "," + (path[i].col));
}
}
}
效果