搜索与图论算法

搜索与图论

深度优先搜索

想象对一棵树进行搜索,深搜会尽可能往深了搜索,不撞南墙不回头,在回溯的时候会搜索该节点其他可能的路径。深搜使用的数据结构一般是栈Stack,于是一般用递归实现,使用的空间是$O(h)$,路径不具有最短性。

DFS最重要的两个部分:回溯和剪枝。用DFS做题时,首先要搞清楚用什么顺序来搜索。

eg: 八皇后问题

char g[N][N];
bool col[N], dg[N], udg[N];
		 //  dg表示正对角线。udg表示斜对角线
void dfs(int u)
{
	if (u == n)
	{
		for (int i = 0; i < n; i ++) puts(g[i]);
		puts("");
		return;
	}
	
	for (int i = 0; i < n; i ++)
	{
		if (!col[i] && !dg(u + i) && !udg(n - u + i))
		{
			g[u][i] = 'Q';
			col[i] = dg[u + i] = udg[n - u + i] = true;
			dfs( u + 1);
			col[i] = dg[u + i] = udg[n - u + i] = false;
			g[u][i] = '.';
		}
	}
}

宽度优先搜索

常用的深搜代码用迭代完成,使用的数据结构一般是队列Queue,使用的空间是$O(2^h)$,但是可以宽搜可以搜索到最短路径。

eg:走迷宫

int g[N][N];  // 存图
int d[N][N];  // 存每一个点到起点的距离
PII q[N * N];

int bfs()
{
    int hh = 0, tt = 0;
    q[0] = {0, 0};
    
    // 首先把距离初始化为-1,表示没有走过
    memset(d, -1, sizeof d);
    d[0][0] = 0;
    
    int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1}; // 表示行走方向
    
    // while队列不空,取出来队头
    while (hh <= tt)
    {
        auto t = q[hh ++];
        for (int i = 0; i < 4; i ++)
        {
            int x = t.first + dx[i], y = t.second + dy[i];
            if (x >= 0 && x < n && y >= 0 && y < m && g[x][y] == 0 && d[x][y] == -1)
            {
                d[x][y] = d[t.first][t.second] + 1;
                q[++ tt] = {x, y}; 
            }
        }
    }
    
    return d[n - 1][m - 1];
}

图分为有向图和无向图,有向图是指只能从一个点走向另一个点a->b,而无向图是两个点互相可以到达a->b, b->a,无向图可以看作成特殊的有向图,在建图的时候多建一条边即可。

有向图的存储一般分为邻接矩阵和邻接表。邻接矩阵的空间是$n^2$的,一般存储稠密图。邻接表其实就是单链表,每个点都是一个单链表,存储该点可以到达的其他点,一般存储稀疏图。

eg:邻接表建图

int h[N], e[M], ne[M], idx;
// h存的是n个链表的链表头,e存的是所有的节点的值,ne是每个节点的next指针是多少
void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}

拓扑序列

有向图求拓扑序是图的宽搜非常经典的应用。图的拓扑序列是针对有向图来说的,无向图是没有拓扑序列的。

拓扑序列:若一个由图中所有点构成的序列 A 满足:对于图中的每条边 (x,y),x 在 A 中都出现在 y 之前,则称 A 是该图的一个拓扑序列。

有环的图无法展开有序序列。有向无环图也被称为拓扑图。

有向图的每个点有两个概念:入度和出度。指向该点的边是入度,指向别的点的边称为出度。因此求拓扑序列可以把入度为0的点放在最前面。

int h[N], e[N], ne[N], idx;
int d[N], q[N];

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

bool topsort()
{
    int 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 j = e[i];
            d[j] -- ;
            if (d[j] == 0) q[++ tt] = j;
        }
    }
    
    return tt == n - 1;
}

最短路问题

一般最短路问题分为两类:单源最短路和多源汇最短路。

单源最短路指的是求一个点到其他所有点的最短距离多源汇最短路指的是可能有多轮询问,每次询问的起点和终点都是不确定的,问这两个点之间的最短距离。

