目錄
(1) 深度優先遍歷 —— 模板題 AcWing 846. 樹的重心
(2) 寬度優先遍歷 —— 模板題 AcWing 847. 圖中點的層次
拓撲排序 —— 模板題 AcWing 848. 有向圖的拓撲序列
Bellman-Ford算法 —— 模板題 AcWing 853. 有邊數限制的最短路
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);
二、樹與圖的遍歷
-
(1) 深度優先遍歷 —— 模板題 AcWing 846. 樹的重心
時間複雜度 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);
}
}
-
(2) 寬度優先遍歷 —— 模板題 AcWing 847. 圖中點的層次
時間複雜度 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);
}
}
}
-
拓撲排序 —— 模板題 AcWing 848. 有向圖的拓撲序列
時間複雜度 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算法 —— 模板題 AcWing 853. 有邊數限制的最短路
注意在模板題中需要對下面的模板稍作修改,加上備份數組,詳情見模板題。
原理:
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];
}
-
spfa判斷圖中是否存在負環 —— 模板題 AcWing 852. spfa判斷負環
原理:
如果某條最短路徑上有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;
}
-
floyd算法 —— 模板題 AcWing 854. Floyd求最短路
模板:
初始化:
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;