二分圖(可以帶權)中的最大匹配問題,一般圖要用帶花樹 (並不會
一些定義
一些定義和性質可能在算法講解中用不到,但是下面的題目中會用到
- 二分圖:能將原圖點集 \(V\) 分成兩個集合 \(A,B\),且 \(A\cap B=\emptyset,A\cup B=V\),使得所有邊的端點一個在 \(A\) 中,一個在 \(B\) 中的圖
- 匹配:一個邊的集合,集合內的任意兩個邊都沒有公共端點,那么集合內的邊是匹配邊,不在集合內但在原圖邊集內的邊是非匹配邊,匹配邊的端點是匹配點,其它點是非匹配點
- 最大匹配:一個圖所有的匹配中,包含匹配邊最多的匹配的匹配邊數
- 交替路:從非匹配點開始,走非匹配邊,匹配邊,非匹配邊.....的路徑
- 增廣路:以非匹配點結束的交替路,顯然,這種路徑可以通過匹配邊非匹配邊的互換來產生更多的匹配。也可以理解為是把增廣路里的匹配邊集合與這條路徑上所有邊的集合做對稱差,對稱差是啥下面有
- 完美匹配:所有點都是匹配點的匹配
- 獨立集:點的集合,集合內任意兩個點沒有邊相連
- 團:點的集合,集合內任意兩個點都有邊,也就是一個完全子圖
性質
二分圖等價於無奇環
匈牙利算法也要基於這點
首先有奇環一定不是二分圖,奇環上的點滿足不了二分圖性質
再說明充分性,下面討論的是每個聯通塊的情況,也就是為了應對圖不連通
任意取一個頂點 \(x\),把點分成兩個集合,\(A\) 中點到 \(x\) 距離為奇數,\(B\) 中點到 \(x\) 距離偶數,嘗試說明所有邊都是一個端點在 \(A\),一個端點在 \(B\)
對於任意一條邊 \((u,v)\),考慮從 \(x\) 到 \(u,v\) 的最短路,它們會有重合的頂點,即使可能只有一個 \(x\) 是重合的
- 如果最后一個重合的頂點是 \(u\) 或 \(v\),那么 \(x\) 到它們的最短路(距離)差 \(1\),奇偶性不同,不在同一集合
- 不是 \(u\) 或 \(v\),設最后一個公共的是 \(p\),設它們在同一集合,那么 \(dis(p,u),dis(p,v)\) 奇偶性相同,那么 \(p\rightarrow u\rightarrow v\rightarrow p\) 是個奇環,矛盾
二分圖中,最大獨立集點數加最大匹配數等於總點數
這個結論在圖論里也是比較常見的,下面一個題也用到了
設點數為 \(V\),最大匹配匹數是 \(M\),最大獨立集點數是 \(U\)
首先,由於 \(M\) 對匹配的點都是相連的,所以必定有 \(M\) 個點不在最大獨立集中,那么 \(U\le V-M\)
其次顯然有 \(U\ge V-2M\),就是不在最大匹配里的點都可以進入最大獨立集。同時注意到,對於最大匹配中的一個匹配邊的兩點,總能找出一種方案讓每對頂點都選出一個進入最大獨立集(構造出來的不行的一定會與最大匹配矛盾)
這樣又多出 \(M\) 個,\(U\ge V-M\)
綜合上述得出 \(U+M=V\)
無向圖最大獨立集等於它補圖的最大團
這個顯然吧,任意兩個點沒有變放在補圖里就是任意兩個點都有邊
增廣路定理
這個也是匈牙利算法的基礎,說的是匹配數最大等價於不存在增廣路
不存在增廣路的必要性很好證明,如果有增廣路,因為增廣路結構是非匹配邊,匹配邊......非匹配邊,那么把所有匹配邊和非匹配邊交換就行了
這樣匹配數加一,則原來的匹配不是最大匹配
充分性稍難,證明思路是看的維基
要先定義兩個集合的“對稱差”,如果某元素在且只在其中的一個集合中(不能同時在兩個中),那么這個元素在這兩個集合的對稱差集合中,類似於異或
我們設當前的匹配是 \(M\),最大匹配是 \(M'\),那么做它們的對稱差:\(P=M\oplus M'\)
這個 \(P\) 中,包含了所有在 \(M\) 中,不在 \(M'\) 中,和在 \(M'\) 不在 \(M\) 中的邊
又因為 \(M,M'\) 都是匹配,則每個 \(P\) 中邊的所有端點度數至多為二,所以更進一步說,\(P\) 包含:
- 一些不相交的環
- 一些路徑
這兩種情況中,在 \(M\) 中的邊和不在 \(M\) 中的邊(在 \(M'\) 中)個數相同
原因是,對於路徑,肯定是一個邊在 \(M\),一個不在,交替下去,長度為偶數
如果不是,那么如果一個路徑開頭結尾都是一個集合內的點,那么必然會對另一個集合產生增廣路
\(M'\) 里沒有增廣路
而對於 \(M\) 里的增廣路,這里為了說明方便先忽略這種情況,會在后面說明
對於環,肯定交替,沒有上面說的產生增廣路的事
又因為 \(M'\) 是最大匹配,\(M\) 不是,所以 \(P\) 中的邊肯定是來自 \(M'\) 中的多
但對於第一種情況,邊數相同,所以,這些多出來的邊,肯定是第二種情況中,開頭結尾都是 \(M'\) 中的邊,也就為 \(M\) 產生了增廣路(就是剛才說先不考慮的那種情況)
這就說明了,對於一個不是最大匹配的匹配,一定存在增廣路
由此,可以得知,沒有了增廣路,一定是最大匹配
匈牙利算法
終於開始正題了
匈牙利算法實際上就是一個不斷找增廣路直到找不到的過程,用於無權二分圖最大匹配
洛谷P3386 【模板】二分圖最大匹配,UOJ#78. 二分圖最大匹配
枚舉二分圖中每個左邊(右邊當然也可以,此處以左邊為例)的點,從他開始找增廣路,並記錄下路徑,做更改
如何尋找?dfs和bfs其實都可以
先說dfs,這里就不用單獨記錄路徑了,直接記錄在dfs的棧里
對每一個左邊的點 \(u\),枚舉出邊 \((u,v)\)
- 如果 \(v\) 當前沒有匹配,那么 \(v\) 就是增廣路的最后一個點,回溯,對於路徑上(也就是棧中)的每一對 \((u,v)\),它們互為對應的匹配點
- 如果 \(v\) 匹配了,把它對應的匹配點作為新的 \(u\),進入下一層遞歸,也就是說,如果再從這個新的 \(u\) 遞歸的時候發現了增廣路,要拆開 \(v\) 和他的原匹配(下一層遞歸中新的 \(u\)),並把它和本次的 \(u\) 匹配上
舉個簡單的例子,雖然是個很特殊的情況但也足夠說明這個尋找的過程
依然是紅色代表匹配邊
- \(u=1,v=2\),發現 \(v\) 有匹配,於是從 \(v\) 的匹配也就是 \(3\) 開始繼續遞歸
- \(u=3,v=4\),然后再遞歸到 \(u=5,v=6\),和上述過程相同
- \(u=5,v=6\),發現 \(6\) 當前沒有匹配,是一個增廣路的終點,於是
match[u]=v,match[v]=u
,回溯 - 回溯到了 \(u=3,v=4\) 的情況,拆開 \(4\) 的原匹配,使得它和 \(3\) 互相匹配
- 回溯到 \(u=1,v=2\) 也是相同
如果搜索了一圈,發現怎么也找不到增廣路,那么此時答案就不能增加,否則答案加一
我看模板題的題解里把這個描述成“協商與匹配”,其實就是通過增廣路的原理重新調整匹配與非匹配邊,當然本質上一樣
放上代碼
int n,m,e;
int match[N],check[N];//match 記錄對應的匹配點,check 記錄有沒有被訪問
inline int dfs(int u){
for(reg int v,i=G.fir[u];i;i=G.nex[i]){
v=G.to[i];
if(check[v]) continue;
check[v]=1;
if(!match[v]||dfs(match[v])) return match[v]=u,match[u]=v,1;//維護新的匹配信息
}
return 0;
}
int main(){
n=read();m=read();e=read();
for(reg int a,b,i=1;i<=e;i++){
a=read();b=read();
G.add(a,b+n);
}
int cnt=0;
for(reg int i=1;i<=n;i++){
std::memset(check,0,sizeof check);cnt+=dfs(i);
}
std::printf("%d",cnt);
return 0;
}
還有一種bfs版本,原理相同,但在稀疏圖中跑的似乎比dfs快得多(網上看的,具體實驗沒自己做)
但碼量更大,而且有的題目只能使用dfs或bfs其中一種,下面例題里有例子
bfs需要開一個 pre
數組來記錄路徑
和上面一樣,對每一個左邊的點 \(u\),枚舉出邊 \((u,v)\),如果 \(v\) 沒匹配,借助 pre
數組,沿着走過的路徑一路走回去,標記匹配,怎么借助在下面
如果匹配了,就把原匹配點加入隊列,並記錄 pre[match[v]]=u
,這里 match
數組記錄的就是原匹配
什么意思呢?具體來說 pre[i]
是路徑上點 \(i\) 往回的第二個節點
是在上一個新的(增廣路修正后的)匹配邊中,同樣位置的點,看下圖,紅邊是當前記錄的匹配邊
注意圖中說的 e=match[d]
指的是更新信息前的 match[d]
那么經過對增廣路的匹配非匹配邊的調換,得到下面的樣子:
那么我們成功從 \(u\) 找到增廣路,答案加一
代碼
int num_left,num_right,m;
int pre[N],match[N];
//match[i],表示一條包含點 i 的匹配邊的另一個端點
//pre[i],維護幾個匹配邊和未匹配邊組成的路徑信息,具體來說 pre[i] 是路徑上往回的第二個節點
//比如增廣路1->2->3->4->5,其中,1->2,3->4 是匹配邊,那么 pre[4]=2
//也就是,我們可以通過 d=pre[d],來讓 d 變成上一個匹配路徑中,和 d 在同一個位置中的點
//這就是更新路徑信息時的做法
int que[N],tail,head,check[N];
inline int max_match(){
int ans=0,u,v;
for(reg int i=1;i<=num_left;i++){
tail=head=0;que[0]=i;
pre[i]=0;
while(tail<=head){
u=que[tail++];
for(reg int j=G.fir[u];j;j=G.nex[j]){
v=G.to[j];
if(check[v]==i) continue;//本次 bfs 訪問過
check[v]=i;
if(match[v]) pre[match[v]]=u,que[++head]=match[v];
//是匹配點,那么把已經和他匹配的另一個點入隊
else{//找到未匹配點,是個增廣路,要更新路徑
int d=u,e=v,t;
while(d){
//d 是增廣路上匹配邊的終點,e 是增廣路上下一個匹配邊的起點
//然后讓 d,e 互相匹配
//然后通過 e=match[d],d=pre[d] 來推到前一條邊,此時 d,e 仍滿足前一行說的性質
t=match[d];
match[d]=e;match[e]=d;
d=pre[d];e=t;
}
goto BREAK;
}
}
}
BREAK:;
if(match[i]) ans++;
}
return ans;
}
int main(){
num_left=read();num_right=read();m=read();
for(reg int u,v,i=1;i<=m;i++){
u=read();v=read()+num_left;G.add(u,v);
}
std::printf("%d",max_match());
return 0;
}
容易發現,復雜度 \(O(nm)\),但其實跑不滿
二分圖最大權匹配
這個是叫KM算法,用在帶權值的二分圖上,找一些匹配邊使得它們權值之和最大
UOJ#80. 二分圖最大權匹配
首先,這個算法是針對於完全圖,不過也沒有什么本質區別,就把不在實際中的邊邊權設為零就好。
並且二分圖左右兩邊的點數相同,這個就取一個 \(\max\) 然后本來不存在虛擬的加進去的點就全都連零邊就好
很顯然,根據上面所說的,完全圖的最大權匹配一定是完美匹配(邊權非負)
為每個頂點確定一個“頂標”,\(lx_u,ly_u\) 分別表示二分圖左右兩邊節點的頂標
定義“可行頂標”,是使得對於邊 \((u,v)\),有
的頂標
在定義“相等子圖”,是對於一個子圖內(包含原圖所有頂點,但不一定包含所有邊),任意邊 \((u,v)\) 有
的子圖
由這個相等子圖性質,我們有以下結論,對於一個包含完美匹配的相等子圖,則這個子圖的最大權匹配的權值和,就是所有頂標加起來
同時,也是這個子圖的最大權匹配
進一步講,也是原圖的最大權匹配和,因為如果去除掉子圖完美匹配的某些邊,加入另一些邊,使得它還是一個完美匹配
那么這些加入的邊的權值肯定是小於等於兩端點頂標和,所以總體地看,他就小於等於所有頂標和,也就不是最大權匹配了
所以總結出來就是:相等子圖存在完美匹配,則該匹配是最大權匹配
那么,我們不斷調整頂標,使得相等子圖存在完美匹配不就行了
於是算法的大體結構出來了
- 以一個點為起點跑bfs或dfs,看能不能增廣
這個bfs或dfs和匈牙利算法中每一次進行的搜索類似,就是注意走的邊要是滿足 \(lx_u+ly_v=W_{u,v}\) 的邊
以下說的“搜索”,就是指這個過程 - 能增廣就再去下一個點,不能就調整頂標
- 再跑bfs或dfs,再調整頂標,直到能增廣
然后實際中,第三步跑好多遍搜索是可以優化掉的,而且事實上不把他優化掉的話交到UOJ會T掉
不過還是先來看如何調整頂標,這是算法的關鍵
要先初始化頂標,一個可行的初始化是對於每個 \(lx_i\),讓它取所以和 \(i\) 相連的邊的權值最大值,同時 \(ly_i=0\)
用 \(vis_i\) 表示每個頂點是否在搜索中訪問過,我習慣把左右邊的編號放在一起,所以就不區分成多個數組了
維護一個 \(d_j=\min(lx_i+ly_j-W_{i,j}\mid vis_i=1,vis_j=0)\),和一個 \(now=\min(d_j\mid vis_j=0)\)
這個什么意思?
就是我們要找到一個最小的 \(now\),使得讓所有可以被搜索到的左邊的節點,減去 \(now\)
這樣,他就一定能訪問到一個右邊的還沒有被訪問的節點,也就能訪問到這個節點的對應匹配(如果有的話)
注意這里是一定,這有關算法復雜度
那為什么要最小呢?我們要讓其它邊兩端節點可行頂標的性質不被破壞
而要讓以前在相等子圖中的點,還在相等子圖,所以要讓所有能搜索到的右節點,頂標加上 \(now\)
其實嚴格來講這里還不能稱作相等子圖,因為還沒有包含所有點
然后頂標的變化需要調整 \(d_j\),實際上就是 \(d_j=d_j-now\),原因很簡單,在注釋里
那么,哪一個 \(d_j\) 被減成了零,左邊能搜索到的點能新訪問到的右邊節點就是哪個(或者哪幾個)
設這個節點為 \(to\),姑且不考慮有好幾個的情況,那會在多遍循環中被處理完
- 則如果 \(match(to)\) 還沒有定義,則說明我們找到了增廣路,退出循環找下一個點即可
- 如果定義了,那么可以把 \(to,match(to)\) 都納入可以搜索到的點,也就是把它們的 \(vis\) 變成 \(1\),然后再對於 \(vis_j=0\) 的 \(j\) 更新 \(d_j\) 即可
再次更新,是因為 \(match(to)\) 被打上了 \(vis\) 標記
屬於了左邊能到達的點,所以要對於每個 \(vis_j=0\) 的 \(j\) 和 \(match(to)\) 來更新 \(d_j\)
直到循環被跳出
然后跳出以后,要再跑一遍搜索,來更新一下 \(match\)
復雜度:最外層循環循環 \(n\) 遍,里層需要被跳出的循環中,更新頂標和 \(d_j\) 啥的都是 \(O(n)\),而每次都會有兩個點 \(vis\) 變成 \(1\),所以最多循環 \(O(n)\) 遍
那么總復雜度 \(O(n^3)\),感覺也不會跑滿,反正UOJ \(n=800\) 能過
然后發現那個第三部的搜索自然而然的優化掉了
其實不優化掉的方法是不維護 \(d_j\),每次求這個值的方法就是跑一遍搜索,而這里是用了動態的更新它來減少搜索次數
這部分可能比較難理解,可以看代碼中的注釋
#define N 808
int num_left,num_right,m;
int G[N][N];
int vis[N],pre[N],match[N],lw[N],d[N];
int que[N],tail,head;
inline int bfs(int u){
tail=head=0;que[0]=u;
pre[u]=0;
while(tail<=head){
u=que[tail++];vis[u]=1;
for(reg int i=num_left+1;i<=num_right;i++)if(G[u][i]==lw[u]+lw[i]){
if(vis[i]) continue;
vis[i]=1;
if(match[i]) pre[match[i]]=u,que[++head]=match[i];
else{
int d=u,e=i,t;
while(d){
t=match[d];
match[d]=e;match[e]=d;
d=pre[d];e=t;
}
return 1;
}
}
}
return 0;
}
inline LL max_match(){
for(reg int i=1;i<=num_left;i++){//初始化 lx
for(reg int j=num_left+1;j<=num_right;j++) lw[i]=std::max(lw[i],G[i][j]);
}
for(reg int i=1;i<=num_left;i++){
std::memset(vis,0,sizeof vis);std::memset(d,0x7f,sizeof d);
if(bfs(i)) continue;//能增廣了就退出找下一個
for(reg int j=1;j<=num_left;j++)if(vis[j])
for(reg int k=num_left+1;k<=num_right;k++)
if(!vis[k]) d[k]=std::min(d[k],lw[j]+lw[k]-G[j][k]);
while(1){
int now=1e9,to,s;
for(reg int j=num_left+1;j<=num_right;j++)if(!vis[j]) now=std::min(now,d[j]);
for(reg int j=1;j<=num_left;j++)if(vis[j]) lw[j]-=now;
for(reg int j=num_left+1;j<=num_right;j++)
if(vis[j]) lw[j]+=now;//為了維持以前在相等子圖的點還在相等子圖,左邊點減 now,右邊加 now
else d[j]-=now,to=d[j]?to:j;//to 記錄了哪個是要被連接到(d[j]=0,加入相等子圖)的右頂點
//d[j]-=now 是因為對於 vis[j]=0 的 j,它們所連到的左邊的滿足 vis[k]=1 的點的 lx[j] 會減 now
//那再取個 min 還是減 now
//這樣更新了 d[j] 還求出了 to
if(!match[to]) break;
s=match[to];vis[to]=vis[s]=1;//打上 vis 標記
for(reg int j=num_left+1;j<=num_right;j++)
//更新 d,這里再次更新,是因為 match[to] 被打上了 vis 標記
//屬於了左邊能到達的點,所以要對於每個 j 和 match[to] 來更新 d[j]
if(!vis[j]) d[j]=std::min(d[j],lw[s]+lw[j]-G[s][j]);
}
std::memset(vis,0,sizeof vis);
bfs(i);
}
LL ans=0;
for(reg int i=1;i<=num_right;i++) ans+=lw[i];
//答案直接把每個 lx,ly 加起來就行了,因為最后是個完美匹配
return ans;
}
int main(){
num_left=read();num_right=read();m=read();
int nnn=num_left;
num_left=num_right=std::max(num_left,num_right);
num_right+=num_left;
for(reg int u,v,i=1;i<=m;i++){
u=read();v=read()+num_left;G[u][v]=G[v][u]=read();
}
std::printf("%lld\n",max_match());
for(reg int i=1;i<=nnn;i++)
std::printf("%d ",(match[i]&&G[i][match[i]])?(match[i]-num_left):0);//邊權要大於一,是實際中的邊
return 0;
}
題目
題目還是匈牙利的居多,二分圖權匹配的題本來好像就不多
P2055 [ZJOI2009]假期的宿舍
here,同bzoj1433
一些學生,一部分在校(有宿舍的床),一部分是外校的(沒床)
在校的學生有一些要回家(不占用床),外校的學生都要來來學校(占用床),當然,不回家的在校學生也占床
給出一些朋友關系(雙向),每個人只能睡自己或朋友的床,問能不能安排合適的方案使得每個人都有床
比較簡單,為每個在校學生新建一個點,表示它們的床的編號,這樣看誰能睡誰的床,向他的床連邊就行
人和人,床和床不會連邊,所以是二分圖,跑一下匈牙利看一看匹配數是不是和需要床的人數相等即可
注意不回家在校生可以睡自己的床
code
P6268 [SHOI2002]舞會
here
求無向圖最大獨立集,用剛才證明的性質,二分圖的最大獨立集大小是頂點個數減去最大匹配數
是二分圖是因為曾經跳過舞的一定是男生與女生
太裸,代碼就不放了
P1129 [ZJOI2007]矩陣游戲
here,同bzoj1059
\(n\times n\) 的黑白格子,每次可以交換兩行或兩列
問能不能通過若干次交換使得左上到右下的對角線全為黑
對於第 \(i\) 行來講,如果他在第 \(a_{i,j}\) 是黑色,那么,顯然說明它可以被交換到第 \(a_{i,j}\) 行上去,來保證對角線是黑
然而列交換就沒有任何意義,如果兩列本來就都有黑色,那么這兩列就都可以被滿足此列的對角線上的格子是黑
而如果其中一個沒有黑色,那么交換一下被交換的那個列又沒有黑了,還是不行
當然把上面描述中的“行”和“列”都互換也是一樣的
所以,只要對於每個黑格 \(a_{i,j}\),就把 \(i,j\) 連邊即可
就是分別建立節點表示行號和列號,是個二分圖,直接匈牙利
code
P1963 [NOI2009]變換序列
here,同bzoj1562
一個 \(1\) 到 \(n\) 的序列,求一個排列 \(T\),要求每個數在 \(T\) 中指定的兩個位置中的一個
問有沒有解,如果有,輸出字典序最小的 \(T\)
首先有沒有解很好判,建個點數 \(2n\) 的圖,分別代表原序列中的每個數,和 \(T\) 中的每個數
這是個二分圖,且左右點數相同,然后看一下有沒有完美匹配就行了
這個 \(T\) 的一種可行解就是所有點的 match
關鍵是如何讓字典序最小
回顧之前說的匈牙利算法的過程,先成為匹配邊的邊,在后面的節點尋找增廣路時,如果它們能組成增廣路,則會在修正這個增廣路以產生更多匹配數而被“抹掉”,也就是成為非匹配邊
字典序是先比較前面字符的大小,那么肯定是最小化前面的點的 match
那么,我們在從每個點為起點找增廣路時,從 \(n\) 到 \(1\) 循環,就行了
然后為了讓每個點在不被其它點影響的情況下,得到最小的對應匹配點,就也要讓編號小的點先被訪問
鄰接表的訪問是倒序的,所以就先加入序號大的邊就行了
這里就是剛才提到的不能用bfs的情況
因為為了字典序最小,找到一個可能成立的較小編號的點后,就要從這開始一直訪問下去,如果不行在考慮其它點
而bfs時,從 \(u\) 開始,訪問到一個可能成立的較小點 \(v\) 后,被加入隊列
但如果又在訪問其它 \(v\) 時,直接成立了(也就是直接有了增廣路),那么就退出循環了,但這樣就錯了
因此一定要根據實際情況看用bfs還是dfs
但似乎目前還並沒有遇到dfs不行的題
code
P1971 [NOI2011]兔兔與蛋蛋游戲
here,同bzoj2437
題面挺復雜,不簡述了
這題做的我簡直想打死兔兔和蛋蛋,一晚上+一上午+一中午差點死在這題上/kk
首先可以看作是這個空格子在移動,而且不會經過重復的點,所以只要經過了一個點不用,也不能再考慮這個格子了
如何說明不重復?假設空格子從 \(u\) 離開,那么就是把離開的這個方向的格子移入到了原來空格的位置
那么要是想讓空格子再回到 \(u\),必然需要先走偶數不,來到 \(u\) 周圍四個格子之一,然后再用一步回去(因為每次移動的顏色不同的限制)
也就是一共走了奇數步,回到原來位置
把移動按橫縱方向拆開,橫縱方向都是走到一個格,再走回來,應該是偶數
基於這一點再做分析,考慮什么時候先手必勝
我們構造一個二分圖,把黑點和空格的起始點放在左邊,白點放在右邊
對於另個相鄰的格子,如果顏色不同,連邊,表示能從這兩個格子之間走過
這是個二分圖,因為邊在黑和白之間
如果對於任意一種最大匹配方式,先手操作前所在的格子都是匹配點,那么先手必勝
先手可以先走一個匹配邊,那么后手走的就是非匹配邊,以此類推,當一定會存在當先手走完一個匹配邊后,后手無路可走的情況
因為一個格子在任意一種最大匹配中都是匹配點,那么以它開始的一個路徑,肯定會是一種“修正過的增廣路”
就是第一條,最后一條邊都是匹配邊
如果不是,那么以匹配邊開始,非匹配邊結束,就可以讓所有邊在匹配/非匹配中互換,則匹配數不變,起點卻不是匹配點了
這種點,叫做最大匹配的關鍵點
如何判斷?先跑出任意一種匹配方式,如果這個點都沒匹配上肯定直接不是關鍵點
否則強制讓這個點和他的原匹配不被匹配在一起,就是把匹配刪掉
從他的原匹配開始搜索,看能不能再通過別的方式增廣,能的話就說明不是,否則就是關鍵點
因為之前提到過,已經經過的點不能再次經過,所以還要用一個 deleted
數組記錄是否被走過,之前一直卡在這
code
CF1107F. Vasya and Endless Credits
這里開始是最大權匹配的題了
here
一共有 \(n\) 中貸款,每個月月初可以辦理到一種貸款,每種貸款只能辦一次
每種可以貸到 \(a\) 元,要在后來(包括剛開始貸款的這一個月)的月末還 \(b\) 元,還 \(k\) 個月為止
一開始沒錢,問在某時刻,手中最多可以有多少錢
顯然,整個貸款的過程不會超過 \(n\) 個月
最終結束也是在一個月的月初,且這個月初剛開始一個貸款
如果第 \(i\) 個貸款在整個過程的倒數第 \(j\) 天開始,那么,一共要還 \(b_i\min(k_i,j-1)\) 元
拿到了 \(\max(a_i-b_i\min(k_i,j-1),0)\) 元,\(0\) 表示如果收益是負數那就不辦這個貸款
所以,可以建圖,讓 \(1\) 到 \(n\) 表示 \(n\) 中貸款,\(n+1\) 到 \(2n\) 表示倒數第 \(j\) 天
連邊表示在倒數第 \(j\) 天買了第 \(i\) 種貸款
然后跑一個最大權匹配就行了
code
P6061 [加油武漢]疫情調查
here,似乎是洛谷題庫里唯一能搜到的二分圖權匹配題
一張有向圖,用一些環和孤立的點覆蓋所有點,環的代價是所有邊的權值和,點的代價是點權,問最小代價
一開始沒看見是有向的直接全WA/kk
先Floyd一下求出任意兩點之間的最短路
然后發現,對於點 \(i,j\),新建一個二分圖,連邊 \(i,j+n\),並把權值設為 \(i,j\) 最短路距離
然后每個 \(i,i+n\) 連一個點權為權值的邊
則一定存在完美匹配,而且每一個完美匹配都對應一個可行的覆蓋方案
然后在模板代碼上改一改改成求最小權匹配就行了
code,發現用一個大數減去實際邊權跑最大匹配再轉換回來是不可行的
其它題目
P3033 [USACO11NOV]Cow Steeplechase G
P4304 [TJOI2013]攻擊裝置
P1640 [SCOI2010]連續攻擊游戲
P2825 [HEOI2016/TJOI2016]游戲
匈牙利算法
最大匹配
作者太菜,一般圖的最大匹配和最大權匹配並沒有講,發現光二分圖的匹配算法和題就學了半天才弄懂
難免會有錯誤,所以希望能在評論區或私信指出,感謝,輕噴