算法基础-搜索与图论

本文介绍了图的存储方法,包括邻接矩阵和邻接表,以及图的遍历如DFS和BFS。接着讲解了单源最短路算法,如Dijkstra和Bellman-Ford,还有多源最短路的Floyd算法。最小生成树算法的Prim和Kruskal方法也被提及。最后讨论了二分图的概念,包括染色法判别和匈牙利算法求最大匹配。
摘要由CSDN通过智能技术生成

算法基础-搜索与图论

  • Dijkstra

  • Bellman-Ford

  • spfa

  • Floyd

  • Prim

  • Kruskal

  • 染色法判定二分图

  • 匈牙利算法

以上是算法基础部分的==搜索与图论==,主要分为搜索,多个最短路算法,最小生成树算法以及二分图。

###1.树与图

首先来考虑==图的存储==

树是一种特殊的图,与图的存储方式相同。同样无向图也是一种特殊的有向图。所以只考虑有向图

对于有向图的存储:

1)邻接矩阵:主要用来存储稠密图

2)邻接表:主要用来存储稀疏图

(对于图的稠密和稀疏的判断,如果图的边和图的点数量差不多,那就是稀疏图。而如果边比点大很多,那就是稠密图)

//邻接矩阵
g[a][b]; //存储从a到b的边

//邻接表,实际上就是二维链表
int h[N], e[N], ne[N], w[M], idx = 0;
void add(int a, int b, int c)
{
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}

//初始化
idx = 0;
memset(h, -1, sizeof h);

###2.搜索

再来考虑==图的遍历==

图的遍历主要有DFS和BFS

考虑图的搜索的时间复杂度,我们规定图的点为n个,而边为m个

那么O(n + m),因为遍历整个图需要遍历所有的点

1)DFS

深度优先搜索,就是尽可能先搜索完纵向的元素

//以图的深度优先搜索为例
int dfs(int u)//遍历节点u
{
    st[u] = true;//节点u已经被遍历
    for (int i = h[u]; i != -1; i = ne[i])
    {
        int j = e[i];//j为当前与节点u相连的点的关键词
        if (!st[j])//没有被遍历
        	dfs(j);
    }
}

2)BFS

宽度优先搜索,就是先遍历完与当前结点相连的所有节点

//以图的宽度优先搜索为例
queue<int> q;
q.push(1);
st[1] = true;

while (q.size())
{
    int t = q.front();//t为队列的队头
    q.pop();//出队
    
    for (int i = h[t]; i != -1; i = ne[i])//遍历当前节点的所有相邻点
    {
        int j = e[i];
        if (!st[j])
        {
            st[j] = true;
            q.push(j);
        }
    }
}

图的拓扑排序是BFS的经典应用,拓扑排序就是将按顺序将每个入度为0的点拿出来,如果没有入度为0的点,那么就不满足拓扑排序。

bool topsort()//拓扑排序
{
int q[N], hh = 0, tt = -1;

for (int i = 1; i <= n; i++)
    if (!d[i])//如果某个点入度为0
       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])
        {
            q[++tt] = j;
        }
    }
}

if (tt == n - 1) return true;
else return false;
}

3.最短路算法

(1)单源最短路(无负权边)

顾名思义,单源最短路算法就是从一个点到另一个点的最短距离,而我们一般处理的是从1号点到n号点的最短路问题。

首先我们考虑没有负权边的情况.。

通常用到dijkstra算法

(a)朴素版dijkstra算法

==基本思想==:

  • 初始化dist数组

  • 创建一个集合S,每次找不在集合S但是距离源点最近的点t,将点t加入到集合S中

  • 并用点t通过松弛操作更新其他所有点到源点的距离。

  • 经过n-1次循环后加入了n-1个点,这样最短路就找到了

==时间复杂度分析==:朴素版dijkstra主要应对的是稠密图的问题,时间复杂度为O(n^2^ + m)

int g[N][N];//邻接矩阵存储边
int dist[N];//dist[i]表示点i到源点的距离
bool st[N];//S集合

void dijkstra()
{
	memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;//初始化
    
    for (int i = 0; i < n - 1; i++)//进行n-1次循环
    {
        int t = -1;//t为-1是为了方便那些不能更新的点加入到点集中,防止死循环
        for (int j = 1; j <= n; j++)//找出不在点集S中但是距离源点最近的点
            if (!st[j] && (t == -1 || dist[t] > dist[j]))
                t = j;
        
       	st[t] = true;//加入点集S
        
        for (int j = 1; j <= n; j++)//利用点t更新其他点到源点的最短距离
        	dist[j] = min(dist[j], dist[t] + g[t][j]);//松弛操作
    }
}//如果dist[n] == 0x3f3f3f3f 说明没有最短路
//否则最短路长度为dist[n];	
(b)堆优化版dijkstra算法

