#AcWing:搜索与图论系列

1 DFS

2 BFS

宽搜问题最重要的是可以寻找最短路(当边权相同时)。深搜保证可以搜到结果但是不一定是最短路!
在这里插入图片描述
当边权值不相同时,选择其他最短路算法。

2.1 走迷宫

从左上角走到右下角最短路径长度

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

typedef pair<int, int> PII;
const int N = 110;
int g[N][N], d[N][N];
int n, m;

int bfs()
{
    queue<PII> q;
    q.push({0, 0});
    memset(d, -1, sizeof d);
    d[0][0] = 0;
    int dx[] = {-1, 1, 0, 0}, dy[] = {0, 0, -1, 1}; //上 下 左 右
    
    while(!q.empty())
    {
        PII t = q.front();
        q.pop();
        for(int i = 0; i < 4; i++)
        {
            int tx = t.first + dx[i];
            int ty = t.second + dy[i];
            //cout << tx << " " << ty << endl;
            if(tx >= 0 && tx < n && ty >= 0 && ty < m && g[tx][ty] == 0 && d[tx][ty] == -1)
            {
                d[tx][ty] = d[t.first][t.second] + 1;
                //cout << tx << " " << ty << endl;
                q.push({tx, ty});
            }
        }
    }
    return d[n - 1][m - 1];
    
}
int main()
{
    cin >> n >> m;
    for(int i = 0; i < n; i++)
        for(int j = 0; j < m; j++)
            cin >> g[i][j];
    cout << bfs();
    return 0;
}
  • 上下左右向量简便写法:int dx[] = {-1, 1, 0, 0}, dy[] = {0, 0, -1, 1}; //上 下 左 右
  • 坐标:typedef pair<int, int> PII; q.push({tx, ty});
  • 距离记录:d[tx][ty] = d[t.first][t.second] + 1;

2.2 八数码

思路是:将每种情况的小方格看成是一个状态,那么转化为已知开始、结束状态和转移方式,求转移的最小步骤?
几个难点:

  • 如何表示状态:字符串,like “12345678x”
  • 如何状态转移:将字符串转化成九宫格,找到要交换的字符交换,再转换回来
  • 如何统计距离:在上一个状态距离值上+1, 用哈希表存储
#include<bits/stdc++.h>
using namespace std;

int bfs(string start)
{
    string end = "12345678x";
    queue<string> q;
    unordered_map<string, int> d;
    q.push(start);
    d[start] = 0;
    
    int dx[] = {-1, 1, 0, 0}, dy[] = {0, 0, -1, 1}; 
    while(!q.empty())
    {
        string t = q.front();
        q.pop();
        int dis = d[t];
        if(t == end)
            return dis;
        //状态转移
        
        for(int i = 0; i < 4; i++)
        {
            int k = t.find('x');
            int tx = k / 3, ty = k % 3;
            int a = tx + dx[i], b = ty + dy[i];
            if(a < 3 && a >= 0 && b < 3 && b >= 0)
            {
                swap(t[k], t[a * 3 + b]);
                if(!d.count(t))
                {
                    d[t] = dis + 1;
                    q.push(t);
                }
                swap(t[k], t[a * 3 + b]);
            }
        }
    }
    return -1;
}
int main()
{
    char ch;
    string start;
    for(int i = 0; i < 9; i++)
    {
        cin >> ch;
        start += ch;
    }
    
    cout << bfs(start);
    return 0;
}

3 树和图的BFS

847 图中点的层次

题目很简单,找到1到n的最短路径(边权为1),还是和走迷宫一样的遍历,只不过存储换成了图。

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

const int N =  1e5 + 10;
int n, m, a, b;
int d[N];
int h[N], e[N], ne[N], idx;

void add(int a, int b)
{
    e[idx] = b;
    ne[idx] = h[a];
    h[a] = idx;
    idx++;
}

