您的位置:首页 > 理论基础 > 计算机网络

网络流+二分图总结

2015-07-20 22:08 531 查看
一. Dinic跑最大流

1.模板(上限V^2*E)

//先init将放边的数组清空,再将[a, b]的g边清空,加边的时候正向边为0, 反向边为1;

//MAXN为顶点个数,每个边的flow为此边跑了多少流.正向边为正数,而反向边为负数,但只看差值即可.

//复杂度V^2E;

const int MAXN = 5000 + 10;

const int INF = 1000000000;

struct Edge

{

int from, to, cap, flow;

};

struct Dinic

{

int s, t;

vector edges; // 边数的两倍

vector G[MAXN]; // 邻接表,G[i][j]表示结点i的第j条边在e数组中的序号

bool vis[MAXN]; // BFS使用

int d[MAXN]; // 从起点到i的距离

int cur[MAXN]; // 当前弧指针

void init()
{
edges.clear();
}

void clearNodes(int a, int b)
{
for(int i = a; i <= b; i++) G[i].clear();
}

void AddEdge(int from, int to, int cap)
{
edges.push_back((Edge)
{
from, to, cap, 0
});
edges.push_back((Edge)
{
to, from, 0, 0
});
int m = edges.size();
G[from].push_back(m-2);
G[to].push_back(m-1);
}

bool BFS()
{
memset(vis, 0, sizeof(vis));
queue<int> Q;
Q.push(s);
vis[s] = 1;
d[s] = 0;
while(!Q.empty())
{
int x = Q.front();
Q.pop();
for(int i = 0; i < G[x].size(); i++)
{
Edge& e = edges[G[x][i]];
if(!vis[e.to] && e.cap > e.flow)
{
vis[e.to] = 1;
d[e.to] = d[x] + 1;
Q.push(e.to);
}
}
}
return vis[t];
}

int DFS(int x, int a)
{
if(x == t || a == 0) return a;
int flow = 0, f;
for(int& i = cur[x]; i < G[x].size(); i++)
{
Edge& e = edges[G[x][i]];
if(d[x] + 1 == d[e.to] && (f = DFS(e.to, min(a, e.cap-e.flow))) > 0)
{
e.flow += f;
edges[G[x][i]^1].flow -= f;
flow += f;
a -= f;
if(a == 0) break;
}
}
return flow;
}

// 求s-t最大流。如果最大流超过limit,则只找一个流量为limit的流
int Maxflow(int s, int t, int limit)
{
this->s = s;
this->t = t;
int flow = 0;
while(BFS())
{
memset(cur, 0, sizeof(cur));
flow += DFS(s, limit - flow);
if(flow == limit) break; // 达到流量限制,直接退出
}
return flow;
}


};

Dinic g;

2.最小割=最大流. 然后最小割:最大权闭合图, 以及:对于有些顶点不能共存,比如矩阵上二分染色,相邻的不能共处,加一些不能存在的边跑最大独立集以及带权最大独立集.

3. 对于无向图可以构造两条边,dinic不担心本身有环.

4. 如果点有最大流限制,拆点加限制.并且定义好出入.如果点有最小流限制e,最大限制c,把点u拆成ua(负责接收 入) ub(负责放出 出). 在原本的s和t的基础上再搞S和T,然后,S连ub,流e, ua连T,流e. 超级S和T接收总流量就行.(因为有新的附加,在S-s,t-T跑之前,先跑一遍S和T,看有没有达到e和,防止附加值下限不对).

5. 增加流和推流,增加流找到相应的增加就行.但是推流:u->v,如果cap>flow,那么直接cap–,如果cap==flow,那么从v遍历到t,找flow>0的路,退一个流.从s找到u,一样.然后最大流–;其实也可以不用退:如果u->v找到一个增广路,就不用退,跑一个1流就行.

