目录
一 回溯法概述
回溯法的思路主要可以概述为以下2点:
(1)把问题的解空间转化成了图或者树的结构
(2)使用深度优先搜索策略进行遍历,遍历的过程中记录和寻找所有可行解或者最优解,并在搜索过程中用剪枝函数避免无效搜索。
二 深度优先搜索策略(DFS)
深度优先遍历一般只在图和树中涉及,当然,树是图的一种特殊结构。DFS简要来说就是对每一个可能的分支路径深入到不能再深入为止,且不能重复遍历,即每个节点只能遍历一次。
下边举两个例子 说明一下DFS,帮助理解。
1 树中的深度优先遍历
如图所示:是一个二叉树,其深度遍历的顺序为:ABD E CF G
遍历路径如图中棕色箭头所示。具体遍历步骤如下:
(1)先从A点出发,既可以选择遍历B也可以选择遍历C,图中选择B;
(2)从B点出发,既可以选择遍历D也可以选择遍历E,图中选择D;
(3)到达D点后,没有可以继续深入的节点,则向上回溯,到达B点,选择B点还没有遍历的其他节点,图中选择E;
(4)到达E点后,没有可以继续深入的节点,则向上回溯到达B,仍没有可以继续深入的节点,向上回溯到A,选择A还没有遍历的其他节点,图中选择C;
(5)到达C点后,既可以选择遍历F也可以选择遍历G,图中选择F;
(6) 到达F点后,没有可以继续深入的节点,则向上回溯到C,选择C点还没有遍历的其他节点,图中选择G;
(7) 达到G点后,没有可以继续深入的节点,则向上回溯到C,仍没有可以继续深入的节点,则向上回溯到A,此时A没有还未遍历的节点,遍历到此结束。
2 图中的深度优先遍历
如图所示,是一个无向图。其深度遍历顺序为:ABDFECGH
遍历路径如图中棕色箭头所示。具体遍历步骤如下:
(1)先从A点出发,既可以选择遍历B、C也可以选择遍历H,图中选择B;
(2)从B点出发,既可以选择遍历D也可以选择遍历E,图中选择D;
(3)到达D点后,向下继续深入,图中选择F;
(4) 到达F点后,向下继续深入,图中选择E;
(5)到达E点后,没有可以继续深入的节点,则向上回溯,到达F点,仍没有可以继续深入的节点,向上回溯到D,到达D点,仍没有可以继续深入的节点,向上回溯到B,到达B点,仍没有可以继续深入的节点,向上回溯到A,选择A点还没有遍历的其他节点,图中选择C;
(6)到达C点后,向下继续深入,图中选择G;
(7)到达G点后,没有可以继续深入的节点,则向上回溯到C;
(8) 到达C点后,没有可以继续深入的节点,则向上回溯到A,选择A点还没有遍历的其他节点,图中选择H;
(9) 达到H点后,没有可以继续深入的节点,则向上回溯到A,此时A没有还未遍历的节点,遍历到此结束。
三 剪枝函数
剪枝函数,顾名思义就是,剪去无用的枝条,即在进行遍历时,提前减去不必要的分支,避免无效遍历。其根本目的,是要减少搜索的次数,使程序运行的时间减少,提高算法效率。
应用剪枝优化的核心问题是设计剪枝判断方法,即确定哪些枝条应当舍弃,哪些枝条应当保留的方法。
剪枝算法按照其判断思路可大致分成两类:可行性剪枝及最优性剪枝。
可行性剪枝: 该方法判断继续搜索能否得出答案,如果不能直接回溯。
最优性剪枝:它记录当前得到的最优值,如果当前结点已经无法产生比当前更优的解时,可以提前回溯。
四 0-1背包问题
1 问题描述:
有N件物品和一个容量为C的背包。第i件物品的价值是P[i],重量是W[i]。求解将哪些物品装入背包可使价值总和最大。所谓0-1背包,表示每一个物品不能拆解,要么装入,要么不装入。
2 问题求解:
0-1背包问题是求最优解的问题。在考虑将物品放进背包的原则是:单位价值(P[i]/W[i])越大的,尽量提前放进背包中(即将性价比更高的物品更早装进背包);但是限制条件是,所装物品必须是完整的且重量不能超过背包承重能力。
问题转化:
按照上述思路,先将各物品按照单位价值递减的顺序排序,其次进行判断是否在承重范围值内。
定义:CW(current weight)表示当前重量,CP(current price)表示当前价值,
根节点代表扩展结点 ,其余每一层代表一个物品,越靠近根节点,单位价值越高。选中该物品,即搜索左子树,进行判断。具体执行操作如下所示:
(1)先计算所有物品的单位价值,将其进行降序排列
(2)排列之后,从根节点(扩展节点)出发;
(3)搜索左子树,判断是否满足约束条件(物品装入背包?):
若选中该物品(可行解),CW+=W[i],CP+=P[i],继续向下遍历;
直至遇到不可行解时,开始向上回溯,取出最后一个装入的物品,进入右子树:
(4)进入右子树,首先计算当前节点的上界bound(i),
若bound(i)小于bestp,剪去右子树,继续向上回溯;
否则进行步骤(3);
(5)遇到叶子节点,比较当前价值与bestp,若CP>bestp,则bestp进行更新;
(6)直到遍历完所有的节点(除剪枝部分外)。
3 举例说明:
已知:p[]={45,25,24}; w[]={16,15,15};背包容量为30,求最优价值?
(1)先将这四件物品的单位价值算出来,按照单位价值降序排列。刚好顺序和原始顺序一致。
(2)开始进行遍历并判断是否装入,
具体步骤如下:(B1,B2,都指的是物品B,C1,C2,C3,C4都指的是物品C ,依此类推 主要是为了区分结点 便于叙述)
首先 物品B1 装入符合条件(最大重量不超过背包容量30的要求),遍历其左子树,此时CW = 16,CP = 45;
到达B1点后,遍历其左子树,发现剩余容量r = 14,C1无法装入背包,剪去B1-C1这条枝;
进入右子树,到达C2后,计算bound(i) = 68.3>45,继续向下遍历。遍历其左子树,发现剩余容量r = 14,D3无法装入背包,剪去C2-D3这条枝;
进入右子树,到达C2后,遇到叶子节点,此时bestp = 45。记录该值。
向上回溯,取出最后一个装入的物品B1,到达A,进入右子树,到达B2;计算bound(i) = 49>45,继续向下遍历;此时CW = 0,CVP= 0;
遍历左子树,到达C3,此时CW = 15,CP= 25;剩余容量r =15。
继续向下遍历 ,到达D5,此时D5可以装入包中,CW = 30,CP = 49。此时CP>bestp = 45,进行更新bestp; bestp = 49。记录bestp和当前包中物品。
由于还有其他结点没有遍历完成,继续进行回溯。
取出最后一个装入的物品D5,到达C3,进入右子树,到达D6,此时CW = 15,CP= 25;到达了叶子节点 判断是否需要更新bestp。由于CP= 25<bestp,不用更新。
继续向上回溯,去除最后一个装入的物品C3,到达B2,向右进行遍历,进入右子树,到达C4,计算bound(i) = 48<bestp,不用向下遍历,进行剪枝。
此时所有结点已经完全遍历,最有价值为49,装入CD即可。
遍历过程如图所示:(图中68.3应该位67.4,bound = 45+14*(24/15)=67.4)
4 算法实现
import java.util.Arrays;
import java.lang.annotation.ElementType;
/**
* Author: PuTongFish
*/
public class Knapsack {
private static class Element implements Comparable {
int id;//物品编号
double d;
private Element(int idd, double dd) {
id = idd;
d = dd;
}
@Override
public int compareTo(Object o) {
double xd = ((Element)o).d;
if (d < xd) return -1;
if (d == xd) return 0;
return 1;
}
public boolean equals(Object x) {
return d == ((Element) x).d;
}
}
static double c;
static int n;
static double[] p;
static double[] w;
static double cw;
static double cp;
static double bestp;
public static double Knapsack(double[] pp,double[] ww,double cc) {
c = cc;
n = pp.length;
cw = 0.0;
cp = 0.0;
bestp = 0.0;
Element[] q = new Element[n];
for (int i = 1; i <= n; i++) {
q[i - 1] = new Element(i, pp[i] / ww[i]);
}
//排序
//MergeSort.mergeSort(q);
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n - i; j++) {
if (q[j - 1].d < q[j].d) {
Element temp = q[j - 1];
q[j - 1] = q[j];
q[j] = temp;
}
}
}
p = new double[n + 1];
w = new double[n + 1];
for (int i = 1; i <= n; i++) {
p[i] = pp[q[n - i].id];
w[i] = ww[q[n - i].id];
}
Backtrack(1);
return bestp;
}
private static void Backtrack(int i){
if (i > n) {
//到达叶结点
bestp = cp;
return;
}
if (cw + w[i] <= c) { //进入左子树
cw += w[i];
cp += p[i];
Backtrack(i + 1);
cw -= w[i];
cp -= p[i];
}
// float u=Bound(i + 1);
// float bb=bestp;
//当前的界是否大于背包当前的值
if (Bound(i + 1) > bestp) { //进入右子树
Backtrack(i + 1);
}
}
private static double Bound(int i){
//计算上界
double cleft = c - cw; //剩余容量
double bound = cp;
//以物品单位重量价值递减序装入物品
while (i <= n && w[i] <= cleft) {
cleft -= w[i];
bound += p[i];
i++;
}
//装满背包
if (i <= n){
double aa = p[i] * cleft;
double bb = w[i];
double temp = aa / bb;
//注意:如果这样写:float temp=p[i] * cleft/w[i];
//则temp计算出来是整数,因为右边是先按整数来算,再将int转float;
bound += temp;
// System.out.println(i);
//System.out.println("界");
//System.out.println(bound);
}
return bound;
}
public static void main(String[] args) {
double p[]={45,25,24};
double w[]={16,15,15};
double c = Knapsack(p,w,30);
System.out.println("0-1背包问题的最优值为:"+c);
}
}