常用代碼模板3——搜索與圖論(Bellman-Ford算法 、spfa 算法、floyd算法、Kruskal算法、染色法、匈牙利算法 )

目錄

一、樹與圖的存儲

二、樹與圖的遍歷

(1) 深度優先遍歷 —— 模板題 AcWing 846. 樹的重心

(2) 寬度優先遍歷 —— 模板題 AcWing 847. 圖中點的層次

拓撲排序 —— 模板題 AcWing 848. 有向圖的拓撲序列

三、樹與圖的最短路問題

Bellman-Ford算法 —— 模板題 AcWing 853. 有邊數限制的最短路

spfa 算法(隊列優化的Bellman-Ford算法)

spfa判斷圖中是否存在負環 —— 模板題 AcWing 852. spfa判斷負環

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

四、最小生成樹和二分圖

Kruskal算法 —— 模板題 AcWing 859. Kruskal算法求最小生成樹

染色法判別二分圖 —— 模板題 AcWing 860. 染色法判定二分圖

匈牙利算法 —— 模板題 AcWing 861. 二分圖的最大匹配


 

 

一、樹與圖的存儲

樹是一種特殊的圖,與圖的存儲方式相同。對於無向圖中的邊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;

 


四、最小生成樹和二分圖

對於最小生成樹問題:我只背kruskal算法,好理解些,反正也要背 並查集。

對於二分圖問題:如果是判斷是不是二分圖就用染色法,如果是求最大匹配對數就用匈牙利算法。

 

int n, m;       // n是點數,m是邊數
int p[N];       // 並查集的父節點數組

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, edges + m);

    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) return INF;
    return res;
}

 

int n;      // n表示點數
int h[N], e[M], ne[M], idx;     // 鄰接表存儲圖
int color[N];       // 表示每個點的顏色,-1表示爲染色,0表示白色,1表示黑色

// 參數:u表示當前節點,c表示當前點的顏色
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] == -1)
        {
            if (!dfs(j, !c)) return 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;
}

 

算法描述:
如果你想找的妹子已經有了男朋友,
你就去問問她男朋友,
你有沒有備胎,
把這個讓給我好吧

多麼真實而實用的算法

TIP: 因爲你要去問的都是男孩子,所以存邊的時候,都是由男孩子指向女孩子

下面用一個gif動圖來演示這個整個配對的遞歸過程:

模板:

int n1, n2;     // n1表示第一個集合中的點數,n2表示第二個集合中的點數
int h[N], e[M], ne[M], idx;     // 鄰接表存儲所有邊,匈牙利算法中只會用到從第一個集合指向第二個集合的邊,所以這裏只用存一個方向的邊
int match[N];       // 存儲第二個集合中的每個點當前匹配的第一個集合中的點是哪個
bool st[N];     // 表示第二個集合中的每個點是否已經被遍歷過

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 res = 0;
for (int i = 1; i <= n1; i ++ )
{
    memset(st, false, sizeof st);
    if (find(i)) res ++ ;
}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章