圖論

 

Outline

  • 圖介紹
  • BFS/DFS
  • 歐拉路徑
  • 樹的介紹
    • 樹上最遠距離
  • DAG (有向無環圖)
  • DSU
  • 樹壓平 、 LCA
  • 最小生成樹
  • 最短路
  • 二分圖匹配

圖的介紹

圖的種類

  • 稀疏圖
    • $$|V| \approx |E|$$
  • 稠密圖
    • $$E| \approx |V|^2$$
  • 無向圖
    • 無向邊所構成的圖
  • 有向圖
    • 有向邊所構成的圖
  • 有向無環圖 (DAG)
    • 有向邊 + 無環

圖的種類

  • 完全圖
    • 每個點對間都有邊相連
  • 二分圖
    • 可把圖分成恰兩部分,使得兩部分內部無邊相連、邊只會連接兩部分

圖的儲存

Edge List

  • 就按照題目要求,用一個陣列把所有邊存下來
  • 缺點:
    • 每條邊之間沒有關聯性,在做圖的走訪時每次要重新枚舉整個陣列

鄰接矩陣

  • 建一個 \(n \times n\) 的陣列, \(e[i][j]\) 存從 \(i \rightarrow \ j\) 的邊
  • 優點:
    • 有表示到節點和邊的關係
  • 缺點:
    • 這樣空間複雜度為 \(O(n^2)\) , 在稀疏圖太慢

鄰接陣列

  • 改善鄰接矩陣,因為稀疏圖實際上邊數很少
  • 所以改成開 \(n\) 個 vector 去存每個點 \(i\) 連出去的邊
vector<int> v[N];
vector<pair<int,int> > e[N];
void add_edge(int a,int b){
	v[a].eb(b);
}
void add_edge(int a,int b,int w) {
	e[a].eb(mp(b,w));
}

圖的遍歷

深度優先搜索 (DFS)

  • 和人走路相同,
  • 會持續從一個點往下走直到沒路可走
  •  實作時通常直接用遞迴
void dfs(int x){
	vis[x]=true;
	for(int i:v[x]){
    	if(!vis[i]) dfs(i);
    }
}

廣度優先搜索 (BFS)

  • 他是再一個點時往他所有相鄰的點走
  • 實作通常是使用 queue
void bfs(int s){
  queue<int> q;
  q.push(s);
  while(!q.empty()){
    int x=q.front();q.pop();
    for(int i:v[x]) {
      if(!vis[i]){
        vis[i]=true;
        q.push(i);
      }
    }
  }
}
  • 判斷給定的圖是不是二分圖
  • 二分圖定義是可以分成兩堆內部無邊的點集
  • 可以把點上色,這樣只要有邊的顏色必定要不同
  • DFS、BFS都可做

歐拉路徑

歐拉路徑

  • 也稱作一筆畫問題
  • 問是否可以在不走重複邊的情況下走完所有邊
  • 並構造一組解

歐拉路徑

  • 有向圖歐拉路徑存在條件
    • 除起終點外其餘點的入度=出度,起點出度多1,終點入度多1
  • 無向圖歐拉路徑存在條件
    • 至多兩個奇點(度數為奇數)(其實只有 0/2)
    • 0個奇點稱為歐拉迴路

歐拉路徑

  • 對邊 DFS 
    • 實作上還是點枚舉
  • 離開一條邊時加入答案
  • 最終再把答案反轉

歐拉路徑

  • 可注意到因為是對邊 DFS
  • 因此一條邊遍歷後要從該點的陣列刪除
  • 或是可以維護一個標記 \(in[i]\) 代表節點 \(i\) 已經枚舉到哪
  • 求字典序最小的歐拉路徑
  • 求字典序最小的歐拉路徑
  • 只要DFS 時把邊按照字典序排好即可

大家來找碴

wiwihorz 讀書會圖論I簡報的 code

大家來找碴

wiwihorz 讀書會圖論I簡報的 code

  • 前面有說過必須避免重複遍歷已走過的邊
  • 若遇到恰兩個點互連爆多邊就會噴到 \(O(nm)\)