6. 对时间的拆点和二分思路:图中有很多个门,每个门一秒只能出去一个人.然后问图中的人均出去的最短时间.到门的分配问题,涉及到最短时间.二分枚举最短时间,然后判定.一个门一秒只能流一个人,门的t拆点.

最大权闭合图.可能需要自己先构造虚拟点,比如太空做实验的题目.正值的点放在s连,并且求一个sum,负值的点放在t,改为正值加边.然后中间有关系的不能断的搞INF.跑一遍最大流.

最小割可以用来求将图割成两块的边权.随便搞一个点最为s,枚举另一个点,求一下割.所有的最小值就是最小值.但是这样复杂度太高.可以用Stoer-Wagner算法.

最大稠密子图.在一个无向图中,找出一部分图:使得边的个数/点的个数 最大. 首先变成分数规划,g = max(e/v). 然后h(g) = max(e – gv), 是一个单调减.且答案是让h(g)=0,的g. 因此二分枚举g,然后搞出max(e-gv), 转化一下:ans = -min(g – (dv/2 – C(v,v’))). 然后ans = -min(g-dv/2 + C(v,v’)), 括号内的构图: S连每个点权值为U(很大的数,这里为它的度);相连的点关系不变,权值为1;每个点连T,权值为U+2*g-dv. 然后ans = U*n – dinic;比如下面的代码:

include

include

include

include

include

include

include

include

include

include

define LL long long

define _LL __int64

define eps 1e-8

using namespace std;

const int INF = 0x3f3f3f3f;

const int maxn = 110;

const int maxm = 1010;

struct node

{

int u,v;

double w;

int next,rev;

}p[maxm],edge[maxm*6];

int s,t,n,m,nn;

int cnt,head[maxn];

int d[maxn];

int dist[maxn],vis[maxn];

int ans;

void init()

{

cnt = 0;

memset(head,-1,sizeof(head));

}

void add(int u, int v, double w)

{

edge[cnt] = (struct node){u,v,w,head[u],cnt+1};

head[u] = cnt++;

edge[cnt] = (struct node){v,u,0,head[v],cnt-1};
head[v] = cnt++;


}

void build(double mid)

{

init();

for(int i = 1; i <= m; i++)

{

add(p[i].u,p[i].v,1.0);

add(p[i].v,p[i].u,1.0);

}

for(int i = 1; i <= nn; i++)

{

add(s,i,m);

add(i,t,m*1.0+2*mid-d[i]*1.0);

}

}

bool bfs()

{

queue que;

memset(dist, 0, sizeof(dist));

memset(vis, 0, sizeof(vis));

while(!que.empty()) que.pop();

vis[s] = 1;

que.push(s);

while(!que.empty())

{

int u = que.front();

que.pop();

for(int i = head[u]; i != -1; i = edge[i].next)

{

int v = edge[i].v;

if(edge[i].w && !vis[v])

{

que.push(v);

vis[v] = 1;

dist[v] = dist[u]+1;

}

}

}

if(dist[t] == 0)

return false;

return true;

}

double dfs(int u, double delta)

{

if(u == t) return delta;

double ret = 0,tmp;

for(int i = head[u]; i != -1; i = edge[i].next)
{
int v = edge[i].v;
if(edge[i].w && dist[edge[i].v] == dist[u]+1 && (tmp = dfs(v,min(delta,edge[i].w))))
{
edge[i].w -= tmp;
edge[edge[i].rev].w += tmp;
return tmp;
}
}
if(!ret) dist[u] = -1;
return ret;


}

double Dinic()

{

double ans = 0,res;

while(bfs())

{

while(res = dfs(s,INF))

ans += res;

}

return ans;

}

void dfs_cut(int u)

{

vis[u] = 1;

if(u <= nn) ans++;

for(int i = head[u]; i != -1; i = edge[i].next)

{

int v = edge[i].v;

if(edge[i].w > 0 && !vis[v])

{

dfs_cut(v);

}

}

}

int main()

