Skip to content

BFS

应用

  • 寻找最短路径,由于水波扩散特点,bfs找到的路径一定是最短路径。

例题

多源bfs(折半搜索)

  • 起点和终点同时搜索,复杂度2n->2*2n/2,通过是否相遇判断是否存在路径
  • 在已知初始状态和结束状态时使用
  • 对于指数级增长的bfs很有用,但对于一些类型(如棋盘移动)使用去重在大部分情况就足够了(color)

  • 两种实现方式:

  • 合用一个队列,适合正反两个BFS平衡的情况,正向搜索和逆向搜索交替进行
  • 分成两个队列,正向和逆向队列分开,适合反正两个BFS不平衡的情况,可以让子状态(队列中元素数目,比较两个queue的size)较少的进行BFS扩展下一层

  • 对于路径不存在(两边搜索不会相遇)的情况,当一方搜索结束时就可以终止了(说明不可能再相遇了)

  • 417. 太平洋大西洋水流问题

  • 286. 墙与门

  • 301. 删除无效的括号

  • 特殊,用queue超时,每到一层先直接检查所有元素,用两个set存储方便遍历
  • P3067Balanced Cow Subsets G - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
  • 本题是dfs但是思想类似,拆分为一半,对两半分别进行搜索

  • 一个有\(20\)头奶牛,那么考虑对于每一头奶牛来说有\(3\)种状态,放在一组,放在另一组,不放任何一组,如果暴力枚举时间复杂度为\(O(3^n)>1E9\),无法接受。考虑将\(n\)头奶牛分为两半,每组分别暴力求解,时间复杂度\(O(3^\frac{n}{2})\)可以通过。

  • 注意这样的到的可能重复(对一个子集的不同平衡分割),还需要用状态压缩表示子集进行去重

1.带有状态压缩参数的bfs

  • 847.访问所有节点的最短路径

  • 特殊bfs,可以重复访问节点,防止重复访问不能仅仅记录一个点是否访问过,而应记录的是,在访问过的节点相同时,是否重复去访问一个节点。

