常用代码模板3——搜索与图论

目录

一、树与图的存储

二、树与图的遍历

(1) 深度优先遍历 —— 模板题 AcWing 846. 树的重心

(2) 宽度优先遍历 —— 模板题 AcWing 847. 图中点的层次

拓扑排序 —— 模板题 AcWing 848. 有向图的拓扑序列

三、树与图的最短路问题

Bellman-Ford算法 —— 模板题 AcWing 853. 有边数限制的最短路

spfa 算法(队列优化的Bellman-Ford算法)

spfa判断图中是否存在负环 —— 模板题 AcWing 852. spfa判断负环

floyd算法 —— 模板题 AcWing 854. Floyd求最短路


 

 

一、树与图的存储

树是一种特殊的图,与图的存储方式相同。对于无向图中的边ab,存储两条有向边a->b, b->a。
因此我们可以只考虑有向图的存储。

  • (1) 邻接矩阵:g[a][b] 存储边a->b
  • (2) 邻接表:
// 对于每个点k,开一个单链表,存储k所有可以走到的点。h[k]存储这个单链表的头结点
int h[N], e[N], ne[N], idx;

// 添加一条边a->b
void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}

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

 


二、树与图的遍历

 

时间复杂度 O(n+m), n 表示点数,m 表示边数

int dfs(int u)
{
    st[u] = true; // st[u] 表示点u已经被遍历过

    for (int i = h[u]; i != -1; i = ne[i])
    {
        int j = e[i];
        if (!st[j]) dfs(j);
    }
}

时间复杂度 O(n+m), n 表示点数,m 表示边数

queue<int> q;  // 将要遍历的点放入队列
st[1] = true;  // 表示1号点已经被遍历过
q.push(1);

while (q.size())
{
    int t = q.front();
    q.pop();

    for (int i = h[t]; i != -1; i = ne[i])
    {
        int j = e[i];
        if (!s[j])
        {
            st[j] = true; // 表示点j已经被遍历过
            q.push(j);
        }
    }
}

时间复杂度 O(n+m), n 表示点数,m 表示边数

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

由于需要输出拓扑序列,如果用stl的queue,需要出队入队,就不好记录,这里就用邻接表

bool topsort()
{
    int hh = 0, tt = -1;

    // d[i] 存储点i的入度

    // 找到是起点的点,入度是0
    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];
            if (-- d[j] == 0)
                q[ ++ tt] = j;
        }
    }

    // 如果所有点都入队了,说明存在拓扑序列;否则不存在拓扑序列。
    return tt == n - 1;
}

 


三、树与图的最短路问题

由于那么多算法模板太难记了!!!我就不总结dijkstra算法了,因为SPFA也能做,而且快

 

注意在模板题中需要对下面的模板稍作修改,加上备份数组,详情见模板题。

原理:

Bellman - ford算法是求含负权图的单源最短路径的一种算法,效率较低,代码难度较小。其原理为连续进行松弛,在每次松弛时把每条边都更新一下,若在n-1次松弛后还能更新,则说明图中有负环,因此无法得出结果,否则就完成。

(通俗的来讲就是:假设1号点到n号点是可达的,每一个点同时向指向的方向出发,更新相邻的点的最短距离,通过循环n-1次操作,若图中不存在负环,则1号点一定会到达n号点,若图中存在负环,则在n-1次松弛后一定还会更新)

具体步骤:

for n次            (n代表经过的边数,若题中要求最多不能超过k调边,则n为k)
      for 所有边 a,b,w     (松弛操作)
             dist[b] = min(dist[b],back[a] + w)

注意:使用backup数组的目的是为了防止松弛的次数大于k,back[]数组是上一次迭代后dist[]数组的备份,由于是每个点同时向外出发,因此需要对dist[]数组进行备份,若不进行备份会因此发生串联效应,影响到下一个点

模板:

int n, m;           // n表示点数,m表示边数
int dist[N];        // dist[x]存储1到x的最短路距离

struct Edge         // 边,a表示出点,b表示入点,w表示边的权重
{
    int a, b, w;
}edges[M];

// 求1到n的最短路距离,如果无法从1走到n,则返回-1。
int bellman_ford()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;

    // 如果第n次迭代仍然会松弛三角不等式,就说明存在一条长度是n+1的最短路径,由抽屉原理,路径中至少存在两个相同的点,说明图中存在负权回路。
    for (int i = 0; i < n; i ++ )
    {
        for (int j = 0; j < m; j ++ )
        {
            int a = edges[j].a, b = edges[j].b, w = edges[j].w;
            if (dist[b] > dist[a] + w)
                dist[b] = dist[a] + w;
        }
    }

    if (dist[n] > 0x3f3f3f3f / 2) return -1;
    return dist[n];
}

 

 

  • spfa 算法(队列优化的Bellman-Ford算法)

时间复杂度平均情况下 O(m),最坏情况下 O(nm), n 表示点数,m 表示边数

SPFA 算法是 Bellman-Ford算法 的队列优化算法的别称,通常用于求含负权边的单源最短路径,以及判负权环。

spfa算法对第二行中所有边进行松弛操作进行了优化,原因是在bellman—ford算法中,即使该点的最短距离尚未更新过,但还是需要用尚未更新过的值去更新其他点,由此可知,该操作是不必要的,我们只需要找到更新过的值去更新其他点即可。

具体步骤:

queue <–   1                                                  (第一个点入队)
while queue 不为空
         t <– 队头
         queue.pop()
          for (int i = h[t] ; i != -1 ; i = ne[i])          (用 t 更新所有出边 t –> j,权值为w )
                      queue <–  j                               (若该点被更新过,则拿该点更新其他点)

spfa也能解决权值为正的图的最短距离问题,且一般情况下比Dijkstra算法还好

模板:

int n;                      // 总点数
int h[N], w[N], e[N], ne[N], idx;       // 邻接表存储所有边
int dist[N];                // 存储每个点到1号点的最短距离
bool st[N];                 // 存储每个点是否在队列中

// 求1号点到n号点的最短路距离,如果从1号点无法走到n号点则返回-1
int spfa()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;

    queue<int> q;
    q.push(1);
    st[1] = 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];
                if (!st[j])     // 如果队列中已存在j,则不需要将j重复插入
                {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }

    if (dist[n] == 0x3f3f3f3f) return -1;
    return dist[n];
}    

 

原理:

如果某条最短路径上有n个点(除了自己),那么加上自己之后一共有n+1个点,由抽屉原理一定有两个点相同,所以存在环。

模板:

int n;                      // 总点数
int h[N], w[N], e[N], ne[N], idx;       // 邻接表存储所有边
int dist[N], cnt[N];        // dist[x]存储1号点到x的最短距离,cnt[x]存储1到x的最短路中经过的点数
bool st[N];                 // 存储每个点是否在队列中

// 如果存在负环,则返回true,否则返回false。
bool spfa()
{
    // 不需要初始化dist数组

    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;       // 如果从1号点到x的最短路中包含至少n个点(不包括自己),则说明存在环
                if (!st[j])
                {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }
    return false;
}

 

 

模板:

初始化:
    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;


// 算法结束后,d[a][b]表示a到b的最短距离
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]);
}

输出的时候:
 if(d[x][y] > INF/2) puts("impossible");    
 //由于有负权边存在所以约大过INF/2也很合理   INF = 0x3f3f3f3f
 else cout << d[x][y] << endl;

 

 

 

 

 

 

发布了152 篇原创文章 · 获赞 149 · 访问量 5万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章