什么是回溯算法
回溯算法的概念:回溯算法是一种类似枚举但优于枚举的算法,它可以通过“剪枝”的方式去掉那些不可能的递归情况,从而得到实现解题目标的所有可行方案。回溯算法一般通过递归来实现,每个次递归前都要判断当前这种方案是不是有继续递归的必要,如果可以,就继续往深入递归,最终实现目标,如果在递归中发现不符合递归条件了,那么就“减掉”这个分支,程序回溯到之前的另一种可能性重新开始,继续往下递归,能递归到最后一步的方案一般称为可行解,我们可以统计可行解的数目,从而最终得到方案总数。
回溯算法的优点:回溯算法可以讨论所有可能的方案,不遗漏地进行判断,同时它也会放弃那些不可能的递归方案,节省程序的运行时间。
回溯算法的解题思路:首先分析题目,如果最终目标由一系列步骤组成,并且让你求最终目标的方案数目,那么可以尝试使用回溯算法。代码一般分为两部分,递归部分和下一步判断部分,递归部分以循环的形式遍历所有可能的情况,判断部分的布尔值反馈结果决定是否下一步递归或者“剪枝”,即写判断条件。最终在递归达到一定深度的时候判断方案可行,进行计数累加或保存方案。
回溯算法的例题1:N皇后问题
题目:大家常见的是八皇后问题,是N皇后问题的一种特殊情况。N皇后问题是指,在一个N*N的棋盘上,放置N个皇后,他们彼此不能处在相同行、列、对角线,求有多少种摆放方案。
分析:我们可以对N个皇后逐一放置,首先我们可以确定一行摆放一个皇后,每个皇后有N个可以摆放的位置,那么我们先假设第一个皇后放在第一行第一个位置,再判断第二个皇后的摆放,如果放在第二行第一个位置在同一列不符合条件,放在第二个位置是对角线也不符合条件,第三个位置就可以,所以放下这个皇后,接下来看第三个皇后的摆放,以此类推,每次判断不可摆放时,即剪去了这个分支。如果某次递归行数大于N,即棋盘摆满,方案可行,计数加一。
算法实现:
int[] x ; //先申明一个数组,等待皇后数量输入
int sum =0; //满足条件的情况的累计
int num ; // 棋盘的宽高
/**
* 回溯算法1:皇后问题入口方法
*/
public void bt1()
{
num = 8;
x= new int[num+1];
BT1(1); //开始第一次递归
System.out.println(sum);
}
/**
* 回溯算法1:递归方法
* @param num
* @return
*/
public void BT1(int t) //摆第t行
{
if(t > num) //棋盘摆完,计数加一
sum++;
else
{
for(int i=1;i<=num;i++) //从第一个位置开始试探直到最后一个
{
x[t]=i; //放在第i个位置
if(place(t)==true) //可以放置就开始放下一个
{
BT1(t+1);//下一行
}
}
}
}
/**
* 回溯算法1:判断位置是否可放
*/
public boolean place(int t)
{
for(int i=1;i<t;i++)
{
if(x[i]==x[t]||Math.abs(i-t)==Math.abs(x[i]-x[t]))//判断对角线及行列
{
return false;
}
}
return true;
}
回溯算法2:图着色问题
问题:给定一个拼图,颜色刚开始都是空白的,我们有四种颜色,要求为图形上色且相邻色块颜色不能相同。
分析:类似N皇后问题,递归是对于上色顺序进行递归,试探所有上色可能性,判断是判断是否和周围颜色相同。相邻关系我们可以用一个二维数组来保存(0表示不相邻,1表示相邻)。
算法实现:
int[][] p = //图的相邻关系矩阵,相邻为1,矩阵长宽和分区数量有关
{
{0,1,0,0,0,0,1}, //图1和1~7的相邻关系
{1,0,1,1,1,1,1},
{0,1,0,1,0,0,0},
{0,1,1,0,1,0,0},
{0,1,0,1,0,1,1},
{0,1,0,0,1,0,1},
{1,1,0,0,1,1,0} //图7和1~7的相邻关系
};
int sum2 = 0; //可行的情况
int[] y = new int[8]; ; //上色情况 1和2和3和4表示不同颜色
int num2=7;
/**
* 回溯算法2:图着色问题
*/
public void bt2()
{
BT2(1);
System.out.println(sum2);
}
/**
* 回溯算法2:递归方法
* @param t
*/
public void BT2(int t)
{
if(t>num2)
{
sum2++;
}
else
{
for(int i=1;i<=4;i++) //四种颜色进行尝试
{
y[t]=i;
if(place2(t)==true)
{
BT2(t+1);
}
}
}
}
/**
* 回溯算法2:判断
* @param t
* @return
*/
public boolean place2(int t)
{
for(int j=1;j<t;j++) //逻辑上的第1个开始
{
if(p[t-1][j-1]==1&&y[t]==y[j])
{
return false;
}
}
return true;
}
回溯算法3:火柴棍摆正方形
题目:给定若干不同长度的火柴棍,要求判断这些火柴棍能不能围成正方形。
分析:如果总长不能被4整除,那么就不能摆成正方形,如果可以被四整除,我们再开始使用回溯算法递归判断。我们选择一个数组,表示四个桶,桶的深度是总长的四分之一,我们选择火柴棍放入桶里,如果长度超出则表示这种摆放方案不可选,取出并进行下一个桶的试探,当四个桶中火柴棍总长度相同并且火柴棍摆完,那么这就是一种可行的解。程序递归的部分递归每个火柴棍的摆放,每个火柴棍有四种摆放方案。
算法实现:
int[] z ;//定义数组用来表示放第几个火柴
int[] w = new int[5]; //定义每个桶的火柴数量
int add=0; //火柴总数
int t=1; //当前放的火柴
int flag=0;//可否摆成正方形
/**
* 回溯算法3:火柴棍摆正方形
*/
public void bt3()
{
int[] c={1,1,1,1,1,1,2}; //各火柴的长度
z=new int[c.length+1]; //1:表示的一个桶,2:表示第二个桶,3:表示第三个桶 4:表示第四个桶
add=0;
for(int i=0;i<c.length;i++)
{
add+=c[i];
}
System.out.println("当前总长度:"+add);
if(add%4!=0)
System.out.println("不可以组成正方形");
else
{
BT(t,c);
if(flag!=1)
System.out.println("不可以组成正方形");
else
System.out.println("可以组成正方形");
}
}
/**
* 回溯算法3:递归方法
* @param t
*/
public void BT(int t,int[] c)
{
for(int i=1;i<=4;i++)
{
if(t==8)
{
flag=1;
break;
}
z[t]=i;
w[i]+=i;
if(place3(t)==true) //true则继续放下一个火柴
{
BT(t+1,c); //递归放下一个火柴
}
w[i]-=i;//不可以放就把重量减掉
}
}
/**
* 回溯算法3:判断可不可以放
* @param t
*/
public boolean place3(int t)
{
for(int i=1;i<5;i++)
{
if(w[i]>add/4)
return false;
}
return true;
}
回溯算法4:求子集
题目:给定一个数组,求它的所有不重复子集。
分析:回溯算法试探所有摆放的方法,每个数字有放入和不放入两种方案,递归摆放数字(在数组中保存),摆完三个就可以去和HashMap中的数组进行判断,如果已经存在就不放入,不存在就添加进HashMap。执行完所有情况,HashMap中的元素就是所有的子集。
算法实现:
ArrayList<int[]> array = new ArrayList<>();
int[] c ; //用来表示存放的是第几个数
int[] temp ; //临时存放数据
/**
* 回溯算法:4:求子集
*/
public void bt4()
{
int[] a = {1,2,3};
c=new int[a.length+1];
temp=new int[a.length];
DT4(1,a);
for(int i=0;i<array.size();i++)
System.out.println(Arrays.toString(array.get(i)));
}
/**
* 回溯算法4:递归方法
*/
public void DT4(int t,int[] a)
{
if(t==a.length+1)
{
place4(a);
}
for(int i=0;i<=1;i++) //0:不放 1:放
{
if(t==a.length+1)
break;
c[t]=i;
DT4(t+1,a);
}
}
/**
* 回溯算法4:判断
* @return
*/
public void place4(int[] a)
{
int count=0;
for(int j=1;j<a.length+1;j++)
{
if(c[j]==1) //表示选择了这个数
{
temp[count]=a[j-1];
count++; //统计要放入的数的数量
}
}
int[] r = new int[count]; //定义一个长度和所选数字数量一致的数组
for(int j=0;j<count;j++) //存值
r[j]=temp[j];
if(!array.contains(r)&&r.length!=0) //判断是否存在
array.add(r); //添加
}