void dfs(int x){
	for(int &p=it[x];p<SZ(e[x]);p++){
		if(!used[e[x][p].S]) {
			used[e[x][p].S]=1;
			dfs(e[x][p].F);
		}
	}
	ans.eb(x);
}

一種正確寫法

  • 圖論中的一種特例
  • 由恰 \(n-1\) 條無向邊所組成的聯通圖

樹的遍歷

  • 通常用 DFS
  • 不同根節點會建出不同樹
  • 可以注意到一個點會恰有一個祖先
  • 其他為子節點
  • 所以DFS 只需判斷該點是不是祖先
void dfs(int x,int p=-1,int d=0){
	dep[x]=d;
	for(int i:v[x]){
		if(i==p) continue;
		dfs(i,x,d+1);
	}
}
  • 求樹上最遠兩點的距離
  • 因為最遠距離必定有一端在根節點的最遠點
  • 所以做兩次 DFS
    • 第一次找到與根節點最遠的點
    • 再從該點出發
  • 可以注意到對 \(x\) 子樹來說距離只有兩種情況
    • 通過\(x\) 的
    • 橫跨\(x\)某一子樹
  • 維護 \(dis[x] , maxdep[x]\) 分別代表 \(x\) 內的最遠距離、距離 \(x\) 的最遠點(最深)
  • \(dis[x]=\max_{c_1,c_2 \in x}(dis[x],maxdep[c_1]+max_dep[c_2]+2\)
  • 實作時可以維護目前為止的 \(maxdep[c_1]\)
  • 所以不需同時枚舉點對 \(c_1,c_2\)
int dis[N],maxdep[x];
void dfs(int x,int p=-1){
	maxdep[x]=dis[x]=0;
	for(int i:v[x]){
    	if(i!=p){
        	dfs(i,x);
            dis[x]=max(dis[x],max(dis[i],maxdep[x]+maxdep[i]+1));
            maxdep[x]=max(maxdep[x],maxdep[i]+1);
        }
    }
}

有向無環圖 (DAG)

有向無環圖

  • 每條邊有方向、沒有出現環

拓撲排序

  • 要怎麼好的遍歷 DAG
  • 走訪一個點之前指向他的邊一定要先走訪
  • 因為保證有向無環,必定有點可以先走訪
  • 沒有被任何人指到的可以先走訪
  • 拔掉該點後一樣是 DAG

拓撲排序

void toplogical sort(){
    queue<int> q;
    for(int i=1;i<=n;i++) if(!deg[i]) q.push(i);
    while(q.size()){
        int x=q.front();
        q.pop();
        for(auto [i,w]:v[x]){
            deg[i]--;
            if(!deg[i]) q.push(i);
        }
    }
}

DAG dp

a454. TOI2010 第二題:專案時程

  • 每個任務 \(x\) 有其花費天數、也有一些順序關係。他能開始做只有當連向 \(x\) 的所有任務都完成了

 

  • 令 \(dp[x]\)代表 \(x\) 完成的天數
  • \(dp[x]=max\{dp[i]+w\}(i,x,w)∈edge\)
  • 按照拓撲排序順序轉移就好
void toplogical sort(){
    queue<int> q;
    for(int i=1;i<=n;i++) if(!deg[i]) q.push(i);
    while(q.size()){
        int x=q.front();
        q.pop();
        for(auto [i,w]:v[x]){
            dp[i]=max(dp[i],dp[x]+w);
            deg[i]--;
            if(!deg[i]) q.push(i);
        }
    }
}

並查集

並查集

  • 一種資料結構可以進行兩種操作

  • 詢問元素所在的集合
  • 把兩集合合併
    這裡的集合在圖論上被稱為連通塊
  • 朋友的朋友也是朋友
    給定\(M\)筆朋友關係,和\(Q\)筆詢問,問\(A,B\)是不是朋友。

並查集

  • 把連通關係當作一顆樹,根節點就是最高層的祖先
  • 同一棵樹代表在同一連通塊
  • 初始每個人都是一棵樹
  • 合併時就等價兩棵樹合併

並查集

並查集

  • 既然是兩顆樹合併,直接拿根節點合併即可
  • 有兩個優化 
  • 路徑壓縮
  • 啟發式合併
  • 用其中一個單次時間複雜度變成 \(O(\log{n})\)
  • 兩者都用變成 \(O(\alpha(n)) \sim O(1)\)

並查集

  • 路徑壓縮
    • 因為你只在乎誰是樹根,尋找時也是要找樹根
    • 找到後直接把尋找路段壓縮起來

並查集

  • 啟發式合併
    • 兩樹合併時用小的指向大的
    • 也可以用樹高低的指向大的

並查集


void init(int n){
    for(int i=1;i<=n;i++) p[i]=i,sz[i]=1;
}
int fp(int x){
    if(x!=p[x]) p[x]=fp(p[x]);//路徑壓縮
    return p[x];
}
void Union(int a,int b){
    a=fp(a);//找到連通塊的祖先
    b=fp(b);
    if(a!=b){
        if(sz[a]<sz[b]){
            swap(a,b);//確保a是數量較大的
        }
        p[b]=a;//把b指向a
        sz[a]+=sz[b];
    }
}
  • 一樣有 CF EDU 教學和習題可練

樹壓平

 

樹壓平

  • 把樹變成序列,維護進入和離開時間戳記
  • \(in_i,out_i\)
  • 有以下性質
  •  \([in_x,out_x]\)  表示 \(x\) 子樹

樹壓平

  • 時間序列: [1,2,6,6,5,5,2,4,4,3,3,1]
int in[N],out[N];
int t=1;
void dfs(int x,int p=-1){
    in[x]=t++; // 進入的時間戳記
    for(int i:v[x]) {
        if(i!=p) dfs(i,x);
    }
    out[x]=t++; // 離開的時間戳記
}

樹壓平

  • 有了時間序列就可以判祖孫關係
  • 剛剛提到在 \([in[x],out[x]\) 內出現的是 \(x\) 子樹,
  • 也就是 \(x\) 的子孫
bool isanc(int a,int b){
	return in[a]<=in[b]&&out[b]<=out[a];
}

最低共同祖先(LCA)

LCA

  • \(O(\log{n})\) 求任意兩點\(a,b\)在樹上的最小共同祖先
  • \(LCA(5,6)=2\)
  • \(LCA(3,6)=1\)

LCA

  • 假設 \(LCA(a,b)=w\) 
  • 那 \(w\) 會是 \(a\) 的 \(d_a\) 層祖先
  • 對 \(a\) \(1 \sim d_a-1\) 都不是 \(b\) 祖先
  • \(d_a \sim \inf \) 都是 \(b\) 祖先
  • 有二分搜性質
  • 可以把 \(d_a\) 二進位分解

LCA 倍增法

  • 既然要二進為分解,代表需要知道 \(2\) 的冪次層祖先
  • 倍增
void build(){
    for(int i=1;i<=K;i++){
        for(int j=1;j<=n;j++) ac[i][j]=ac[i-1][ac[i-1][j]];
    }
}

LCA

  • 二進位分解後就依序看每一個 \(bit\) 要是 \( 1/0 \)
int LCA(int a,int b){
    if(isanc(a,b)) return a;
    if(isanc(b,a)) return b;
    for(int i=K;i>=0;i--){//跳到洽 $k_1-1$ 層的位置
        if(!isanc(ac[i][a],b)) a=ac[i][a];
    }
    return ac[0][a];
}

LCA

int in[N],out[N];
int t=1;
void dfs(int x,int p=-1){
    in[x]=t++; // 進入的時間戳記
    if(p!=-1) ac[0][x]=p;
    else ac[0][x]=x; // 預處理父親
    for(int i:v[x]) {
        if(i!=p) dfs(i,x);
    }
    out[x]=t++; // 離開的時間戳記
}
void build(){
    for(int i=1;i<=K;i++){
        for(int j=1;j<=n;j++) ac[i][j]=ac[i-1][ac[i-1][j]];
    }
}
bool isanc(int a,int b){//判斷 a 是否為 b 的祖先
  return in[a]<=in[b]&&out[b]<=out[a];  
}
int LCA(int a,int b){
    if(isanc(a,b)) return a;
    if(isanc(b,a)) return b;
    for(int i=K;i>=0;i--){//跳到洽 $k_1-1$ 層的位置
        if(!isanc(ac[i][a],b)) a=ac[i][a];
    }
    return ac[0][a];
}

LCA

  • 有了 \(LCA\) 可以幹嘛
  • 樹上兩點距離會恰巧通過 \(LCA(a,b)\)
  • \(dis(a,b)=dis(a,LCA(a,b))+dis(LCA(a,b),b)\)
  • 可以透過建深度陣列求出距離

最小生成樹

最小生成樹

  • 給一堆邊,求權重總和最小的樹
  • 同時也會是權重最大值最小的樹

Kruskal’s algorithm

  • 把邊按照權重由小到大排序
  • 依序確認這條邊是否可加入(是否不形成環)
  • 最終即可得到答案

Kruskal’s algorithm

  • 把邊權由小到大排序
  • 一條邊要加入等價會合併兩聯通塊
  • 並查集

Kruskal’s algorithm

DSU s; //並查集
void Kruskal(vector<edge> e){
    sort(e.begin(),e.end(),cmp);//按照邊權由小到大排
    int sum=0;
    for(edge ei:e){
        if(!s.same(ei.a,ei.b)) s.union(ei.a,ei.b),sum+=ei.w;
    }
}

Kruskal’s algorithm

  • 證明:
    • 假設目前找到的為 \(T\) ,權重更小者為 \(T^*\)
    • \(T^*\) 按照 Kruskal 加邊順序第一條\(T\) 沒有的邊 \(e\)
    • 那考慮把這條邊加上去,那會形成還需要再拔一條邊 \(e^*\)
    • 但因為是按照邊權由小到大,所以 \(w(e^*)<w(e) \) 
    • 故與假設矛盾
  • 出現邊權最小的生成樹證明也相同

裏表次元 (Dimension) (校內賽pF)

裏表次元 (Dimension) (校內賽pF)

  • 題目等價有一張圖和 \(Q\) 次詢問
  • 每次詢問從 \(s \rightarrow t\) 經過的最大邊最小可能是多少

裏表次元 (Dimension) (校內賽pF)

  • 給定的是圖,但可以簡化成樹
  • 最小生成樹
  • 之後題目剩下要怎麼快速求兩點路徑上最大邊
  • 倍增、LCA

最短路

最短路

  • Dijkistra
  • Bellman-ford
  • Floyd-Washall
  • 給你 \(n\) 點 \(m\) 邊的正權圖
  • 求 \(1\) 到所有點的最短距離
  • \(1 \leq n \leq 10^5\)
  • \(1 \leq m \leq 2 \cdot 10^5\)
  • 考慮 BFS
  • 當你 BFS 時依序更新路 ...
  • 但問題是可能會重複 鬆弛 (後來才該走的路先走了)
  • 鬆弛 (relax) : 對節點 \(v\) 更新他指向的所有節點 \(u\)
  • 時間複雜度會噴到 \(O(nm)\)
  • 但假如可以定一個走的順序就只用走一次

Dijkistra

  •  BFS 時的問題是可能後來反而距離較小
  • 所以如果可以保證每次都走距離小的就好了
  • queue \(\rightarrow\) priority_queue
  • 按照當前到所有點的距離,每次走最小的更新

Dijkistra

Dijkistra

  • 實作時和 BFS Code 大致相同,改成 priority_queue 就好
typedef pair<int,int> pii;
void dijkistra(int s,int t){
	priority_queue<pii,vector<pii>,greater<pii>> pq;
    pq.push(mp(0,s));
    fill(dis,dis+N,INF);
    dis[s]=0;
    while(pq.size()){
    	pii now=pq.top();
        pq.pop();
        for(pii p2:v[now.S]){
        	if(dis[p2.F]>dis[now.S]+p2.S) {
            	dis[p2.F]=dis[now.S]+p2.S;
                pq.push(mp(dis[p2.F],p2.F));
            }
        }
    }
}

Dijkistra

  • 每條邊只會走一次、每個點也只會進入一次
  • 時間複雜度是 \(O(|V|+|E|\log{|V|})\)

Dijkistra

  • 那如果有負權呢 ?
  • 允許重複入隊
typedef pair<int,int> pii;
void dijkistra(int s,int t){
	priority_queue<pii,vector<pii>,greater<pii>> pq;
    pq.push(mp(0,s));
    fill(dis,dis+N,INF);
    dis[s]=0;
    while(pq.size()){
    	pii now=pq.top();
        pq.pop();
        if(dis[now.S]<now.F) continue; //允許重複入隊寫法
        for(pii p2:v[now.S]){
        	if(dis[p2.F]>dis[now.S]+p2.S) {
            	dis[p2.F]=dis[now.S]+p2.S;
                pq.push(mp(dis[p2.F],p2.F));
            }
        }
    }
}

Dijkistra

  • 可判負環的時間複雜度不是好的

Bellman-Ford 算法

  • relax 一次不夠,就 relax 很多次
  • 一條最短路必定是至多走\(n-1\) 條邊
  • 因此只要 \(n-1\) 次 relax 即可
  • 時間複雜度為 \(O(nm)\)

SPFA 算法

  • 可注意到不一定每次都要 relax 所有點
  • 第 \(t-1\) 次沒被 relax , 第 \(t\) 次必定不會
  • code 和前面重複入隊 Dijkistra 很像,只是又把 priority_queue \(\rightarrow\) queue
  • 期望時間複雜度 \(O(|V|+|E|)\)
  • 但最差可能和 Bellman-ford 一樣 \(O(nm)\)
  • 但師大測資一定可以

SPFA 算法

wiwihorz 暑培圖論II簡報的 code

  • 給 \(n\) 點 \(m\) 邊的有向有權圖
  • 輸出 \(n\) 行,其中第 \(i\) 行包含一個整數,表示從節點 \(1\) 走到節點 \(i\) 的最短路徑長,如果無法走到 \(i\),輸出 QwQ;如果從節點 \(1\) 到 \(i\) 的路徑長可以任意小,輸出 OAO。
  • 只要 \(n-1\) 次 relax 即可
  • 因此只要第 \(n\) 次還被 relax 到,代表負環可走到該點
  • 負環上的點也都會在 \(n\) 次被 relax
  • 因此只要從這些點做 BFS 即可

wiwihorz 暑培圖論II簡報的 code

wiwihorz 暑培圖論II簡報的 code

  • 給 \(n\) 點 \(m\) 邊的有向有權圖
  • 求圖上任一個負環
  • 前面講的是判有經過起始點的環的作法
  • 唯一與起始點 \(s\) 相關是 \(dis[s]:=0\)
  • 但這次只需要判負環,只在乎是否會 relax \(n\) 次
  • 把 \(dis[s]\) 丟掉
  • 一樣 relax \(n\) 次,找到與負環相通的點
  • 從該點倒退走 \(n\) 次後,必在負環上
  • 再從該點倒退走找負環

Floyd-Washall 算法

  • 求全點對最短距離算法
  • 其實是一種 \(dp\)
  • 假設有經過點 \(k\)
  • \(dis[l][r]=dis[l][k]+dis[k][r]\)

Floyd-Warshall 算法

  • \(dis[l][r]=dis[l][k]+dis[k][r]\)
  • 先枚舉轉移點 \(k\) , 內部再枚舉距離兩端
  • 等價是做當轉移點限用 \(1 \sim k\) 情況下的最短距離
  • 迴圈順序錯誤只要跑 3 次就會對
void FloydWarshall(){
	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][k]+dis[k][j]);
            }
        }
    }
}