{

while(~scanf(“%d %d”,&n,&m))

{

if(m == 0)

{

printf(“1\n1\n”);

continue;

}

memset(d,0,sizeof(d));

for(int i = 1; i <= m; i++)

{

scanf(“%d %d”,&p[i].u,&p[i].v);

d[p[i].u]++;

d[p[i].v]++;

}

s = n+1;
t = n+2;
nn = n;
n = t;

double low,high,mid,h;
low = 0.0;
high = m;
while(high - low > 1.0/nn/nn)  //其实随便定义一个精度就好
{
mid = (high + low)/2;
build(mid);  箭头建图
h = (m*nn*1.0 - Dinic() )/2;  //m就是U,/2是因为人为扩充了.

if(h > eps)  //单调递减
low = mid;
else high = mid;
}

build(low);  //再跑一次,要构造答案.
Dinic();

ans = 0;
memset(vis,0,sizeof(vis));

dfs_cut(s);

printf("%d\n",ans);
for(int i = 1; i <= nn; i++)
{
if(vis[i])
printf("%d\n",i);
}
}
return 0;


}

有边权:求的是子图的边权和除以点数最大。此时U为所有边权的和,中间相连的点权值为we,连T权值为U+2*g-dw(dw为所有连v的边权和)

有点权和边权:求的是子图的边权和和点权和除以点数最大。此时U为所有边权和加二倍点权的和,中间相连的点权值为we,连T的权值为U+2*(g-pv)-dw

二. 二分图匹配

1. 模板(不一定有dinic快的模板……上限VE)

///max_v, V, get_G, 0 ~ V - 1;

//v*E;

const int MAXN = 2e4 + 100;

const int INF = INT_MAX/2 - 1;

vector G[MAXN];

int V;

int m, n;

int match[MAXN], used[MAXN];

void add_edge(int from, int to)

{

G[from].push_back(to);

G[to].push_back(from);

}

int dfs(int v)

{

used[v] = 1;

for(int i = 0; i < G[v].size(); i++)

{

int u = G[v][i], w = match[u];

if(w < 0 || (!used[w] && dfs(w)))

{

match[u] = v;

match[v] = u;

return 1;

}

}

return 0;

}

int b_match()

{

int res = 0;

fill(match, match + V, -1);

for(int i = 0; i < V; i++)//其实到m就行了.

{

if(match[i] < 0)

{

CLR(used);

if(dfs(i))

{

res++;

}

}

}

return res;

}

/*

//可以找出是那些点覆盖了边.

//方法:x集合中未匹配的大胆表used, 然后与之相连的y集合中的要标成used,之后只有与y相匹配的x中的点才敢标成used,再深搜次x,最终x集合中未标记的和y集合中标记//的选出来.

*/

void find_dfs(int v)

{

used[v] = 1;

REP(i, 0, G[v].size())

{

if(!used[G[v][i]])

{

used[G[v][i]] = 1;

int new_v = match[G[v][i]];

if(new_v != -1)

{

find_dfs(new_v);

}

}

}

}

void find_who()

{

CLR(used);

REP(i, 0, n)

{

if(match[i] == -1 && !used[i])

{

find_dfs(i);

}

}

REP(i, 0, n)

{

if(!used[i])//不是多余的

{

printf(” r%d”, i + 1);

}

}

REP(i, n, n + m)

{

if(used[i])//右边集合要有的.

{

printf(” c%d”, i - n + 1);

}

}

printf(“\n”);

}

2. KM算法跑边带权匹配(会将x节点找到匹配,y节点可以多 n^3)

//nx, ny, w[i][j] 有就写,没有就弄一个特别大的,使得结果可以判定.

//可能要改成double的:w, lx, ly, slack, t, t与0的比较, d;

const int maxn = 200;

const int INF = 1e6;

int w[maxn][maxn];

int lx[maxn], ly[maxn];

int linky[maxn];

int visx[maxn], visy[maxn];

int slack[maxn];

int nx, ny;

int find(int x)

