fatman
圖的定義及名詞
遍歷,拓樸排序
歐拉路徑,迴路
最短路
併查集
最小生成樹
連通性
連通分量
差分約束
2-SAT
constexpr int maxN=...;
{//鄰接矩陣
//存圖
int G[maxN][maxN];
//加邊
G[u][v]=1;
//使用
for(int i = 0;i<maxN;i++){
int to=g[from][i];
dfs(to);
}
}
{//鄰接串列
//存圖
vector<int> adj[maxN];
//加邊
adj[u].emplace_back(v);
//使用
for(int to:adj[from])dfs(to);
}DFS(深度優先搜尋)
constexpr int maxN=...;
vector<int> adj[maxN];
bitset<maxN> vis;
void dfs(int now){
vis[now]=1;
for(int i:adj[now])if(!vis[i])dfs(i);
}BFS(廣度優先搜尋)
constexpr int maxN=...;
vector<int> adj[maxN];
bitset<maxN> vis;
queue<int> q;
inline void bfs(int s){
vis[q.emplace(s)]=1;
for(int tmp;!q.empty();){
tmp=q.front(),q.pop();
for(int i:adj[tmp])if(!vis[i])
vis[q.emplace(s)]=1;
}
}給你一張圖,要你構出一組路徑從1走到n並經過全部邊(點可重複)
\(N \le 10^5, M \le 2 \times 10^5\)
存在條件?
除了起終點外,全部點的度數是偶數
起終點則要奇偶相同
看圖
看圖
看圖
看圖
看圖
看圖
看圖
看圖
看圖
看圖
整個流程就是這樣
1.找到沒有限制的點
2.拔他的出邊
3.去看他拔掉的邊對到的點還有沒有限制
看圖
整個流程就是這樣
1.找到沒有限制的點
2.拔他的出邊
3.去看他拔掉的邊對到的點還有沒有限制
一定可以做完全部事件?
動不了了
動不了了
動不了了
動不了了
定義:對於一張有像圖,每個點的出度皆為1
可以發現對於每個大小為\(n\)的連通塊,他都有\(n\)條邊,也就代表最多只有一個環。
這樣可以幹嘛?
對它拓樸排序,沒走到的點就在那個環上
為甚麼這麼大?可以發現不管一開始在哪,走到最後一定會在環上。
為甚麼這麼大?可以發現不管一開始在哪,走到最後一定會在環上。
這樣可以得出一個暴力\(O(\sum{k})\)的作法,但顯然不會過。
因為他是functional graph,所以它的路徑一定,可以預處理算出來。
空間:\(O(n\max(k))\) MLE
因為他是functional graph,所以它的路徑一定,可以預處理算出來。
空間:\(O(n\max(k))\) MLE
倍增!
空間:\(O(n\log{k})\) 時間:\(O(q\log{k})\) AC
主要分成幾種:
你的皮炎
偷
看這張圖,找到從0出發,到每個點的最短距離
int dis[5]={0,1,1,2,1};0
1
4
3
2
怎麼做出這個距離表格?
BFS 複雜度\(O(V+E)\)
看這張圖,找到從0出發,到每個點的最短距離
int dis[5]={0,1,1,2,1};0
1
4
3
2
怎麼做出這個距離表格?
BFS 複雜度\(O(V+E)\)
每一張圖都可以這樣做嗎
看這張圖,找到從0出發,到每個點的最短距離
0
1
4
3
2
3
10
4
2
6
int dis[5]={0,3,6,5,7};單純BFS行不通了
流程
開一個while(true),對於每條邊不斷更新他與0的最短距離
TLE?
發現在沒有負環的圖上,路徑上最多經過\(V\)個人,所以更新\(V\)次後一定可以找到最短路
負環則是可以無限更新,發現跑\(V\)次之後還能鬆弛就代表這張圖有負環
時間複雜度:\(O(VE)\)
空間複雜度:\(O(V+E)\)
vector<int> Bellman_Ford(int n,int s,vector<tuple<int,int,int>> edge){
// n代表這張無向圖有n個點(0~n-1)
// s代表起點
// auto [u,v,c] = edge[i] 代表u和v間有一條邊權為c的雙向邊
vector<int> dis(n,inf);// 初始成無限遠
dis[s]=0;
bool flag = 0;
for(int i = 0;i<=n;i++){
for(auto [u,v,c]:edge){
if(dis[u]+c<dis[v])dis[v]=dis[u]+c,flag=1;
if(dis[v]+c<dis[u])dis[u]=dis[v]+c,flag=1;
}
if(i==n)break;
flag=0;
}
assert(!flag); // 如果有負環這裡會炸
return dis;
}\(V \le 10^3, E \le V \times (V-1)/2\) ?
好慢喔
SPFA(範例扣有錯)
流程
1.找到離原點最接近的點
2.用他鬆弛他的全部鄰居
複雜度?
時間 : \(O(V^2+E)\)
空間 : \(O(V+E)\)
一種可以處理無負邊的圖的最短路算法
vector<int> Dijkstra(int n,int s,vector<vector<pair<int,int>>> adj){
// n代表這張無向圖有n個點(0~n-1)
// s代表起點
// auto [v,c] = adj[u] 代表u和v間有一條邊權為c的雙向邊
vector<int> dis(n,inf);// 初始成無限遠
vector<int> vis(n,0);
dis[s]=0;
for(int i = 0;i<n;i++){
int id = -1;
for(int i = 0;i<n;i++)if(!vis[i]&&(id==-1||dis[id]>dis[i]))id=i;
vis[id]=1;
for(auto [v,c]:adj[id])dis[v]=min(dis[v],dis[u]+c);
}
return dis;
}\(V \le 10^5,E \le 10^6\)
還是太慢ㄝ
優化第一個步驟
用一個min heap維護每個人到原點的距離
時間複雜度\(O((V+E) \log(V+E))\)空間複雜度\(O(V+E)\)
vector<int> Dijkstra(int n,int s,vector<vector<pair<int,int>>> adj){
// n代表這張無向圖有n個點(0~n-1)
// s代表起點
// auto [v,c] = adj[u] 代表u和v間有一條邊權為c的雙向邊
vector<int> dis(n,inf);// 初始成無限遠
dis[s]=0;
priority_queue<pair<int,int>,vector<pair<int,int>>,greater<pair<int,int>> pq;// min heap
// auto [d,v] = pq.top() 代表v是當前距離原點最近的點且距離原點'可能'是d
// 按照距離原點的距離排序 所以應該是距離放pair前,點的index放後
for(;!pq.empty();){
auto [d,u] = pq.top();pq.pop();
if(dis[u] < d)continue;// 代表從原點到v有一條比d更短的路
for(auto [v,c]:adj[u])if(dis[v]>d+c)pq.emplace(dis[v]=d+c,v);
}
return dis;
}\(V \le 500, E \le V \times (V-1) / 2\)
dijkstra?
\(O(VE \log V)\)
倒了
不要用pq優化?
\(O(V^3+VE)\)
可是我不屌這個
我要講新東西
一點dp的想法:
\(dp[i][j] = 從i走到j的最短路徑\)
轉移:\(dp[i][j] = \min_{1 \le k \le n}(dp[i][k]+dp[k][j])\)
複雜度:\(O(V^3)\)
其實就是矩陣乘法的複雜度
注意迴圈裡\(i, j, k\)的順序
原本的dp狀態:
\(dp[i][j][k] = i到j之間的最短路上i和j以外index最大是k\)
轉移:
\(chmin(dp[i][j][k],dp[i][k][any]+dp[k][j][any]) ,any \lt k\)
發現最後一維可以直接丟掉
vector<vector<int>> Floyd_Warshall(int n,vector<vector<int>> adj){
vector dis(n,vector(n,inf));
for(int i = 0;i<n;i++)dis[i][i]=0;
for(int k = 0;k<n;k++)
for(int i = 0;i<n;i++)
for(int j = 0;j<n;j++)
dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);
return dis;
}幫每個點設一個函數\(d(v)\)代表他的數量(現在還不知道是多少)
可以發現三個關係都可以轉成一種限制兩點d(x)差的邊:
1.\(d(a)-d(b) \le c\)
\(\to d(a) \le d(b) + c\)
\(\to b到a建一條長度為 \ c \ 的邊\)
2.\(d(a)-d(b) \ge c\)
\(\to d(a) \le d(b) - c\)
\(\to b到a建一條長度為- c \ 的邊\)
3.
\(d(a)-d(b) = 0\)
\(\to b到a,a到b各建一條長度為0的邊\)
怎麼樣會不可行?
\(\to\)有兩點間距離沒辦法決定的時候
\(\to\)最短路會出事
\(\to\)有負環
\(\to\)Bellman-Ford判負環
複雜度:\(O(VE)\)
\(現在有N個人,M個操作,操作有兩種 :\)
\(1.給你一個關係a,b代表b是a的pa\)
\(2.問你現在為止a最上面的pa是誰\)
\((定義每個人一開始的pa都是自己且保證不會有矛盾的操作)\)
暴力?
\(修改O(1) ,查詢O(V)\)
並查集?
\(修改O(\log V) ,查詢O(\log V)\)(均攤)
想法:
原本暴力的瓶頸是在於你每次都要看過全部的\(pa\)才能找到答案,但其實中間的\(pa\)根本不重要,所以可以做路徑壓縮
想法:
原本暴力的瓶頸是在於你每次都要看過全部的\(pa\)才能找到答案,但其實中間的\(pa\)根本不重要,所以可以做路徑壓縮
想法:
原本暴力的瓶頸是在於你每次都要看過全部的\(pa\)才能找到答案,但其實中間的\(pa\)根本不重要,所以可以做路徑壓縮
扣
struct DSU{
int n;
vector<int> dsu;
DSU(int _):n(_),dsu(_){
iota(dsu.begin(),dsu.end(),0);
}
inline int findpa(int pos){
return dsu[pos]==pos?pos:(dsu[pos] = find(dsu[pos]));
}
inline void modify(int a,int b){
dsu[a] = findpa(b);
}
inline int query(int a){
return findpa(a);
}
};如果modify沒有規定祖先關係
還可以用啟發式合併優化複雜度到
修改、詢問\(O(V \alpha (V))\)(均攤)
struct DSU{
int n;
vector<int> dsu;
DSU(int _):n(_),dsu(_){
fill(dsu,dsu+n,-1);
}
inline int findpa(int pos){
return dsu[pos]>=0?dsu[pos]=findpa(dsu[pos]):pos;
}
inline void modify(int a,int b){
a=find(a),b=find(b);
if(a==b)return;
if(dsu[a]<dsu[b])swap(a,b);
dsu[b]+=dsu[a],dsu[a]=b;
}
inline int query(int a){
return findpa(a);
}
};給你一張圖,問你連通塊數量和最大連通塊大小
給你一些關係,要你判斷每個關係的真假
想想看每組關係代表甚麼
\(1.x和y同類:\)
\(i \in \{0,1,2\}, val[x][i] = val[y][i]\)
\(2.x吃y\)
\(i \in \{0,1,2\}, val[x][(i+1) \mod 3] = val[y][i]\)
可以用\(dsu\)做耶
#include<bits/stdc++.h>
using namespace std;
#define IOS cin.tie(nullptr)->sync_with_stdio(0),cin.exceptions(cin.failbit);
#define lb(x) (x)&-(x)
#define all(x) (x).begin(),(x).end()
#define ll long long
constexpr int maxN=5e5+5;
int n,q,cnt,dsu[maxN<<2];
inline int find(int pos){
return dsu[pos]<0?pos:(dsu[pos]=find(dsu[pos]));
}
inline void merge(int a,int b){
a=find(a),b=find(b);
if(a==b)return;
if(dsu[a]<dsu[b])swap(a,b);
dsu[b]+=dsu[a],dsu[a]=b;
}
int main(){
IOS
cin>>n>>q;
fill(dsu,dsu+3*n+1,-1);
for(int a,b,c;q--;){
cin>>c>>a>>b;
if((c==2&&b==a)||b>n||a>n)cnt++;
else if(c==1){
if(find(a)==find(b+n)||find(a+n)==find(b))cnt++;
else merge(a,b),merge(a+n,b+n),merge(a+2*n,b+2*n);
}
else {
if(find(a+n)==find(b+n)||find(a+2*n)==find(b+n))cnt++;
else merge(a,b+n),merge(a+n,b+2*n),merge(a+2*n,b);
}
}
cout<<cnt<<'\n';
}給你一張圖耀你找到
(邊權中位數最小/邊權平均最小)
的子圖讓整張圖連通
流程:
\(1.幫所有邊按照邊權排序\)
\(2.按照順序 如果這個邊的兩端不連通 mst就要加上這條邊\)
反之不用
\(3.答案就會是那幾條邊\)
複雜度:\(O(E \log E + E \alpha(E))\)
流程:
\(1.隨便選一個人當起點\)
\(2.把這個人連到還沒走過的點的邊加進pq並按邊權排序\)
\(3.對於pq.top()的人 如果要更新的點還沒經過就把這條邊加入mst\)
\(4.跑第2點直到全部點被走過\)
複雜度:\(O((E+V) \log(E+V))\)
\(1.對於每個連通塊找到連出去最賺的邊 連起來\)
\(2.重複 \log V次\)
複雜度:\(O((V+E) \log V)\)
通常叫CC(connecting component)
一張圖連在一起的就叫連通分量
\(\to\)同一塊裡面可以分成很多塊
通常討論最外面最大那塊
就是連通塊,用\(dsu\)維護就好
一些後面會用到的名詞:
割點(cut):
拔掉這個點和所有和他相連的邊後這個連通塊會變兩塊
橋(bridge):
拔掉這條邊後這個連通塊會變兩塊
實作想法:
看剛剛的動畫可以發現對於bridge兩端的連通塊裡的各一點\(u,v\)之間必定只能有一條路徑
\(\to\)找完橋後直接把整個連通塊放在同一個BCC裡
口糊:
假設同一個BCC裡有點對\((u,v)\)無法滿足條件就代表他們之間只能有一條路徑
\(\to\)連通塊裡有bridge
\(\to\)矛盾
實作:
(1)找橋(例題)
在dfs tree上記錄每個點的dfn和low分別代表他的dfs序和往下走能走到的最小dfs序
對於一條邊是不是橋需要在上面的端點判,判斷下面的端點是不是能走到上面即可
(2)縮BCC
用stack在dfs的時候同時縮或是找完橋後再幫每塊跑dfs縮
複雜度:\(O(V+E)\)(一次dfs)
扣
vector<int> bcc_edge_find(int n,vector<vector<int>>& adj){
int cnt = 0,cc_cnt = 0;
vector<int> dfn(n),low(n,inf),stk,bcc_id(n);
auto dfs = [&](int now,int last) -> void {
dfn[now] = low[now] = ++cnt;
stk.emplace_back(now);
for(int i:adj[now])if(i!=last){
if(!dfn[i]){
dfs(i,now);
low[now] = min(low[now],low[i]);
if(low[i]>dfn[now]){
for(++cc_cnt;stk.back()!=i;stk.pop_back())
bcc_id[stk.back()] = cc_cnt;
bcc_id[stk.back()] = cc_cnt;
stk.pop_back();
}
}
else low[now] = min(low[now],dfn[i]);
}
};
for(int i = 0;i<n;i++)if(!dfn[i])dfs(i,i);
return bcc_id;
}實作想法:
發現對於一個割點的兩端必定不會在同一個BCC
\(\to\)當你找到割點就把他連出去的連通塊縮起來
懶得口糊
實作想法:
(1)找割點(例題)
與找橋大致相同,但你當成根的點在dfs的點需要特判
(判斷是不是有一個以上的獨立子樹)
(2)縮BCC
同樣是用stack在dfs過程中縮
複雜度:\(O(V+E)\)
扣
vector<vector<int>> bcc_vertex_find(int n,vector<vector<int>>& adj){
int cnt = 0,cc_cnt = 0;
vector<int> dfn(n),low(n,inf),stk;
vector<vector<int>> bcc_id(n);
auto dfs = [&](int now,int last) -> void {
dfn[now] = low[now] = ++cnt;
stk.emplace_back(now);
for(int i:adj[now])if(i!=last){
if(!dfn[i]){
dfs(i,now);
low[now] = min(low[now],low[i]);
if(low[i]>=dfn[now]){
++cc_cnt;
for(int p = -1;p!=i;stk.pop_back())
p=stk.back(),bcc_id[p].emplace_back(cc_cnt);
bcc_id[now].emplace_back(cc_cnt);
}
}
else low[now] = min(low[now],dfn[i]);
}
};
for(int i = 0;i<n;i++)if(!dfn[i])dfs(i,i);
return bcc_id;
}tarjanSCC:
在dfs tree上記錄dfn和low,如果low=dfn的時候代表可以縮SCC,用stack實作
需要注意對於dfs tree上的交錯邊,需要判斷指到的節點是否已經被標記SCC
複雜度:\(O(V+E)\)
tarjanSCC:
扣
vector<int> scc_tarjan(int n,vector<vector<int>>& adj){
int cnt = 0,cc_cnt = 0;
vector<int> dfn(n),low(n,inf),stk,scc_id(n);
auto dfs = [&](int now) -> void {
dfn[now] = low[now] = ++cnt;
stk.emplace_back(now);
for(int i:adj[now])if(!scc_id[i]){
if(!dfn[i])dfs(i,now),low[now] = min(low[now],low[i]);
else low[now] = min(low[now],dfn[i]);
}
if(dfn[now]==low[now]){
++cc_cnt;
for(int p = -1;p!=now;stk.pop_back())
p=stk.back(),scc_id[p]=cc_cnt;
}
};
for(int i = 0;i<n;i++)if(!dfn[i])dfs(i,i);
return scc_id;
}kosarajuSCC:
對於正圖先dfs一次透過後序加入stack找出反的拓排序,
再根據正的拓排序在反圖上dfs一次並對於走過的點都縮進同一個SCC
複雜度:\(O(V+E)\)
kosarajuSCC:
扣
vector<int> scc_kosaraju(int n,vector<vector<int>>& adj){
int cc_cnt=0;
vector<vector<int>> jda(n);
vector<int> scc_id(n),vis(n),topo_order;
for(int i = 0;i<n;i++)for(int j:adj[i])jda[j].emplace_back(i);
auto dfs1 = [&](int now) -> void {
for(int i:adj[now])if(!vis[i])dfs1(i);
topo_order.emplace_back(now);
};
auto dfs2 = [&](int now) -> void {
scc_id[now] = cc_cnt;
for(int i:jda[now])if(!scc_id[i])dfs2(i);
};
for(int i = 0;i<n;i++)if(!vis[i])dfs(i);
reverse(topo_order.begin(),topo_order.end());
for(int i:topo_order)if(!scc_id[i])++cc_cnt,dfs2(i);
return scc_id;
}kosarajuSCC:
扣
vector<int> scc_kosaraju(int n,vector<vector<int>>& adj){
int cc_cnt=0;
vector<vector<int>> jda(n);
vector<int> scc_id(n),vis(n),topo_order;
for(int i = 0;i<n;i++)for(int j:adj[i])jda[j].emplace_back(i);
auto dfs1 = [&](int now) -> void {
for(int i:adj[now])if(!vis[i])dfs1(i);
topo_order.emplace_back(now);
};
auto dfs2 = [&](int now) -> void {
scc_id[now] = cc_cnt;
for(int i:jda[now])if(!scc_id[i])dfs2(i);
};
for(int i = 0;i<n;i++)if(!vis[i])dfs(i);
reverse(topo_order.begin(),topo_order.end());
for(int i:topo_order)if(!scc_id[i])++cc_cnt,dfs2(i);
return scc_id;
}開心(水族箱)建模
先幫每個配料開兩個點代表要不要他
對於每個人的要求就變成
\(1.不滿足a就要滿足b\)
\(\to反a指向b\)
\(2.不滿足b就要滿足a\)
\(\to反b指向a\)
施竣耀升級了!