二分圖匹配

二分圖匹配

  • 二分圖 (bipartite graph)
    • 可以把點分成兩堆且同一堆內的節點間都沒有邊
  • 匹配 (matching)
    • 找一些點兩兩配對,每個點只能在一個匹配中且同一匹配的點必須相鄰
  • 最大匹配 (maximum matching)
    • 有最多配對的匹配
  • 匹配是一個邊的獨立集

名詞介紹

  • 匹配點 有被配對的點
  • 匹配邊 兩端是一個配對的邊 
  • 交錯路徑 
    • 交替經過未匹配邊、匹配邊的路徑
  • 增廣路 
    • 一種滿足頭尾都是未匹配點的交錯路徑

Berge's lemma

找二分圖最大匹配

  • 根據 Berge's lemma 就一直找增廣路即可
  • 把圖分成兩半 \(X,Y\) 
  • 枚舉 \(X\)的點 如果有找到增廣路則反轉邊,匹配數+1
  • 時間複雜度 \(O(|V||E|)\)

找二分圖最大匹配

Code

const int N=1e3+7;
vector<int> v[N];//X的鄰點
int match[N]; // Y所匹配的節點
bool vis[N]; //在這次尋找中是否被走過
int dfs(int x){
	for(int y:v[x]){
		if(vis[y]) continue;
		vis[y]=true;
		if(match[y]==-1||dfs(match[y])){
			match[y]=x;
			return true;
		}
	}
	return false;
}
int bipartiteMatching(int n){
	fill(match,match+n+1,-1);
	int ans=0;
	rep(i,1,n){
		fill(vis,vis+n+1,0);
		if(dfs(i)) ans++;
	}
	return ans;
}