{

visx[x] = 1;

for(int y = 0;y < ny;y++)

{

if(visy[y])

continue;

int t = lx[x] + ly[y] - w[x][y];

if(t == 0)

{

visy[y] = 1;

if(linky[y] == -1 || find(linky[y]))

{

linky[y] = x;

return 1;

}

}

else if(slack[y] > t)

{

slack[y] = t;

}

}

return 0;

}

int KM()

{

memset(linky, -1, sizeof(linky));

CLR(ly);

for(int i = 0;i < nx;i++)

{

lx[i] = -INF;

for(int j = 0;j < ny;j++)

{

if(w[i][j] > lx[i])

{

lx[i] = w[i][j];

}

}

}

for(int x = 0;x < nx;x++)

{

for(int i = 0;i < ny;i++)

slack[i] = INF;

while(1)

{

CLR(visx);

CLR(visy);

if(find(x))

{

break;

}

int d = INF;

for(int i = 0;i < ny;i++)

{

if(!visy[i] && d > slack[i])

d = slack[i];

}

for(int i = 0;i < nx;i++)

{

if(visx[i])

lx[i] -= d;

}

for(int i = 0;i < ny;i++)

{

if(visy[i])

{

ly[i] += d;

}

else

{

slack[i] -= d;

}

}

}

}

int res = 0;

for(int i = 0;i < ny;i++)

{

if(linky[i] > -1)

{

res += w[linky[i]][i];

}

}
return res;


}

最小边覆盖点 = V-匹配. 边带权值的话:如果是二分图:KM算法. 或者是用网络流,左边要建2*n个点,右边2*m个点,因为每一个点并不是流为1,但是我们要统计流为1,这样总和才对.

最小顶点覆盖边. 二分图就等于最大匹配数. 带权的点:用最小割来解决.u->v,那么割u或者割v.和最大权独立集构造方法一样(本身就是互反的).

最大独立集.应用很广泛,不能有边的最大点的个数.总的点数-最大匹配数. 如果带权:经典的构造出来二分图,然后总和减去割.

最大独立集的思维转换.有男生和女生,有的认识.选择一个最大的集合,使得这里面的男生和女生都认识.反过来看,就是不能出现两个不认识的人.那么把不认识的人建边,找最大独立集. 逆向思维.

矩阵网格,格子里有东西,然后行和列什么的,以行和列分成二分图,格子的东西为边.

矩阵网格还经常二分图染色.比如相邻的不能存在,那么就是二分图独立集.相邻的1*2的一块涨分,那么染色后分供应的和被动接受的.

对时间的拆点和二分思路:图中有很多个门,每个门一秒只能出去一个人.然后问图中的人均出去的最短时间.到门的分配问题,涉及到最短时间.二分枚举最短时间,然后判定.一个门一秒只能流一个人,门的t拆点.

牛必须吃认定的饮料和蔬菜.每个饮料和蔬菜都有限制.问同时满足的牛个数:两个一起供应到的问题,不能用匹配.两个刚好可以饮料+牛拆a+牛拆b+蔬菜来建图.

一个原件i在A加工和在B加工代价不一样.一共n个,ij可能有连接关系,不在一块有额外的费用. 用最小割来描述很合适:sB iA t,然后如果i和j有联系,在i和j之间连一条边w. 注意,最大流都是有向流,对于i和j的边应该建两条.

n玩具在工厂里制作,有玩具需要等,求玩具等待的总时间.只有一个工厂时,是贪心,有很多工厂时,考虑每一个玩具对答案的贡献,发现对每一个玩具弄一个这样的匹配:对每一个工厂建立n个点,表示倒数第几个在工厂里将,一个点用一次,边为x*cost,然后求最小匹配.

