搜索
搜索
所谓搜索就是列举所有的可能,也是一种暴力的算法。
根据搜索优先级,一般分为深度优先和广度优先。
深度优先搜索(Depth First Search):一条路走到底,走不通再回头。
广/宽度优先搜索(Breadth First Search):一层一层的搜索。
搜索问题解决步骤
-
确定问题:连通块(能不能走通),最短路,方案数...
不同问题的解决方法在本质上就可能不同,所以一定要明确自己的问题。 -
确定方法:
常用搜索无非 dfs/bfs,不过其时间复杂度较高,所以时常伴随着剪枝。
dfs:一般用于盲目搜索,一直寻找下去,直到满足结果或者没有答案就退出。
bfs:是先搜索附近的,一圈那一圈的向外扩展,所以整体时间复杂度不高,并且由于先扩展的区域被标记了,后面再次遍历到该点位的时候,不会二次入队,于是可以很容易求得最短路问题。
其实从本质上 bfs 能解决的问题 dfs 都能解决,反之相同,但是对于某些问题而言,dfs/bfs效率会有很大的不同,于是就限制了搜索方式。
dfs 能处理的问题:连通块(能不能走通),方案数...
bfs 能处理的问题:连通块(能不能走通),最短路,方案数... -
优化剪枝
搜索算法的时间复杂度是指数级的,很容易超时,为了降低时间复杂度,可以使用剪枝对其进行优化。
dfs 的结果就是一棵搜索树,而剪枝就是剪去构建这棵树的过程中不必要构建的子树部分。
剪枝情况一般出现在 dfs中,bfs一般通过队列实现求最短路步数问题,在入队的同时标记处理,这也就不会出现同一个点二次入队的情况,所以无需剪枝。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1e6 + 10;
int n, a[N]; // a[i] 表示第 i 个凳子坐的是谁
bool st[N]; // st[i] 表示编号为 i 的人是否落座
// 1. dfs(u): 表示该第 u 个凳子坐人
void dfs(int u) {
if (u > n) { // 2. 出口
for (int i = 1; i <= n; i++)
cout << setw(5) << a[i]; cout << endl;
// printf("%5d",a[i]); puts("");
return;
}
// 3. 枚举所有情况
for (int i = 1; i <= n; i++) {
if (!st[i]) { // 如果没有坐下
st[i] = 1, a[u] = i, dfs(u + 1); // 坐下
st[i] = 0;
}
}
}
int main() {
cin >> n; dfs(1); // 从第 1 个凳子坐人
return 0;
}
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N = 12;
int t, n, m, st[N], ans[N];
// st[i] : 编号为 i 的人是否坐下
// ans[i] : 第 i 个凳子坐的是谁
// dfs(u) : 该 第 u 个凳子坐人
struct q {
int T, D, L;
} a[N];
bool flag = 0;
void dfs(int u) {
if (flag) return; // 排除等效冗余
if (u > n) { // 如果 n 个凳子都坐了人,就该输出答案
int p = 0, time = 0;
for (int i = 1; i <= n; i++) {
int j = ans[i];
if (time > a[j].T + a[j].D) break;
if (time < a[j].T) time = a[j].T;
time += a[j].L, p++;
}
if (p == n) flag = 1;
return;
}
for (int i = 1; i <= n; i++) {
if (!st[i]) { // 如果编号为 i 的选手没有落座
st[i] = 1, ans[u] = i, dfs(u + 1); // 坐下
st[i] = 0; // 不坐, 回溯 恢复现场
}
}
}
int b[N];
void solve() {
for (int i = 1; i <= n; i++) b[i] = i;
do {
int p = 0, time = 0;
for (int i = 1; i <= n; i++) {
int j = b[i];
if (time > a[j].T + a[j].D) break;
if (time < a[j].T) time = a[j].T;
time += a[j].L, p++;
}
if (p == n) {flag = 1; break; }
} while (next_permutation(b + 1, b + 1 + n));
}
int main() {
scanf("%d", &t);
while (t--) {
scanf("%d", &n);
for (int i = 1, T, D, L; i <= n; i++) {
scanf("%d%d%d", &T, &D, &L);
a[i] = {T, D, L};
}
flag = 0;
// dfs(1); // 写法 1
solve(); // 写法 2
printf("%s\n", flag ? "YES" : "NO");
}
return 0;
}
P1219 八皇后
P1746 离开中山路
P1379 八数码难题
搜索剪枝
普通的搜索会搜到很多不必要的情况,那么可以想办法剪掉这部分,于是剪枝出现了。
剪枝:就是把不会产生答案的或者不必要的枝条剪掉。
剪枝原则
- 正确性:保证不丢失正解的结果;
- 准确性:尽量剪去多余枝条;
- 高效性:降低时间复杂度的同时,提高判断操作本身的效率。
DFS优化技巧
- 优化搜索顺序:确定一个较优的搜索顺序,例如求最小值,就从最小值先搜索,效率或许更为优秀。
- 排除等效冗余:当多个枝条具有完全相同的结果的时候,只选择一个即可。
- 可行性剪枝:当前结果不可行,就不再继续搜索。
- 最优性剪枝:当前结果与已有结果相比,不是最优了,就不必继续搜索。
- 记忆化搜索(DP):将当前位置的结果记录下来,以后再次搜索该位置结果的时候直接返回存储的结果
- 奇偶性判断:提前确定奇数与偶数的情况下的不同状态,看能否提前处理。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 110;
int n, m, s[N][N], dp[N][N], ans;
int d[][2] = {-1, 0, 0, 1, 1, 0, 0, -1};
// dp[i][j] : 表示 从 (i,j) 开始的最长路径
// 1. dfs(x,y) : 表示 从 (x,y) 开始的最长路径
int dfs(int x, int y) {
// 2. 出口
if (dp[x][y]) return dp[x][y]; // 记忆化剪枝
dp[x][y] = 1; // 自身开始计数
for (int i = 0; i < 4; i++) {
int a = x + d[i][0], b = y + d[i][1];
if (a < 1 || a > n || b < 1 || b > m || s[a][b] >= s[x][y]) continue;
// if (a >= 1 && a <= n && b >= 1 && b <= m && s[a][b] < s[x][y])
dp[x][y] = max(dp[x][y], dfs(a, b) + 1);
}
return dp[x][y];
}
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++) cin >> s[i][j];
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++) ans = max(ans, dfs(i, j));
cout << ans;
return 0;
}
点击查看代码
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 25;
int n, st[N];
struct T {
double x, y;
} t[N];
double ans = 1e18;
double dis(int a, int b) {
double x = t[a].x - t[b].x, y = t[a].y - t[b].y;
return sqrt(x * x + y * y);
}
// dfs(u,k,s): 当前位置 u, 已选择 k, 经过距离 s
void dfs(int u, int k, double s) {
if (s >= ans) return; // 最优性剪枝
if (k >= n) {
ans = min(ans, s); return;
}
for (int i = 1; i <= n; i++) {
if (!st[i]) {
st[i] = 1, dfs(i, k + 1, s + dis(u, i)); // 选
st[i] = 0; // 不选
}
}
}
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
double x, y; cin >> x >> y;
t[i] = {x, y};
}
dfs(0, 0, 0);
cout << fixed << setprecision(2) << ans;
return 0;
}
P1025 [NOIP2001 提高组] 数的划分
P2383 狗哥玩木棒
P5194 [USACO05DEC] Scales S
优化技巧
- 双端队列:将最优解放在队首,每次从队首开始扩展。
- 优先队列:每次用最优来扩展
- 双向BFS:分别从起点和终点进行搜索,相遇就结束。
- A* :设定估价函数
- IDFS:设定搜索的层数,迭代加深