一些集合

  • 點覆蓋 用點去覆蓋所有邊的點集合
  • 點獨立集 互不相鄰的點集
  • 邊覆蓋 用邊去覆蓋所有點的邊集合
  • 邊獨立集 互不共點的邊集 ( 匹配 )

符號

  • 二分圖左右兩側 \(X,Y\)

  • 最小點覆蓋 \(C_v\)

  • 最小邊覆蓋 \(C_e\)

  • 最大點獨立集 \(I\)

  • 最大邊獨立集 = 最大匹配 = \(M\)

性質

  • \(|C_v|=M\)
  • \(|I|=|V|-|C_v|\)
  • \(|M|=|V|-|C_e|\)

Kőnig’s theorem

 因為覆蓋 \(M\) 條匹配至少需要 \(|M|\)個點

所以 \(|C_v| \geq |M|\)

所以只要構造出一組點獨立集 \(|C_v|=|M|\)即可

構造方法如下:

從 左邊沒有被匹配的點去走符合增廣路 "交替出現" 要求的路徑(未匹配邊-匹配邊-未匹配邊...) 並標記路過的所有點。

之後左邊沒被標記到的點 + 右邊被標記的點 即為最小點覆蓋

Kőnig’s theorem

證明- 1 ( 該得到的點集可以覆蓋所有的邊 ):

