建國中學 陳仲肯
圖(X
圖(O
點
邊
有一些點,點和點之間可能會有邊連接
一條邊只能連接兩個點,但一個點可能連出很多條邊
表示一堆東西的關係,比如親戚關係、車站路線
邊可能有「權重」,簡稱邊權,常代表邊的長度
點也可能有點權,例如走到那個點會得到多少錢
1
2
5
3
3
4
6
5
1
2
1
重邊
自環
沒有重邊或自環的圖稱為簡單圖
一個點連接的邊數
有向圖的話:
連出去的叫出度
連進來的叫入度
1
2
3
4
5
3
1
1
1
0
兩個點藉由一條邊連在一起稱這兩個點相鄰
從圖中隨便選一些點和邊
1
2
3
4
5
1
2
3
4
1
2
3
4
5
從一個點開始走,經由一些邊走到一個點
走過的邊就是一條路徑
簡單路徑:沒有重複經過一個點的路徑
1
2
3
4
5
1
2
3
4
5
6
8
7
9
沒有環的簡單連通圖
沒有環的圖,通常會畫成這樣,像是掛在天花板上
1
3
2
4
6
5
隨便指定的點,通常畫圖時會畫在樹的最上面
1
3
2
4
6
5
子節點/兒子:下面(遠離根節點方向)的點
父節點/爸爸:上面(往根節點方向)的點
子節點/兒子:下面(遠離根節點方向)的點
父節點/爸爸:上面(往根節點方向)的點
沒有子節點的點
1
3
2
4
6
5
7
以X為根的子樹:以X為根,包含X以及X的子孫的樹
1
3
2
4
6
5
以3為根的子樹
到根節點的邊數
1
3
2
4
6
5
1
2
2
3
3
3
最深的點的深度
兩種常用的方法
1 | 2 | 3 | 4 | |
1 | 0 | 1 | 1 | 0 |
2 | 0 | 0 | 1 | 1 |
3 | 0 | 0 | 0 | 0 |
4 | 1 | 0 | 0 | 0 |
1
2
3
4
若有重邊可以把1改成邊的數量
bool G[N][N];
int n,m;
cin>>n>>m;
int u,v;
for(int i=0;i<m;++i){
cin>>u>>v;
G[u][v]=G[v][u]=1;
}
無向圖
兩向的邊都加入
有向圖
bool G[N][N];
int n,m;
cin>>n>>m;
int u,v;
for(int i=0;i<m;++i){
cin>>u>>v;
G[u][v]=1;
}
通常會先給你點數、邊數
再給每條邊的起點、終點
#include <cstring>
bool G[N][N];
int n,m;
memset(G,-1,sizeof(G));
cin>>n>>m;
int u,v,w;//weight
for(int i=0;i<m;++i){
cin>>u>>v>>w;
G[u][v]=w;
}
-1代表不可能出現的值,如果有負邊權就改成不可能出現的數
memset(a,b,c):把a開始c個位元都改成b,可以把陣列通通改成一個值,b可以是0,-1,但不能是1,因為位元的特性,-1=滿滿的1
空間複雜度不管多少邊,都是\(O(點數^2)\)
枚舉一個點連出的邊:\(O(點數)\)
枚舉每一條邊:\(O(點數^2)\)
找u到v的邊:\(O(1)\)
1
2
3
4
1:2,3
2:3,4
3:
4:
用vector
#include <vector>
vector<int> G[N];
int n,m;
cin>>n>>m;
int u,v;
for(int i=0;i<m;++i){
cin>>u>>v;
G[u].push_back(v);
}
無向圖就加上
G[v].push_back(u);
#include <vector>
#include <utility>
#define pii pair<int,int>
vector<pii> G[N];
int n,m;
cin>>n>>m;
int u,v,w;
for(int i=0;i<m;++i){
cin>>u>>v>>w;
G[u].push_back({v,w});
G[v].push_back({u,w});
}
pair<int,int> 存{點,邊權}
struct E{
int v,w;
};
vector<E> G[N];
//input
G[u].push_back({v,w});
//枚舉一個點連出去的邊
for(auto [v,w]:G[u])
這個寫法pair也可用
空間複雜度是\(O(邊數)\)
枚舉一個點連出的邊:\(O(他的度數)\)
枚舉每一條邊:\(O(點數+邊數)\)
找u到v的邊:\(O(u的度數)\)
因為 \(點數-1\le 邊數\le \frac{點數*(點數-1)}{2} \in O(點數^2)\)
空間複雜度是\(O(邊數)\)
枚舉一個點連出的邊:\(O(他的度數)\)
枚舉每一條邊:\(O(點數+邊數)\)
找u到v的邊:\(O(u的度數)\)
空間複雜度:\(O(點數^2)\)
枚舉一個點連出的邊:\(O(點數)\)
枚舉每一條邊:\(O(點數^2)\)
找u到v的邊:\(O(1)\)
鄰接串列
鄰接矩陣
矩陣適用於稠密圖,串列適用於稀疏圖
題目通常是稀疏圖,結論是用串列就好
兩種存圖方法比較
DFS、BFS
滿滿題目
目的:把每個點看過一遍
方法:DFS、BFS
1
2
3
4
5
1
2
3
4
5
DFS的路徑一定是一棵樹,之後會用到
vector<int> G[N];
bool vis[N];
void dfs(int cur){//current
vis[cur]=1;
for(int nxt:G[cur]){//next
if(vis[nxt])continue;
dfs(nxt);
}
}
int main(){
for(int i=0;i<n;++i){
if(!vis[i])dfs(i);
}
}
vector<int> G[N];
bool vis[N];
void dfs(int cur,int fa){//current, father
for(int nxt:G[cur]){//nxt
if(nxt==fa)continue;
dfs(nxt,cur);
}
}
int main(){
//根是1的連通樹
dfs(1,1);
}
超好寫
所以很常用
1
2
3
4
5
1
2
3
4
5
#include <vector>
#include <queue>
queue<int> Q;
vector<int> G[N];
for(int i=0;i<n;++i){
//i作為起點
if(vis[i])continue;
Q.push(i);
while(!Q.empty()){
int cur=Q.front();Q.pop();
for(int nxt:G[cur]){
if(vis[nxt])continue;
Q.push(nxt);
}
}
//Q必是空的
}
拜訪的節點到起點的最短距離非嚴格遞增
也就是說越先拜訪的點離起點越近
(不管邊權的話)
n=m=0時結束,但n!=0,m=0要做
給你一張帶權圖,求一個點到所有點的最短距離
前提:不能有負環
不然可以一直繞
繞其他邊可能會更好
起點到\(u,v\)的距離是\(d_u,d_v\),\(u,v\)距離\(d\)
\(d_u+d\le d_v\)時,更新\(d_v=d_u+d\)
這時考慮用這條邊鬆弛
已經看過了這兩條邊
枚舉所有邊進行鬆弛,鬆弛 \(V-1\) 次
時間複雜度:\(O(VE)\)
為什麼\(V-1\)次一定可以?
因為一條簡單路徑最多只有\(V\)個點\(V-1\)條邊
#define pii pair<int,int>
vector<pii> G[N];
int dis[N];
//Bellman-Ford,起點1
fill(dis,dis+n+1,INF);
dis[1]=0;
for(int t=1;t<n;++t){
for(int u=1;u<=n;++u){
for(auto [v,d]:G[u]){
dis[v]=min(dis[v],dis[u]+d);
}
}
}
每次只枚舉上次被鬆弛的點的邊
裸題
如果鬆弛V-1次後還能鬆弛就代表有一條V條邊的最短路徑
一定有負環
想不開的話可以用SPFA寫
如果沒有負邊的話...
最短路徑樹
子節點的距離一定不比父節點小
priority_queue<pii,vector<pii>,greater<pii>>
這裡如果放「小於」的話,pq會是從大到小
所以放「大於」就會從小到大
用struct存邊
要寫自定義的cmp,給priority queue用
好處是不用管pair的first跟second分別代表什麼
struct E{
int v,w;
};
vector<E> G[N];
struct cmp{
bool operator()(E a,E b){
return a.w>b.w;
}
};
priority_queue<E,vector<E>,cmp> pq;
把每條邊都push鬆弛一次、每個點pop一次
push pop都是\(O(log n)\)
\(n\)最多到\(E\)
\(O((E+V)log E)\)
如果有鬆弛到才加入priority_queue
vector<E> G[N];//存邊
priority_queue<E,vector<E>,cmp> pq;//dijkstra
ll dis[N];
void Dijsktra(){
//init
fill(dis,dis+n+1,INF);
dis[1]=0;
pq.push({1,0});//到1的距離為0(起點
while(!pq.empty()){
E cur=pq.top();pq.pop();
if(dis[cur.v]<cur.w)continue;
for(auto [v,w]:G[cur.v]){
if(dis[v]>cur.w+w){
pq.push({v,cur.w+w});
dis[v]=cur.w+w;
}
}
}
}
核心想法:每條路徑由起點、中繼點、終點構成
dp狀態
\(dp[k][i][j]\):利用前\(k\)個點當中繼點,\(i\)到\(j\)的最短距離
沒路過k 有路過k
dp轉移
\(dp[k][i][j]=min(dp[k-1][i][j],dp[k-1][i][k]+dp[k-1][k][j])\)
複雜度:\(O(V^3)\)
如果能考慮到所有兩點間的路徑,就能找到最短路徑
i到j的路徑分為中繼點有0、沒有0
各自又分成有1、沒有1...於是包含了所有路徑
如果能考慮到所有兩點間的路徑,就能找到最短路徑
i到j的路徑分為中繼點有0、沒有0
各自又分成有1、沒有1...於是包含了所有路徑
舉例
1
2
7
4
5
Directed Acyclic Graph 有向無環圖
每個點代表一個狀態
有向邊代表可能的轉移方法
要找出一個順序,能夠把所有狀態一次算好
入度為0的代表結果已經確定,所以可以拔掉
並且更新他連出去的點的入度、狀態
int deg[N];
vector<int> G[N],order;
queue<int> Q;
void topological_sort(){
memset(deg,0,sizeof(deg));
for(int i=0;i<n;++i){
for(int j:G[i])++deg[j];
}
for(int i=0;i<n;++i){
if(deg[i]==0)Q.push(i);
}
while(!Q.empty()){
int cur=Q.front();//以下要拔掉cur,並更新連出去的點
Q.pop();
order.push_back(cur);
for(int nxt:G[cur]){
//cur轉移到nxt
if(--deg[nxt]==0)Q.push(nxt);
}
}
}
可以拓樸排序完再DP
也可以邊拓樸排序邊DP
<---噁心寫法
DSU Disjoint Set Union
快速的合併兩個集合
快速的合併兩個連通塊
查詢一個點所在的連通塊(代表點)
每個集合/連通塊當作一棵有根樹
根節點當作代表元素
在樹上一直往上走,根節點就是代表
把其中一個樹的根節點隨便接在另外一棵樹上
1
3
2
4
每個集合/連通塊當作一棵有根樹
根節點當作代表元素
在樹上一直往上走,根節點就是代表
把其中一個樹的根節點隨便接在另外一棵樹上
1
3
2
4
舉例:一開始每個點都獨立,一直合併
int f[N];
void init(){
for(int i=1;i<=n;++i)f[i]=i;
}
int fa(int me){
if(f[me]==me)return me;
return fa(f[me]);
}
void join(int a,int b){
int A=fa(a),B=fa(b);
f[A]=B;
}
路徑壓縮:直接把阿公(祖先)當作爸爸
啟發式合併:把小集合合併到大的集合
其實沒寫啟發式合併也還好
平均複雜度\(O(\alpha(n))\),可以當作常數
最差複雜度\(log n\)
int f[N],sz[N];
void init(){
for(int i=1;i<=n;++i)f[i]=i,sz[i]=1;
}
int fa(int me){
if(f[me]==me)return me;
f[me]=fa(f[me]);//路徑壓縮
return f[me];
}
void join(int a,int b){
int A=fa(a),B=fa(b);
if(sz[A]<sz[B]){
//A併到B
f[A]=B;
sz[A]+=sz[B];
}else{
//B併到A
f[B]=A;
sz[B]+=sz[A];
}
}
先做一次塗色,每次加點時...
把每個顏色當成一個元素
加入新的邊u--v時
如果u,v的顏色在同一個集合就衝突了,否則
把(u的顏色)跟(v的同事的顏色)合併
把(v的顏色)跟(u的同事的顏色)合併
MST Minimum Spanning Tree
對於所有連通圖,點數與原圖相同、邊數最少的連通子圖
一定是一棵樹,稱為生成樹
邊權總和最小的生成樹即稱為最小生成樹
一個連通圖內兩個未完成的最小生成樹
用最短的邊連接一定最好
每次找當前權重最小的邊,
看端點是否在兩個不同的最小生成樹中,
是的話就把兩棵樹合併起來。
如何合併?
並查集
struct E{int u,v,w;};
bool cmp(E a,E b){return a.w<b.w;}
E edge[N];
int Kruskal(){
sort(edge, edge+m, cmp);
int ans=0;
for(int i=0;i<m;i++){
if(fa(edge[i].u)!=fa(edge[i].v)){
ans+=edge[i].w;
join(edge[i].u, edge[i].v);
}
}
return ans;
}
跟Dijkstra類似
把「與原點距離」改成「與樹距離」
struct E{int v,w;};
struct cmp{bool operator()(E a,E b){return a.w>b.w;}};
priority_queue<E,vector<E>,cmp> pq;
pq.push({1,0});
int ans=0;
dis[1]=0;//到樹距離
while(!pq.empty()){
E cur=pq.top();pq.pop();
if(vis[cur.v])continue;
vis[cur.v]=1;
ans+=cur.w;
for(auto [v,w]:G[cur.v]){
if(!vis[v]&&dis[v]>w){
dis[v]=w;
pq.push({v,w});
}
}
}
裸題