一堆x,一堆y,给出可能配对的信息,问为了保证配对最多.一定能够确定的对数是多少.其实就是看边是不是关建边.先跑一遍二分图匹配,然后扫x集合,找到对应的y,把x和y的match都清除,边的关系也暂时清除,直接dfs(x)找增广路,能找到,说明不是关键边,之后把边补上去就行,不用还原match,如果找不到,那么要还原match,并且补上边,而且是关建边.

注意从一般图中看出来是二分图.二分图:如果分成x和y集合,在x或者y集合内部不可能存在边的就是二分图.就是说x和y有关系,y和z有关系,那么z一定和x是一个集合,并且z和x一定不能发生关系.

矩阵上的消除某些格子,把某一个格子当成边,两端的顶点往行和列上靠然后往最小点覆盖边上靠.比如:矩阵上有草地和泥,现在用1*x的木板来盖泥,不能盖草地,那么最少需要多少块木板.想象泥是一条边,那么用木板来盖它,行的连续的木板放在x集合,列的连续的木板放在Y集合.这样每一个泥坑都连一条边,之后求最小点覆盖边.

一个人可以往x走,也可以往y走,但只能选一个地方的感觉.一共有l个地方,每两个地方都有时间消耗,要求ti时刻有人在i点,至少需要几个人.一个人的话,从i走到j和k都可能,到底走到哪?其实在整个图上看是走到哪可以连接最多的边.每连一条边就能够减小1个人.所以拆点成二分图然后匹配.

最小k路径覆盖.每次只能走简单路,最少多少条能够覆盖.每个点拆成两个点,一个出,一个接收(入).然后每匹配一个相当于减少一次.魔术球问题:反着问,k条路可以走多少个点.二分枚举点,看<=k否. 于是二分枚举可以做,暴力枚举然后跑残余的二分图也可以做.其实这个二分图也可以建最大流.s向左边的每一个点(出)都搞一个1的流,右边(入)都向t搞一个1的流,然后左边可以留到右边的搞一个INF的流(因为路径可以重复).然后跑最大流.

搞多匹配问题:供应多个,接收接收多个.然后最大流是不是满足.加上费用就是花费最少了.

多线程过程中一个数的使用唯一性限定.最长递增子序列,一个数只能用一次.有几个同时最长的序列.一个数用一次,可以拆点限定.然后需要描述:走通一条流就是一个递增子序列.那么先跑n^2的f[i],只有i到j连边只有i结尾的通过上上了j然后f[j]=f[i]+1.并且s只和f[i]=1的连,f[i]=max_len的和t连.保证一条路一定是对的. 注意:如果允许某些点同时用,不能简单的放拆点的是INF.因为还涉及到和s和t的关系,到底放不成INF?

最小费用流

模板(spfa跑, F*(kn))

//get_G; V; const int MAXN = 1000 + 10;

const int INF = INT_MAX/2 - 1;

struct edge

{

int to, cap, cost, rev;

edge(int a = 0, int b = 0, int c = 0, int d = 0)

{

to = a;

cap = b;

cost = c;

rev = d;

}

};

vector G[MAXN];

int V;

void add_edge(int from, int to, int cap, int cost)

{

G[from].push_back(edge(to, cap, cost, G[to].size()));

G[to].push_back(edge(from, 0, -cost, G[from].size() - 1));

}

int dist[MAXN], inq[MAXN];

int prev_v[MAXN], prev_i[MAXN];

int spfa(int s, int t)

{

queue myq;

for(int i = 0;i < V;i++)

{

dist[i] = INF;

}

CLR(inq);

dist[s] = 0;

inq[s] = 1;

myq.push(s);

while(!myq.empty())

{

int v = myq.front();inq[v] = 0; myq.pop();

for(unsigned int i = 0;i < G[v].size();i++)

{

edge &e = G[v][i];

if(e.cap > 0 && dist[v] + e.cost < dist[e.to])

{

dist[e.to] = dist[v] + e.cost;

prev_v[e.to] = v;

prev_i[e.to] = i;

if(inq[e.to] == 0)

{

inq[e.to] = 1;

myq.push(e.to);

}

}

}

}

if(dist[t] == INF)

{

return 0;

}

else

{

return 1;

}

}