證明不可能有一條邊 左端有標記,右端沒標記

  • 假設這條邊屬於匹配
    • 則右端的標記必定是從左邊而來,故兩端都有或都沒有
  • 假設這條邊不屬於匹配
    • 則右端點可以從左端點走去,故兩者都會有標記

 

Kőnig’s theorem

假定得到點集為 \(|P|\)

證明-2 \(|P|=|M|\)

  • 左邊未被標記點代表他是一條匹配邊左端(且該條匹配邊沒被標記過)
  • 右邊被標記點也代表他是一條匹配邊右端
  • 所以 \(|P|=|M|\)

Kőnig’s theorem

由證明-1,證明-2 再加上前面所述,

可保證這構造出來的是最小點覆蓋

故得證 \(|C_v|=|M|\)

如果還有不懂可以參考這個網頁的證明

最大點獨立集

  • 任意點獨立集的補集就是點覆蓋
    • 可以想一下
  • 最小點覆蓋的補集就是最大點獨立集
  • \(I=V \setminus C_v\)

最大點獨立集

  • 最小點覆蓋的補集就是最大點獨立集
  • \(I=V \setminus C_v\)

最小邊覆蓋

首先圖不會有孤點,不然一定無解

 

 1. 把 \(M\)中的邊都加入 \(C_e\) 則覆蓋\(2|M|\)個點,

