分支限界法和之前講的回溯法有一點相似,兩者都是在問題的解的空間上搜索問題的解。但是兩者還是有一些區別的,回溯法是求解在解的空間中的滿足的所有解,分支限界法則是求解一個最大解或最小解。這樣,兩者在解這一方面還是有一些不同的。之前回溯法講了N后問題,這個問題也是對於這有多個解,但是今天講的01背包問題是只有一個解的。下面就講講分支限界法的基本思想。
分支限界法常以廣度優先或以最小消耗(最大效益)優先的方式搜索問題的解空間樹。問題的解空間樹是表示問題解空間的一顆有序樹,常見的有子集樹和排列樹。分支限界法和回溯法的區別還有一點,它們對於當前擴展結點所采用的擴展方式也是不相同的。分支限界法中,對於每一個活結點只有一次機會成為擴展結點。活結點一旦成為了擴展結點,就一次性產生其所有的子結點,子結點中,不符合要求的和非最優解的子結點將會被舍棄,剩下的子結點將加入到活結點表中。再重復上面的過程,直到沒有活結點表中沒有結點,至此完成解決問題的目的。
分支限界法大致的思想就是上面的敘述,現在就可以發現,對於結點的擴展將會成為分支限界法的主要核心。所以,分支限界法常見的有兩種擴展結點的方式,1.隊列式(FIFO)分支限界法,2.優先隊列式分支限界法。兩種方法的區別就是對於活結點表中的取出結點的方式不同,第一種方法是先進先出的方式,第二種是按優先級取出結點的方式。兩中方法的區別下面也會提到。
在背包問題中還會提到一個子樹上界的概念,其實就是回溯法中的剪枝函數,只不過,分支限界法里的剪枝函數改進了一些,剪枝函數同樣也是分支限界法里比較重要的東西。
下面就講一講01背包問題的實現。01背包問題和前面講的背包問題的區別不大,就是01背包問題的物品不可以只放入部分,01背包問題的物品只能放入和不放入兩個選擇,這也是名字中01的原因。其他的和背包問題相差不大,這里也不再累述。
算法的主體是比較容易想的,首先,將數據進行處理,這也是上面講到的第二種取結點的方式(優先隊列式)。因為要給每個物品設置優先級,這里將價值作為優先級顯然不夠好,就想到了將價值與重量的比值(權重值)作為優先級。要寫一個排序算法,將物品數組中物品按權重值排序。下面就要想一下子樹上界函數了,這里上界的函數借鑒了一下背包問題的結局方案,因為子樹的最大值一定小於非01背包問題的最優解,所以用到了之前背包問題的代碼,將代碼進行了處理,需要傳入參數,也要有返回值,這里就不再重復敘述了。
下面就是代碼的主體,這一部分我想了大概兩個星期,一開始的思路出現了問題,總是想着使用數組來實現算法的主體,同時在使用遞歸來循環遍歷數組,后來發現,這樣做就和回溯法一模一樣,后來借鑒了一下網上的代碼,將自己的主體代碼改了改,多寫了一個類(結點類),剩下的就比較好實現了,將初始結點添加到結點表中,找到左節點和右節點,同時利用函數判斷結點是否符合要求,在將左節點和右節點添加到結點表中,在循環遍歷結點表,知道結點表中沒有活結點,一直重復這個步驟。在左節點的判斷中,同時還要判斷左節點的值是不是大於最優解,算法的大部分都是在判斷。算法主體就結束了。
再來講一講算法的構建最優解,這個我也是想不出來,老師最后提醒了我,先是將結點添加到另一個節點表中,在問題的結束后,遍歷找到最優解的父節點,判斷父節點是不是左節點,在重復這個步驟,直到沒有父節點。完成后將左節點的索引標記為放入背包中,這樣就完成了最優解的構建。剩下的問題就是一些細節問題了。
代碼如下:
package sf;
import java.util.LinkedList;
import java.util.Scanner;
/*
* 01背包問題,分支限界法
*/
public class demo8 {
/*
* 主方法
*/
public static void main(String[] args) {
//輸入數據
System.out.println("請輸入背包的容量w和物品的個數n");
Scanner reader = new Scanner(System.in);
int w = reader.nextInt();// 背包的容量
int n = reader.nextInt();// 物品的個數
int solution=-1;
BLBag[] p = new BLBag[n];
System.out.println("請依次輸入各個物品的重量w和價值v和名稱s");
int weigth;
int value;
String pid;
for (int i = 0; i < n; i++) {
pid = reader.next();
weigth = reader.nextInt();
value = reader.nextInt();
p[i] = new BLBag(pid, weigth, value);
}
// 輸入數據結束
/*
* 數據
* 001 16 45 002 15 25 003 15 25
*/
// 算法開始
//聲明狀態數組並初始化為空
Integer[] a=new Integer[n];
for(int i=0;i<n;i++) a[i]=null;
//對p數組按權重排序
sort(p);
//打印結果
int haha=branchandlimit(p, w, a, solution);
System.out.println("最優解為:"+haha);
}
/*
* 權重排序,選擇排序
*/
public static void sort(BLBag[] p) {
BLBag t;
for (int i = 0; i < p.length; i++) {
int max = i;
t = p[i];
for (int j = i; j < p.length; j++) {
if (t.wi < p[j].wi) {
t = p[j];
max = j;
}
}
t = p[i];
p[i] = p[max];
p[max] = t;
}
}
/*
* 求上界的函數 數組p 當前位置 當前背包重量 返回是最大價值(不包含背包的已有價值)
*/
public static double findbound(BLBag[] p,int i,int weight)
{
double value = 0;
//將狀態位后面的物品求貪心算法的解,上界函數的解為返回值+當前背包價值
forLOOP:for(int k=i;k<p.length;k++)//循環名字
{
//貪心算法求解問題(修改版)
if(p[k].weight<weight){
value=value+p[k].value;
weight=weight-p[k].weight;
}else{
double a=weight*p[k].wi;//當前價值
value=value+a;
weight=0;
break forLOOP;//跳出循環
}
}
return value;
}
/*
* 分支限界法主體 參數分別為物品數組p,重量,價值,狀態數組,當前考慮位置i ,最優解
*/
public static int branchandlimit(BLBag[] p,int weight,Integer[] a,double solution)
{
//聲明隊列
LinkedList<Node> nodelist=new LinkedList<Node>();
LinkedList<Node> nodesolution=new LinkedList<Node>();
nodelist.add(new Node(0, 0, 0));
nodesolution.add(new Node(0,0,0));
while(!nodelist.isEmpty())
{
//取出元素
Node node = nodelist.pop();
//判斷條件,節點的不放入的最大值大於當前最優解,節點小於數組的長度
//這里不用等於,必須要大於
if(node.getUnbounvalue()+node.getCurrvalue()>solution && node.getIndex()<p.length)
{
//左節點
int leftWeight=node.getCurrweight()+p[node.getIndex()].weight;
int leftvalue=node.getCurrvalue()+p[node.getIndex()].value;
Node left=new Node(leftWeight, leftvalue, node.getIndex()+1);
//設置左節點的父節點
left.setFather(node);
left.setIsleft(true);
//將左節點添加到最優解隊列中
nodesolution.add(left);
//設置左節點的上界價值
left.setUnbounvalue((int)findbound(p, node.getIndex(), weight-node.getCurrweight()));
//左節點的重量小於等於背包的承重,且左節點的上界價值大於最優解
if(left.getCurrweight()<=weight && left.getUnbounvalue()+left.getCurrvalue()>solution)
{
//將節點加入隊列中
nodelist.add(left);
a[node.getIndex()]=1;
//將最優值重新賦值 條件就是節點的當前價值大於問題的最優解
if(left.getCurrvalue()>solution)
{
solution=left.getCurrvalue();
//System.out.println("放入的物品有:"+p[node.getIndex()].pid);
}
}
//右節點 右節點的設置不需要太多,和父節點差不多
Node right=new Node(node.getCurrweight(), node.getCurrvalue(), node.getIndex()+1);
//將右節點添加到最優解隊列中
right.setFather(node);
right.setIsleft(false);
nodesolution.add(right);
right.setUnbounvalue((int)findbound(p,node.getIndex(),weight-node.getCurrweight()));
//右節點的上界價值大於當前最優解
if(right.getUnbounvalue()+node.getCurrvalue()>solution)
{
//添加右節點
nodelist.add(right);
a[node.getIndex()]=0;
}
}
}
/*
* 調用最優解方法
*/
pr(nodesolution,(int)solution,p);
//返回最優解
return (int) solution;
}
/**
*
* @Description: 求解最優解的方法
* @param @param nodesolution
* @return void
* @throws
* @author yanyu
* @date 2018年5月21日
*/
//參數為
public static void pr(LinkedList<Node> nodesolution,int solution,BLBag[] p)
{
int[] a=new int[p.length];
Node prnode=null;
//從list中循環遍歷最優解的節點
for(Node node:nodesolution)
{
if(node.getCurrvalue()==solution){
//System.out.println("最優解的父節點的索引為:"+node.getFather().getIndex());
prnode=node;
}
}
//循環遍歷最優節點的父節點,判斷其是否為左節點
while (prnode.getFather()!=null)
{
if(prnode.isIsleft())
{
a[prnode.getIndex()-1]=1;
}
prnode=prnode.getFather();
}
//打印
for(int i=0;i<p.length;i++)
{
if(a[i]==1) System.out.println("放入了物品:"+p[i].pid);
}
}
}
/*
* 背包類
*/
class BLBag {
public int weight;// 重量
public int value;// 價值
public double wi;// 權重
public String pid;// 背包名稱
public BLBag(String pid, int weight, int value) {
this.weight = weight;
this.value = value;
this.pid = pid;
this.wi = (double) value / weight;
}
}
/**
*
* ClassName: Node
* @Description: 節點類
* @author yanyu
* @date 2018年5月17日
*/
class Node
{
//當前物品的屬性
private int currweight;//當前重量
private int currvalue;//當前價值
private int unbounvalue;//上界價值
private int index;//索引
private Node father;//父節點
private boolean isleft;//是否為左節點
public boolean isIsleft() {
return isleft;
}
public void setIsleft(boolean isleft) {
this.isleft = isleft;
}
public Node getFather() {
return father;
}
public void setFather(Node father) {
this.father = father;
}
public int getCurrweight() {
return currweight;
}
public void setCurrweight(int currweight) {
this.currweight = currweight;
}
public int getCurrvalue() {
return currvalue;
}
public void setCurrvalue(int currvalue) {
this.currvalue = currvalue;
}
public int getUnbounvalue() {
return unbounvalue;
}
public void setUnbounvalue(int unbounvalue) {
this.unbounvalue = unbounvalue;
}
public int getIndex() {
return index;
}
public void setIndex(int index) {
this.index = index;
}
//構造函數
public Node(int currweight,int currvalue,int index)
{
this.currweight=currweight;
this.currvalue=currvalue;
this.index=index;
}
}
上面就是代碼,下面說一說具體的細節,隊列中添加結點的方式是先進先出,這里將左節點放到前面就是將優先的結點先出,其中每個判斷的判斷條件都是要注意的點,還有就是結點的索引要注意和物品數組中下標相差一,這個要注意。要不然構建最優解的時候會越界,其他的一些細節都寫在注釋了里。不再累述。
下面講一講自己的一些想法,對於自己一開始想用數組實現的想法,我倒現在還是認為是對的,因為看到現在,算法是實現了,但是估計效率不是很高,我估計沒有數組實現的方式效率高,這里因為能力有限,只能放棄數組的實現方式。也是算法下一步進步的方式吧。
結束。
