用dfs思想解决的一些问题
数独游戏
算法思想:利用dfs一条路走到黑(不满足条件)的思想,在每一个空位置依次填1-9九个数字,检验其合理性,若出现矛盾就回溯;若能走到底,则直接返回到最高层。
import java.util.Scanner;
public class dfs {
public static void main(String[] args){
Scanner sc=new Scanner(System.in);
//数独的整个方格
char[][] table=new char[9][];
for(int i=0;i<9;i++){
table[i]=sc.nextLine().toCharArray();
}
int x=0,y=0;
solve(table,x,y);//填数解决问题
}
//dfs
private static void solve(char[][] table,int x,int y){
//结束的条件
if(x==9){
print(table);
System.exit(0);//直接退到最高层
}
if(table[x][y]=='0'){//虚位以待
for(int k=1;k<10;k++){
if(check(table,x,y,k)){
table[x][y]=(char)('0'+k);//转化为字符
solve(table,x+(y+1)/9,(y+1)%9);//******
}
}
//如果进行的了这一步就说明这条路没有走通,回溯
table[x][y]='0';//*****
}else{
solve(table,x+(y+1)/9,(y+1)%9);
}
}
//二维数组输出函数
private static void print(char[][] table){
for(int i=0;i<table.length;i++){
System.out.println(new String(table[i]));//***
}
}
//检查是否合法
private static boolean check(char[][] table,int x,int y,int k){
//检查行和列
for(int i=0;i<9;i++){
if(table[i][y]==k+'0')
return false;
if(table[x][i]==k+'0')
return true;
}
//检查九宫格*******
for(int l=(x/3)*3;l<(x/3+1)*3;l++){
for(int m=(y/3)*3;m<(y/3+1)*3;m++){
if(table[l][m]=='0'+k)
return false;
}
}
return true;
}
}
部分和
问题描述:给定整数序列,判断是否可以从中选出若干数,使它们的和恰好为k。
解法一:穷举所有的子集,将子集相加看是否等于k
解法二:利用dfs求解:在dfs中参数就表示你现在的状态,注意一些共享的集合或者元素,注意回溯。在遍历数组时,面对每一个元素时,都有两种状态(两条路):选择或不选择,选择其中一条一直往下走,走不通的话再回来尝试另一个状态。
解法二代码实现(不记录结果,只探测有没有和为k的组合):
import java.util.Scanner;
//dfs
//部分和
public class Main {
public static void main(String[] args){
Scanner sc=new Scanner(System.in);
int n=sc.nextInt();
int[] a=new int[n];
for(int i=0;i<a.length;i++){
a[i]=sc.nextInt();
}
int k=sc.nextInt();//所要凑的和
int cur=0;//起始的位置
dfs(a,k,cur);
}
//递归过程中k越来越小
private static void dfs(int[] a,int k,int cur){
//退出的条件
if(k==0){
System.out.println("Yes");
System.exit(0);//直接退出
}
//走不通的情况
if(k<0||cur==a.length){
return;//返回上一层,尝试其他状态
}
//走到每一个结点都会有这两种情况可以选择,依次尝试,看哪个能走得通
dfs(a,k,cur+1);//不要当前这个元素
dfs(a,k-a[cur],cur+1);//要当前这个元素
}
}
记录筛选的元素集合的算法:
import java.util.ArrayList;
import java.util.Scanner;
//dfs
//部分和
public class Main {
public static void main(String[] args){
Scanner sc=new Scanner(System.in);
int n=sc.nextInt();
int[] a=new int[n];
for(int i=0;i<a.length;i++){
a[i]=sc.nextInt();
}
int k=sc.nextInt();//所要凑的和
int cur=0;//起始的位置
//初始化一个列表用于存放结果
dfs(a,k,cur,new ArrayList<Integer>());
}
//递归过程中k越来越小
private static void dfs(int[] a,int k,int cur,ArrayList<Integer> ints){
//退出的条件
if(k==0){
//将结果列表输出
System.out.print("Yes("+"k"+"=");
for(int i=0;i<ints.size();i++){
System.out.print(ints.get(i)+(i+1==ints.size()?"":"+"));
}
System.out.println(")");
System.exit(0);//直接退出
}
//走不通的情况
if(k<0||cur==a.length){
return;//返回上一层,尝试其他状态
}
//走到每一个结点都会有这两种情况可以选择,依次尝试,看哪个能走得通
dfs(a,k,cur+1,ints);//不要当前这个元素
//当不要这个元素走不通时,尝试要这个元素的情况
ints.add(a[cur]);//将该元素添加到结果列表当中
int index=ints.size()-1;
dfs(a,k-a[cur],cur+1,ints);//要当前这个元素
//回溯,两种情况都不行的话,就将该元素从结果列表中删去******
ints.remove(index);
}
}
水洼数目(经典)
问题描述:有一个大小为N*M的园子,雨后积起了水。八连通的积水被认为是连接在一起的。请求出园子里总共有多少水洼?(八连通指的是下图中相对W的0的部分)
000
0W0
000
算法思想:dfs,为了避免走着走着返回回来,就将走过的有水的地方变干燥
import java.util.Scanner;
//dfs
//水洼数
public class Main {
public static void main(String[] args){
Scanner sc=new Scanner(System.in);
//行和列
int n=sc.nextInt();
int m=sc.nextInt();
//存放水洼的情况
char[][] a=new char[n][];
for(int i=0;i<n;i++){
a[i]=sc.nextLine().toCharArray();
}
int cnt=0;//水洼数目
for(int i=0;i<n;i++){
for(int j=0;j<m;j++){
if(a[i][j]=='W'){
dfs(a,i,j);//消除一个水洼
cnt++;
}
}
}
System.out.print(cnt);//打印水洼的数量
}
//并不是所有的dfs都要回溯
private static void dfs(char[][] a,int i,int j){
a[i][j]='.';//将该位置水洼清除
//遍历上下左右八个位置
for(int m=-1;m<2;m++){//-1,0,1
for(int n=-1;n<2;n++){//-1,0,1
if(m==0&&n==0)
continue;
if(i+m<a.length&&i+m>=0&&j+n<a.length&&j+n>=0){
if(a[i+m][j+n]=='W')
dfs(a,i+m,j+n);
}
}
}
}
}
回溯和剪枝:n皇后问题
1、回溯:递归调用代表开启一个分支,如果希望这个分支返回后某些数据恢复到分支开启前的状态以便重新开始,就要使用回溯技巧。
全排列交换法,数独,部分和用到了回溯。
2、剪枝:深搜时,如已明确从当前状态无论如何转移都不存在(更优)解,就应该中断往下的继续搜索,或者是在递归前有一些条件限定,这种方法称为剪枝,有可能要进行预判。
数独、部分和里面有剪枝。
3、n皇后问题:
问题描述:在n*n的棋盘上摆放n个皇后,使其任意两个皇后都不能处于同一行、同一列或同一斜线上。
import java.util.Scanner;
//dfs
//n皇后问题
public class Main {
static int n=8;//八皇后问题
static int cnt=0;//记录有多少种解法
//下标表示行,值表示该行所填数所在的列
static int[] rec=new int[n];
public static void main(String[] args){
//初始化,因为列下表可能为0,故所有都初始化为-1
for(int i=0;i<rec.length;i++){
rec[i]=-1;
}
dfs(0);//从第零行开始
System.out.print(cnt);//输出有几种解法
}
//一行一行依次向下搜索
private static void dfs(int row){
//结束的条件
if(row==n){
cnt++;//方案数增一
return;//返回上一层
}
//依次尝试在某列上放一个皇后
for(int col=0;col<n;col++){
boolean k=true;
//第row及其以下都还没有放皇后
for(int i=0;i<row;i++){
//同一行肯定不会有皇后,只用看同一列和对角线
//左上到右下对角线的元素的差是相同的,右上到左下元素的和是相同********
if(rec[i]==col||i+rec[i]==col+row||i-rec[i]==row-col){
k=false;
break;
}
}
//这里可以认为是剪枝
//如果这一行的这一列可以放
if(k){
rec[row]=col;//标记
dfs(row+1);//继续找下一行
//回溯,这里其实不回溯也是可以的,因为check时不会check同行或者该行以下的元素
rec[row]=-1;
}
}
}
}
素数环
题目描述:输入正整数n,对1到n进行排列,使得相邻两个数之和均为素数。输出时从整数1开始,逆时针排序。同一个环应恰好输出一次。
算法思想:dfs,回溯和剪枝
import java.util.ArrayList;
import java.util.Scanner;
//dfs
//素数环
public class Main {
public static void main(String[] args){
Scanner sc=new Scanner(System.in);
int n=sc.nextInt();
ArrayList<Integer> rec=new ArrayList<Integer>();//用于保存结果顺序
rec.add(1);//1作为队列中的第一个 元素
int[] temp=new int[n+1];//记录已经进入队列中的元素
temp[1]=1;//1作为队列中的第一个 元素,已经使用
dfs(n,1,rec,temp);
}
private static void dfs(int n,int a,ArrayList<Integer> rec,int[] temp){
//退出的条件:全部数都进入了列表
if(rec.size()==n){
for(int i=0;i<n;i++){
System.out.print(rec.get(i));
}
System.out.println();
return;//这里直接返回上一层不用将temp数组清空,因为有回溯
}
for(int i=1;i<=n;i++){
//若该元素为被选用过并且满足条件
if(temp[i]==0&&check(a+i)){
if(rec.size()==n-1){//最后一个元素要加1互素
if(!check(1+i))
continue;
}
rec.add(i);//添加该元素
int b=rec.size()-1;
temp[i]=1;//该元素已经被选用
dfs(n,i,rec,temp);//继续进行
//回溯
temp[i]=0;
rec.remove(b);
}
}
}
//判断是否为素数
private static boolean check(int a){
boolean ok=true;
if(a==1||a==2||a==3){
return ok;
}
for(int i=2;i*i<=a;i++){
if(a%i==0){//i是a的因数
ok=false;
break;
}
}
return ok;
}
}
复杂的串
问题描述:如果一个字符串包含两个相邻的重复子串,则称它为容易的串,其他的称为困难的串。输入正整数n,L,输出由前L个字符(大写英文字母)组成的,字典序第n小的困难的串。
import java.util.Scanner;
//dfs
//困难的串
public class Main {
public static void main(String[] args){
int n=10;
int l=4;
dfs(l,n,"");
}
static int count=0;//排序的个数
private static void dfs(int l,int n,String prefix){
//尝试在prefix后面加上一个字符
for(char i='A';i<'A'+l;i++){
if(isHard(prefix,i)){
String x=prefix+i;
System.out.println(x);
count++;
//退出条件
if(count==n){
System.exit(0);//直接全部退出
}
dfs(l,n,x);
count--;//回溯、、、、、、、、、
}
}
}
//判断prefix是否是一个困难的串
//因为prefix本来就是一个困难得串,故只需看看加入一个字符后对原来字符串的影响即可
private static boolean isHard(String prefix,char i){
int c=1;//截取的宽度
String x=prefix+i;
for(int j=x.length()-1;j-c>=0;j--){
String s1=x.substring(j);//后面的字符串
String s2=x.substring(j-c,j);//前面的字符串
c++;//c自增1
if(s1.equals(s2))
return false;
}
return true;
}
}
总结
这类问题,解的空间很大(往往是阶乘级别的),要在所有可能型中找到答案,只能进行试探,尝试往前走一步,走不通再退回来,这就是dfs+回溯+剪枝,对这类问题的优化,使用剪枝,越早剪越好,但这很难。