其中单源最短路也可以分为两大类:

  1. 所有边权都是正数
    1. 朴素Dijkstra算法$O(n^2)$,适合稠密图,因为时间复杂度和边没有关系,和点数有关
    2. 堆优化版的Dijkstra算法$O(mlogn)$,适合稀疏图,m和n为一个级别
  2. 存在负权边
    1. Bellman-Ford算法$O(mn)$
    2. SPFA算法$O(m)$,最坏为$O(mn)$

而多源汇最短路只有Floyd算法$O(n^3)$

朴素Dijkstra算法

步骤:

  1. 先初始化距离,只有起点的距离是确定的为0,其他的所有点的距离都是不确定的,设为正无穷
  2. 进行迭代,每次迭代找到不在集合s中的距离最近的点t,将t加入集合s,然后用t更新其他点的距离(集合s存的是当前已确定最短距离的点)
int g[N][N];  // 稠密图用邻接矩阵来存
int dist[N];  // 每个点到起点的最短距离
bool st[N];   // 用来表示当前点是否已经确定最短距离

int dijkstra()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    
    for (int i = 0; i < n; i ++)
    {
        int t = -1;  // 找到当前距离最小的点t
        for (int j = 1; j <= n; j ++)
            if (!st[j] && (t == -1 || dist[t] > dist[j]))
                t = j;               // 当前t不是最短的距离,那么更新t
        
        st[t] = true;  // 将t加到集合里去
        
        for (int j = 1; j <= n; j ++)
            dist[j] = min(dist[j], dist[t] + g[t][j]); // 用t更新到其他点的距离
    }
    
    if (dist[n] == 0x3f3f3f3f) return -1;  // 两点不连通
    return dist[n];
}

堆优化版Dijkstra算法

朴素版寻找最小距离的点需要循环n次,而用数据结构堆来存储可以优化至O(1)

int h[N], w[N], e[N], ne[N]; // 邻接表,w数组是权重
int dist[N];
bool st[N];

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

int dijkstra()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    
    priority_queue<PII, vector<PII>, greater<PII>> heap;
    heap.push({0, 1});  // Pair存储的是{距离,该点编号},因为要按照距离排序
    
    while (heap.size())
    {
        auto t = heap.top();
        heap.pop();
        
        int ver = t.second, distance = t.first;
        if (st[ver]) continue;
        st[ver] = true;
        
        for (int i = h[ver]; i != -1; i = ne[i])
        {
            int j = e[i];
            if (dist[j] > distance + w[i])
            {
                dist[j] = distance + w[i];
                heap.push({dist[j], j});
            }
        }
    }
    
    if (dist[n] == 0x3f3f3f3f) return -1;
    return dist[n];
}

Bellman-Ford算法

两层循环,首先迭代n次,每次循环所有边,然后更新该点距离。如果有父权回路,那么不存在最短距离。

// 求出从 1 号点到 n 号点的最多经过 k 条边的最短距离,如果无法从 1 号点走到 n 号点,输出 impossible。
int dist[N], backup[N];

struct Edge
{
    int a, b, w;
} edges[M];

void 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 ++)
        {
            int a = edges[j].a, b = edges[j].b, w = edges[j].w;
            dist[b] = min(dist[b], backup[a] + w);
        }
    }
    
    if (dist[n] > 0x3f3f3f3f / 2) puts("impossible");
    else printf("%d", dist[n]);
}

SPFA算法

只要没有负环,就可以用

int h[N], w[N], e[N], ne[N];
int dist[N];
bool st[N];

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

void spfa()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    
    queue<int> q;
    q.push(1);
    st[1] = true;
    
    while (q.size())
    {
        int t = q.front();
        q.pop();
        
        st[t] = false;
        
        for (int i = h[t]; i != -1; i = ne[i])
        {
            int j = e[i];
            if (dist[j] > dist[t] + w[i])
            {
                dist[j] = dist[t] + w[i];
                if (!st[j])
                {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }
    
    if (dist[n] == 0x3f3f3f3f) puts("impossible");
    else printf("%d ", dist[n]);
}

Floyd算法

用邻接矩阵存储图,三层循环更新点的最短距离

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]);
}

最小生成树