int min_cost_flow(int s, int t, int f)

{

int res = 0;

while(f > 0)

{

if(spfa(s, t) == 0)

{

return -1;

}

else

{

int d = f;

for(int v = t;v != s;v = prev_v[v])

{

d = min(d, G[prev_v[v]][prev_i[v]].cap);

}

f -= d;

res += d*dist[t];

for(int v = t;v != s;v = prev_v[v])

{

edge &e = G[prev_v[v]][prev_i[v]];

e.cap -= d;

G[e.to][e.rev].cap += d;

}

}

}

return res;

}

这个不能跑环.因为一旦有环一定会出现负环.所以要先分出来二分图再跑费用流.

对于给出的一个已经跑出来最小费用的图,如何判断是在f流下的真正最小费用?就是在残余图上跑cap>0(有效边),然后看有没有负环(spfa跑不出来).于是有一种查负环方法:用q_time记数,一旦超了,那么u返回.然后用pre_v一直找到一个真正的环.查出来之后,绕着环跑一个流就好.

一般只能跑有向图.如果有向图中边有最少流量要求.那么可以这样做:u->v,最少e,最大f,u到v建 cost-M(足够大) 一共e条流,然后f-e条cost的正常流. 最后的花费+e*M就好.

如果有向图中的花费有负边,那么本来是不能搞的,但是通过下面转换:如果最大流F一定,那么新建S和T,超级,往s和t流F,然后对于u->v,流量f,cost<0的情况,S先连到v(f,0),u先连到T(f,0),之后从v向u连-cost,(-cost是正值).最终结果在上cost(负值)*f.

多线程问题.从s走到t,再走回来,不能重复.一下跑流量为2的流. 还有最大K区间覆盖问题.分别取尝试覆盖k次,并且用到的区间边不能重复,因此跑一个k的流.特别的,对于k区间覆盖,两种方法:如果离散的点,区间作为边来跑. 或者:每个区间当做点,拆两个点,然后排序后给能够在后面取的均连一条边.

最大k覆盖的一个应用.题目:一共有10个桶,有n~1e4个球,编号ai,有一个k~1e4的序列,每次放相应编号的球,如果已经有了,不用放,没有的话挑一个免费挤跑,然后放进去,放的时候需要花wi的钱.问放的费用最小.一共有10个桶,意思就是同时不能有大于10的流出现,假设每个球都花钱,看哪些能省,相同的连一个区间,可以省,这些区间中跑最大10覆盖.就是可以省的最大权值.注意,i和i+1用的球是一个的时候,并不会占用一个桶也可以省,因此在最初的时候要消去这种情况.哈哈哈自己想到的.

如果不是流量*边费用,而是边费用的和,可能是最小树形图

纸巾.就是i天的可能给j天用.i天有要求流到t ai,但是i天同时又给其他天提供了ai的流,怎么办?拆点,右端的搞接收,然后传给t,左端的给出,并且已知左端一定会给出总的是ai的流(而且是免费获得).注意i->i+1建有向边减少总的边数.

费用流按照时间来拆点分层.注意t->t+1的无条件联通.还可能按照剩余的油值来分层,可能直接跑最短路就成了.

费用流解决多线程问题,通过拆点限制流可以限制点的使用次数.

如果将原DAG权值取反,然后从最后一关连一条正权边到第一关,权值是最短路(负权值最短路=传统意义上的最长路)的长度的话,那么那些正圈中的负权边就是应该增加权值的边,具体应该加多少,就是正圈的权值。

再看怎么用网络流计算这些正圈权值,新建源点汇点,对于所有顶点,如果入度>出度,从源点连一条边到它,否则,从它连一条边到汇点,容量都是是度数差。

还没弄懂: http://www.hankcs.com/program/algorithm/aoj-2230-how-to-create-a-good-game.html
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: