一、回溯法的思路:
(1)回溯法,简单来讲就是一个走不通过就退回的过程。是穷举法的一种表现形式,有着通用解题法的美称。
(2)回溯法的基本计算过程每次只构造一个部分解,立即对此部分解进行评估,若此部分解有可能成为所求解,则继续扩展,否则继续尝试其他部分解,直到穷尽所有可能。
(3)从根节点出发,深度优先搜索整个解空间树。
二、几个与回溯有关的概念:
(1)解空间:所有可能解的集合。
(2)剪枝函数:为了避免无效搜索,提高回溯法的搜索效率而存在的函数。通常可以用两种方式进行剪枝:
1)用约束函数在扩展结点处减去不满足约束条件的子树。
2)用限界函数减去得不到最优解的子树。
(3)扩展结点:一个正在产生儿子的结点称为扩展结点
活结点:一个自身已生成但其儿子还没有全部生成的节点称做活结点
死结点:一个所有儿子已经产生的结点称做死结点
三、解题步骤:
(1)针对所给问题,定义问题的解空间。
(2)确定易于搜索得解空间结构。
(3)以深度优先方式搜索解空间,并在搜索过程中用剪枝函数避免无效搜索。
四、模板框架:
(1)递归回溯:
void backtrack(int t) //t表示递归深度,即当前扩展节点在解空间树的深度
{
if ( t > n ) output(x); //n控制递归深度,如果算法已经搜索到叶节点,记录输出可行解X
else
{
for(int i = f(n,t) ; i <= g(n,t) ; i++) //在深度t,i从未搜索过得起始编号到终止编号
{
x[t] = h(i); //查看i这个点的值是否满足题目的要求
if( constraint(t) && bound(t))
backtrack(t+1)
//constraint(t)为true表示在当前扩展节点处x[1:t]的取值满足问题的约束条件;
//bound(t)为true表示当前扩展节点取值没有使目标函数越界;
//为true表示还需要进一步的搜索子树,否则减去子树。
}
}
}
如果不满足条件,就返回上一层,这个搜索算法按深度优先方式进行,调用一次backtrack(1)便可完成整个搜索过程。
如果看不明白,可以看一下皇后问题,这里就很好理解了。
(2)迭代回溯:(以后补上)
五、例题
1.皇后问题:
1)题目描述:在n*n的棋盘上放置旗子,同一行,同一列,同一斜线只能有一个旗子,找出符合题意的解。
2)解题思路:虽然棋盘是n*n的大小,但是由于棋盘是一行一行搜索,x的坐标就是递归的深度,即传给递归函数的t,这里我们只需要定义一个一维数组表示y。这样既节省了内存空间,又又方便了计算。从第一行开始向后进行计算,如果不满足条件(行列斜线不能保证唯一一颗)剪枝(约束函数),向上一层回溯,然后查找下一个可能的节点。
在约束函数里,判断能不能满足条件,同一行同一列比较一下即可。在斜线上的比较可以利用斜率的性质进行判断。在同一斜线上的斜率为+-1.
具体代码如下:
Queen.java
package trackback;
public class Queen
{
int n; //棋盘的大小 n*n
int sum; //计算保存解得个数
String all; //保存解得值
int []x; //保存坐标的值
Queen(int n)
{
this.n = n;
}
void calculate() //计算
{
sum = 0;
all = "";
x = new int[n];
backtrack(0); //从根节点递归 即可遍历整颗解空间树
System.out.println(all); //全部递归完成,输出保存到字符串的字符
}
void backtrack(int t)
{
if(t >= n) //如果递归遍历到了最后一行,就进入output函数,记录解得值,然后再向上递归,因为此时并没有遍历玩整个数,就继续检查记录其他没有便利到的可行解。
{
output();
return ;
}
for(int i = 0 ; i < n ; i++) //从第t行的第0个数开始逐个检查到第n个数,检查是否满足条件
{
x[t] = i; //检查第t行的第i个数是不是可行解
if(ok(t))
{
backtrack(t+1); //如果可行的话,就向上一行检查。不可行就继续i++,检查下一个数。
}
}
}
boolean ok(int t)
{
for(int i = 0 ; i < t ;i++)
{
if(x[t] == x[i]) return false;
if(x[t] - x[i] == t-i) return false;
if(x[t] - x[i] == i-t) return false;
}
return true;
}
void output()
{
sum++;
for(int i = 0 ; i < n ; i++)
{
all+=x[i]+" ";
}
all += "\n";
}
}
这是主函数的调用。
package trackback;
public class test
{
public static void main(String[] args)
{
// TODO Auto-generated method stub
Queen queen = new Queen(4);
queen.calculate();
}
}
最后的运行结果:
1 3 0 2
2 0 3 1
完善上面的算法:
我们可以发现,我们定义的sum这个值,并没有用到。试着想一想,当n过大的时候,这个算法的 计算量就会过大,及结果也会很多,如果想只输出其中的部分解,那么就可以在ok这个函数的开头加入: if(sum >= 10 ) return false; 这条语句,这样当backtrack在进行i++的时候,检测到sum>=10了之后,就会不在向下递归了。
紧接着问题又来了,当sum已经>=10了之后,在for循环里,i++还会每次都进入ok函数,会浪费时间空间,为了避免这个现象,在calculate()的函数前,加入: for(int i = 0 ; i < n ; i++) x[i] = i;会减少检查的次数。
这个算法就用到了递归的那个模板。递归的方法求回溯比迭代要更多一些,也更好一点。
2.0-1背包问题
(1)问题描述:
给定n件物体和一个容量为C的背包,物品i的重量为wi,价值为vi。试求如何选择装入背包的物品,使得物品的总价值最大。
(2)解题思路:
01背包是子集选取问题,通常是NP难的(NP为何物见下一篇博客)。所以先构造01背包的解空间树。在搜索的时候,左节点是可行节点的时候,搜索就进入左子树。当右子树可能包含最优解的时候,就进入右子树搜索。否则减去右子树。
举例:
n = 4 ,c = 7. value ={9,10,7,4} weight = {3,5,2,1}
第一步,按单位价值排序:
序号 | 价值 | 重量 | 单位价值 |
3 | 9 | 3 | 3 |
4 | 10 | 5 | 2 |
2 | 7 | 2 | 3.5 |
1 | 4 | 1 | 4 |
第二步,从单位价值最大的开始,依次装入背包。直至放不下,然后开始用限界函数推
算,如果继续下去的最大价值()。和上几次作对照,大于继续求解,回溯到上一个不为0的地方,将其置于0,界限函数继续判断价值,向后继续,回溯到x[1]的地方。到尽头停止。
在解空间树的当前扩展节点处是才需要计算上界,以判断右子树是否可以减去。进入左子树不需要计算上界,因为其上界与父节点相同。
package OneZero;
public class Knapsack
{
static double c;
static int n;
static double []weight;
static double []value;
static double nowWeight;
static double nowValue;
static double bestValue;
public static class Element implements Comparable
{
int id;
double d;
private Element(int id , double d)
{
this.id = id;
this.d = d;
}
@Override
public int compareTo(Object x) //比较d的大小
{
// TODO Auto-generated method stub
double xd = ((Element)x).d;
if( d < xd) return -1;
if( d == xd) return 0;
return 1;
}
public boolean equals(Object x)
{
return d ==((Element)x).d;
}
}
public double knapsack(double []pp , double []ww , double cc)
{
c = cc;
n = pp.length - 1;
nowWeight = 0;
nowValue = 0;
bestValue = 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); //排序函数,需要自己定义排序
value = new double[n+1];
weight = new double[n+1];
for(int i = 1; i <= n ; i++)
{
value[i] = pp[q[n-i].id];
weight[i] = ww[q[n-i].id];
}
backtrack(1);
return bestValue;
}
private static void backtrack(int i)
{
if(i > n)
{
bestValue = nowValue;
return ;
}
if(nowWeight + weight[i] <= c)
{
nowWeight += weight[i];
nowValue += value[i];
backtrack(i+1);
nowWeight -= weight[i];
nowValue -=value[i];
}
if(bound(i+1) > bestValue)
{
backtrack(i+1);
}
}
private static double bound(int i)
{
double cleft = c - nowValue;
double bound = nowWeight;
while( i <= n && weight[i] <= cleft)
{
cleft -= weight[i];
bound += value[i];
i++;
}
if(i <= n)
{
bound += value[i] * cleft /weight[i];
}
return bound;
}
}