【ACM回顾】网络流 & 二分图匹配
杂
写二分的一点小感悟
关键在于下一个区间的确定。
由当前的l,r怎么推出下一阶段的l,r。mid是否要包含在新区间里,是写二分的关键
STL中自带的
lower_bound查到的是第一个【大于等于所查值】的位置
upper_bound是查到第一个【大于所查值】的位置
网络流
//Dinic 模板
struct max_flow{
int st, ed;
int lev[N];
struct edge{
int u,v,c;
}E[N<<2];int edge_size;
vector <int> G[N];
void clear(int size)
{
edge_size = 0;
for(int i=0;i<=size;i++) G[i].clear();
}
void addedge(int u, int v, int c)
{
// printf("%d to %d with %d\n",u,v,c);
E[edge_size++] = (edge){u, v, c};
E[edge_size++] = (edge){v, u, 0};
G[u].push_back(edge_size-2);
G[v].push_back(edge_size-1);
}
int bfs()
{
memset(lev, -1, sizeof lev);
queue <int> q; while(!q.empty()) q.pop();
q.push(st); lev[st] = 0;
while(!q.empty())
{
int p=q.front(); q.pop();
int vsize = G[p].size();
for(int i=0;i<vsize;i++)
{
edge e = E[G[p][i]];
if(e.c > 0 && lev[e.v] == -1)
{
lev[e.v] = lev[p] + 1;
if(e.v == ed) return 1;
q.push(e.v);
}
}
}
return (lev[ed] != -1);
}
int dfs(int p, int delta)
{
if(p == ed || delta == 0) return delta;
int flow = 0, ret;
int vsize = G[p].size();
for(int i=0;i<vsize && delta;i++)
{
edge &e = E[G[p][i]];
edge &e_ = E[G[p][i]^1];
if(lev[e.v] == lev[p] + 1 && e.c && (ret = dfs(e.v, min(e.c, delta))))
{
e.c -= ret;
e_.c += ret;
delta -= ret;
flow += ret;
}
}
return flow;
}
int dinic()
{
int ret = 0;
while(bfs()) ret+=dfs(st, INF);
return ret;
}
}graph;
dfs函数的理解
模拟一下多路增广的过程就会发现单路增广的性价比太低了多路增广就是断一处残量边以后并不是回到原点重新增广而是继续“长出支路”当前弧优化的解释:dfs应该跳过已经出现在之前的搜索树里的边,和已经在队列的元素不需要二次入队差不多道理
Tip:如果要用^1的形式对应反向边,edge_size必须从0开始!
匹配问题常常可以转化为网络流模型
POJ 3281 Dining
题意就是有N头牛,F个食物,D个饮料。N头牛每头牛有一定的喜好,只喜欢几个食物和饮料。每个食物和饮料只能给一头牛。一头牛只能得到一个食物和饮料。而且一头牛必须同时获得一个食物和一个饮料才能满足。问至多有多少头牛可以获得满足。
最初想到的是二分匹配。但是明显不行,因为要分配两个东西,两个东西还要同时满足。最大流建图是把食物和饮料放在两端。一头牛拆分成两个点,两点之间的容量为1.喜欢的食物和饮料跟牛建条边,容量为1.加个源点和汇点。源点与食物、饮料和汇点的边容量都是1,表示每种食物和饮料只有一份
POJ 1087 A Plug for UNIX
题目大意为总共有n种插头和m个电器和k种无限供应的转接器,每个插头用一个字符串表示,每个电器对应一种插头,但是该插头可能并不包含在n个已有类型里,因此必须依赖转接器。问最多有多少电器能配对。
超级源点向电器建一条1边,表示每个电器只有一个。把电器向各个对应插头连一条1边所有已有插头向超级汇点连一条1边,表示每个插头最多只能用一次所有转接器表示为对应插头的一条INF边,表示转接器可以同时被多个电器使用
二分图匹配
//匈牙利算法 & 二分图判定
struct max_match{
int path[N][N];
int match[N];
int mark[N];
bool used[N];
void clear()
{
memset(path, 0, sizeof path);
memset(match, -1, sizeof match);
memset(mark, -1, sizeof mark);
}
void addedge(int u, int v)
{
path[u][v] = path[v][u] = 1;
}
bool dfs(int p)
{
for(int i=1;i<=n;i++)
if(path[p][i] && !used[i])
{
used[i] = 1;
if(match[i] == -1 || dfs(match[i]))
{
match[i] = p;
return 1;
}
}
return 0;
}
int hungarian()
{
int ret = 0;
for(int i=1;i<=n;i++)
{
memset(used, 0, sizeof used);
if(dfs(i)) ret++;
}
return ret;
}
bool check(int p, int c)
{
mark[p] = c;
for(int i=1;i<=n;i++)
if(path[p][i])
{
if(mark[i]==-1)
{
if(!check(i, c^1)) return 0;
}
else if(mark[i]==c) return 0;
}
return 1;
}
bool judge()
{
for(int i=1;i<=n;i++)
{
if(mark[i] == -1)
{
if(!check(i, 0)) return 0;
}
}return 1;
}
}graph;
(定理1)最小点集覆盖 = 最大匹配
简单证明:
要覆盖最大匹配的匹配边,至少要有n个点覆盖,最小点集≥最大匹配
对于任何一个最小点集覆盖,对每一个点一定都能找到【专属于它的边】(否则就不是最小覆盖,这个点多余),取这些边为匹配边,每个点一条,最小点集≥最大匹配
(定理2)最小边覆盖 = 节点数 - 最大匹配 = 最大独立集
简单证明:
首先,把最大匹配数对应成最小覆盖点集
首先,除了最小覆盖点集以外所有点构成的点集,显然是一个独立集
如果还要更大,则必须把最小覆盖点纳入独立集,但为了保持独立显然要把和覆盖点相邻的非覆盖点踢出独立集(至少一个),显然亏了。因此,最小覆盖点集的补集就是最大独立集。
DAG最小不相交路径覆盖
定义:用最少的不相交路径覆盖所有顶点。
算法:
把原图中的每个点V拆成Vx和Vy,如果有一条有向边A->B,那么就加边Ax-By。这样就得到了一个二分图,最小路径覆盖=原图的节点数-新图最大匹配。
简单证明:
一开始每个点都独立的为一条路径,总共有n条不相交路径。我们每次在二分图里加一条边就相当于把两条路径合成了一条路径,因为路径之间不能有公共点,所以加的边之间也不能有公共点,这就是匹配的定义。所以有:最小路径覆盖=原图的节点数-新图最大匹配。
DAG最小可相交路径覆盖
定义:用最小的可相交路径覆盖所有顶点。
算法:
先用floyd求出原图的传递闭包,即如果a到b有路,那么就加边a->b。
然后就转化成了最小不相交路径覆盖问题。
二分图判定(交叉染色法)
算法:就是从一个为染色点开始交叉染色,如果出现矛盾,则为非二分图
简单证明:染色实际上就是在分组,如果出现分组矛盾,则显然非二分图
匈牙利算法
对于每一个点做如下操作
i) 如果当前边可以匹配,直接匹配
ii) 如果不能匹配,寻找交替路,如果出现两头为非匹配边的交替路,则为增广路,匹配当前点,增广