最多再選\(V-2|M|\)條邊集可覆蓋所有點

故\(|C_e|\leq |V|-|M|\)

 

最小邊覆蓋

2. 因為 \(C_e\) 上若有環則至少可以拔掉一條邊,

故 \(C_e\) 為森林,連通塊數量為 \(|V|-|C_e|\)

則從每個連通塊選一條邊會形成邊獨立集

因為邊獨立集不會超過最大邊獨立集

所以連通塊數量必須小於最大邊獨立集(最大匹配)

故 \(|V|-|C_e|\leq |M|\)

最小邊覆蓋

由 1. ,2. 得證 \(|C_e|=|V|-|M|\)

例題

CSES Coin Grid

在\(N \times N\) 上的棋盤上有\(K\)個錢,

每次你可以選擇清空一列、一行上的錢,

求最少操作幾次才能把棋盤清空

並把操作的方法列出來

例題

  • 把題目放在二分圖上思考,
  • 每個皮皮\(x,y\)要被打掉代表 \(x\)被選或是\(y\)被選
  • 最小點覆蓋

例題

  • 最小點覆蓋構造方法
    • 如前面證明時所使用的方法,就是枚舉 \(Y\) 中未被匹配的點,然後去找部分增廣路徑然後把經過的點標記
    • 左邊被標記的點 + 右邊未被標記的點即為答案
  • 實作方法:
    • 把未匹配的邊改成右指向左
    • 匹配邊為左指向右
    • 之後就是 DFS
  • 參考Code

