一种可行的方案是,对于所有右部点 y,记录 slv_y=\text{argmin}_{x\in T}(z_x+z_y-w(x,y)) ,即固定了 y 后, z_e 最小值对应 x ,其中 T 为交错树。每次扩展的时候,都是取 slv_y 计算出结果最小的 y 进行扩展,这样就能够保留已有的交错树进而继续增广,每次增广的复杂度变成 O(n^2),总复杂度 O(n^3)。下面给出示例代码,其中为方便增广,使用 pre 记录黑点在交错上父亲的父亲(最近黑点祖先)。
const ll inf=0x3f3f3f3f3f3f3f3f;
ll w[N][N],lx[N],ly[N];
int mx[N],my[N],visx[N],visy[N],n;
int slv[N],pre[N];
queue<int>Q;
ll calc_slv(int u,int v){return lx[u]+ly[v]-w[u][v];}
void add_to_tree(int x){ // add x to tree, update all slv[y]
Q.push(x);
visx[x]=1;
int y;
for(y=1;y<=n;y++)
if(!slv[y]||calc_slv(x,y)<calc_slv(slv[y],y))slv[y]=x;
}
void link(int x,int y){
int tx,ty;
while(x){
tx=pre[x];ty=mx[x];
my[y]=x;mx[x]=y;
x=tx,y=ty;
}
}
int augment(int p){
int x,y;
while(1){
while(!Q.empty()){
int x=Q.front();Q.pop();
for(y=1;y<=n;y++)if(!visy[y]&&!calc_slv(x,y)){
visy[y]=1;
if(!my[y])return link(x,y),1;
pre[my[y]]=x;
add_to_tree(my[y]);
}
}
ll d=inf;
for(y=1;y<=n;y++)
if(!visy[y])
d=min(d,calc_slv(slv[y],y));
if(d==inf)break;
for(x=1;x<=n;x++)if(visx[x])lx[x]-=d;
for(y=1;y<=n;y++)if(visy[y])ly[y]+=d;
// add new edges
for(y=1;y<=n;y++)if(!visy[y]&&!calc_slv(slv[y],y)){
visy[y]=1;
if(!my[y])return link(slv[y],y),1;
pre[my[y]]=slv[y]; // tree: slv -> y == my[y], == stands for match
add_to_tree(my[y]);
}
}
return 0;
}
ll km(){
int i,x,y;
memset(lx,0,sizeof(lx));
memset(ly,0,sizeof(ly));
memset(mx,0,sizeof(mx));
memset(my,0,sizeof(my));
for(x=1;x<=n;x++)
for(y=1;y<=n;y++)
lx[x]=max(lx[x],w[x][y]);
for(i=1;i<=n;i++){
memset(visx,0,sizeof(visx));
memset(visy,0,sizeof(visy));
memset(slv,0,sizeof(slv));
Q=queue<int>();
add_to_tree(i);
augment(i);
}
ll ans=0;
for(i=1;i<=n;i++)ans+=w[my[i]][i];
return ans;
}
Bonus
还有一种使用同样思想,利用局部最优获得全局最优的实现方式。对于每一个黑点 x,找到某个白点加入交错树,这个过程中始终是选择 z_e 较小的边进行扩展的,直到找到 x 所对应的增广路。
#define N M*2+1
struct edge{
int u,v;T w;
edge(){}
edge(int u,int v,T w):u(u),v(v),w(w){}
};
// Graph
int n,n_x; // [1, n]: point; [n+1, n_x]: flower
edge g[N][N]; // adjacent matrix
// flower
vector<int>flower[N]; // nodes in flower i (outer flower)
int root[N]; // flower root, root<=n root=i: normal nodes
int flower_from[N][N]; // flower_from[b][x]: outermost flower in b that contains x
其中,与普通带花树不同,这里的花是额外进行存储的(存储在 n+1 到 n_x 内),因此需要开两倍空间。花可以嵌套,而且花需要在某些时刻进行展开(后面会详细讲述),因此需要保存额外的信息。对于一个花 B,存储其根 root,对于花内任意点有 root_{i\in B}=B;存储花内所有节点 flower_B,是以花托为起始的环;同时,花内每一点 i 保存“花内最大父花” flower\_from_{B,i} 表示最大的包含 i 的 B 的子花。
// slack
T label[N]; // node label, [1, n] point label, [n+1, n_x] flower label
int col[N]; // color saved at flower root
int slv[N]; // slack node of NON LEAF NODES, slv[y]=x z(x,y) min_x
// match
int mat[N]; // match, mat[x]=y (x,y)\in E
int fa[N]; // fa in cross tree
int vis[N]; // if in path
queue<int>Q; // bfs queue
$mat$ 可能是花之间的匹配,即 $mat_x=y$ 中 $x>n,y>n$ 均是被允许的。$vis$ 只用来计算 $LCA$ ,可以暂时忽略。$fa$ 表示某白点在交错树上的父节点。注意到黑点交错树上的父节点是 $mat_i$,**利用这两个东西就可以在交错树上追溯到根了**。
接下来是对 $slv$ 的更新和计算:
```cpp
// calculate slv
inline T calc_slv(edge e){return label[e.u]+label[e.v]-e.w;}
inline void update_slv(int u,int v){
if(!slv[v]||calc_slv(g[u][v])<calc_slv(g[slv[v]][v]))slv[v]=u;
}
inline void recalc_slv(int u){
slv[u]=0;
for(int i=1;i<=n;i++)
if(g[i][u].w>0&&root[i]!=u&&col[root[i]]==1)
update_slv(i,u);
}
```
对于 BFS 队列,在将花加入队列时需要将花中所有点都加入队列;设置最外层花时,对于花中所有点都要进行设置。
```cpp
// only push nodes, not flowers
void q_push(int x){
if(x<=n)Q.push(x);
else for(auto p:flower[x])q_push(p);
}
// set root of all nodes in x to r
void set_root(int x,int r){
root[x]=r;
if(x>n)for(auto p:flower[x])set_root(p,r);
}
```
获取某朵花 $b$ 中,从花托到 $x$ 一条交错路径:对花连出去边的两种情况分别处理,返回一个指针。
```cpp
// return a (+-)^k path in flower b from root[b] to x
int get_even_path_in_flower(int b,int x){
int pr=find(flower[b].begin(),flower[b].end(),x)-flower[b].begin();
assert(b>n&&b<=n_x&&pr<flower[b].size()); // b is flower, x in b
if(pr%2==0)return pr;
reverse(flower[b].begin()+1,flower[b].end());
return flower[b].size()-pr;
}
```
某次增广使得需要设置 $u$ 匹配 $v$,$u$ 可能是花($v$ 不管):对于 $u$ 为花的情况,从花托到真实 $u$ 的边都进行翻转匹配,最后花托移位,旋转花到正确位置。
```cpp
// set (u->v) match, can be flower
void set_match(int u,int v){
mat[u]=g[u][v].v;
if(u>n){
edge e=g[u][v];
int xr=flower_from[u][e.u];
int pr=get_even_path_in_flower(u,xr);
for(int i=0;i<pr;i++)set_match(flower[u][i],flower[u][i^1]);
set_match(xr,v);
rotate(flower[u].begin(),flower[u].begin()+pr,flower[u].end()); // change receptacle
}
}
```
连接两个**不在同一个花里**的 S 点(黑点):两个点到根的路径,加上两点之间的边,能构成一条增广路。
```cpp
// link 2 S points
void side_augment(int u,int v){
int nv=root[mat[u]],nu=root[fa[nv]];
while(1){
set_match(u,v);
u=nu,v=nv;
if(!nv)break;
set_match(nv,nu);
nv=root[mat[u]],nu=root[fa[nv]];
}
}
void linkSS(int u,int v){
side_augment(u,v);
side_augment(v,u);
}
```
暴力查询两点 LCA:直接跳父链打标记。
```cpp
int get_lca(int u,int v){
static int t=0;
++t; // to avoid clearing vis
while(u||v){
if(vis[u]==t)return u;
vis[u]=t;
u=root[mat[u]];
if(u)u=root[fa[u]];
if(!u)swap(u,v);
}
return 0;
}
```
增加一朵奇花:需要申请一个 $id$ 设为 $b$,清空所有数据,并重构。需要重构的部分有:颜色;匹配(继承花托的匹配);花内节点;花内节点的 `root`、`flower_from`;邻接矩阵;`slv` 值。其中,邻接矩阵取的是 $z_e$ 最小的值,这可以保证正确性。
```cpp
void add_blossom(int u,int v,int r){
int i,b=n+1;
while(b<=n_x&&root[b])b++;
if(b>n_x)++n_x;
// clear
col[b]=1;label[b]=0;mat[b]=mat[r];flower[b].clear();
for(i=1;i<=n_x;i++)g[i][b].w=g[b][i].w=0;
for(i=1;i<=n;i++)flower_from[b][i]=0;
// construct flower
while(u!=r){
flower[b].pb(u);u=root[mat[u]];q_push(u);
flower[b].pb(u);u=root[fa[u]];
}
flower[b].pb(r);
reverse(flower[b].begin(),flower[b].end());
while(v!=r){
flower[b].pb(v);v=root[mat[v]];q_push(v);
flower[b].pb(v);v=root[fa[v]];
}
// set as outermost flower
set_root(b,b);
// calculate slack
for(auto p:flower[b]){
for(i=1;i<=n_x;i++){
// set to min slave
if(!g[b][i].w||calc_slv(g[p][i])<calc_slv(g[b][i])){
g[b][i]=g[p][i];
g[i][b]=g[i][p];
}
}
for(i=1;i<=n;i++)if(flower_from[p][i])flower_from[b][i]=p;
}
recalc_slv(b);
}
```
尝试增广一条等边:如果对点未染过色,则必然已经有匹配,将其匹配染色后丢入队列;如果对点是黑点,分两种情况,如果 $LCA=0$ 即不在同一花内,则 `linkSS`,否则添加花。
```cpp
// found_edge
int augment_path(edge e){
int u=root[e.u],v=root[e.v];
if(!col[v]){
assert(mat[v]);
fa[v]=e.u;
col[v]=2;
int nu=root[mat[v]];
slv[nu]=slv[v]=0;
col[nu]=1;
q_push(nu);
}else if(col[v]==1){
int r=get_lca(u,v);
if(r)add_blossom(u,v,r);
else return linkSS(u,v),1;
}
return 0;
}
```
一次增广:将所有未匹配点入队进行 BFS。先尝试使用当前 $label$ 下的等边构成的子图跑增广,如果增广成功直接返回。如果无法成功,则需要计算出松弛的大小,并将 $z_u,z_B$ 分别进行更新。更新后,如果某点 $(i,slv_i)$ 路径被加入等边子图中,则进行尝试增广。如果没有,则进行新的一轮增广。
有几个细节需要注意:
1. 白花 $z_B$ 会减掉一个正数,但要求保证 $z_B\ge 0$。
2. 如何计算松弛的大小
由于有 $z_B\ge 0$ 这样的特殊约束,当一朵白花 $z_B=0$ 时,需要对其进行开花操作 `expand_blossom`,即将花展开,将花中在交错树上的部分链加入交错树中,并将其他节点设置为“没有听说过”,即 $col_i=0$。
上面也提到过,每次松弛需要考虑的只有下列情况:
1. 某个黑点 $u$ 和某个未匹配点 $v$ 之间的边
2. 某两个不在同一个花内的黑点 $u,v$ 之间的连边
加上 $z_B\ge 0,z_u\ge 0$ 两个条件,总共需要找的是四种情况,**其中要用到 $\frac{z_B}2$、$\frac{z_e}{2}$ 的形式,因此边权整体乘二**;但 $z_B$ 要小于零了可以拆花,$z_u$ 要小于零可就真的找不到匹配路径了,因此如果某次结束后 $z_u=0$,直接返回增广失败。
```cpp
int augment(){
int i;
memset(col,0,sizeof(int)*(n_x+1));
memset(slv,0,sizeof(int)*(n_x+1));
memset(fa,0,sizeof(int)*(n_x+1));
Q=queue<int>();
for(i=1;i<=n_x;i++)
if(root[i]==i&&!mat[i]){
// add all unmatched points
col[i]=1;
q_push(i);
}
if(Q.empty())return 0;
while(1){
while(!Q.empty()){
int p=Q.front();Q.pop();
assert(col[root[p]]==1);
for(i=1;i<=n;i++){
if(g[p][i].w==0||root[i]==root[p])continue;
// not in same flower
T d=calc_slv(g[p][i]);
if(!d){if(augment_path(g[p][i]))return 1;}
else if(col[root[i]]!=2)update_slv(p,root[i]);
}
}
T delta=INF;
// calc delta
for(i=1;i<=n;i++)if(col[root[i]]==1)delta=min(delta,label[i]);
for(i=n+1;i<=n_x;i++)if(root[i]==i&&col[i]==2)delta=min(delta,label[i]/2);
for(i=1;i<=n_x;i++){
if(root[i]!=i||!slv[i])continue;
if(!col[i])delta=min(delta,calc_slv(g[slv[i]][i]));
else if(col[i]==1)delta=min(delta,calc_slv(g[slv[i]][i])/2);
}
// update label
for(i=1;i<=n;i++){
if(col[root[i]]==1)label[i]-=delta;
else if(col[root[i]]==2)label[i]+=delta;
}
for(i=n+1;i<=n_x;i++){
if(root[i]!=i)continue;
if(col[i]==1)label[i]+=2*delta;
else if(col[i]==2)label[i]-=2*delta;
}
for(i=1;i<=n;i++)if(label[i]<=0)return 0;
for(i=1;i<=n_x;i++){
if(root[i]!=i||!slv[i]||root[slv[i]]==i)continue;
if(calc_slv(g[slv[i]][i])==0&&augment_path(g[slv[i]][i]))return 1;
}
// expand
for(i=n+1;i<=n_x;i++)
if(root[i]==i&&col[i]==2&&label[i]==0)
expand_blossom(i);
}
return 0;
}
```
~~两~~开花的具体实现:
```cpp
// only expand outermost blossom b, b is T(white) blossom
void expand_blossom(int b){
int i,x;
for(auto p:flower[b])set_root(p,p);
x=flower_from[b][g[b][fa[b]].u];
// [0,pr]: (+-)^k, insert into tree, add black to queue
int pr=get_even_path_in_flower(b,x);
col[x]=2;fa[x]=fa[b];
for(i=0;i<pr;i+=2){
// from bottom to upper layer in tree
int white=flower[b][i];
int black=flower[b][i+1];
col[black]=1;col[white]=2;
fa[white]=g[black][white].u;
slv[black]=slv[white]=0;
q_push(black);
}
// others: color=0
for(i=pr+1;i<flower[b].size();i++){
col[flower[b][i]]=0;
recalc_slv(flower[b][i]);
}
// delete b
root[b]=0;
flower[b].clear();
}
```
### 参考代码
加上主函数,下面放一下完整的模板实现(题目是 UOJ #81):
```cpp
#define M 403
using T=ll;
const T INF=0x3f3f3f3f3f3f3f3f;
namespace blossom_tree{
#define N M*2+1
struct edge{
int u,v;T w;
edge(){}
edge(int u,int v,T w):u(u),v(v),w(w){}
};
// Graph
int n,n_x; // [1, n]: point; [n+1, n_x]: flower
edge g[N][N]; // adjacent matrix
// flower
vector<int>flower[N]; // nodes in flower i (outer flower)
int root[N]; // flower root, root<=n root=i: normal nodes
int flower_from[N][N]; // flower_from[b][x]: outermost flower in b that contains x
// slack
T label[N]; // node label, [1, n] point label, [n+1, n_x] flower label
int col[N]; // color saved at flower root
int slv[N]; // slack node of NON LEAF NODES, slv[y]=x z(x,y) min_x
// match
int mat[N]; // match, mat[x]=y (x,y)\in E
int fa[N]; // fa in cross tree
int vis[N]; // if in path
queue<int>Q; // bfs queue
// calculate slv
inline T calc_slv(edge e){return label[e.u]+label[e.v]-e.w;}
inline void update_slv(int u,int v){if(!slv[v]||calc_slv(g[u][v])<calc_slv(g[slv[v]][v]))slv[v]=u;}
inline void recalc_slv(int u){
slv[u]=0;
for(int i=1;i<=n;i++)if(g[i][u].w>0&&root[i]!=u&&col[root[i]]==1)update_slv(i,u);
}
// only push nodes, not flowers
void q_push(int x){
if(x<=n)Q.push(x);
else for(auto p:flower[x])q_push(p);
}
// set root of all nodes in x to r
void set_root(int x,int r){
root[x]=r;
if(x>n)for(auto p:flower[x])set_root(p,r);
}
// return a (+-)^k path in flower b from root[b] to x
int get_even_path_in_flower(int b,int x){
int pr=find(flower[b].begin(),flower[b].end(),x)-flower[b].begin();
assert(b>n&&b<=n_x&&pr<flower[b].size()); // b is flower, x in b
if(pr%2==0)return pr;
reverse(flower[b].begin()+1,flower[b].end());
return flower[b].size()-pr;
}
// set (u,v) match, can be flower
void set_match(int u,int v){
mat[u]=g[u][v].v;
if(u>n){
edge e=g[u][v];
int xr=flower_from[u][e.u];
int pr=get_even_path_in_flower(u,xr);
for(int i=0;i<pr;i++)set_match(flower[u][i],flower[u][i^1]);
set_match(xr,v);
rotate(flower[u].begin(),flower[u].begin()+pr,flower[u].end()); // change receptacle
}
}
// link 2 S points
void side_augment(int u,int v){
int nv=root[mat[u]],nu=root[fa[nv]];
while(1){
set_match(u,v);
u=nu,v=nv;
if(!nv)break;
set_match(nv,nu);
nv=root[mat[u]],nu=root[fa[nv]];
}
}
void linkSS(int u,int v){
side_augment(u,v);
side_augment(v,u);
}
int get_lca(int u,int v){
static int t=0;
++t; // to avoid clearing vis
while(u||v){
if(vis[u]==t)return u;
vis[u]=t;
u=root[mat[u]];
if(u)u=root[fa[u]];
if(!u)swap(u,v);
}
return 0;
}
void add_blossom(int u,int v,int r){
int i,b=n+1;
while(b<=n_x&&root[b])b++;
if(b>n_x)++n_x;
// clear
col[b]=1;label[b]=0;mat[b]=mat[r];flower[b].clear();
for(i=1;i<=n_x;i++)g[i][b].w=g[b][i].w=0;
for(i=1;i<=n;i++)flower_from[b][i]=0;
// construct flower
while(u!=r){
flower[b].pb(u);u=root[mat[u]];q_push(u);
flower[b].pb(u);u=root[fa[u]];
}
flower[b].pb(r);
reverse(flower[b].begin(),flower[b].end());
while(v!=r){
flower[b].pb(v);v=root[mat[v]];q_push(v);
flower[b].pb(v);v=root[fa[v]];
}
// set as outermost flower
set_root(b,b);
// calculate slack
for(auto p:flower[b]){
for(i=1;i<=n_x;i++){
// set to min slave
if(!g[b][i].w||calc_slv(g[p][i])<calc_slv(g[b][i])){
g[b][i]=g[p][i];
g[i][b]=g[i][p];
}
}
for(i=1;i<=n;i++)if(flower_from[p][i])flower_from[b][i]=p;
}
//recalc_slv(b);
}
// only expand outermost blossom b, b is T(white) blossom
void expand_blossom(int b){
int i,x;
for(auto p:flower[b])set_root(p,p);
x=flower_from[b][g[b][fa[b]].u];
// [0,pr]: (+-)^k, insert into tree, add black to queue
int pr=get_even_path_in_flower(b,x);
col[x]=2;fa[x]=fa[b];
for(i=0;i<pr;i+=2){
// from bottom to upper layer in tree
int white=flower[b][i];
int black=flower[b][i+1];
col[black]=1;col[white]=2;
fa[white]=g[black][white].u;
slv[black]=slv[white]=0;
q_push(black);
}
// others: color=0
for(i=pr+1;i<flower[b].size();i++){
col[flower[b][i]]=0;
recalc_slv(flower[b][i]);
}
// delete b
root[b]=0;
flower[b].clear();
}
// found_edge
int augment_path(edge e){
int u=root[e.u],v=root[e.v];
if(!col[v]){
assert(mat[v]);
fa[v]=e.u;
col[v]=2;
int nu=root[mat[v]];
slv[nu]=slv[v]=0;
col[nu]=1;
q_push(nu);
}else if(col[v]==1){
int r=get_lca(u,v);
if(r)add_blossom(u,v,r);
else return linkSS(u,v),1;
}
return 0;
}
int augment(){
int i;
memset(col,0,sizeof(int)*(n_x+1));
memset(slv,0,sizeof(int)*(n_x+1));
memset(fa,0,sizeof(int)*(n_x+1));
Q=queue<int>();
for(i=1;i<=n_x;i++)
if(root[i]==i&&!mat[i]){
// add all unmatched points
col[i]=1;
q_push(i);
}
if(Q.empty())return 0;
while(1){
while(!Q.empty()){
int p=Q.front();Q.pop();
assert(col[root[p]]==1);
for(i=1;i<=n;i++){
if(g[p][i].w==0||root[i]==root[p])continue;
// not in same flower
T d=calc_slv(g[p][i]);
if(!d){if(augment_path(g[p][i]))return 1;}
else if(col[root[i]]!=2)update_slv(p,root[i]);
}
}
T delta=INF;
// calc delta
for(i=1;i<=n;i++)if(col[root[i]]==1)delta=min(delta,label[i]);
for(i=n+1;i<=n_x;i++)if(root[i]==i&&col[i]==2)delta=min(delta,label[i]/2);
for(i=1;i<=n_x;i++){
if(root[i]!=i||!slv[i])continue;
if(!col[i])delta=min(delta,calc_slv(g[slv[i]][i]));
else if(col[i]==1)delta=min(delta,calc_slv(g[slv[i]][i])/2);
}
// update label
for(i=1;i<=n;i++){
if(col[root[i]]==1)label[i]-=delta;
else if(col[root[i]]==2)label[i]+=delta;
}
for(i=n+1;i<=n_x;i++){
if(root[i]!=i)continue;
if(col[i]==1)label[i]+=2*delta;
else if(col[i]==2)label[i]-=2*delta;
}
for(i=1;i<=n;i++)if(label[i]<=0)return 0;
for(i=1;i<=n_x;i++){
if(root[i]!=i||!slv[i]||root[slv[i]]==i)continue;
if(calc_slv(g[slv[i]][i])==0&&augment_path(g[slv[i]][i]))return 1;
}
// expand
for(i=n+1;i<=n_x;i++)
if(root[i]==i&&col[i]==2&&label[i]==0)
expand_blossom(i);
}
return 0;
}
void init(int _n,vector<pair<T,pii>>edges){
int i,j;
n=n_x=_n;
memset(mat,0,sizeof(mat));
for(i=0;i<=n;i++){
root[i]=i;
flower[i].clear();
for(j=0;j<=n;j++){
flower_from[i][j]=(i==j)?i:0;
g[i][j]=edge(i,j,0);
}
}
T w_max=0;
for(auto pr:edges){
int u=pr.se.fi,v=pr.se.se;
T w=pr.fi;
g[u][v]=edge(u,v,w*2);
g[v][u]=edge(v,u,w*2);
w_max=max(w_max,w);
}
for(i=1;i<=n;i++)label[i]=w_max;
}
pair<int,T>calc(){
int i,cnt=0;T s=0;
while(augment())++cnt;
for(i=1;i<=n;i++)if(mat[i]>i)s+=g[i][mat[i]].w/2;
return mp(cnt,s);
}
}
int main(){
int i,n,m;
vector<pair<T,pii>>edges;
scanf("%d%d",&n,&m);
for(i=0;i<m;i++){
int u,v;T w;
scanf("%d%d%lld",&u,&v,&w);
edges.pb(mp(w,mp(u,v)));
}
blossom_tree::init(n,edges);
printf("%lld\n",blossom_tree::calc().se);
for(i=1;i<=n;i++)printf("%d ",blossom_tree::mat[i]);
return 0;
}
```
(全抄会 WA 的哦!)
## 参考资料与工具
1. OI-wiki,[一般图最大匹配](https://oi-wiki.org/graph/graph-matching/general-match/)
2. Fuyuki,[题解 P6113 【模板】一般图最大匹配](https://www.luogu.com.cn/blog/Fuyuki/solution-p6113)
3. 胡拉哥,[算法设计技巧:Primal-Dual](https://blog.csdn.net/qx3501332/article/details/105546208/)
4. wenruo,[KM算法详解+模板](https://www.cnblogs.com/wenruo/p/5264235.html)
5. Shawn-Yang,[KM算法--学习笔记](https://blog.csdn.net/yangss123/article/details/88716680)
6. 陈胤伯,2015 年国家集训队论文《浅谈图的匹配算法及其应用》
7. jacky860226,[general-graph-weighted-match-slides](https://github.com/jacky860226/general-graph-weighted-match-slides)
8. vfleaking,[妈妈我终于会一般图最大权匹配了!](https://vfleaking.blog.uoj.ac/blog/339)
9. Joris_VR,[Maximum Weighted Matching](http://jorisvr.nl/article/maximum-matching)
10. zhongzihao,[一般图最大权(最大)匹配](https://wiki.buaaacm.com/doku.php?id=technique:general_matching_weighted)
11. Kurt Mehlhorn et al.,[Implementation of O(nmlogn) weighted matchings in general graphs: the power of data structures](https://dl.acm.org/doi/pdf/10.1145/944618.944622)
12. [Visualizations of Graph Algorithms](https://algorithms.discrete.ma.tum.de/)
13. [Graph Editor](https://csacademy.com/app/graph_editor/)
14. [NetworkX](https://networkx.org/)
## 提交通道
1. [洛谷](https://www.luogu.com.cn/problem/P6699)
2. [UOJ](https://uoj.ac/problem/81)
3. [Library checker](https://judge.yosupo.jp/problem/general_weighted_matching)
## 结语
欢迎纠错与讨论。感谢阅读!