深度优先搜索是一种搜索策略,但是他是通过递归(或循环)来实现的,那么时间复杂度便不会低。通过dfs,我们可以得到一颗搜索树,但实际上这棵树的许多枝条是不需要的,那么我们没必要对这个分支进行搜索,砍掉这个子树,便是剪枝。
1.可行性剪枝
如果当前条件不合法就不再继续搜索,直接return。这是非常好理解的剪枝。
问题:给定n个数,要求选出k个数,使得选出的数和为sum。
代码如下:
import java.util.Scanner;
public class L1 {
static int n, k, sum, ans = 0;
static int[] a = new int[1005];
static void dfs(int i, int cnt, int s) {
if (cnt > k)// 限制条件1,选的个数超过k
return;
if (s > sum)// 限制条件2,和大于sum
return;
if (i == n) {
if (cnt == k && s == sum) {
ans++;
}
return;
}
dfs(i + 1, cnt, s);
dfs(i + 1, cnt + 1, s + a[i]);
}
public static void main(String[] args) {
// TODO Auto-generated method stub
Scanner sc = new Scanner(System.in);
n = sc.nextInt();
k = sc.nextInt();
sum = sc.nextInt();
for (int i = 0; i < n; i++)
a[i] = sc.nextInt();
dfs(0, 0, 0);
System.out.println(ans);
}
}
2.最优性剪枝
如果当前条件所创造出的答案必定比之前的答案大,那么剩下的搜索就毫无必要,甚至可以剪掉。我们利用某个函数估计出此时条件下答案的‘下界’,将它与已经推出的答案相比,如果不比当前答案小,就可以剪掉。尤其是求解迷宫最优解时。
题目:有一个n x m大小的迷宫,字符 'S'表示起点,字符'T'表示终点, '*'表示墙壁,字符'.'表示平地。求S到T的最少步数。
代码如下:
import java.util.Scanner;
public class L9 {
static int n, m, ans = Integer.MAX_VALUE;
static char[][] map = new char[110][110];
static boolean[][] vis = new boolean[110][110];
static int[][] dir = { { -1, 0 }, { 0, -1 }, { 1, 0 }, { 0, 1 } };
static boolean in(int x, int y) {
return 0 <= x && x < n && 0 <= y && y < m;
}
static void dfs(int x, int y, int step) {
if (step >= ans) {// 如果步数已经超过当前最优解,那么直接剪掉
return;
}
if (map[x][y] == 'T') {
ans = step;
return;
}
vis[x][y] = true;
for (int i = 0; i < 4; i++) {
int tx = x + dir[i][0];
int ty = y + dir[i][1];
if (in(tx, ty) && !vis[tx][ty] && map[tx][ty] != '*') {
dfs(tx, ty, step + 1);
}
}
vis[x][y] = false;
}
public static void main(String[] args) {
// TODO Auto-generated method stub
Scanner sc = new Scanner(System.in);
n = sc.nextInt();
m = sc.nextInt();
for (int i = 0; i < n; i++) {
map[i] = sc.next().toCharArray();
}
int x=0,y=0;
for(int i=0;i<n;i++) {
for(int j=0;j<m;j++) {
if(map[i][j]=='S') {
x=i;
y=j;
}
}
}
dfs(x,y,0);
System.out.println(ans);
}
}
3.重复性剪枝
对于某一些特定的搜索方式,一个方案会被搜索很多次,这样是没有必要的。
对于第一题,通过每次选取一个数,下次再选另外的数的思路,可以通过规定选取数的次序使用重复性剪枝。
代码如下:
import java.util.Scanner;
public class L2 {
static int n, k, sum, ans;
static int[] a = new int[40];
static boolean[] xuan = new boolean[40];
static void dfs(int s, int cnt, int pos) {
if (s > sum || cnt > k)
return;
if (s == sum && cnt == k) {
ans++;
return;
}
for (int i = pos; i < n; i++) {
if (!xuan[i]) {
xuan[i] = true;
dfs(s + a[i], cnt + 1, i + 1);
xuan[i] = false;
}
}
}
public static void main(String[] args) {
// TODO Auto-generated method stub
Scanner sc = new Scanner(System.in);
n = sc.nextInt();
k = sc.nextInt();
sum = sc.nextInt();
for (int i = 0; i < n; i++)
a[i] = sc.nextInt();
dfs(0, 0, 0);
System.out.println(ans);
}
}
4.奇偶性剪枝
先看题目:有一个n x m 的迷宫,其中字符 ‘S’代表起点,字符‘D’代表终点,字符‘X’代表墙壁,字符‘.’代表平地,从S到D,每次行动消耗1时间,走过的路都会坍塌,因此不能原地不动或回头,现在一直大门会在T时间打开,判断在0时间能否逃离迷宫。
我们只需要dfs每条路径,只搜到T时间就可以了,但是还可以继续剪枝。
代码如下:
import java.util.Scanner;
public class L10 {
static int n, m, T;
static char[][] map = new char[10][10];
static boolean[][] vis = new boolean[10][10];
static int[] dx = { 0, 0, -1, 1 };
static int[] dy = { 1, -1, 0, 0 };
static boolean ok;
static void dfs(int x, int y, int t) {
if (ok)
return;
if (t == T) {
if (map[x][y] == 'D')
ok = true;
return;
}
vis[x][y] = true;
for (int i = 0; i < 4; i++) {
int tx = x + dx[i];
int ty = y + dy[i];
if (tx < 0 || tx >= n || ty < 0 || ty >= m || map[tx][ty] == 'X' || vis[tx][ty])
continue;
dfs(tx, ty, t + 1);
}
vis[x][y] = false;
}
public static void main(String[] args) {
// TODO Auto-generated method stub
Scanner sc = new Scanner(System.in);
n = sc.nextInt();
m = sc.nextInt();
T = sc.nextInt();
for (int i = 0; i < n; i++) {
map[i] = sc.next().toCharArray();
}
int sx = 0, sy = 0, ex = 0, ey = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (map[i][j] == 'S') {
sx = i;
sy = j;
}
if (map[i][j] == 'D') {
ex = i;
ey = j;
}
}
if ((sx + sy + ex + ey + T) % 2 != 1) {// 奇偶性剪枝
System.out.println("NO");
} else {
ok = false;
dfs(sx, sy, 0);
if (ok) {
System.out.println("Yes");
} else {
System.out.println("No");
}
}
}
}
}