搜索算法的时间复杂度大多是指数级的,难以满足对程序运行时间的限制要求,为使降低时间复杂度,对深度优先搜索可以进行一种优化的基本方法——剪枝。
搜索的进程可以看做是从树根出发,遍历一颗倒置树(搜索树)的过程,所谓剪枝,就是通过某些判断,避免一些不必要的遍历过程,形象的说,就是减去搜索树中的某些枝条。
显而易见,应用剪枝优化的核心问题是设计剪枝判断方法,即确定哪些枝条舍弃哪些枝条保留,设计出好的剪枝判断方法,可以使得程序运行时间大大缩短,否则会适得其反。
剪枝的原则:正确、准确、高效
BFS的剪枝通常是判重,如果搜索到某一层时,出现重复的状态,就剪枝。例如经典的八数码问题,核心问题就是去重,把曾经搜过的八数码的组合剪去。
常见的几种剪枝方式:
对当前状态进行检查,如果当前条件不合法就不再继续,直接返回。
可行性剪枝也叫上下界剪枝,其是指在搜索过程中,及时对当前状态进行检查,若发现分支已无法到达递归边界,就执行回溯。
例题:求从n
个数中选k
个数,使得和是target
的方案数
如:从0~29这30个数中,选出8个数使得和为200,康康有几种方案
思路:dfs深搜,每个数有两种选择:选或者不选,要是直接暴力搜索,很容易就超时了!!!因此,要进行必要的剪枝优化。
cnt > k
就没必要再搜索了sum > target
也没必要再搜索了【代码实现】
#include
#include
#include
using namespace std;
const int N = 40;
int a[N];
int n, k, target, ans;
// 从1~29这30个数中,选出8个数使得和为200,康康有几种方案
void dfs(int u, int cnt, int now)
{
// 可行性剪枝
if(cnt > k || now > target) return ;
// 递归出口
if(u == n)
{
if(cnt == k && now == target)
ans ++;
return ;
}
// 选或者不选
dfs(u + 1, cnt + 1, now + a[u]);
dfs(u + 1, cnt, now);
}
int main()
{
cin >> n;
for (int i = 0; i <= n; i ++ ) a[i] = i + 1;
target = 200;
k = 8;
dfs(0, 0, 0);
cout << ans;
return 0;
}
搜索树有多个层次和分支,不同的搜索顺序会产生不同的搜索树形态,复杂度也相差很大。
在最优化问题的搜索过程中,如果当前花费的代价已超过前面搜索到的最优解,那么本次搜索已经没有继续进行下去的意义,此时停止对当前分支的搜索进行回溯。
比如在求解迷宫最短路(最小步数)的时候(这里用的是dfs
实现),如果发现当前的步数已经超过了当前最优解,那从当前状态开始的搜索都是多余的,因为这样搜索下去永远都搜不到更优的解。通过这样的剪枝,可以省去大量冗余的计算。
此外,在搜索是否有可行解的过程中,一旦找到了一组可行解,后面所有的搜索都不必再进行了,这算是最优性剪枝的一个特例。
例题:有一个n x m大小的迷宫。其中字符’S’表示起点,字符’T’表示终点,字符’*’ 表示墙壁,字符’.’ 表示平地。你需要从’S’出发走到’T’,每次只能向上下左右相邻的位置移动,并且不能走出地图,也不能走进墙壁。保证迷宫至少存在一种可行的路径,输出’S’走到’T’的最少步数。
通常我们会用BFS
(广度优先搜索)解决这个问题,搜到的第一个结果就是答案。
现在我们考虑用DFS(深度优先搜索)来解决这个问题,第一个搜到的答案ans并不一定是正解(存在多中可能),但是正解一定小于等于ans.于是如果当前步数大于等于ans就直接剪枝,并且每找到一个可行的答案,都会更新ans.
【代码实现】
#include
#include
#include
using namespace std;
const int N = 50, INF = 0x3f3f3f3f;
char g[N][N];
bool st[N][N];
int ans = INF;// 求的是最小, 初始值置为无穷大
int n, m;
int x1, y1;
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
void dfs(int x, int y, int step)
{
// 最优先剪枝
if(step >= ans) return ;
// 递归出口
if(g[x][y] == 'T')
{
ans = min(ans, step);
return ;
}
for(int i = 0; i < 4; i ++)
{
int a = x + dx[i], b = y + dy[i];
if(a < 0 || a >= n || b < 0 || b >= m) continue;
if(st[a][b]) continue;
if(g[a][b] == '*') continue;
st[a][b] = true;
dfs(a, b, step + 1);
st[a][b] = false;
}
}
int main()
{
cin >> n >> m;
for (int i = 0; i < n; i ++ )
for (int j = 0; j < m; j ++ )
{
cin >> g[i][j];
if(g[i][j] == 'S')
{
x1 = i, y1 = j;
}
}
st[x1][y1] = true;
dfs(x1, y1, 0);
cout << ans;
return 0;
}
对于某一些特定的搜索方式,一个方案可能会被搜索很多次,这样是没必要的。
在搜索过程中,若能判断从搜索树当前节点上沿某几条不同分支到达的子树是相同的,那么只需对其中一条分支执行搜索。(避免重复计算)
例题:
思路:我们规定选出来的数的位置是递增的,在搜索时,用一个参数记录上一次选数的位置,那么本次选择我们从这个数之后开始选取,这样方案就不会重复了。
【代码实现】
#include
#include
#include
using namespace std;
const int N = 40;
int a[N];
bool st[N];
int n, k, target, ans;
void dfs(int cnt, int pos, int now)
{
// 可行性剪枝
if(cnt > k || now > target) return ;
// 递归出口
if(cnt == k && now == target)
{
ans ++;
return ;
}
for(int i = pos; i < n; i ++)
{
if(!st[i])
{
st[i] = true;
dfs(cnt + 1, i + 1, now + a[i]);
st[i] = false;
}
}
}
int main()
{
cin >> n;
for (int i = 0; i <= n; i ++ ) a[i] = i + 1;
target = 6;
k = 3;
dfs(0, 0, 0);
cout << ans;
return 0;
}
【题目链接】P1433 吃奶酪 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
思路:暴力枚举所有可能的路线(全排列形),求总距离最短的方案即可。
【代码实现】
#include
#include
#include
#include
#define x first
#define y second
using namespace std;
typedef pair<double, double> PII;
const int N = 1010;
PII q[N];
double dist[N][N];
bool st[N];
int n;
double ans = 1e14;
double calc(int i, int j)
{
return sqrt((q[i].x - q[j].x)*(q[i].x - q[j].x) + (q[i].y - q[j].y)*(q[i].y - q[j].y));
}
void dfs(int u, int past, double sum)// u:已经走过的点,past:上一个走过的点 sum:当前的距离
{
// 最优性剪枝
if(sum >= ans) return ;
// 可行性剪枝
if(u > n) return ;
if(u == n)
{
ans = min(ans, sum);
return ;
}
// 枚举所有可能情况
for (int i = 1; i <= n; i ++ )
{
if(!st[i])
{
st[i] = true;
dfs(u + 1, i, sum + dist[past][i]);
st[i] = false;
}
}
}
int main()
{
scanf("%d", &n);
for (int i = 1; i <= n; i ++ )
{
double x, y;
scanf("%lf%lf", &x, &y);
q[i] = {x, y};
}
// 预处理两点之间的距离
q[0] = {0, 0};
for (int i = 0; i <= n; i ++ )
for (int j = 0; j <= n; j ++ )
dist[i][j] = calc(i, j);
dfs(0,0 ,0);
printf("%.2lf", ans);
return 0;
}
【题目链接】[P1025 NOIP2001 提高组] 数的划分 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
思路:重复性剪枝
【代码实现】
#include
#include
#include
#include
#define x first
#define y second
using namespace std;
const int N = 210;
int way[N];
bool st[N];
int n, k, ans;
void dfs(int u, int cnt, int sum, int pos)
{
if(cnt > k || sum > n) return ;
if(cnt == k)
{
if(sum == n)
{
ans ++;
// for (int i = 0; i < k; i ++ ) printf("%d ", way[i]);
// puts("");
}
return ;
}
// 这里是 n - sum,若果是n的活会超时
for(int i = pos; i <= n - sum; i ++)
{
// way[u] = i;
dfs(u + 1, cnt + 1, sum + i, i);
// way[u] = 0;
}
}
int main()
{
scanf("%d%d", &n, &k);
dfs(0, 0, 0, 1);
printf("%d", ans);
return 0;
}
题目
给出N个正整数组成的数组A,求能否从中选出若干个,使他们的和为K。如果可以,输出:“Yes”,否则输出"No"。输入
第1行:2个数N, K, N为数组的长度, K为需要判断的和(2 <= N <= 20,1 <= K <= 10^9)
第2 - N + 1行:每行1个数,对应数组的元素A[i] (1 <= A[i] <= 10^6)输出
如果可以,输出:“Yes”,否则输出"No"。输入样例
5 13
2
4
6
8
10输出样例
No
【代码实现】
超时:
对于每一个数:选或者不选
#include
#include
#include
#include
#define x first
#define y second
using namespace std;
const int N = 15;
int a[N];
bool st[N];
int n, m;
bool flag;
void dfs(int u, int sum)
{
if(flag) return ;
if(u == n + 1)
{
if(sum == m)
{
flag = true;
}
return ;
}
dfs(u + 1, sum + a[u]);
dfs(u + 1, sum);
}
int main()
{
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i ++ ) cin >> a[i];
dfs(1, 0);
if(flag) puts("YES");
else puts("NO");
return 0;
}
优化搜索顺序:每次 dfs 在当前结点的下一个结点开始搜索即可
【代码实现】
#include
#include
#include
#include
#define x first
#define y second
using namespace std;
const int N = 1e5 + 10;
int a[N];
bool st[N];
int n, m;
bool flag;
void dfs(int pos, int sum)
{
if(flag || pos > n) return ;
if(sum == m)
{
flag = true;
return ;
}
for(int i = pos; i <= n; i ++)
{
if(!st[i])
{
st[i] = true;
dfs(i + 1, sum + a[i]);
st[i] = false;
}
}
}
int main()
{
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i ++ ) cin >> a[i];
dfs(1, 0);
if(flag) puts("YES");
else puts("NO");
return 0;
}
【题目链接】[P1118 USACO06FEB]Backward Digit Sums G/S - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
暴力法。对1~N这N个数做从小到大的全排列,对每个全排列进行三角形的计算,判断是否等于N。
对每个排列进行三角形计算,需要 O(N2)次,总复杂度是O(N!N2)的,显然会超时。
枚举找一下规律:
假如初始数组长度为n
a b c d
a+b b+c c+d
a+2b+c b+2c+d
a+3b+3c+d (sum)
那么我们来观察一下最后一行的数的系数: 1 3 3 1
没错,就是杨辉三角的第i行(如下):
1
1 1
1 2 1
1 3 3 1
1 4 6 4 1
...
样例:
a b c d
3 1 2 4(1 3 3 1)
3 3 6 4
思路:杨辉三角形 + 剪枝,当当前的和大于目标值时不用再计算了,剪枝
【代码实现】
#include
#include
#include
#include
#define x first
#define y second
using namespace std;
const int N = 210;
int f[N][N];
int way[N];
bool st[N];
int n, m;
void dfs(int u, int sum)
{
// 剪枝
if(sum > m) return ;
if(u == n + 1)
{
if(sum == m)
{
for(int i = 1; i <= n; i ++) printf("%d ", way[i]);
exit(0);
}
return ;
}
// 递归全排列类型
for(int i = 1; i <= n; i ++)
{
if(!st[i])
{
st[i] = true;
way[u] = i;
dfs(u + 1, sum + i * f[n][u]);
st[i] = false;
way[u] = 0;
}
}
}
int main()
{
scanf("%d%d", &n, &m);
// 预处理杨辉三角形(系数)
f[1][1] = 1;
for(int i = 2; i <= n; i ++)
for(int j = 1; j <= i; j ++)
f[i][j] = f[i - 1][j] + f[i - 1][j - 1];
dfs(1, 0);
return 0;
}