一、实验目的
1 理解搜索树与解空间树的区别
2 理解回溯法\分支限界法\穷举搜索的区别
3 理解回溯法的特征(基于解空间树\纵向优先\剪枝) 和 分支限界法的特征(基于解空间树\横向优先\剪枝)
4掌握回溯法的简单实现和时间复杂度分析
- 实验内容
1、图着色问题
2、0/1背包问题
三、问题分析
1、图着色问题:
回溯法求解图着色问题时,首先把所有顶点的颜色初始化为0,然后依次为每个顶点着色,如果当前顶点着色没有冲突,则继续为下一个顶点着色,否则,为当前顶点着下一个颜色,如果所有m种颜色都试探过并且不发生冲突,则回溯到当前顶点的上一个顶点,以此类推;
2、0/1背包问题
回溯法求解0/1背包问题时,不断的去试探每个重量的物品,然后如若发生冲突,就是重量已经超过背包了,那就回溯到上一个物品;
四、问题解决
1、图着色问题
(1)、算法:回溯法求解图着色问题GrapghColor
输入:图G=(V,E),m种颜色
输出:n个顶点的着色情况color[n]
过程:1、将数组color[n]初始化为0;
2、i=0;
3、当i>=0为顶点i着色:
3.1、依次考查每一种颜色,若顶点i的着色与其他顶点的着色不发生冲突,则转步骤3.2;否则,搜索下一颜色;
3.2、如果color[i]大于m,重置顶点i的着色情况,i=i-1,转步骤3回溯;
3.3、若顶点i是一个合法的着色且顶点尚未全部着色,则i=i+1,转步骤3处理下一个顶点;
3.4、若顶点已全部着色,则输出数组color[n],算法结束;
(2)、代码:
void GraphColor(int m,int [][]arc,int []color,int n){/*m种颜色,n个顶点,数组arc[]存储图,color[]存储颜色*/
int i,j;
for (i=0;i<n;i++){
color[i]=0;
}
for (i=0;i>=0;){
color[i]=color[i]+1;
while (color[i] <=m && OK(i,arc,color)==1)
color[i]=color[i]+1;
if (color[i]>m) color[i--]=0;
else if (i<n-1) i=i+1;
else {
System.out.println(Arrays.toString(color));
}
}
}
int OK(int i,int [][]arc,int []color){ /*判断着色是否发生冲突*/
for (int j=0;j<i;j++){
if (arc[i][j]==1 && color[i]==color[j]){
return 1;
}
}
return 0;
}
该算法的时间复杂度为:O(m^n)
(3)、
2、0/1背包问题:
(1)、算法:回溯法求解0/1背包问题
输入:物品的重量w[n],价值p[n],背包容量C
输出:获得的最大价值
过程:1、将数组x[n]全部初始化为-1;
2、i=0;
3、当i>=0时对物品i进行选择:
3.1、x[i]=x[i]+1,试探装入物品i,计算cw和cp
3.2、如果物品i试探失败,则x[i]=-1,i=i-1,转步骤3进行回溯;
3.3若数组x[i]构成部分解,则i=i+1,转步骤3考查下一个物品;
3.4、若数组x[i]构成全部解,更新变量bestP
4、输出bestP
(2)、代码:
public class Zero_One {
private static int[] p;//物品的价值数组
private static int[] w;//物品的重量数组
private static int c;//最大可以拿的重量
private static int count;//物品的个数
private static int cw;//当前的重量
private static int cp;//当前的价值
static int bestp;//目前最优装载的价值
private static int r;//剩余物品的价值
private static int[] cx;//存放当前解
private static int[] bestx;//存放最终解
public static int Loading(int[] ww, int[] pp, int cc) {
//初始化数据成员,数组下标从1开始
count = ww.length - 1;
w = ww;
p = pp;
c = cc;
cw = 0;
bestp = 0;
cx = new int[count + 1];
bestx = new int[count + 1];
//初始化r,即剩余最大价格
for (int i = 1; i <= count; i++) {
r += p[i];
}
//调用回溯法计算
BackTrack(1);
return bestp;
}
/**
* 回溯
*
* @param t
*/
public static void BackTrack(int t) {
if (t > count) {//到达叶结点
if (cp > bestp) {
for (int i = 1; i <= count; i++) {
bestx[i] = cx[i];
}
bestp = cp;
}
return;
}
r -= p[t];
if (cw + w[t] <= c) {//搜索左子树
cx[t] = 1;
cp += p[t];
cw += w[t];
BackTrack(t + 1);
cp -= p[t];//恢复现场
cw -= w[t];//恢复现场
}
if (cp + r > bestp) {//剪枝操作
cx[t] = 0;//搜索右子树
BackTrack(t + 1);
}
r += p[t];//恢复现场
}
public static void main(String[] args) {
//测试
int[] w1 = {0, 15, 25, 40, 20, 15, 24};
int[] p1 = {0, 10, 5, 20, 2, 14, 23};
int c1 = 30;
Loading(w1, p1, c1);
System.out.println("最优装载为:" + bestp);
for (int i = 1; i <= count; i++) {
System.out.print(bestx[i] + " ");
}
}
}
该算法时间复杂度:O(n*2^n)
(3)、截图:
五、实验结果总结
回答以下问题:
- 请从你实现的级别中选择一题,按照你的实现绘制搜索树。
- 你觉得回溯法和蛮力搜索有相同点吗?有不同点吗?请举例说明。
回溯法实际上一个类似穷举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”(即回退),尝试别的路径。
回溯法搜索解空间时,通常采用两种策略【剪枝策略】避免无效搜索,提高回溯的搜索效率:
用 约束函数 在扩展结点处剪除不满足约束的子树;
用 限界函数 剪去得不到问题解或最优解的子树。
蛮力法是一种简单直接地解决问题的方法,通常直接基于问题的描述和所涉及的概念定义,找出所有可能的解。然后选择其中的一种或多种解,若该解不可行则试探下一种可能的解。
- 你觉得回溯法能用递归实现吗?如果能,请把你实现的一道题目改为回溯法的递归程序。
可以,
/** 递归实现回溯 (打印出所有的可行解)
* 算法缺陷:当找不到构成素数环的数时,无任何结果
* @param num 从数组num中 填充素数环的n个位置
* @param k 第k个位置(解分量)
*/
public void getAllPrimeCircleByBackTrace(int[] num, int k){
if(k >=n ){
for (int c : circle) {
System.out.println(c);
}
return;
}
int len = num.length;
for (int i = 0; i < len; i++) {
circle[k] = num[i];
if(checkPrimeCircle(k) == 1) { //第k个分量的取值满足约束条件,则解决第k+1个分量
getPrimeCircleByBackTrace(num,k+1);
//break;
} else{
circle[k] = 0;
}
}
}
/** 递归实现回溯 (打印出所有的可行解)
* 算法缺陷:当找不到构成素数环的数时,无任何结果
* @param num 从数组num中 填充素数环的n个位置
* @param k 第k个位置(解分量)
*/
private boolean flag = false;
public void getPrimeCircleByBackTrace(int[] num, int k){
if(k >=n ){
for (int c : circle) {
System.out.println(c);
}
flag = true;
return;
}
int len = num.length;
for (int i = 0; i < len; i++) {
circle[k] = num[i];
if(checkPrimeCircle(k) == 1) { //第k个分量的取值满足约束条件,则解决第k+1个分量
getPrimeCircleByBackTrace(num,k+1);
if(flag) break;
} else{
circle[k] = 0;
}
}
}
/**
* 循环实现回溯法求解素数环问题
* @param num
*
*/
public void getPrimeCircleByBackTraceLoop(int[] num){
int k = 0;//表示解分量素数环中的第k个位置
int[] location = new int[n]; //将素数环每个位置存放的数的坐标保留
int len = num.length;
for (int i = 0; i < n ; i++) {
circle[i] = num[0];
location[i]=0;
}
int i = 0;
while( k >= 0){
//对第k个位置(解分量)在num中试探逐一取值
i = location[k];
for (; i < len ; i++) {
circle[k] = num[i];
if(checkPrimeCircle(k) == 0 ) { //第k个分量取当前值不满足约束,试探num中下一个值
circle[k] = 0;
continue;
}
else { //第k个分量的取当前值满足约束条件,就退出num的试探
location[k] = i;
break;
}
}
if(k==n-1 && circle[k] != 0) { //说明对所有分量都有取到值
for (int c: circle) {
System.out.println(c);
}
return;
}
else if(k < n-1) { //试探下一个分量
k++;
}
else{ //需要回溯
location[k] = 0;
circle[k] = 0;
k--;
location[k] = location[k] + 1;
}
}
}
/* 素数环问题中,
判断第k个分量是否满足2个隐式约束条件
条件一:第k个分量的取值和之前各分量的取值不重复
条件二:第k个分量和第k-1个分量的和是否为素数,如最后一个数,则还要判断第1个数和最后1个数之和也为素数
* */
private int checkPrimeCircle(int k){
//先判断第k个分量的取值是否与前面各分量的取值冲突
if(k==0) return 1;
for (int i = k-1; i >= 0 ; i--) {
if(circle[i] == circle[k]) return 0;
}
//再判断第k个分量和第k-1个分量的和是否为素数
int tSum = circle[k] + circle[k-1];
if(isPrime(tSum) == 0) return 0;
//最后,如果第k个分量是最后一个分量,还需要判断第1个分量和最后1个分量的和是素数
if(k == n-1){
tSum = circle[k] + circle[0];
if (isPrime(tSum) == 0) return 0;
}
return 1;
}
/*
* 判断n是否为素数*/
private int isPrime(int n){
if(n <=1 ) return 0;
for(int i=n-1;i>=2;i--){
if(n % i == 0) return 0;
}
/* // 或者优化为下面的形式
for( int i = (int)Math.sqrt(n); i>=2;i--){
if (n % i == 0) return 0;
}*/
return 1;
}
private int subsetSum = 0;
void getSubsetByBackTrace(int[] num, int target,int k){
int len = num.length;
if(k>= len){
for (int c : choice) {
System.out.println(c);
}
return;
}
if(subsetSum + num[k] <= target){
choice[k] = 1;
subsetSum += num[k];
getSubsetByBackTrace(num,target,k+1);
subsetSum -= num[k];
}
else {
choice[k] = 0;
getSubsetByBackTrace(num,target,k+1);
}
}
/* 子集数和问题中,
判断第k个分量是否满足约束条件
前k个数字选择的结果之和<=target
* */
private boolean checkSubset(int[] num, int target,int k){
subsetSum += choice[k] * num[k];
if( k == 0) {
return num[k] <= target;
}
return subsetSum <= target;
}