北一女中 2022 黑魔法訓練營
建國中學 -> 台大資工 賴昭勳
為了向 hhhhaura 學習基礎圖論所以來當基礎圖論講師
去年在做這份講義的時候我在這裡放了一個爛梗,今年覺得不好笑了
唯利是圖
點
邊
重邊
自環
環?
連通塊?
度數?
路徑?
入度 =
出度 =
環?
連通塊?
//Challenge: Accepted
#include <bits/stdc++.h>
#pragma GCC optimize("Ofast")
using namespace std;
#define ll long long
#define maxn 505
#define pii pair<int, int>
#define ff first
#define ss second
#define io ios_base::sync_with_stdio(0);cin.tie(0);
vector<int> adj[maxn]; //很多程式碼會缺這行
二維陣列 A,若(u,v)有連邊就讓A[u][v]=1,否則A[u][v]=0
無向圖的時候記得A[v][u]也要看
空間需要O(n2)
較常見的儲存方式
對每一個點維護一個 vector ,紀錄他連到的所有點
空間需要O(m)
vector<int> adj[maxn];
int main() {
int n, m;
cin >> n >> m;
for (int i = 0;i < m;i++) {
int u, v;
cin >> u >> v;
adj[u].push_back(v);
adj[v].push_back(u);
}
}
vector<int> adj[maxn];
bool vis[maxn];
void dfs(int u) {
vis[u] = 1;
for (int v:adj[u]) {
if (!vis[v]) {
dfs(v);
}
}
}
int main() {
for (int i = 1;i <= n;i++) {
if (!vis[i]) dfs(i);
}
}
能走的就先走
記得紀錄每個點是否走過
vector<int> adj[maxn];
bool vis[maxn];
queue<int> que;
que.push(1);
while (que.size()) {
int cur = que.front();
vis[cur] = 1;
que.pop();
for (int v:adj[cur]) {
if (!vis[v]) {
que.push(v);
vis[v] = 1; //重要!
}
}
}
用一個佇列 (queue) 存下走得到並且還沒處理的點。
每次拿掉最前面的點並枚舉與他相鄰的點。
vector<int> adj[maxn];
bool vis[maxn];
int dis[maxn];
queue<int> que;
que.push(1);
for (int i = 1;i <= n;i++) dis[i] = -1;
dis[1] = 0;
while (que.size()) {
int cur = que.front();
vis[cur] = 1;
que.pop();
for (int v:adj[cur]) {
if (!vis[v]) {
que.push(v);
dis[v] = dis[cur] + 1;
vis[v] = 1; //重要!
}
}
}
計算每個點與原點的最短距離!
這裡的邊權重都是1
證明?
一個圖G是二分圖,代表可以把點集V拆成兩個點集X,Y,X∪Y=V,使得每一條邊的端點都是從X的點連到Y的點。
練習寫寫看?
vector<int> adj[maxn];
bool vis[maxn];
int color[maxn];
bool dfs(int n, int c) { //回傳是否形成二分圖
color[n] = c;
vis[n] = 1;
bool ret = 1;
for (int v:adj[n]) {
if (!vis[v]) {
ret &= dfs(v, 3 - c); //1->2, 2->1
} else if (color[v] == color[n]) {
return 0;
}
}
return ret;
}
int main() {
bool bipartite = 1;
for (int i = 1;i <= n;i++) {
if (!vis[i]) bipartite &= dfs(i, 1);
}
}
有n件事情要做,但是有m組限制,每組限制第i件事情需要比第j件事情早做完。每次只能做一件事,輸出做完事情的順序,或是輸出無解。
把事件看成點,限制看成有向邊。每次選擇一個沒有被限制的點,並把他連出去的限制拔掉。
無解條件?
vector<int> adj[maxn];
int deg[maxn];
int main() {
for (int i = 0;i < m;i++) {
int u, v;
cin >> u >> v;
adj[u].push_back(v);
deg[v]++;
}
queue<int> que;
for (int i = 1;i <= n;i++) {
if (deg[i] == 0) que.push(i);
}
while (que.size()) {
int cur = que.front();que.pop();
for (int v:adj[cur]) {
deg[v]--;
if (deg[v] == 0) {
que.push(v);
}
}
}
}
紀錄每個點目前的入度
根節點
葉節點
子節點(小孩)
父節點(父親)
子樹?
森林?
有根/無根樹
不用紀錄 visited
void dfs(int n, int par) {
for (int v:adj[n]) {
if (v != par) dfs(v, n);
}
}
與根節點之間的距離
int dep[maxn];
void dfs(int n, int par, int d) {
dep[n] = d;
for (int v:adj[n]) {
if (v != par) dfs(v, n, d+1);
}
}
樹上距離最遠的兩個點
O(n2)作法?
O(n)作法?
對於任意一點,距離他最遠的點是(其中一個)樹直徑上的點
證明?
int dep[maxn];
int dfs(int n, int par, int d) {
dep[n] = d;
int far = n;
for (int v:adj[n]) {
if (v != par) {
int tmp = dfs(v, n, d+1);
if (dep[tmp] > dep[far]) far = tmp;
}
}
return far;
}
int main() {
int u = dfs(1, 0, 0);
int v = dfs(u, 0, 0);
//Length: dep[v]
}
int siz[maxn];
void dfs(int n, int par) {
siz[n] = 1;
for (int v:adj[n]) {
if (v != par) {
dfs(v, n);
siz[n] += siz[v];
}
}
}
定義:一個點是樹重心,代表以該點為根,每個小孩的子樹大小皆不超過 n/2
O(n2)作法?
O(n)作法?
對於任意一點,如果有相鄰的點子樹大小>n/2就往那邊走
證明?
int siz[maxn];
void dfs(int n, int par) {
siz[n] = 1;
for (int v:adj[n]) {
if (v != par) {
dfs(v, n);
siz[n] += siz[v];
}
}
}
int tot; //總節點數
int get_centroid(int n, int par) {
for (int v:adj[n]) {
if (v != par && siz[v] * 2 > tot) {
return get_centroid(v, n);
}
}
return n;
}
int main() {
dfs(1, 0);
int centroid = get_centroid(1, 0);
}
若點x是點y的祖先,那麼
1. x=y 或是
2. x是y的父親的祖先。
兩個點u,v的最低共同祖先(LCA)就是深度最大的點x,使得x同時是u和v的祖先
先預處理每個點的深度。
每次拿兩個點中較深的點走到他的父親,當兩個點第一次重合的時候就是LCA。
走的步數是兩點之間的距離,最差是O(n)
要怎麼更快?
對每個人紀錄他的1,2,4,⋯,2k倍祖先。
用 Sparse Table 的方式!
先紀錄每個點的深度以及2k倍祖先,要詢問a跟b的LCA的時候:
時間複雜度:O(nlogn)預處理, O(logn)詢問
為什麼?
int anc[18][maxn];
int dep[maxn];
void dfs(int n, int par, int d) {
anc[0][n] = par;
dep[n] = d;
for (int v:adj[n]) {
if (v != par) dfs(v, n, d+1);
}
}
int lca(int a, int b){
if (dep[a] < dep[b]) swap(a, b);
for (int i = 17;i >= 0;i--) {
if (dep[anc[i][a]] >= dep[b]) {
a = anc[i][a];
}
}
if (a == b) return a;
for (int i = 17;i >= 0;i--) {
if (anc[i][a] != anc[i][b]) {
a = anc[i][a];
b = anc[i][b];
}
}
return anc[0][a];
}
int main() {
dep[0] = -1;
dfs(1, 0, 0);
for (int i = 1;i < 18;i++) {
for (int j = 1;j <= n;j++) {
anc[i][j] = anc[i-1][anc[i-1][j]];
}
}
}
但感覺需要再講一遍
可以加一條邊,
並查詢目前兩個點是否連通的資料結構
有路徑壓縮和啟發式合併兩種優化
struct DSU{
int par[maxn];
void init(int n) {
for (int i = 1;i <= n;i++) par[i] = i;
}
int find(int a) {
if (a == par[a]) return a;
int ret = find(par[a]);
par[a] = ret;
return ret;
}
bool Union(int a, int b) {
a = find(a), b = find(b);
if (a == b) return 0;
par[a] = b;
return 1;
}
} dsu;
有一張n個點的圖,一開始沒有任何邊。有m次修改,每次新增一條邊,回答那張圖是不是二分圖。
n,m≤2×105
離線?
在線?
有一張n個點的圖,一開始沒有任何邊。在時間i,1≤i≤m的時候新增(u,v)這條邊,接下來有q筆詢問a,b,回答a,b兩點最早在什麼時間連通。
n,m,q≤2×105
有一張n點m邊的連通帶權無向圖,每一條邊的花費是wi,從這張圖的邊選出一棵包含所有點的樹,並且最小化邊的花費總和。
生成樹:從一張圖的邊取出一棵樹,使得所有點連通。
生成樹的權重是邊的權重總和,最小生成樹就是要最小化權重。
把邊按照權重從小到大排序,維護目前的連通塊(DSU),從小的邊開始加:
複雜度O(mlogm)
證明?
維護「目前的最少生成樹」
隨便選一個點當起點,每次把距離生成樹上最近的點加到樹中,再更新其他點的值。
複雜度O(mlogm)或O(n2)
證明?
struct DSU{
int par[maxn];
void init(int n) {
for (int i = 1;i <= n;i++) par[i] = i;
}
int find(int a) {
return a == par[a] ? a : (par[a] = find(par[a]));
}
bool Union(int a, int b) {
a = find(a), b = find(b);
if (a == b) return 0;
par[a] = b;
return 1;
}
} dsu;
struct edge{
int u, v, w;
edge(){u = v = w = 0;}
edge(int a,int b, int c){u = a, v = b, w = c;}
};
int main() {
int n, m;
cin >> n >> m;
vector<edge> ed;
for (int i = 0;i < m;i++) {
int u, v, w;
cin >> u >> v >> w;
ed.push_back(edge(u, v, w));
}
sort(ed.begin(), ed.end(),
[&] (edge x, edge y){return x.w < y.w;});
dsu.init(n);
ll ans = 0;
for (edge e:ed) {
int u = e.u, v = e.v, w = e.w;
if (dsu.Union(u, v)) {
ans += w;
}
}
cout << ans << "\n";
}
有一張n點m邊的帶權圖,對於一條u到v的路徑,他的權重為每條邊的權重總和。u到v的最短路徑是權重最小的,u到v的路徑。
假設我紀錄了u到v目前找到的最短路徑長,叫做dis[u][v](沒找到路徑的話就用∞表示),
那如果存在一個中間點x,使得
dis[u][x]+dis[x][v]<dis[u][v],
就能把 dis[u][v] 更新為 dis[u][x]+dis[x][v],這個動作叫做鬆弛 (relax)
一個起點s到所有點v的最短路
紀錄每個點目前與原點s的最短距離 dis[i]
一開始dis[s]=0,dis[i]=∞(i=s)
重複n次:
枚舉所有邊(u,v),假設邊權是 w,如果dis[u]+w<dis[v],就對 v 進行鬆弛。
註:無向邊的話(u,v)和(v,u)都要看
時間複雜度O(nm)
證明?
有負邊的時候可以用!
判斷負環?
看n回合後是否有邊可以鬆弛
Shortest Path Faster Algorithm
優化過的Bellman-Ford!
每回合只更新「前一回合有被鬆弛」的點相鄰的邊。
用 queue 維護?
const ll inf = 1LL<<60;
vector<pii> adj[maxn];
ll dis[maxn];
bool vis[maxn];
int main() {
int n, m;
cin >> n >> m;
for (int i = 0;i < m;i++) {
int u, v, w;
cin >> u >> v >> w;
adj[u].push_back({v, w});
}
for (int i = 1;i <= n;i++) dis[i] = inf;
dis[1] = 0;
bool negative_cycle = 0;
for (int round = 0;round <= n;round++) {
for (int u = 1;u <= n;u++) {
for (auto [v, w]:adj[u]) {
if (dis[u] + w < dis[v]) {
if (round == n) {
negative_cycle = 1;
} else {
dis[v] = dis[u] + w;
}
}
}
}
}
}
這題比較難!
提示 ->
無解條件是:能從1走到一個正環再走到n
很重要 一定要念對
考慮不帶權的單點源最短路,我們用BFS維護一個 queue,每次處理一個點時最短路徑大小已知,因此只需要拿該點去更新其他點一次
在沒有負邊的假設下,Dijkstra 就像是有帶權的BFS。
DP的想法!
很重要 一定要念對
複雜度 O(mlogn)或O(n2)
const ll inf = 1LL<<60;
vector<pii> adj[maxn];
ll dis[maxn];
int main() {
int n, m;
cin >> n >> m;
for (int i = 0;i < m;i++) {
int u, v, w;
cin >> u >> v >> w;
adj[u].push_back({v, w});
}
for (int i = 1;i <= n;i++) dis[i] = inf;
dis[1] = 0;
priority_queue<pii, vector<pii>, greater<pii> > pq;
pq.push({0, 1});
while (pq.size()) {
ll d = pq.top().ff;
int cur = pq.top().ss;
pq.pop();
if (d != dis[cur]) continue; //重要
for (pii p:adj[cur]) {
int v = p.ff, w = p.ss;
if (d + w < dis[v]) {
dis[v] = d + w;
pq.push({dis[v], v});
}
}
}
for (int i = 1;i <= n;i++) cout << dis[i] << " ";
cout << "\n";
}
最後一部分了!
全點對最短路
想法:利用DP紀錄所有點對之間目前最短的距離!
令 dis[i][j] 為點 i 到 j 的最短距離
一開始如果(i,j)有連邊的話就令 dis[i][j]=wi,j
如果 i=j 令 dis[i][j]=0
否則 dis[i][j]=∞
從 1 到 n 枚舉路徑上的中繼點
每次更新所有點對
int dis[maxn][maxn];
for (int k = 1;k <= n;k++) {
for (int i = 1;i <= n;i++) {
for (int j = 1;j <= n;j++) {
dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]);
}
}
}
複雜度 O(n3)
證明?
const ll inf = 1LL<<60;
ll dis[maxn][maxn];
int main() {
int n, m;
cin >> n >> m;
for (int i = 1;i <= n;i++) {
for (int j = 1;j <= n;j++) dis[i][j] = inf;
dis[i][i] = 0;
}
for (int i = 0;i < m;i++) {
int u, v, w;
cin >> u >> v >> w;
dis[u][v] = min(dis[u][v], w);
dis[v][u] = min(dis[v][u], w);
}
for (int k = 1;k <= n;k++) {
for (int i = 1;i <= n;i++) {
for (int j = 1;j <= n;j++) {
dis[i][j] = min(dis[i][j],
dis[i][k] + dis[k][j]);
}
}
}
}
Bellman-Ford | Dijkstra | Floyd-Warshall | |
---|---|---|---|
問題類型 | 單點源 | 單點源 | 全點對 |
時間複雜度 | |||
空間複雜度 | |||
限制 | 無 | 邊權為正 | 無 |
多點源最短路
給一張無向帶權圖,有k個起點,對於每個點輸出他到任意一個起點的最短距離
n,m≤105,k≤n
加一條邊?
給一張無向帶權圖,有q個假想的方案,每個方案會考慮加上一條權重為 wi 的邊 (ui,vi),問加上那條邊之後最短路距離。
(不會真的加邊,也就是說每個方案彼此獨立)
n,m,q≤105
希望大部分有聽懂w
記得要做練習題喔