構造Code

const int N=1e3+7;
vector<int> v[N];//X的鄰點
int match[N]; // Y所匹配的節點
bool vis[N]; //在這次尋找中是否被走過
int dfs(int x){
	for(int y:v[x]){
		if(vis[y]) continue;
		vis[y]=true;
		if(match[y]==-1||dfs(match[y])){
			match[y]=x;
			return true;
		}
	}
	return false;
}
int bipartiteMatching(int n){
	fill(match,match+n+1,-1);
	int ans=0;
	rep(i,1,n){
		fill(vis,vis+n+1,0);
		if(dfs(i)) ans++;
	}
	return ans;
}
vector<int> v2[2*N];
int tag[2*N];
vector<pii> e;
void paint(int x){
	tag[x]=true;
	for(int i:v2[x]){
		if(!tag[i]){
			paint(i);
		}
	}
}
void getce(int n){
	int ans=bipartiteMatching(n);
	cout<<ans<<"\n";
	for(pii p2:e){
		if(match[p2.S]!=p2.F){
			v2[p2.S+n].eb(p2.F);
		}
		else v2[p2.F].eb(p2.S+n);
	}
	rep(i,1,n){
		if(match[i]==-1){
			paint(i+n);
		}
	}
	rep(i,1,n){
		if(tag[i]) cout<<1<<" "<<i<<"\n";
	}
	rep(i,n+1,2*n){
		if(!tag[i]) cout<<2<<" "<<i-n<<"\n";
	}
}

例題

TIOJ 1069 . E.魔法部的任務

有一個棋盤,然後接下來會有 \(m\) 個事件發生,第 \(i\)個事件會在時間 \(t_i\) 發生在 (\(x_i\),\(y_i\)),你可以派一些人去處理這些事件。一個人被派出去的時候,他一開始可以在任何位置,接下來他移動到別的地方花的時間等於曼哈頓距離。求你至少要派幾個人。
\( m \leq 1000 \)

例題

對事件建點, \(i_{out},i_{in}\) 

若從 \(i\) 事件結束後可以接著做 \(j\) 事件

則建邊 \(i_{out} \rightarrow j_{in}\)

等價於要用最少的路徑去使每個節點都被走訪

可以先假設答案是 \(m\)

那之後會發現如果有一個匹配 \(i,j)\) 則 答案會 -1。

故最佳解即為 \(|V|-|M|\)

參考Code

練習

題目講解

咖哩採買(Shopping) (校內賽pD)

  • 給定一張有向帶權圖,問每條邊是否可能成為 \(1\) 走到 \(N\) 的最短路上的邊
  • \( N,M \leq 5 \cdot 10^5\)

咖哩採買(Shopping) (校內賽pD)

  • 求 \(1 \sim N\) 的最短路的值並不難 (Dijkstra就可以搞定)
  •  
Made with Slides.com