常用代碼模板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萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章