int bfs()
{
    queue<int> q;
    q.push(1);
    memset(d, -1, sizeof d);
    d[1] = 0;
    
    while(!q.empty())
    {
        int t = q.front();
        q.pop();
        
        for(int i = h[t]; i != -1; i = ne[i])
        {
            if(d[e[i]] == -1)
            {
                q.push(e[i]);
                d[e[i]] = d[t] + 1;
            }
        }
    }
    return d[n];
}

int main()
{
    cin >> n >> m;
    memset(h, -1, sizeof h);
    while(m--)
    {
        cin >> a >> b;
        add(a, b);
    }
    cout << bfs();
    return 0;
}
  • 图的存储int h[N], e[N], ne[N], idx; `void add(int a, int b){e[idx] = b; ne[idx] = h[a]; h[a] = idx;idx++;}
  • 寻找邻接节点for(int i = h[t]; i != -1; i = ne[i])

4 树和图的DFS

846 树的重心

对树进行深度优先遍历可以求树的子树大小。
在这里插入图片描述

#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
const int M = N * 2;
int n, a, b, ans = N;
int h[N], e[M], ne[M], idx;
bool st[N];


void add(int a, int b)
{
    e[idx] = b;
    ne[idx] = h[a];
    h[a] = idx;
    idx++;
}

//返回以u为根的子树数量和
int dfs(int u)
{
    st[u] = true;
    int sum = 1; //包含u在内的下面子树的数量和
    int res = 0; //以u为根的子树数量的最大值
    for(int i = h[u]; i != -1; i = ne[i])
    {
        int p = e[i];
        if(!st[p])
        {
            int s = dfs(p);
            res = max(res, s);
            sum += s;
        }
    }
    res = max(res, n - sum);//父节点的那个子树数量
    ans = min(ans, res);
    return sum;
}
int main()
{
    cin >> n;
    memset(h, -1, sizeof h);
    for(int i = 0; i < n - 1; i++)
    {
        cin >> a >> b;
        add(a, b);
        add(b, a);
    }
    dfs(1);
    cout << ans;
    return 0;
}

5 拓扑序列

有向图的拓扑序列,判断DAG图是否存在环?(BFS的应用)
有向无环图一定存在拓扑序列。
箭头都是从前往后指的,入度为0说明没有节点指向它,排在最前面

#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
int n, m, a, b;
int h[N], e[N], ne[N], idx, d[N];
int q[N];
int hh, tt;

void add(int a, int b)
{
    e[idx] = b;
    ne[idx] = h[a];
    h[a] = idx;
    idx++;
}
bool toposort()
{
    hh = 0, tt = -1;
    for(int i = 1; i <= n; i++)
    {
        if(!d[i])
            q[++tt] = i;
    }
    
    while(hh <= tt)
    {
        int t = q[hh++];
        for(int i = h[t]; i != -1; i = ne[i])
        {
            int p = e[i];
            d[p]--;
            if(!d[p])
                q[++tt] = p;
        }
    }
    if(tt == n - 1)
        return true;
    else
        return false;
}
int main()
{
    cin >> n >> m;
    memset(h, -1, sizeof h);
    while(m--)
    {
        cin >> a >> b;
        add(a, b);
        d[b]++;
    }
    if(toposort())
    {
        for(int i = 0; i <= tt; i++)
            cout << q[i] << " ";
    }
    else
        cout << -1;
    return 0;
}
  • 是否存在拓扑序列,就是看tt是否是n-1,包含全部n个节点即可
  • 拓扑序列是什么:队列中的排列

数组模拟队列

  • 初始化:hh = 0, tt = -1;
  • push:q[++tt] = num;
  • pop:tmp = q[hh++];
  • empty:hh <= tt
今天立志排名进入4000!

6 最短路问题

在这里插入图片描述

6.1 dijkstra

朴素版dijkstra

这个简单代码是把第一个节点自动加入st数组

  • 初始化,dist[1] = 0; 其他为INF
  • 迭代更新n次
    • 找到不在st中dist最小的节点midx,加入st
    • 使用midx更新松弛dist
#include<bits/stdc++.h>
using namespace std;
const int N = 510;
const int M = 1e5 + 10;
int n, m, a, b, c;
int g[N][N], dist[N], st[N];

//可以实现从第一个点开始自动加入st中

int dijkstra()
{
    //初始化
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    
    //迭代n次,将n个点加入st中
    for(int i = 0; i < n; i++)
    {
        //遍历dist数组,找到不在st数组中距离最小的节点midx
        int midx = -1;
        for(int j = 1; j <= n; j++)
        {
            if(!st[j] && (midx == -1 || dist[midx] > dist[j]))
                midx = j;
        }
        st[midx] = true;
        for(int j = 1; j <= n; j++)
            dist[j] = min(dist[j], dist[midx] + g[midx][j]); //松弛dist数组
    }
    //无法到达n节点
    if(dist[n] == 0x3f3f3f3f)
        return -1;
    else
        return dist[n];
    
}

int main()
{
    scanf("%d %d", &n, &m);
    memset(g, 0x3f, sizeof g);
    while(m--)
    {
        scanf("%d %d %d", &a, &b, &c);
        g[a][b] = min(g[a][b], c); //存在重边
    }
    cout << dijkstra();
    return 0;
}
  • 没有必要加上!st[j],因为如果j被加入到里面去了,表面他已经在之前就是最小的了,不可能再用之后更大的去更新他。
堆优化的dijkstra

暂时还不是很清晰,我还需要再缕缕

  • 为什么st判断写在外面?
  • 什么时候用堆优化?邻接表? 复杂度?
  • cnt++在哪里
    在这里插入图片描述
#include<bits/stdc++.h>
using namespace std;

const int N = 150010;
int h[N], e[N], ne[N], w[N], idx;
int n, m, a, b, c;
int st[N], dist[N];
typedef pair<int, int> PII;

void add(int a, int b, int c)
{
    e[idx] = b; ne[idx] = h[a]; h[a] = idx; w[idx] = c; idx++;
}

int dijkstra()
{
    priority_queue<PII, vector<PII>, greater<PII>> heap;
    memset(dist, 0x3f, sizeof dist);
    heap.push({0, 1});//dist[0] = 1;
    
    while(!heap.empty())
    {
        //heap中最多有m个元素,复杂度为mlogm
        auto t = heap.top();
        heap.pop();
        int distance = t.first, ti = t.second;
        
        if(st[ti]) continue;
        st[ti] = true;
        //对每一个头都进行了遍历,相当于遍历了所有边m,最多push了m次,外层while循环最多m次
        for(int i = h[ti]; i != -1; i = ne[i])
        {
            int j = e[i];
            if(distance + w[i] < dist[j]) //只有产生距离更小的点时才去更新j,减少堆中的数
            {
                dist[j] = distance + w[i];
                heap.push({dist[j], j});
            }
        }
    }
    
    if(dist[n] == 0x3f3f3f3f)
        return -1;
    else
        return dist[n];
}
int main()
{
    scanf("%d %d", &n, &m);
    memset(h, -1, sizeof h);
    while(m--)
    {
        scanf("%d %d %d", &a, &b, &c);
        add(a, b, c);
    }
    cout << dijkstra();
    return 0;
}

6.2 bellman-ford

我感觉bf算法有点像暴力(带负权值),dijkstra像是贪心选择最小的(非负权值)。
算法流程O(nm):

  • 初始化dist, 起始点为0其余为INF
  • for k次迭代(k:最多使用k条边)
    • 备份dist,防止更新是发生串联,一次外层迭代只能加一条边
    • for 遍历所有边
      • 松弛 dist[b] = min(dist[b], backup[a] + w);
有边数限制的最短路
#include<iostream>
#include<string.h>
using namespace std;


const int N = 510;
const int M = 10010;
int n, m, k, a, b, w;
int dist[N], backup[N];
struct edge
{
    int a, b, w;
}e[M];

int bellman_ford()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    for(int i = 0; i < k; i++)
    {
        memcpy(backup, dist, sizeof dist);
        for(int j = 0; j < m; j++)
        {
            a = e[j].a, b = e[j].b, w = e[j].w;
            dist[b] = min(dist[b], backup[a] + w);
        }
    }
    return dist[n];
}
int main()
{
    scanf("%d %d %d", &n, &m, &k);
    for(int i = 0; i < m; i++)
        scanf("%d %d %d", &e[i].a, &e[i].b, &e[i].w);
    
    int ans = bellman_ford();
    
    if(ans > 0x3f3f3f3f / 2)
        puts("impossible");
    else
        cout << ans;

    return 0;
}

这个题有负环没有关系是因为有k次数限制
其他点:

  • 如果最短路存在,一定存在一个不含环的最短路。环分为正环、零环、负环。若有负环,可以一直走环减少路径长度,可能不存在最短路。正环和零环没必要走环,路径会增加。
  • 判断是否存在负环?存在负环那么第n次还可以对其进行松弛,加一个flag判断一下

6. 3 spfa

spfa算法是对bf算法的优化,dist[b] = min(dist[b], backup[a] + w); 只有在backup[a]更新了,dist[b]才会更新。因此选择BFS对其进行优化,把变小了的backup[a]放进队列,然后取出更新其他的。
此题不能存在负环,存在负环可能会无限循环下去

spfa求最短路
#include<iostream>
#include<string.h>
#include<queue>
using namespace std;

const int N = 100010;
int n, m, a, b, ww;
int h[N], e[N], ne[N], w[N], idx;
int dist[N], st[N];

void add(int a, int b, int ww)
{
    e[idx] = b;
    ne[idx] = h[a];
    h[a] = idx;
    w[idx] = ww;
    idx++;
}

int spfa()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    
    
    queue<int> q;
    q.push(1);
    st[1] = true;//表示是否在队列中
    
    while(!q.empty())
    {
        a = q.front();
        q.pop();
        st[a] = false;//spfa可以多次更新,多次加入队列
        
        for(int i = h[a]; i != -1; i = ne[i])
        {
            b = e[i];
            ww = w[i];
            if(dist[a] + ww < dist[b])
            {
                dist[b] = min(dist[b], dist[a] + ww);
                if(!st[b])
                {
                    q.push(b);
                    st[b] = true;
                }
            }
        }
    }
    return dist[n];
}
int main()
{
    scanf("%d %d", &n, &m);
    memset(h, -1, sizeof h);
    while(m--)
    {
        scanf("%d %d %d", &a, &b, &ww);
        add(a, b, ww);
    }
    
    int t = spfa();
    if(t == 0x3f3f3f3f)//加入队列的点都是从起点开始的,不会存在 不在最短路中且更新的dist[n]这种情况
        puts("impossible");
    else
        cout << t;
    return 0;
}
spfa判断负环

抽屉原理
如果经过了多于n条边,说明存在环,因为只有n-1条边
环存在的原因是最短路变小了,那环就是负环
cnt[b]表示从1到b中经过了多少条边,cnt[b] = cnt[a] + 1;//1是边a->b

#include<iostream>
#include<string.h>
#include<queue>
using namespace std;

const int N = 100010;
int n, m, a, b, ww;
int h[N], e[N], ne[N], w[N], idx;
int dist[N], st[N], cnt[N];

void add(int a, int b, int ww)
{
    e[idx] = b;
    ne[idx] = h[a];
    h[a] = idx;
    w[idx] = ww;
    idx++;
}

int spfa()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;//可以不初始化
    
    
    queue<int> q;
    //加入所有节点
    for(int i = 1; i <= n; i++)
    {
        st[i] = true;
        q.push(i);
    }
    
    while(!q.empty())
    {
        a = q.front();
        q.pop();
        st[a] = false;
        
        for(int i = h[a]; i != -1; i = ne[i])
        {
            b = e[i];
            ww = w[i];
            if(dist[a] + ww < dist[b])
            {
                dist[b] = min(dist[b], dist[a] + ww);
                cnt[b] = cnt[a] + 1; //cnt更新
                if(cnt[b] >= n)//判断是否存在负环
                    return true;
                if(!st[b])
                {
                    q.push(b);
                    st[b] = true;
                }
            }
        }
    }
    return false;
}
int main()
{
    scanf("%d %d", &n, &m);
    memset(h, -1, sizeof h);
    while(m--)
    {
        scanf("%d %d %d", &a, &b, &ww);
        add(a, b, ww);
    }
    
    if(spfa()) puts("Yes");
    else puts("No");
    return 0;
}

评论里的一些问题:

  • 是否存在负环:不是从1开始的最短路是否存在负环,因此将初始节点加入。这个y总说可以这样理解(nb!):想象一个虚拟源节点假设为0,距离所有节点距离都是0,这样在spfa第一次更新时就会把所有节点加入,dist[i]更新为0,这样有0的图找负环就等于没0的图找负环。
  • dist为什么可以不初始化?不初始化dist都是0,甚至可以初始化任何值。因为只有在dist[b] < dist[a] + w时,才会cnt++,不初始化更新只可能是w < 0,从负边出现才开始更新统计cnt,由于如果有负环的话,一定会无数次更新,一定会导致cnt>=n的,所以不必在意是否初始化
  • cnt >= m?说是只有m条边超过了肯定是有负环,是对的。但是其实只有n个顶点,每条最短路顶多有n-1条边,用cnt>=n即可(一般n < m)

6.4 Floyd

多源最短路径 每两个点之间的最短路径
算法思路:三重循环

  • for k in (1, n)
    • for i in (1, n)
      • for j in (1, n)
        • d[i][j] = min(d[i][j], d[i][k] + d[k][j])

原理是个动态规划

  • d[k][i][j] = min(d[k][i][j], d[k - 1][i][k] + d[k - 1][k][j]d[k][i][j]是指使用前k个节点中i和j的最短路径,可以使用前k-1个更新
  • 第一维可以省略,d[i][j] = min(d[i][j], d[i][k] + d[k][j]) 不使用之前的
#include<iostream>
using namespace std;

const int N = 210;
const int INF = 1e9;
int n, m, k, a, b, w;
int d[N][N];

void floyd()
{
    for(int k = 1; k <= n; k++)
        for(int i = 1; i <= n; i++)
            for(int j = 1; j <= n; j++)
                d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}

int main()
{
    scanf("%d %d %d", &n, &m, &k);
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= n; j++)
            if(i == j)  d[i][j] = 0;
            else d[i][j] = INF;
    
    while(m--)
    {
        scanf("%d %d %d", &a, &b, &w);
        d[a][b] = min(d[a][b], w);
    }
    floyd();
    while(k--)
    {
        scanf("%d %d", &a, &b);
        if(d[a][b] > INF / 2)   puts("impossible");//存在负权边,可能导致不是最短路但是距离减少
        else cout << d[a][b] << endl;
    }
    return 0;
}

7 最小生成树

7.1 Prim

朴素版Prim算法
算法原理和dikstra很像,差别在于dist数组含义不同(dij:到1号点的距离,prim到集合的距离)

  • 初始化 dist=INF,选择第一个点为1
  • for n次循环(选n个点加入mst)
    • 从dist数组选择一个最小的节点t, 加入st
    • (结束判断:dist[t]==INF)
    • 用t更新到集合的距离 dist[j] = min(dist[j], g[t][j])
#include<iostream>
#include<string.h>
using namespace std;

const int N = 510;
const int INF = 0x3f3f3f3f;
int g[N][N];
int dist[N], st[N];
int n, m, u, v, w;

int prim()
{
//初始化为INF,选择第一个点为1
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    int ans = 0;
    //选择最小的点加入
    for(int i = 0; i < n; i++)//选n个点
    {
        int t = 0;
        for(int j = 1; j <= n; j++)
        {
            if(!st[j] && dist[j] < dist[t])
                t = j;
        }
        if(dist[t] == INF)//最小的是INF,全是INF,非连通
            return INF;
            
        ans += dist[t];
        
        st[t] = true;
        //更新到集合的距离
        for(int j = 1; j <= n; j++)
        {
            if(!st[j] && g[t][j] < dist[j])
                dist[j] = g[t][j];
        }
    }
    return ans;
}
int main()
{
    scanf("%d %d", &n, &m);
    memset(g, 0x3f, sizeof g);
    while(m--)
    {
        scanf("%d %d %d", &u, &v, &w);
        g[u][v] = min(g[u][v], w);
        g[v][u] = min(g[v][u], w);
    }
    int t = prim();
    if(t == INF) puts("impossible");
    else printf("%d\n", t);
    return 0;
}

堆优化版prim算法
和堆优化版的dijkstra差不多,不过用cnt统计多少个点加入了mst

#include<iostream>
#include<string.h>
#include<queue>
using namespace std;

const int N = 510;
const int INF = 0x3f3f3f3f;
int g[N][N];
int dist[N], st[N];
int n, m, u, v, w;
typedef pair<int, int> PII;

int prim()
{
    priority_queue<PII, vector<PII>, greater<PII>> q;
    q.push({0, 1});
    
    
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    int ans = 0, cnt = 0;
    
    while(q.size())//选n个点
    {
        auto t = q.top();
        q.pop();
        int distance = t.first, pos = t.second;//选择最小边
        
        if(st[pos]) continue;
        st[pos] = true;
        
        cnt++;
        ans += distance;
        
        //cout << pos << " " << ans << endl;
        
        for(int j = 1; j <= n; j++)
        {
            if(g[pos][j] < dist[j])
            {
                //cout << dist[j] << " " << j << endl;
                dist[j] = g[pos][j];
                q.push({dist[j], j});
            }
        }
    }
    if(cnt == n) return ans;
    return INF;
}
int main()
{
    scanf("%d %d", &n, &m);
    memset(g, 0x3f, sizeof g);
    while(m--)
    {
        scanf("%d %d %d", &u, &v, &w);
        g[u][v] = min(g[u][v], w);
        g[v][u] = min(g[v][u], w);
    }
    int t = prim();
    if(t == INF) puts("impossible");
    else printf("%d\n", t);
    return 0;
}

7.2 Kruskal

算法思路:
之前各条边都分别在各自的集合里面,从小到大将其连通起来——并查集

  • 从小到大排序
  • 遍历所有边,a->b,如果a和b不连通,则将其连通加入mst
#include<iostream>
#include<algorithm>
using namespace std;

const int N = 200010;
const int INF = 0x3f3f3f3f;
int n, m, a, b, w;
int f[N];

struct edge
{
    int a, b, w;
    bool operator<(const edge& e)
    {
        return w < e.w;
    }
}e[N];


int find(int a)
{
    if(f[a] != a)
        f[a] = find(f[a]);//?
    return f[a];
}
int kruskal()
{
    sort(e, e + m);//默认less,重载operator<
    int cnt = 0, ans = 0;
    
    for(int i = 0; i < m; i++)
    {
        a = e[i].a, b = e[i].b, w = e[i].w;
        int fa = find(a), fb = find(b);
        //cout << fa << " " << fb << endl;
        if(fa != fb)
        {
            cnt++;
            ans += w;
            f[fa] = fb;//union ?
        }
    }
    //n - 1条边
    if(cnt == n - 1)    return ans;
    return INF;
}
int main()
{
    scanf("%d %d", &n, &m);
    
    for(int i = 1; i < n; i++)
        f[i] = i;
        
    for(int i = 0; i < m; i++)
        scanf("%d %d %d", &e[i].a, &e[i].b, &e[i].w);
        
    int t = kruskal();
    if(t == INF) puts("impossible");
    else printf("%d\n", t);
    return 0;
}

8 二分图

8.1 染色法

8.2 匈牙利算法

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值