c++ int shortestPathLength(vector<vector<int>>& graph) { int n=graph.size(); queue<tuple<int,int,int>>q;//记录当前位置,点的访问状态,路径长度(这个也可以不记录在这里,可以加一层循环来计算) vector<vector<bool>>seen(n,vector<bool>(1<<n));//判断是否到过,用访问状态和当前点共同判断。 for(int i=0;i<n;i++)//初始化 { q.emplace(i,1<<i,0); seen[i][1<<i]=true; } int ans=0; while(!q.empty()) { auto [u,mask,dist]=q.front(); q.pop(); if(mask==(1<<n)-1)//结束 { ans=dist; break; } for(int v:graph[u]) { int mask_v=mask|(1<<v); if(!seen[v][mask_v])//去重 { q.emplace(v,mask_v,dist+1); seen[v][mask_v]=true; } } } return ans; }

与双端队列结合

与优先队列结合

  • poj2227:一个棋盘,给每个店的高度,二维接雨水
  • 从边界向内部进行反向bfs
  • 现将边界节点加入到优先队列(小根堆),每次出队的为边界中最小的点
  • 出队之后检查周边还没有加入过队列的点,作为移除优先队列front节点后新的边界,如果比移除的点高,则不能存水,否则可以存水,计算差值并将高度修改为原先移除节点的高度
while(!q.empty()) {
        tp=q.top();
        q.pop();
        for(int i=0;i<4;i++) {
            int xx=tp.x+to[i][0],yy=tp.y+to[i][1];
            if( 0<=xx&&xx<n && 0<=yy&&yy<m && !vis[xx][yy]) {
                vis[xx][yy]=1;
                t.x=xx,t.y=yy,t.h=mp[xx][yy];
                if(t.h<tp.h) ans+=(tp.h-t.h),t.h=tp.h;
                q.push(t);
            }
        }
    }

A*启发式搜索

  • 常用于求解确定起点、终点的最短路径

  • 普通BFS:只看起点

  • A*:既看起点,又看终点

  • 贪心最优搜索并不一定能得到最优结果“只看终点不看起点”(如使用曼哈顿距离估计,在有障碍图上不一定可以获取正确结果)

  • A算法既看起点也看终点*,能得到最优解,估价函数f(i)=g(i)+h(i)

  • g(i)表示从s到i的代价,由dj保证最优性

  • h(i)表示从i到t的代价,由贪心计算
  • 由于在终点有h(i)=0,即f(i)=g(i)保证了算法的正确性

  • h函数设计

  • 4方向移动:abs(i.x-t.x)+abs(i.y-t.y)

  • 8方向:max{abs(i.x-t.x),abs(i.y-t.y)}
  • 任意:sqrt((i.x-t.x)^2+(i.y-t.y)^2)

  • 原则:

  • g与h采用相同的距离计算方式

  • h要正确反映远近关系
  • h(i)要小于等于i-t的实际最短距离

  • ```c++ truct node { // 使用A*时所需的结构体 int x; double v;

    bool operator<(node a) const { return v + f[x] > a.v + f[a.x]; } };

```

  • 在dj算法的基础上进行修改

例题

  • 八数码h

  • h函数可以定义为,不在应该在的位置的数字个数。

  • f=已经移动的次数+h()
  • 依据f放入优先队列中进行类bfs

  • k短路问题

  • image-20230826193312682

  • ```c++ #include

    using namespace std;

    // #define int long long

    typedef long long ll; typedef unsigned long long ull; typedef pair PII;

    const int N = 1e4 + 9; const int INF = 0x3f3f3f3f;

    int n, m, st, ed, k; int dist[N], cnt[N];

    struct Edge { int u, w; // 出点,权值 bool operator < (const Edge &u) const { return w > u.w; } }; // 存边用的结构体

    vector g[N]; // 正向建图 vector rg[N]; // 反向建图

    struct Node { int f, g, idx; // 估价值 + 真实值,真实值,编号 bool operator<(const Node &u) const { return f > u.f; } };

    void dijkstra() // 求出终点到起点时扩展过的所有点的路(被用来当作估价函数,因为其值不会小于实际距离) { priority_queue heap; bitset vis; memset(dist, 0x3f, sizeof dist); heap.push({ed, 0}); dist[ed] = 0;

    while (!heap.empty())
    {
        auto t = heap.top(); heap.pop();
        int u = t.u;
        if (vis[u]) continue ;
        vis[u] = true;
    
        for (auto y : rg[u])
        {
            int v = y.u, w = y.w;
            if (dist[v] > dist[u] + w)
            {
                dist[v] = dist[u] + w;
                heap.push({v, dist[v]});
            }   
        }
    }
    

    }

    int astar() { priority_queue heap; heap.push({dist[st], 0, st}); // 初始点到终点的f值为dist[st] + 0,起点到自身的距离为 0

    while (!heap.empty())
    {
        auto t = heap.top(); heap.pop();
    
        int idx = t.idx, distance = t.g;    // 取出编号和st点到该点的实际距离
        cnt[idx] ++;    // 小优化
        if (cnt[ed] == k) return distance;  // 如果终点遍历了 k 次,直接return掉
    
        for (auto y : g[idx])
        {
            int v = y.u, w = y.w;
            if (cnt[v] < k)
                heap.push({distance + w + dist[v], distance + w, v});   // 该状态仍可扩展
        }
    }
    return -1;  // 无解时直接return
    

    }

    signed main() { // ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); cin >> n >> m; for (int i = 1; i <= m; i ++) { int u, v, w; cin >> u >> v >> w; g[u].push_back({v, w}); // 正向建图 rg[v].push_back({u, w}); // 反向建图 }

    cin >> st >> ed >> k;
    if (st == ed) k ++;                 // 题目要求,至少要有一条路
    
    dijkstra();
    
    // cout << dist[st] << '\n';
    // if (dist[st] == INF) cout << -1 << '\n';
    cout << astar() << '\n';            // 输出 k 短路
    
    return 0;
    

    } ```

DFS

例题

回溯

记忆化搜索

思想

动态规划是一种自底向上的思想,而记忆化搜索是一种自顶向下的思想。 当便利顺序难以确认时,使用记忆化搜索由dfs自动决定 逻辑上相比动态规划更为清晰简单。

剪枝

例题

剪枝

思路

  • 搜索顺序剪枝:先排序再搜搜
  • 可行性剪枝:条件不合法则不再继续
  • 最优性剪枝:与已经得到的最优解进行比较
  • 排除冗余
  • 记忆化搜索
  • 优化搜索顺序

实例

  • 棋盘问题
  • 如使用曼哈顿距离估计图上剩余最短距离,如果当前距离加上最短距离都超了那么就剪掉
  • 奇偶判断,图上两点间所有路径奇偶性一定与其曼哈顿距离是一致的(绕路不会改变奇偶性)
    • 通常在main中而不是dfs中完成
  • 背包问题
  • 从大到小的开始装

  • 数独

  • 从未知量少的行开始搜索

例题

优化

IDDFS

  • BFS与DFS结合,减少空间浪费(BFS)和无效搜索(DFS)
  • 限制深度的dfs
  • 每次从第一层进行有层数上限的dfs
  • 第一次搜索1
  • 第二次1~2
  • ...
  • 第k次1~k

IDA*

  • 与A*相比IDA*的应用更为广泛

  • 在IDDFS基础上添加预测,如果当前深度+未来需要的步数>深度限制则立即返回false(使用估价函数剪枝)

  • c++ const int N = 100; //最大层次 int num[N]; //记录一条路径上的数字,num[i]是路径上第i层的数字 int n, depth; bool dfs(int now, int d) { //now:当前路径走到的数字,d:now所在的深度 if (d > depth) return false; //当前深度大于层数限制 if (now == n) return true; //找到目标。注意:这一句不能放在上一句前面 if (now << (depth - d) < n) //剪枝:剩下的层数用最乐观的倍增也不能达到n return false; num[d] = now; //记录这条路径上第d层的数字 for(int i = 0; i <= d; i++) { //遍历之前算过的数,继续下一层 if (dfs(now + num[i], d + 1)) return true; //加 else if (dfs(now - num[i], d + 1)) return true; //减 } return false; } int main() { while(~scanf("%d", &n) && n) { for(depth = 0;; depth++) { //IDDFS:每次限制最大搜索depth层 memset(num, 0, sizeof(num)); if (dfs(1, 0)) break; //从数字1开始,当前层0 } printf("%d\n", depth); } return 0; }

例题

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

int main()
{
    vector<vector<int>>g(4,vector<int>(4,0));
    for(int i=0;i<4;i++){
        for(int j=0;j<4;j++){
            char c;
            cin>>c;
            if(c=='B')
                g[i][j]=1;
            else if(c=='W')
                g[i][j]=2;
        }
    }
    auto check=[&]()->bool{
        bool res=false;
        for(int i=0;i<4;i++){
            if(g[i][0]==g[i][1]&&g[i][1]==g[i][2]&&g[i][2]==g[i][3])
                res=true;
            if(g[0][i]==g[1][i]&&g[1][i]==g[2][i]&&g[2][i]==g[3][i])
                res=true;
        }
        if(g[0][0]==g[1][1]&&g[1][1]==g[2][2]&&g[2][2]==g[3][3])
            res=true;
        if(g[0][3]==g[1][2]&&g[1][2]==g[2][1]&&g[2][1]==g[3][0])
            res=true;
        return res;
    };
    set<vector<vector<int>>>st;
    vector<vector<int>>moves={{1,0},{-1,0},{0,1},{0,-1}};
    function<bool(int,int,int)>dfs;
    dfs=[&](int sum,int target,int n)->bool{
        vector<vector<int>>state=g;
        state[0].push_back(n);
        if(sum>target||st.count(state))
            return false;
        st.insert(state);
        if(check())
            return true;
        for(int i=0;i<4;i++){
            for(int j=0;j<4;j++){
                if(g[i][j]==0){
                    for(auto move:moves){
                        int x=i+move[0];
                        int y=j+move[1];
                        if(x>=0&&x<4&&y>=0&&y<4){
                            if(g[x][y]==n+1){
                                swap(g[i][j],g[x][y]);
                                if(dfs(sum+1,target,n^1))
                                    return true;
                                swap(g[i][j],g[x][y]);
                            }
                        }
                    }
                }
            }
        }
        return false;
    };
    int res;
    for(int i=0;i<INT_MAX;i++){
        st.clear();
        if(dfs(0,i,0)||dfs(0,i,1)){
            res=i;
            break;
        }
    }
    cout<<res<<endl;
    return 0;