其实就是把博主记不住的概念明确一下
二分图
图上的点可以完美的被分成两部分,使得每一个部分的点中互相没有连边
或者说:可以黑白染色、没有奇环
判定
直接 \(dfs\) 染色
\(Code\)
inline void dfs1(int x)
{
for(int i=head[x];i;i=e[i].nxt)
{
int t=e[i].to;
if(col[t]==-1) col[t]=1-col[x],dfs1(t);
}return ;
}
这里的 \(col[\space]\) 就是表示每个点所属于的点集标号
我第一次写的是按照 \(0,1\) 这样走的,还可以有 \(1,2\) (就是在 \(col\) 计算的时候拿 \(3\) 减呗)
最大匹配
匹配:二分图的一个边集,使得边集中的所有边没有公共端点
最大匹配:边集中 \(size\) 最大的一个
匹配边:顾名思义,在匹配的边集中的边
增广路(交错路):一个从连接左部非匹配点和右部非匹配点的路径
使得匹配边和非匹配边交替出现,长度应为奇数
正是增广路的这个定义,使得我们可以在找到一条增广路的时候把路径上的所有的边取反,然后边集大小会 \(+1\)
求法:(两种)
1.网络最大流:\(Dinic\)
这种做法就是偷懒不想学二分图的人做的(博主一开始想直接搞网络流不学二分图来着)
代码放的是 \(LGOJ2055\) 假期的宿舍
#include<bits/stdc++.h>
using namespace std;
#define int long long
namespace yspm{
inline int read()
{
int res=0,f=1; char k;
while(!isdigit(k=getchar())) if(k=='-') f=-1;
while(isdigit(k)) res=res*10+k-'0',k=getchar();
return res*f;
}
const int N=1010;
struct node{
int nxt,to,lim;
}e[N<<1];
int head[N],cnt=1,dep[N],n,S,T,inf=1e15+10;
inline void add2(int u,int v,int w)
{
e[++cnt].lim=w; e[cnt].nxt=head[u]; e[cnt].to=v;
return head[u]=cnt,void();
}
inline void add1(int u,int v,int w)
{
add2(u,v,w); add2(v,u,0);
return ;
}
inline bool bfs()
{
queue<int> q; q.push(S); memset(dep,-1,sizeof(dep)); dep[S]=0;
while(!q.empty())
{
int fr=q.front(); q.pop();
for(int i=head[fr];i;i=e[i].nxt)
{
if(e[i].lim<=0) continue;
int t=e[i].to;
if(dep[t]!=-1) continue;
dep[t]=dep[fr]+1; q.push(t);
}
}
return dep[T]!=-1;
}
inline int dfs(int now,int in)
{
if(now==T) return in; int out=0;
for(int i=head[now];i;i=e[i].nxt)
{
if(e[i].lim<=0) continue;
int t=e[i].to;
if(dep[t]!=dep[now]+1) continue;
int res=dfs(t,min(e[i].lim,in));
e[i].lim-=res; e[i^1].lim+=res;
in-=res; out+=res;
}
if(!out) dep[now]=0;
return out;
}
inline void prework()
{
memset(head,0,sizeof(head)); memset(e,0,sizeof(e)); cnt=1;
return ;
}
int st[N],at[N];
inline void work()
{
prework(); int num=0;
n=read(); S=n<<1|1; T=S+1;
for(int i=1;i<=n;++i)
{
st[i]=read();
if(st[i]) add1(i+n,T,1);
}
for(int i=1;i<=n;++i) at[i]=read();
for(int i=1;i<=n;++i)
{
if(!st[i]||(st[i]&&!at[i])) add1(S,i,1),num++;
}
for(int i=1;i<=n;++i)
{
for(int j=1;j<=n;++j)
{
int fl=read();
if(i==j||fl) add1(i,j+n,1);
}
}
int ans=0;
while(bfs()) ans+=dfs(S,inf);
puts(num<=ans?"^_^":"T_T");
return ;
}
signed main()
{
int T=read(); while(T--) work();
return 0;
}
}
signed main(){return yspm::main();}
2.匈牙利算法
就是每次dfs找增广路,如果找到了答案加 \(1\)
代码放的还是这个题的,大概可以考虑对比一下,匈牙利是有好处的:好背 \(+\) 短
#include<bits/stdc++.h>
using namespace std;
#define int long long
namespace yspm{
inline int read()
{
int res=0,f=1; char k;
while(!isdigit(k=getchar())) if(k=='-') f=-1;
while(isdigit(k)) res=res*10+k-'0',k=getchar();
return res*f;
}
const int N=1010;
struct node{
int to,nxt;
}e[N<<1];
int head[N],cnt,n,to[N];
inline void add(int x,int y)
{
e[++cnt].nxt=head[x]; e[cnt].to=y;
return head[x]=cnt,void();
}
inline void prework()
{
cnt=0; memset(head,0,sizeof head); memset(e,0,sizeof(e));
memset(to,-1,sizeof(to));
return ;
}
bool r[N][N],fl[N],at[N],vis[N];
inline bool dfs(int x)
{
for(int i=head[x];i;i=e[i].nxt)
{
int t=e[i].to; if(vis[t]) continue;
vis[t]=1; if(to[t]==-1||dfs(to[t])) return to[t]=x,1;
}
return 0;
}
inline void work()
{
prework(); n=read();
for(int i=1;i<=n;++i) fl[i]=read();
for(int i=1;i<=n;++i) at[i]=read();
int cnt=0; for(int i=1;i<=n;++i) if(!fl[i]) cnt++,at[i]=0; else if(!at[i]) cnt++;
for(int i=1;i<=n;++i) for(int j=1;j<=n;++j) r[i][j]=read();
for(int i=1;i<=n;++i)
{
for(int j=1;j<=n;++j)
{
if(i==j&&fl[i]) add(i,i);
else if(r[i][j]&&fl[i]) add(j,i);
}
}
int ans=0;
for(int i=1;i<=n;++i)
{
memset(vis,0,sizeof(vis));
if(!at[i]&&dfs(i)) ans++;
}
puts(ans>=cnt?"^_^":"T_T");
return ;
}
signed main()
{
int T=read(); while(T--) work();
return 0;
}
}
signed main(){return yspm::main();}
到了这里,考察上面两个板块的有一个题:SHOI2002舞会
博主比较菜,所以题目都是板子题(真的真的板子诶)
最小点覆盖
在二分图中的一个点集,使得图中所有边有端点在这个点集中(定义真的可懂)
\(König\) 定理:
二分图最小点覆盖集的大小 \(=\) 二分图最大匹配集的大小
先说构造最小点覆盖的方法:
-
求出最大匹配。
-
从右部每个未匹配点出发寻找增广路(一定失败,要不然就不是最大匹配了),标记访问过的节点。
-
取左部标记点、右部未标记点,构成一组最小覆盖。
画画图就可以理解了
然后是证明:
经过上述构造方法后
右部未匹配点一定是标记点——因为它们是出发点
左部未匹配点一定是未标记点——如果被标记则找到了增广路(两边都是未匹配点诶)
一对匹配点都被标记或者都未标记——因为左部匹配点只能通过右部到达
取左部标记点、右部未标记点,恰好使得每对匹配点被取走一个
所以数量值相等
匹配边一定被覆盖——每对匹配点取走一个
不存在连接两个未匹配点的边——否则出现仅包含1条边的增广路
连接左部匹配点和右部未匹配点的边——后者是出发点,前者一定被标记
连接左部未匹配点和右部匹配点的边——后者未被标记,否则存在增广路
所以所有的边都被覆盖了,大功告成
例题:UVA1194 - Machine Schedule
还是个板子题,读懂题意就可以了
#include<bits/stdc++.h>
using namespace std;
#define int long long
namespace yspm{
inline int read()
{
int res=0,f=1; char k;
while(!isdigit(k=getchar())) if(k=='-') f=-1;
while(isdigit(k)) res=res*10+k-'0',k=getchar();
return res*f;
}
const int N=1e5+10;
struct node{
int to,nxt;
}e[N<<1];
int head[N],cnt,n,m,k;
inline void add(int u,int v)
{
e[++cnt].nxt=head[u]; e[cnt].to=v;
return head[u]=cnt,void();
}
int to[N]; bool vis[N];
inline bool dfs(int x)
{
for(int i=head[x];i;i=e[i].nxt)
{
int t=e[i].to; if(vis[t]) continue;
vis[t]=1; if(!to[t]||dfs(to[t])) return to[t]=x,1;
}return 0;
}
int ans=0;
inline void prework()
{
memset(head,0,sizeof(head));
memset(e,0,sizeof(e));
memset(to,0,sizeof(to));
ans=0; cnt=0;
return ;
}
inline void work()
{
m=read(); k=read();
for(int i=1;i<=k;++i)
{
read();
int u=read(),v=read();
add(u,v);
}
for(int i=1;i<=n;++i)
{
memset(vis,0,sizeof(vis));
if(dfs(i)) ans++;
} cout<<ans<<endl;
return prework();
}
signed main()
{
while(n=read()) work();
return 0;
}
}
signed main(){return yspm::main();}
最大独立集
定义:任意两点在图中都没有边相连的点集称为图的独立集。
定理:二分图最大独立集 \(=\) 图的点数 \(-\) 二分图最大匹配
证明:选出最多的点构成独立集
\(\Leftrightarrow\) 在图中去掉最少的点,使剩下的点之间没有边。
\(\Leftrightarrow\) 用最少的点覆盖所有的边,去掉的是最小覆盖。
挺好理解的吧
例题:骑士放置
给定一个\(n \times m\) 的矩形和几个禁止放置点,求最多可以放几个骑士
首先看出来是求最大独立集不难吧
建图就是对当前点和可以被攻击的点建图就行
一个比较好的 \(trick\) :
我们发现骑士所在的位置和可以攻击到的位置的 \(id\) 是不一样的
(这里的 \(id\) 是横纵坐标之和)
这样就可以省略 \(dfs\) 染色的过程了,直接建图的时候看一下奇偶性就可以了
注意: 这里的障碍点不要写\(while(t--)\)
原因是 \(t\) 在后面统计答案的时候还要被减掉,如果减没了就完蛋了(笔者卡了半小时)
Code:
#include<bits/stdc++.h>
using namespace std;
#define int long long
namespace yspm{
inline int read()
{
int res=0,f=1; char k;
while(!isdigit(k=getchar())) if(k=='-') f=-1;
while(isdigit(k)) res=res*10+k-'0',k=getchar();
return res*f;
}
const int N=10010;
struct node{
int to,nxt;
}e[N<<6];
int n,m,head[N],cnt,to[N],k;
bool g[210][201],vis[N];
inline void add(int u,int v)
{
e[++cnt].nxt=head[u],e[cnt].to=v;
return head[u]=cnt,void();
}
inline int get(int x,int y){return m*(x-1)+y;}
inline bool dfs(int x)
{
for(int i=head[x];i;i=e[i].nxt)
{
int t=e[i].to; if(vis[t]) continue;
vis[t]=1; if(!to[t]||dfs(to[t])) return to[t]=x,1;
}
return 0;
}
inline void max_match()
{
int ans=n*m-k;
for(int i=1;i<=n;++i)
{
for(int j=1;j<=m;++j)
{
if(g[i][j]||(i+j)%2==1) continue;
else
{
memset(vis,0,sizeof(vis));
if(dfs(get(i,j)))
{
ans--;
}
}
}
}
return printf("%lld\n",ans),void();
}
int fx[8]={-1,-1,-2,-2,1,1,2,2};
int fy[8]={-2,2,-1,1,-2,2,-1,1};
inline bool in(int x,int y){return x>=1&&x<=n&&y>=1&&y<=m;}
signed main()
{
n=read(); m=read(); k=read();
for(int i=1;i<=k;++i)
{
int x=read(),y=read(); g[x][y]=1;
}
for(int i=1;i<=n;++i)
{
for(int j=1;j<=m;++j)
{
if((i+j)%2==1||g[i][j]) continue;
for(int k=0;k<8;++k)
{
int tx=i+fx[k];
int ty=j+fy[k];
if(!in(tx,ty)||g[tx][ty]) continue;
add(get(i,j),get(tx,ty));
}
}
}
max_match();
return 0;
}
}
signed main(){return yspm::main();}