搜索

搜索

所谓搜索就是列举所有的可能,也是一种暴力的算法。
根据搜索优先级,一般分为深度优先和广度优先。
深度优先搜索(Depth First Search):一条路走到底,走不通再回头。
广/宽度优先搜索(Breadth First Search):一层一层的搜索。

搜索问题解决步骤

  1. 确定问题:连通块(能不能走通),最短路,方案数...
    不同问题的解决方法在本质上就可能不同,所以一定要明确自己的问题。

  2. 确定方法:
    常用搜索无非 dfs/bfs,不过其时间复杂度较高,所以时常伴随着剪枝。
    dfs:一般用于盲目搜索,一直寻找下去,直到满足结果或者没有答案就退出。
    bfs:是先搜索附近的,一圈那一圈的向外扩展,所以整体时间复杂度不高,并且由于先扩展的区域被标记了,后面再次遍历到该点位的时候,不会二次入队,于是可以很容易求得最短路问题。
    其实从本质上 bfs 能解决的问题 dfs 都能解决,反之相同,但是对于某些问题而言,dfs/bfs效率会有很大的不同,于是就限制了搜索方式。
    dfs 能处理的问题:连通块(能不能走通),方案数...
    bfs 能处理的问题:连通块(能不能走通),最短路,方案数...

  3. 优化剪枝
    搜索算法的时间复杂度是指数级的,很容易超时,为了降低时间复杂度,可以使用剪枝对其进行优化。
    dfs 的结果就是一棵搜索树,而剪枝就是剪去构建这棵树的过程中不必要构建的子树部分。
    剪枝情况一般出现在 dfs中,bfs一般通过队列实现求最短路步数问题,在入队的同时标记处理,这也就不会出现同一个点二次入队的情况,所以无需剪枝。

例题
P1451 求细胞数量
P1706 全排列问题

点击查看代码
#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;
}

P9241 飞机降落

点击查看代码
#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):将当前位置的结果记录下来,以后再次搜索该位置结果的时候直接返回存储的结果
  • 奇偶性判断:提前确定奇数与偶数的情况下的不同状态,看能否提前处理。

例题
P1434 [SHOI2002] 滑雪

点击查看代码
#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;
}

P1433 吃奶酪

点击查看代码
#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:设定搜索的层数,迭代加深
posted @ 2023-06-03 16:20  HelloHeBin  阅读(207)  评论(0编辑  收藏  举报