堆优化适合于应对稀疏图

时间复杂度分析:首先是n-1次循环,循环内找最近的点t只要O(1),但是更新其他点到源点的最短距离需要logm。因此总的时间复杂度为O(nlogm)

#include <queue>
typedef pair<int, int> PII;

void dijkstra()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;//初始化
    priority_queue<PII, vector<PII>, greater<PII>> heap;
    heap.push({0, 1});//这里需要先存距离,因为pair在排序的时候,先看第一个元素
    
    whlie (heap.size())//遍历所有被更新距离的点
    {
        auto t = heap.top();//O(1)的时间找到最小值
        heap.pop();
        int distance = t.first, ver = t.second;
        
        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})//O(logm)
            }
        }
    }
}
(2)单源最短路(有负权边)

处理有负权边的单源最短路有两个主要算法,Bellman-Ford和spfa算法

至于为什么dijkstra算法不能处理负权边的问题,因为dijkstra算法基于贪心思想,在有负权边的问题上,局部最优解不一定能组成全局最优解。

(a)Bellman-Ford算法

==基本思想==:进行n-1次循环,每次循环将图中所有边进行松弛操作,若在n-1次操作之后还能更新,说明有负环。

==注意==:

  • 在判断是否不能达到n号点,判断依据是dist[n] > INF / 2,因为如果n号点相连的边进行了松弛操作,那么一定是小于INF的,但是是在INF的一个量级。

  • 此算法擅长解决有边数限制的最短路问题

  • ==ATTENTION==:由于每次松弛操作是对于所有边的循环,需要用上一次迭代后的dist数组的备份进行松弛操作,否则会发生串联效应。

==时间复杂度分析==:O(nm),进行n次循环,遍历所有的边

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

void Bellman_ford()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    
    for (int i = 0; i < n; i++)//有时候为k,k为限制的边数
    {
        memcpy(back, dist, sizeof dist);//复制上一次迭代的数组,防止串联效应
        for (int j = 0; j < m; j++)
        {
            auto t = edges[j];
            dist[t.b] = min(dist[t.b], back[t.a] + t.w);
        }
    }
}

//判断是否能到n号点
if (dist[n] > 0x3f3f3f3f / 2) cout << "impossible";
else cout << dist[n];
(b)spfa算法

==基本思想==:实际上是队列优化版的bellman-ford算法

  • 只考虑前驱结点更新的点,即使用队列存储点,当一个点出队,寻找能更新的后继结点,将这些点入队。

  • 使用st数组来进行判断当前点是否已经进入队列,如果进入的话,就不要再次进入,只需要更新一下到源点的距离。st数组就是记录当前正在更新的点,如果已经在队列,就不需要再入队。

==注意事项==:

  • spfa最后判断能否到达n号点是用dist[n] == 0x3f3f3f3f,因为spfa与上一个算法不同,不是遍历每一条边!

  • Bellman_ford算法可以直接处理有负权回路的问题,但是如果spfa要判断是否有负权回路,那么需要使用cnt数组记录每个点到源点经过的边数,如果边数到n,说明有负环。(抽屉原理

==时间复杂度分析==:spfa是队列优化版的bellman-ford算法,那么最坏的时间复杂度是O(nm),平均为O(m)。(所以还是很牛的)

void spfa()
{
    //依然是先初始化
	memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    queue<int> q;
    q.push(1);
    
    while (q.size())
    {
		auto 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])
                 {
                     st[j] = true;
                     q.push(j);
                 }
             }
         }
    }
}

==补充==:利用spfa算法判断有无负权回路

但是呢,一般情况下,spfa求最短路和判断负环不在一起进行

bool spfa()
{
    queue<int> q;
    for (int i = 1; i <= n; i++)//考虑连通分量比较大的情况
    {
        q.push(i);
        st[i] = true;
    }
    
    while (q.size())
    {
        auto 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];
                cnt[j] = cnt[t] + 1;
                if (cnt[j] >= n) return true;//有负环
                if (!st[j])
                {
                    st[j] = true;
                    q.push(j);
			   }
		   }
        }
    }
    return false;
}
(3)多源最短路(含负权边)

Floyd算法:

==基本思路==:基于动态规划的思路

==时间复杂度分析==:三层循环O(n^3^)

//初始化
for (int i = 1; i <= n; i++)
    for (int j = 1; j <= n; j++)
        if (i == j) dist[i][j] = 0;
	    else dist[i][j] = INF;
//Floyd
void Floyd()
{
	for (int k = 1; k <= n; k++)
        for (int i = 1; i <= n; i++)
            for (int j = 1; j <= n; j++)
                dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);
}

