内容
设二分图左部点点数为 \(x\),右部点点数为 \(y\),且满足 \(x<y\)。定义一张二分图的完备匹配为:对于任意一个左部点都有与之匹配的右部点。
\(\text{Hall}\) 定理的内容是:一张二分图有完备匹配,等价于对于任意左部点的集合 \(S\),满足 \(|\{y|\exists x\in S, 边 (x,y) 存在\}|\ge |S|\),即 \(S\) 的邻域大小(记为 \(N(S)\))不小于 \(S\) 本身。
证明
必要性显然。反证即可。
下面证明充分性。
用归纳法证明。当 \(n=1\) 时 \(\text{Hall}\) 定理显然成立。
当 \(n>1\) 时,选取其中任意一对匹配的点删去,剩下的点仍然满足 \(\text{Hall}\) 定理。
于是由归纳法得证。
拓展
\(\text{Hall}\) 定理在带权情况下(一个点能和多个点匹配)仍然成立。将一个点拆成 \(a_i\) 个点即可。
二分图最大匹配等于 \(x+\min\{|S|-N(S)\}\)。
应用
QOJ12015 撸猫
将源点向每只猫连边 \(p_i\),每只猫和每个集合连边 \(\infty\),再从每个集合向汇点连边 \(x_i\) 即可。然后发现这是一个二分图最大匹配的形式,于是用 \(\text{Hall}\) 定理求出每一个猫的集合对应的 \(c\),取最小值即可。用高维前缀和,时间复杂度 \(\mathcal{O}(n2^n)\)。
要注意应当把 \(\sum\limits{x_i}\) 当做 \(1\)。
#include <bits/stdc++.h>
using namespace std;
constexpr int N=1<<20;
constexpr double eps=1e-12;
int n;
double a[N],b[N];
signed main(){ios::sync_with_stdio(false);cin.tie(0),cout.tie(0);cin>>n;cout<<fixed<<setprecision(8);for(int i=0;i<1<<n;i++){cin>>a[i];for(int j=0;j<n;j++)if((i>>j)&1)b[j]+=a[i];}for(int i=0;i<n;i++){for(int j=0;j<1<<n;j++){if((j>>i)&1)a[j]+=a[j^(1<<i)];}}double ans=1;for(int i=0;i<1<<n;i++){double s=0;for(int j=0;j<n;j++)if((i>>j)&1)s+=b[j];if(s<eps)continue;ans=min(ans,(1-a[((1<<n)-1)^i])/s);}cout<<ans<<'\n';return 0;
}
CF1519F Chests and Keys
题目的限制等价于花 \(c_{i,j}\) 的代价连一条 \(i\) 到 \(j\) 的边,要求整张图满足对于任意的箱子集合 \(S\),有 \(\sum\limits_{i\in S}a_i\ge\sum\limits_{\exists i\in S,存在(i,j)}b_j\)。
观察到这个限制与上面 \(\text{Hall}\) 定理的第一个扩展形式类似,于是可以转化为最大流问题。然后 \(a,b\) 的值域都很小直接 \(5\) 进制状压类似模拟最大流即可。时间复杂度 \(O(n5^{2m})\),跑不满。
#include <bits/stdc++.h>
using namespace std;
int n,m,a[6],b[6],c[6][6],p5[7],dp[7][15625];
bool check(int x,int y){for(int i=0;i<m;i++)if(y/p5[i]%5<x/p5[i]%5)return 1;for(int i=0;i<m;i++)if(y/p5[i]%5>b[i])return 1;return 0;
}
signed main(){ios::sync_with_stdio(false);cin.tie(0),cout.tie(0);cin>>n>>m;for(int i=0;i<n;i++)cin>>a[i];for(int i=0;i<m;i++)cin>>b[i];for(int i=0;i<n;i++)for(int j=0;j<m;j++)cin>>c[i][j];p5[0]=1;for(int i=1;i<=m;i++)p5[i]=p5[i-1]*5;memset(dp,0x3f,sizeof(dp));int mask=0;for(int i=0;i<m;i++)mask+=b[i]*p5[i];dp[0][mask]=0;for(int i=1;i<=n;i++){for(int j=0;j<p5[m];j++){int s=0;for(int k=0;k<m;k++)s+=j/p5[k]%5;if(s!=a[i-1])continue;int sum=0;for(int k=0;k<m;k++)if(j/p5[k]%5)sum+=c[i-1][k];for(int k=0;k<p5[m];k++){if(check(j,k))continue;dp[i][k-j]=min(dp[i][k-j],dp[i-1][k]+sum);}}}int ans=0x3f3f3f3f;for(int i=0;i<p5[m];i++)ans=min(ans,dp[n][i]);cout<<ans<<'\n';return 0;
}
ARC076F Exhausted?
将每个人向每个在他范围内的坐标连边,则原问题相当于要加多少个点才能有完备匹配。完备匹配转化为最大匹配,然后就可以用第二个扩展形式,求的就是:
扫描线即可。
注意可能有区间不交的情况,所以至少加 \(n-m\) 个椅子。
#include <bits/stdc++.h>
using namespace std;
constexpr int N=2e5+5;
int n,m;
struct Node{int l,r;}a[N];
struct SegmentTree{int maxn[N<<2],tag[N<<2];#define ls(p) (p<<1)#define rs(p) (p<<1)|1void pushup(int p){maxn[p]=max(maxn[ls(p)],maxn[rs(p)]);}void pushdown(int p){maxn[ls(p)]+=tag[p];maxn[rs(p)]+=tag[p];tag[ls(p)]+=tag[p];tag[rs(p)]+=tag[p];tag[p]=0;}void update(int p,int l,int r,int val,int pl,int pr){if(l<=pl&&pr<=r){maxn[p]+=val;tag[p]+=val;return;}pushdown(p);const int mid=(pl+pr)>>1;if(mid>=l)update(ls(p),l,r,val,pl,mid);if(mid<r)update(rs(p),l,r,val,mid+1,pr);pushup(p);}int query(int p,int l,int r,int pl,int pr){if(l<=pl&&pr<=r)return maxn[p];pushdown(p);const int mid=(pl+pr)>>1;if(mid>=r)return query(ls(p),l,r,pl,mid);if(mid<l)return query(rs(p),l,r,mid+1,pr);return max(query(ls(p),l,r,pl,mid),query(rs(p),l,r,mid+1,pr));}
}tr;
signed main(){ios::sync_with_stdio(false);cin.tie(0),cout.tie(0);cin>>n>>m;for(int i=1;i<=n;i++)cin>>a[i].l>>a[i].r;sort(a+1,a+n+1,[](Node x,Node y){return x.l<y.l||(x.l==y.l&&x.r>y.r);});for(int i=0;i<=m+1;i++)tr.update(1,i,i,i,0,m+1);int ans=max(0,n-m);for(int i=1;i<=n;i++){tr.update(1,0,a[i].r,1,0,m+1);ans=max(ans,tr.query(1,0,m+1,0,m+1)-a[i].l-m-1);}cout<<ans<<'\n';return 0;
}