最小生成树的问题对应的都是无向图

Prim算法

找到集合外距离最近的点,用该点更新其他点到集合的距离

int g[N][N], dist[N];
bool st[N];

int prim()
{
    memset(dist, 0x3f, sizeof dist);
    
    int res = 0; // 最小生成树的所有边权重之和
    for (int i = 0; i < n; i ++)
    {
        int t = -1;
        for (int j = 1; j <= n; j ++) // 找到集合外距离最近的点
            if (!st[j] && (t == -1 || dist[t] > dist[j]))
                t = j;
        
        if (i && dist[t] == INF) return INF; // 如果不是第一个点并且没有边连向集合
        
        if (i) res += dist[t];
        st[t] = true;
        
        for (int j = 1; j <= n; j ++) dist[j] = min(dist[j], g[t][j]);
    }
    
    return res;
}

Kruskal算法

首先将所有边按权重从小大排序,枚举每条边,如果这两条边不连通,则加入到集合中

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 200010;

int n, m;
int p[N];

struct Edge
{
    int a, b, w;
}edges[N];

int find(int x) // 并查集
{
    if (p[x] != x) p[x] = find(p[x]);
    return p[x];
}

int main()
{
    scanf("%d%d", &n, &m);

    for (int i = 0; i < m; i ++)
    {
        int a, b, w;
        scanf("%d%d%d", &a, &b, &w);
        edges[i] = {a, b, w};
    }
    
    sort(edges, edges + m, [=] (Edge x, Edge y) {return x.w < y.w;}); // 重载小于号
    
    for (int i = 1; i <= n; i ++) p[i] = i;
    
    int res = 0, cnt = 0;
    for (int i = 0; i < m; i ++)
    {
        int a = edges[i].a, b = edges[i].b, w = edges[i].w;
        
        a = find(a), b = find(b);
        if (a != b)
        {
            p[a] = b;
            res += w;
            cnt ++;
        }
    }

    if (cnt < n - 1) puts("impossible");
    else printf("%d\n", res);

    return 0;
}

二分图

二分图当且仅当图中不含奇数环(奇数环表示该环的边数量是不是奇数)

染色法判定二分图

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 100010, M = 200010;

int n, m;
int h[N], e[M], ne[M], idx;
int color[N];

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

bool dfs(int u, int c)
{
    color[u] = c;
    
    for (int i = h[u]; i != -1; i = ne[i])
    {
        int j = e[i];
        if (!color[j])
        {
            if (!dfs(j, 3 - c)) return false;
        }
        else if (color[j] == c) return false;
    }
    
    return true;
}


int main()
{
    scanf("%d%d", &n, &m);
    
    memset(h, -1, sizeof h);
    
    while (m --)
    {
        int a, b;
        scanf("%d%d", &a, &b);
        add(a,b), add(b, a);
    }
    
    bool flag = true;
    for (int i = 1; i <= n; i ++)
        if (!color[i])
        {
            if (!dfs(i, 1))
            {
                flag = false;
                break;
            }
        }
    
    if (flag) puts("Yes");
    else puts("No");
    
    return 0;
}   

匈牙利算法

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 510, M = 100010;

int n1, n2, m;
int h[N], e[M], ne[M], idx;
int match[N];
bool st[N];

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

bool find(int x)
{
    for (int i = h[x]; i != -1; i = ne[i])
    {
        int j = e[i];
        if (!st[j])
        {
            st[j] = true;
            if (match[j] == 0 || find(match[j]))
            {
                match[j] = x;
                return true;
            }
        }
    }
    return false;
}

int main()
{
    scanf("%d%d%d", &n1, &n2, &m);
    
    memset(h, -1, sizeof h);
    
    while (m --)
    {
        int a, b;
        scanf("%d%d", &a, &b);
        add(a, b);
    }
    
    int res = 0;
    for (int i = 1; i <= n1; i ++)
    {
        memset(st, false, sizeof st);
        if (find(i)) res ++;
    }
    
    printf("%d", res);
    
    return 0;
}
posted @ 2022-05-24 08:59  FailBetter  阅读(54)  评论(0编辑  收藏  举报