Hall 定理
对于二分图 G = ( V , E ) G=(V,E) G=(V,E) ,令 N ( v ) N(v) N(v) 表示点 v v v 的邻居集,则关于图 G G G 的最大匹配我们有如下结论:
Hall 定理 :设二分图 G G G 的两部分分别为 V L V_L VL , V R V_R VR 且 ∣ V L ∣ ≤ ∣ V R ∣ |V_L|\le|V_R| ∣VL∣≤∣VR∣ ,则其存在一个大小为 ∣ V L ∣ |V_L| ∣VL∣ 的匹配当且仅当 ∀ S ⊆ V L \forall S\subseteq V_L ∀S⊆VL ,都有 ∣ S ∣ ≤ ∣ ⋃ v ∈ S N ( v ) ∣ |S|\le|\bigcup\limits_{v\in S}N(v)| ∣S∣≤∣v∈S⋃N(v)∣ 。
推论 1 :对于一个 k k k 正则二分图(每个点度数都为 k k k ,其中 k ≥ 1 k\ge1 k≥1 ),若其左右点数相等,那么其必有完美匹配。
推论 2 :设二分图 G G G 的两部分分别为 V L V_L VL , V R V_R VR ,则其最大匹配为 ∣ V L ∣ − max S ⊆ V L ( ∣ S ∣ − ∣ ⋃ v ∈ S N ( v ) ∣ ) |V_L|-\max\limits_{S\subseteq V_L}(|S|-|\bigcup\limits_{v\in S}N(v)|) ∣VL∣−S⊆VLmax(∣S∣−∣v∈S⋃N(v)∣) ,也等于 min S ⊆ V L ( ∣ V L ∣ − ∣ S ∣ + ∣ ⋃ v ∈ S N ( v ) ∣ ) \min\limits_{S\subseteq V_L}(|V_L|-|S|+|\bigcup\limits_{v\in S}N(v)|) S⊆VLmin(∣VL∣−∣S∣+∣v∈S⋃N(v)∣) 。
广义Hall定理 :给定一张二分图。如果存在一个匹配覆盖左部图中的点集 X X X ,且存在一个匹配覆盖右部图中的点集 Y Y Y ,那么存在一个匹配同时覆盖 X X X 和 Y Y Y 。
A.HDU 6667 Roundgod and Milk Tea
这是二分图的最大匹配问题,如果建图直接跑肯定爆炸,考虑使用
H
a
l
l
Hall
Hall 定理的推论
2
2
2 。现在问题就是
max
S
⊆
V
L
(
∣
S
∣
−
∣
⋃
v
∈
S
N
(
v
)
∣
)
\max\limits_{S\subseteq V_L}(|S|-|\bigcup\limits_{v\in S}N(v)|)
S⊆VLmax(∣S∣−∣v∈S⋃N(v)∣) 是多少。显然,对于
S
S
S ,我们只需要找每一个人,和所有人加起来的集合以及空集就可以了。
#include<bits/stdc++.h>
using namespace std;
int n,a[1000010],b[1000010];
long long sum1,sum2,maxx;
int main()
{
freopen("tea.in","r",stdin);
freopen("tea.out","w",stdout);
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%d%d",&a[i],&b[i]);
sum1+=a[i];
sum2+=b[i];
}
maxx=max(sum1-sum2,0LL);
for(int i=1;i<=n;i++)
maxx=max(maxx,a[i]-(sum2-b[i]));
printf("%lld",sum1-maxx);
return 0;
}
B.ARC076D-Exhausted
感觉跟上一题很像,但是这个
S
S
S 我们不太好直接确定一个小的范围了。我们先使用推论
2
2
2 把要求的东西写出来。
a
n
s
=
n
−
(
∣
V
L
∣
−
max
S
⊆
V
L
(
∣
S
∣
−
⋃
v
∈
S
N
(
v
)
)
)
=
max
S
⊆
V
L
(
∣
S
∣
−
⋃
v
∈
S
N
(
v
)
)
ans=n-(|V_L|-\max\limits_{S\subseteq V_L}(|S|-\bigcup\limits_{v\in S}N(v)))=\max\limits_{S\subseteq V_L}(|S|-\bigcup\limits_{v\in S}N(v))
ans=n−(∣VL∣−S⊆VLmax(∣S∣−v∈S⋃N(v)))=S⊆VLmax(∣S∣−v∈S⋃N(v))
我们注意到这里
S
S
S 的邻居集是上下两部分,取并很难搞,故将它转成交集来求。
a
n
s
=
max
S
⊆
V
L
(
∣
S
∣
−
(
m
−
(
l
,
r
)
)
)
=
max
S
⊆
V
L
(
∣
S
∣
+
(
l
,
r
)
)
−
m
ans=\max\limits_{S\subseteq V_L}(|S|-(m-(l,r)))=\max\limits_{S\subseteq V_L}(|S|+(l,r))-m
ans=S⊆VLmax(∣S∣−(m−(l,r)))=S⊆VLmax(∣S∣+(l,r))−m
现在问题就变为了:给定一些区间,任取区间,求区间交大小和区间个数的和的最大值。
我们考虑把区间按左端点排序,枚举区间,用线段树来维护右端点和区间个数。
这样的好处在于,我们加入一个区间之后,对于后面的区间的左端点一定大于前面区间的左端点,这样就可以忽视左端点的影响,只需要考虑右端点即可。同时我们相当于枚举交集的左端点,考虑所有可能的右端点,保证了不重不漏。
细节:因为
(
l
,
r
)
(l,r)
(l,r) 有可能为
ϕ
\phi
ϕ ,所以线段树每个端点存的右端点为
r
−
1
r-1
r−1 ,也就是叶子结点为空集,这样就避免了讨论空集的情况。
#include<bits/stdc++.h>
using namespace std;
int n,m,ans;
struct node
{
int l,r;
} a[200010];
struct node1
{
int l,r,maxx,add;
} tree[800000];
bool mycmp(node x,node y)
{
return x.l<y.l;
}
void build(int p,int l,int r)
{
tree[p].l=l;
tree[p].r=r;
if(l==r)
{
tree[p].maxx=r-1;
return;
}
int mid=(l+r)/2;
build(2*p,l,mid);
build(2*p+1,mid+1,r);
tree[p].maxx=max(tree[2*p].maxx,tree[2*p+1].maxx);
}
void spread(int p)
{
if(tree[p].add)
{
tree[2*p].add+=tree[p].add;
tree[2*p].maxx+=tree[p].add;
tree[2*p+1].add+=tree[p].add;
tree[2*p+1].maxx+=tree[p].add;
tree[p].add=0;
}
}
void change(int p,int l,int r)
{
if(tree[p].l>=l&&tree[p].r<=r)
{
tree[p].maxx++;
tree[p].add++;
return;
}
spread(p);
int mid=(tree[p].l+tree[p].r)/2;
if(l<=mid)
change(2*p,l,r);
if(r>mid)
change(2*p+1,l,r);
tree[p].maxx=max(tree[2*p].maxx,tree[2*p+1].maxx);
}
int ask(int p,int l,int r)
{
if(tree[p].l>=l&&tree[p].r<=r)
return tree[p].maxx;
spread(p);
int mid=(tree[p].l+tree[p].r)/2,x=0;
if(l<=mid)
x=max(x,ask(2*p,l,r));
if(r>mid)
x=max(x,ask(2*p+1,l,r));
return x;
}
int main()
{
freopen("exhausted.in","r",stdin);
freopen("exhausted.out","w",stdout);
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
scanf("%d%d",&a[i].l,&a[i].r);
sort(a+1,a+n+1,mycmp);
build(1,1,m+1);
ans=max(n-m,0);//记得空集和 0
for(int i=1;i<=n;i++)
{
change(1,a[i].l+1,a[i].r);
ans=max(ans,ask(1,a[i].l+1,a[i].r)-a[i].l-m);
}
printf("%d",ans);
return 0;
}
C.CF981F Round Marriage
先把新郎和新娘从小到大排序,最小的最大考虑二分,二分一个距离之后考虑把新娘的环给破掉,复制两倍,每一个新郎可以配对的就是一段区间,且区间左右端点单调不降。之后使用
H
a
l
l
Hall
Hall 定理判断是否具有完美匹配。设第
i
i
i 个新郎能匹配的新娘区间为
[
L
i
,
R
i
]
[L_i,R_i]
[Li,Ri] ,因为连续的一段新郎区间的约束一定比零散的要强,我们只需要考虑连续的新郎区间即可,设新郎区间为
[
l
,
r
]
[l,r]
[l,r] (这里也需破环为链)。则需满足
r
−
l
+
1
≤
R
r
−
L
l
+
1
⇒
R
r
−
r
≥
L
l
−
l
r-l+1\le R_r-L_l+1 \Rightarrow R_r-r\ge L_l-l
r−l+1≤Rr−Ll+1⇒Rr−r≥Ll−l 这里我们可以直接枚举新郎,记录最大的
L
i
−
i
L_i-i
Li−i 再与
R
i
−
i
R_i-i
Ri−i 判断即可。
对于每一个新郎的匹配新娘区间,因为区间端点单调不降,可以使用双指针来获得。
#include<bits/stdc++.h>
using namespace std;
int n,L,a[600010],b[800010];
bool check(int x)
{
int ll=1,rr=0,maxx=-1e9;
for(int i=n+1;i<=3*n;i++)
{
while(a[i]-b[ll]>x)
ll++;
while(rr<4*n&&b[rr+1]-a[i]<=x)
rr++;
maxx=max(maxx,ll-i);
if(rr-i<maxx)
return 0;
}
return 1;
}
int main()
{
scanf("%d%d",&n,&L);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
for(int i=1;i<=n;i++)
scanf("%d",&b[i]);
sort(a+1,a+n+1);
sort(b+1,b+n+1);
for(int i=1;i<=2*n;i++)
a[i+n]=a[i]+L;
for(int i=1;i<=3*n;i++)
b[i+n]=b[i]+L;
int l=0,r=L/2;
while(l+1<r)
{
int mid=(l+r)/2;
if(check(mid))
r=mid;
else
l=mid;
}
if(check(l))
printf("%d",l);
else
printf("%d",r);
return 0;
}
D.[POI2009]LYZ-Ice Skates
考虑使用
H
a
l
l
Hall
Hall 定理判定是否足够。对于
S
S
S 显然只需要判定连续的一段区间就可以了。设
a
[
i
]
a[i]
a[i] 表示
i
i
i 号脚有多少人,则就需要判断
max
∑
l
≤
i
≤
r
a
[
i
]
≤
(
r
−
l
+
1
+
d
)
⋅
k
⇒
max
∑
l
≤
i
≤
r
(
a
[
i
]
−
k
)
≤
d
⋅
k
\max\sum\limits_{l\le i\le r}a[i]\le(r-l+1+d)\cdot k\Rightarrow\max\sum\limits_{l\le i\le r}(a[i]-k)\le d\cdot k
maxl≤i≤r∑a[i]≤(r−l+1+d)⋅k⇒maxl≤i≤r∑(a[i]−k)≤d⋅k ,发现这就是最大子段和,使用线段树维护即可。
#include<bits/stdc++.h>
using namespace std;
int n,m,k,d;
struct node
{
int l,r;
long long lmax,rmax,maxx,sum;
} tree[800000];
void build(int p,int l,int r)
{
tree[p].l=l;
tree[p].r=r;
if(l==r)
{
tree[p].lmax=-k;
tree[p].maxx=-k;
tree[p].rmax=-k;
tree[p].sum=-k;
return;
}
int mid=(l+r)/2;
build(2*p,l,mid);
build(2*p+1,mid+1,r);
tree[p].lmax=max(tree[2*p].lmax,tree[2*p].sum+tree[2*p+1].lmax);
tree[p].rmax=max(tree[2*p+1].rmax,tree[2*p+1].sum+tree[2*p].rmax);
tree[p].maxx=max({tree[2*p].maxx,tree[2*p+1].maxx,tree[2*p].rmax+tree[2*p+1].lmax});
tree[p].sum=tree[2*p].sum+tree[2*p+1].sum;
}
void add(int p,int x,int y)
{
if(tree[p].l==x&&tree[p].r==x)
{
tree[p].lmax+=y;
tree[p].maxx+=y;
tree[p].rmax+=y;
tree[p].sum+=y;
return;
}
int mid=(tree[p].l+tree[p].r)/2;
if(x<=mid)
add(2*p,x,y);
else
add(2*p+1,x,y);
tree[p].lmax=max(tree[2*p].lmax,tree[2*p].sum+tree[2*p+1].lmax);
tree[p].rmax=max(tree[2*p+1].rmax,tree[2*p+1].sum+tree[2*p].rmax);
tree[p].maxx=max({tree[2*p].maxx,tree[2*p+1].maxx,tree[2*p].rmax+tree[2*p+1].lmax});
tree[p].sum=tree[2*p].sum+tree[2*p+1].sum;
}
int main()
{
freopen("skates.in","r",stdin);
freopen("skates.out","w",stdout);
scanf("%d%d%d%d",&n,&m,&k,&d);
build(1,1,n-d);
while(m--)
{
int r,x;
scanf("%d%d",&r,&x);
add(1,r,x);
if(tree[1].maxx<=1LL*d*k)
puts("TAK");
else
puts("NIE");
}
return 0;
}
E.CF103E Buying Sets
先建一个网络流图,左边是集合,右边是元素,集合里有哪个元素就向哪个元素连一条容量为正无穷的边,发现这很权闭合图,如果把权值取反,把元素的点权当成
0
0
0 ,就是求最大权闭合图,如果没有"子集并的大小等于子集个数"这个条件,直接跑最大权闭合图就可以了(虽然没有这个条件直接枚举就行…),加上这个条件怎么做呢?考虑一个神奇构造,我们把所有集合的点权加上
i
n
f
inf
inf ,所有元素的点权减去
i
n
f
inf
inf (注意这个
i
n
f
inf
inf 小于中间连的边的正无穷,又远大于原先子集的点权)。这么做有两点好处:1.所有集合都与源点相连,所有元素都与汇点相连。2.最小割的割边数一定等于
n
n
n 。
好处
1
1
1 显然,好处
2
2
2 证明如下:因为任意
k
k
k 个子集的并的大小
≥
k
\ge k
≥k ,所以最小割的割边数一定
≥
n
\ge n
≥n(反证法自证),又因为最小割的割边数可以等于
n
n
n (全割一边),以及如果比
n
n
n 多割一条边因为增加了
i
n
f
inf
inf 所以一定不为最小割,所以最小割的割边数一定等于
n
n
n 。
最大权闭合图的最小割有一个实际意义:割掉与源点相连的边相当于不选这个点,割掉与汇点相连的边相当于选这个点。配合好处
2
2
2 ,我们容易得到这个图的最小割一定会使集合数等于元素数。综上,这样构造完后直接跑最小割求最大值即可(最后记得对答案取反)。
#include<bits/stdc++.h>
using namespace std;
int n,S,T,head[1000],tot=-1,d[1000],cur[1000];
long long ans;
struct node
{
int to,nex;
long long v;
} edge[200000];
void add(int x,int y,long long z)
{
edge[++tot].nex=head[x];
edge[tot].to=y;
edge[tot].v=z;
head[x]=tot;
}
bool bfs()
{
queue<int> q;
memset(d,-1,sizeof d);
q.push(S);
d[S]=0,cur[S]=head[S];
while(!q.empty())
{
int x=q.front();
q.pop();
for(int i=head[x];~i;i=edge[i].nex)
{
if(d[edge[i].to]==-1&&edge[i].v)
{
d[edge[i].to]=d[x]+1;
cur[edge[i].to]=head[edge[i].to];
if(edge[i].to==T)
return 1;
q.push(edge[i].to);
}
}
}
return 0;
}
long long find(int x,long long limit)
{
if(x==T)
return limit;
long long flow=0;
for(int i=cur[x];~i&&flow<limit;i=edge[i].nex)
{
cur[x]=i;
if(d[edge[i].to]==d[x]+1&&edge[i].v)
{
long long c=find(edge[i].to,min(limit-flow,edge[i].v));
if(!c)
d[edge[i].to]=-1;
flow+=c;
edge[i].v-=c;
edge[i^1].v+=c;
}
}
return flow;
}
long long Dinic()
{
long long ans=0,flow;
while(bfs())
{
while(flow=find(S,1e18))
ans+=flow;
}
return ans;
}
int main()
{
freopen("z.in","r",stdin);
freopen("z.out","w",stdout);
scanf("%d",&n);
memset(head,-1,sizeof head);
T=2*n+1;
for(int i=1;i<=n;i++)
{
int t;
scanf("%d",&t);
for(int j=1;j<=t;j++)
{
int x;
scanf("%d",&x);
add(i,n+x,1e18);
add(n+x,i,0);
}
}
for(int i=1;i<=n;i++)
{
int p;
scanf("%d",&p);
p=-p;
add(S,i,p+1e9);
add(i,S,0);
ans+=(p+1e9);
add(i+n,T,1e9);
add(T,i+n,0);
}
printf("%lld",-(ans-Dinic()));
return 0;
}
F. [ARC106E] Medals
首先二分答案转化为判定性问题,对于
1
1
1 个人至少
k
k
k 次可以转化为
k
k
k 个周期相同的人至少
1
1
1 。对于判定,我们使用
H
a
l
l
Hall
Hall 定理,因为
n
n
n 很小,所以
S
S
S 集合数量不多,考虑状压,我们考虑一下二分的上界是多少,
a
=
1
a=1
a=1 的周期约束性最强,也就是说最坏的情况是每两天匹配一个人,所以
r
=
2
n
k
r=2nk
r=2nk 。所以天数是在我们可以接受的范围内,我们考虑预处理出每一天有哪些人休息,状压成一个状态,那么对于这个状态的子集,都会缺少这一天,做一遍高维前缀和就可以处理出每一个
S
S
S 的缺少的天数,用二分值一减就是邻居集大小,直接判断即可。
#include<bits/stdc++.h>
using namespace std;
int n,k,a[20],st[3600010],f[300000],num[300000];
bool check(int x)
{
memset(f,0,sizeof f);
for(int i=1;i<=x;i++)
f[st[i]]++;
for(int i=0;i<n;i++)
{
for(int j=(1<<n)-1;~j;j--)//高维前缀和内层循环顺序无影响
if(((j>>i)&1)==0)
f[j]+=f[j^(1<<i)];
}
for(int i=1;i<(1<<n);i++)
if(x-f[i]<num[i]*k)
return 0;
return 1;
}
int main()
{
freopen("medals.in","r",stdin);
freopen("medals.out","w",stdout);
scanf("%d%d",&n,&k);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
for(int i=1,now=0;i<=2*n*k;i++)
{
st[i]=now;
for(int j=1;j<=n;j++)
if(i%a[j]==0)
now^=(1<<(j-1));
}
for(int i=1;i<(1<<n);i++)
num[i]=num[i&(i-1)]+1;
int l=n*k,r=2*n*k;
while(l+1<r)
{
int mid=(l+r)/2;
if(check(mid))
r=mid;
else
l=mid;
}
if(check(l))
printf("%d",l);
else
printf("%d",r);
return 0;
}
G. [AGC029F] Construction of a tree
设
A
=
{
E
1
,
E
2
,
.
.
.
,
E
n
−
1
}
,
S
⊆
A
且
S
≠
ϕ
A=\{E_1,E_2,...,E_{n-1}\},S\subseteq A 且 S \ne \phi
A={E1,E2,...,En−1},S⊆A且S=ϕ,
f
(
S
)
f(S)
f(S) 表示
S
S
S 中每个集合元素并集的大小,那么有
f
(
S
)
>
∣
S
∣
f(S)>|S|
f(S)>∣S∣ ,不然不可能构成一颗树。发现这个十分像
H
a
l
l
Hall
Hall 定理,先建图,左部图是集合,右部图是元素,集合连向自己含有的元素。跑一个二分图最大匹配,如果最大匹配大小不等于
n
−
1
n-1
n−1 ,则一定无解。
之后考虑如何构造方案。我们从没有匹配到的那个点
x
x
x 出发,做
d
f
s
dfs
dfs ,访问所有没有访问过的集合,再从集合出发,访问集合所匹配的点
y
y
y ,那么这个集合的选出就是
x
,
y
x,y
x,y 。之后从
y
y
y 点重复上述操作。做完之后,如果元素没有全部被访问过,说明无解,因为,访问过的元素数等于访问过的集合数加
1
1
1 ,剩下的集合数等于剩下的元素数,而剩下的集合的连边一定在剩下的元素之中,也就是说
S
≥
f
(
S
)
S\ge f(S)
S≥f(S) ,与有解条件矛盾,故无解;如果有解,直接输出方案即可。
#include<bits/stdc++.h>
using namespace std;
int n,S,T,head[300000],tot=-1,d[300000],cur[300000],st;
bool v[300000];
struct node
{
int l,r,id;
bool operator <(const node &x)const{
return id<x.id;
}
};
vector<node> s;
struct node1
{
int to,nex,v;
} edge[1000000];
void add(int x,int y,int z)
{
edge[++tot].nex=head[x];
edge[tot].to=y;
edge[tot].v=z;
head[x]=tot;
}
bool bfs()
{
queue<int> q;
q.push(S);
memset(d,-1,sizeof d);
d[S]=0,cur[S]=head[S];
while(!q.empty())
{
int x=q.front();
q.pop();
for(int i=head[x];~i;i=edge[i].nex)
{
if(d[edge[i].to]==-1&&edge[i].v)
{
d[edge[i].to]=d[x]+1;
cur[edge[i].to]=head[edge[i].to];
if(edge[i].to==T)
return 1;
q.push(edge[i].to);
}
}
}
return 0;
}
int find(int x,int limit)
{
if(x==T)
return limit;
int flow=0;
for(int i=cur[x];~i&&flow<limit;i=edge[i].nex)
{
cur[x]=i;
if(d[edge[i].to]==d[x]+1&&edge[i].v)
{
int c=find(edge[i].to,min(edge[i].v,limit-flow));
if(!c)
d[edge[i].to]=-1;
flow+=c;
edge[i].v-=c;
edge[i^1].v+=c;
}
}
return flow;
}
int Dinic()
{
int ans=0,flow;
while(bfs())
{
while(flow=find(S,(int)1e9))
ans+=flow;
}
return ans;
}
void dfs(int x,int lx,int fa,int ls)
{
v[x]=1;
if(lx==2)
s.push_back((node){fa,x,ls});
if(lx==0||lx==2)
{
for(int i=head[x];~i;i=edge[i].nex)
{
if(edge[i].to!=T&&!v[edge[i].to])
dfs(edge[i].to,1,x,edge[i].to);
}
return;
}
for(int i=head[x];~i;i=edge[i].nex)
{
if(!edge[i].v)
{
dfs(edge[i].to,2,fa,ls);
return;
}
}
}
int main()
{
freopen("tree.in","r",stdin);
freopen("tree.out","w",stdout);
scanf("%d",&n);
T=2*n;
memset(head,-1,sizeof head);
for(int i=1;i<n;i++)
{
int c;
scanf("%d",&c);
for(int j=1;j<=c;j++)
{
int x;
scanf("%d",&x);
add(i,n-1+x,1);
add(n-1+x,i,0);
}
add(S,i,1);
add(i,S,0);
add(n-1+i,T,1);
add(T,n-1+i,0);
}
add(2*n-1,T,1);
add(T,2*n-1,0);
if(Dinic()<n-1)
{
printf("-1");
return 0;
}
for(int i=0;i<=tot;i+=2)
{
if(edge[i].to==T&&edge[i].v==1)
{
st=edge[i^1].to;
break;
}
}
dfs(st,0,0,0);
if(s.size()<n-1)
{
printf("-1");
return 0;
}
sort(s.begin(),s.end());
for(int i=0;i<s.size();i++)
printf("%d %d\n",s[i].l-(n-1),s[i].r-(n-1));
return 0;
}
H. [AGC037D] Sorting a Grid
考虑从
D
D
D 倒退
C
C
C ,不妨先把
D
D
D 染色,
D
D
D 的每一行是一种相同的颜色,不同行颜色不同。那么
C
C
C 的每一行颜色一定相同,
B
,
C
B,C
B,C 每一列的颜色种类一定为
n
n
n 。从
B
B
B 变到
C
C
C 是简单的,那么现在的问题就是:给定一个矩阵
A
A
A ,矩阵中每一个元素都有一个颜色,一共有
n
n
n 种颜色,每一种颜色都有
m
m
m 个元素,只通过行变换使得每一列都恰好有
n
n
n 种颜色。
考虑这样一个构造:构建一个二分图,左边有
n
n
n 个节点,每个节点代表
A
A
A 矩阵中的每一行, 右边也是
n
n
n 个节点,每个节点代表一种颜色,每个行节点向本行有的颜色连边(这个颜色有几个元素就连几条边)。那么跑一次二分图完美匹配就相当于每行选择一个颜色,正好有
n
n
n 种,也就是一列的选择,做
m
m
m 次即可。一定可以做
m
m
m 次完美匹配吗?考虑当做到第
k
k
k 次时,每一个行节点向外连的边是
m
−
k
+
1
m-k+1
m−k+1 条,每一个颜色节点的入边也是
m
−
k
+
1
m-k+1
m−k+1 条,也就是说这是一个
m
−
k
+
1
m-k+1
m−k+1 正则二分图,因为左右两边节点数相同,根据推论
1
1
1 ,一定有完美匹配。
#include<bits/stdc++.h>
using namespace std;
int n,m,S,T,head[210],tot=-1,b[110][110],d[210],cur[210],co[10010];
queue<int> a[210][210];
struct node
{
int to,nex,v;
} edge[30000];
void add(int x,int y,int z)
{
edge[++tot].nex=head[x];
edge[tot].to=y;
edge[tot].v=z;
head[x]=tot;
}
bool bfs()
{
queue<int> q;
q.push(S);
memset(d,-1,sizeof d);
d[S]=0,cur[S]=head[S];
while(!q.empty())
{
int x=q.front();
q.pop();
for(int i=head[x];~i;i=edge[i].nex)
{
if(d[edge[i].to]==-1&&edge[i].v)
{
d[edge[i].to]=d[x]+1;
cur[edge[i].to]=head[edge[i].to];
if(edge[i].to==T)
return 1;
q.push(edge[i].to);
}
}
}
return 0;
}
int find(int x,int limit)
{
if(x==T)
return limit;
int flow=0;
for(int i=cur[x];~i&&flow<limit;i=edge[i].nex)
{
cur[x]=i;
if(d[edge[i].to]==d[x]+1&&edge[i].v)
{
int c=find(edge[i].to,min(edge[i].v,limit-flow));
if(!c)
d[edge[i].to]=-1;
flow+=c;
edge[i].v-=c;
edge[i^1].v+=c;
}
}
return flow;
}
void Dinic()
{
while(bfs())
while(find(S,(int)1e9));
}
bool mycmp(int x,int y)
{
return co[x]<co[y];
}
int main()
{
freopen("grid.in","r",stdin);
freopen("grid.out","w",stdout);
scanf("%d%d",&n,&m);
T=2*n+1;
memset(head,-1,sizeof head);
for(int i=1;i<=n;i++)
{
add(S,i,1);
add(i,S,0);
for(int j=1;j<=m;j++)
{
int x,c;
scanf("%d",&x);
c=(x-1)/m+1;
co[x]=c;
a[i][c].push(x);
add(i,n+c,1);
add(n+c,i,0);
}
}
for(int i=1;i<=n;i++)
{
add(n+i,T,1);
add(T,n+i,0);
}
for(int i=1;i<=m;i++)
{
Dinic();
for(int j=0;j<=tot;j+=2)
{
if(edge[j^1].to==S)
{
edge[j].v=1;
edge[j^1].v=0;
}
else if(edge[j].to>n&&edge[j].to<=2*n)
{
if(edge[j].v==0&&edge[j^1].v==1)
{
b[edge[j^1].to][i]=a[edge[j^1].to][edge[j].to-n].front();
a[edge[j^1].to][edge[j].to-n].pop();
edge[j^1].v=0;
}
}
else
{
edge[j].v=1;
edge[j^1].v=0;
}
}
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
printf("%d ",b[i][j]);
puts("");
}
for(int i=1;i<=m;i++)
{
vector<int> s;
for(int j=1;j<=n;j++)
s.push_back(b[j][i]);
sort(s.begin(),s.end(),mycmp);
for(int j=1;j<=n;j++)
b[j][i]=s[j-1];
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
printf("%d ",b[i][j]);
puts("");
}
return 0;
}
I. [CERC2016]二分毯 Bipartite Blanket
根据广义
H
a
l
l
Hall
Hall 定理,我们只需要分别计算
A
,
B
A,B
A,B 中有多少个点集可以被覆盖,最后合并统计答案即可。
发现
n
n
n 很小,那么我们可以对每个子集分别计算,对于一个子集可以被覆盖,需要本身满足
∣
S
∣
≤
∣
⋃
v
∈
S
N
(
v
)
∣
|S|\le|\bigcup\limits_{v\in S}N(v)|
∣S∣≤∣v∈S⋃N(v)∣,同时子集也需要满足,我们可以先算出本身是否满足,然后高维前缀和即可。
算出
A
,
B
A,B
A,B 分别有哪几个集合可以被覆盖(记得算上空集),排序之后双指针统计答案即可。
#include<bits/stdc++.h>
using namespace std;
int n,m,id[30],f[1100000],v[30],w[30],g[1100000],num[1100000],t;
long long ans;
bool h[1100000];
char s[30][30];
vector<int> a,b;
inline int lowbit(int x)
{
return x&(-x);
}
int main()
{
freopen("blanket.in","r",stdin);
freopen("blanket.out","w",stdout);
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
scanf("%s",s[i]+1);
for(int i=1;i<=n;i++)
scanf("%d",&v[i]);
for(int i=1;i<=m;i++)
scanf("%d",&w[i]);
for(int i=1;i<=m;i++)
{
int now=0;
for(int j=1;j<=n;j++)
{
if(s[j][i]=='0')
now^=(1<<(j-1));
}
f[now]++;
}
for(int i=0;i<n;i++)
{
for(int j=(1<<n)-1;~j;j--)
if((j>>i)&1)
f[j^(1<<i)]+=f[j];
}
for(int i=1;i<(1<<n);i++)
{
g[i]=g[i&(i-1)]+v[(int)log2(lowbit(i))+1];
if(m-f[i]>=__builtin_popcount(i))
h[i]=1;
}
h[0]=1;
for(int i=0;i<n;i++)
{
for(int j=0;j<(1<<n);j++)
{
if((j>>i)&1)
h[j]&=h[j^(1<<i)];
}
}
for(int i=0;i<(1<<n);i++)
{
if(h[i])
a.push_back(g[i]);
}
memset(f,0,sizeof f);
for(int i=1;i<=n;i++)
{
int now=0;
for(int j=1;j<=m;j++)
{
if(s[i][j]=='0')
now^=(1<<(j-1));
}
f[now]++;
}
for(int i=0;i<m;i++)
{
for(int j=(1<<m)-1;~j;j--)
if((j>>i)&1)
f[j^(1<<i)]+=f[j];
}
memset(h,0,sizeof h);
for(int i=1;i<(1<<m);i++)
{
g[i]=g[i&(i-1)]+w[(int)log2(lowbit(i))+1];
if(n-f[i]>=__builtin_popcount(i))
h[i]=1;
}
h[0]=1;
for(int i=0;i<m;i++)
{
for(int j=0;j<(1<<m);j++)
{
if((j>>i)&1)
h[j]&=h[j^(1<<i)];
}
}
for(int i=0;i<(1<<m);i++)
{
if(h[i])
b.push_back(g[i]);
}
scanf("%d",&t);
sort(a.begin(),a.end());
sort(b.begin(),b.end());
int l=a.size();
for(int i=0;i<b.size();i++)
{
while(l>0&&a[l-1]+b[i]>=t)
l--;
ans+=a.size()-l;
}
printf("%lld",ans);
return 0;
}