4.最小生成树

最小生成树就是在图中找到能生成的权重和最小的树

(1)Prim算法

==基本思路==:与dijkstra算法很像,但是dist数组表示的含义不同

  • 循环n遍,为的是不断为点集S加入点,直到到达n点

  • 找到不在点集S中的点t,点t是距离点集最近的点

  • 将点t加入到S中,并用t更新其他点到点集S的距离

==注意事项==:

  • dist数组表示到点集的距离

  • 每次对于找到的点t就行判断,如果dist[t] == INF那么直接返回INF

  • 利用点t更新其他点到点集S的距离,这个操作和松弛操作很像,但是不一样,dist[j] = min(dist[j], g[t][j]);

==时间复杂度分析==:O(n^2^+m)与朴素版dijkstra时间复杂度差不多

int prim()
{
	int res = 0;
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    
    for (int i = 0; i < n; i++)
    {
        int t = -1;
        for (int j = 1; j <= n; j++)//找点t
            if (!st[j] && (t == -1 || dist[t] > dist[j]))
                t = j;
        
        if (dist[t] == INF) return INF;
        st[t] = true;
        res += dist[t];
        
        for (int j = 1; j <= n; j++)
            dist[j] = min(dist[j], g[t][j]);
    }
    return res;
}
(2)Kruskal算法

==基本思路==

  • 首先对所有的边进行排序

  • 找到最短的边,如果边和生成树的边集不在一个连通块,就直接加入。

  • 知道加入到第n个点相连的边,如果没有加到第n个点,就是无法生成最小生成树

==注意==

  • 由于此算法只考虑边,那么直接用结构体进行存储,而且要用到运算符重载

  • 为了判断是否在同一连通块,那么就要用到并查集的查询操作

  • 如何将新边加入,也要用到并查集的合并操作

==时间复杂度分析==:首先是对边的排序mlogm,并查集的复杂度近乎为1,因此O(mlogm)

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

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

int Kruskal()
{
	sort(edges + 1, edges + 1 + m);
    
    for (int i = 1; i <= n; i++)
        p[i] = i;//并查集初始化!!!
    
    for (int i = 1; i <= m; i++)
    {
        auto t = edges[i];
        int a = t.a, b = t.b, w = t.w;
        if (find(a) != find(b))
        {
            p[find(a)] = find(b);
            res += w;
            cnt ++;
        }
    }
    
    if (cnt < n - 1) return INF;
    else return res;
}

5.二分图

关于二分图,我们只考虑二分图的判别和二分图的最大匹配问题。关于二分图的问题,我们一般使用邻接表的方式存储。

(1)染色法判别二分图

关于染色法,在离散数学中有提到,为了判别二分图,就进行染色,如果相邻的两个点出现相同的颜色,那么就一定不是二分图

==时间复杂度分析==:O(n + m)

bool dfs(int u, int c)//u为当前点的关键词,c为当前点的颜色
{
    color[u] = c;//为当前点染色,0为白,1为黑,-1表示没染色
    for (int i = h[u]; i != -1; i = ne[i])
    {
        int j = e[i];
        if (color[j] == -1)//如果没有染色
        {
            if (!dfs(j, 1 - c)) return false;//染色,如果返回false,那么这里也返回false
        }
        else if (color[j] == c) return false;//如果染色了,但是颜色一样,直接判定为不是二分图
    }
    return true;//成功遍历
}

bool check()
{
    memset(color, -1, sizeof color);
    bool flag = true;
    for (int i = 1; i <= n; i++)
        if (color[i] == -1)
            if (!dfs(i, 0))
            {
                flag = false;
                break;
            }
    return flag;
}
(2)匈牙利算法获取二分图最大匹配

==时间复杂度分析==:O(nm)

int n1, n2;//分别为俩个集合的点的个数
int match[N];//存储第二个集合中每个点当前匹配的第一个点
bool st[N];//表示第二个集合中的每个点是否已经被遍历

bool find(int x)
{
	for (int i = h[x]; i != -1; i = ne[i])//遍历x喜欢的女生
    {
        int j = e[i];
        if (!st[j])//如果在这一轮的匹配中,女生没有被预定
        {
            st[j] = true;
            if (match[j] == 0 || find(match[j]))//如果1.当前女生没有匹配的对象或者2.当前女生匹配的对象可以找到别的女生
            {
                match[j] = x;//将女生j匹配给x
                return true;
            }
        }
    }
    return false;
}

int res = 0;
for (int i = 1; i <= n1; i++)
{
    //每次为一个男生进行匹配之前,需要对st数组进行初始化
	memset(st, false, sizeof st);
    if (find(i)) res++;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值