网络流

一个网络 \(G=(V,E)\) 是一张有向图,图中每条有向边 \((x,y)\in E\) 都有一个给定的权值 \(c(x,y)\),称为边的容量。特别地,若 \((x,y) \notin E\),则 \(c(x,y)=0\)。图中还有两个指定的特殊节点 \(S,T \in V(S \neq T)\) 分别被称为源点和汇点

\(f(x,y)\) 是定义在节点二元组 \((x \in V,y \in V)\) 上的实数函数,且满足:

容量限制:\(f(x,y) \leq c(x,y)\)

斜对称:\(f(x,y)=-f(y,x)\)

流量守恒:\(\forall x \neq S,\ x \neq T,\ \sum_{(u,x)\ \in E} f(u,x) = \sum_{(x,v)\ \in E}f(x,v)\)

\(f\) 称为网络的流函数,对于 \((x,y) \in E\)\(f(x,y)\) 称为边的流量,\(c(x,y)-f(x,y)\) 称为边的剩余流量

\(\sum_{(S,v)\ \in E} f(S,v)\) 称为整个网络的流量(\(S\) 为源点)

最大流

Edmond—Karp算法

若一条从源点 \(S\) 到汇点 \(T\) 的路径上各条边的剩余容量都大于 \(0\),则称这条路径为一条增广路

\(EK\) 算法为用 \(bfs\) 不断寻找增广路,直到网络上不存在增广路为止

\(bfs\) 找到任意一条从 \(S\)\(T\) 的路径,记录路径上各边的剩余容量的最小值,则网络的流量就可以增加这个最小值

利用邻接表成对存储来实现 \((x,y)\) 剩余容量的减小,\((y,x)\) 剩余容量的增大

时间复杂度上界为 \(O(nm^2)\),一般可以处理 \(1e3 \sim 1e4\) 规模的网格

\(code\)

bool bfs()
{
	memset(vis,0,sizeof(vis));
	queue<int> q;
	q.push(s);
	vis[s]=true;
	res[s]=inf;
	while(!q.empty())
	{
		int x=q.front();
		q.pop();
		for(int i=head[x];i;i=e[i].nxt)
		{
			int y=e[i].to,v=e[i].v;
			if(vis[y]||!v) continue;
			res[y]=min(res[x],v);
			pre[y]=i;
			q.push(y);
			vis[y]=true;
		}
	}
	return vis[t];
}
void update()
{
	int x=t;
	while(x!=s)
	{
		int i=pre[x];
		e[i].v-=res[t];
		e[i^1].v+=res[t];
		x=e[i^1].to;
	}
	ans+=res[t];
}

......

while(bfs()) update();

Dinic算法

在任意时刻,网络中所有节点以及剩余容量大于\(0\)的边构成的子图称为残量网络

\(Dinic\)算法引入分层图的概念,\(d_x\) 表示从 \(S\)\(x\) 最少需要经过的边数,为了方便处理设 \(d_S=1\),分层图为残量网络中满足 \(d_y =d_x +1\) 的边 \((x,y)\) 构成的子图

时间复杂度上界为 \(O(n^2m)\),一般可以处理 \(1e4 \sim 1e5\) 规模的网格,求解二分图最大匹配的时间复杂度为 \(O( \sqrt nm)\)

\(Dinic\) 算法中还可以加入若干剪枝来优化

\(res\),表示当前节点的流量剩余,若 \(res \leqslant 0\),停止寻找增广路

\(cur_x\),表示当到达到\(x\)节点时,直接从 \(cur_x\) 对应的边开始遍历,实际表示上一次从 \(x\) 遍历到了哪一条边,因为在这之间的边都已经被彻底增广过了,所以可以直接跳转,称为当前弧优化

\(code\)

bool bfs()
{
    queue<int> q;
    for(int i=s;i<=t;++i) d[i]=0,cur[i]=head[i];
    d[s]=1,q.push(s);
    while(!q.empty())
    {
        int x=q.front();
        q.pop();
        for(int i=head[x];i;i=e[i].nxt)
        {
            int y=e[i].to;
            if(d[y]||!e[i].v) continue;
            d[y]=d[x]+1,q.push(y);
        }
    }
    return d[t];
}
int dfs(int x,int lim)
{
    if(x==t) return lim;
    int res=lim,flow;
    for(int &i=cur[x];i;i=e[i].nxt)
    {
        int y=e[i].to,v=e[i].v;
        if(d[y]!=d[x]+1||!v) continue;
        if(flow=dfs(y,min(v,res)))
        {
            res-=flow;
            e[i].v-=flow;
            e[i^1].v+=flow;
            if(!res) break;
        }
    }
    return lim-res;
}
int dinic()
{
    int flow,ans=0;
    while(bfs())
        while(flow=dfs(s,inf))
            ans+=flow;
    return ans;
}

若要求每条边的所用的流量,可以将原图备份,跑完最大流后,用原图的容量减去当前的剩余容量即可求得所用流量

对于容量为实数,应该这样写:

\(code:\)

bool bfs()
{
	queue<int> q;
	for(int i=s;i<=t;++i) d[i]=0,cur[i]=head[i];
	q.push(s),d[s]=1;
	while(!q.empty())
	{
		int x=q.front();
		q.pop();
		for(int i=head[x];i;i=e[i].nxt)
		{
			int y=e[i].to;
            double v=e[i].v;
			if(d[y]||fabs(v)<eps) continue;
			d[y]=d[x]+1,q.push(y);
		}
	}
	return d[t];
}
double dfs(int x,double lim)
{
	if(x==t) return lim;
	double res=lim,flow,sum=0;
	for(int &i=cur[x];i;i=e[i].nxt)
	{
		int y=e[i].to;
        double v=e[i].v;
		if(d[y]!=d[x]+1||fabs(v)<eps) continue;
		flow=dfs(y,min(res,v)),sum+=flow;
		res-=flow,e[i].v-=flow,e[i^1].v+=flow;
		if(fabs(res)<eps) break;
	}
	return sum;
}

不能像之前一样用流量限制减去剩余流量,因为限制可能为一个极大值,其减去一个较小值后,会舍掉精度

二分图最大匹配的必须边和可行边

跑完网络流后在残量网络上进行 \(Tarjan\) 缩点

必须边:\((x,y)\) 流量为 \(1\),且 \(x\)\(y\) 在残量网络上属于不同的强连通分量

可行边:\((x,y)\) 流量为 \(1\),或 \(x\)\(y\) 在残量网络上属于同一个强连通分量

最小割

图中所有的割中,边权值和最小的割为最小割,最大流 \(=\) 最小割

利用最小割,将求解最大收益转化为最小代价

平面图最小割 \(=\) 对偶图最短路,应用有狼抓兔子海拔

最小割的必须边和可行边

可行边:被某一种最小割的方案包含

判断:满流,在残量网络上不存在 \(x\)\(y\) 的路径。(缩点后判断,等价于 \(x,y\) 属于不同的强连通分量)

证明:存在路径,说明其该边不存在必要性

必须边:一定在最小割中、扩大容量后能增大最大流

判断:满流,是可行边,在残量网络上存在 \(S\)\(x\)\(y\)\(T\) 的路径。(缩点后判断,等价于 \(x\)\(S\) 属于同一个强连通分量,\(y\)\(T\) 属于同一个强连通分量)

证明:不割的话,\(S\)\(T\) 就会连通

最大权闭合子图

若有向图 \(G\) 的子图 \(V\) 满足:\(V\) 中顶点的所有出边均指向 \(V\) 内部的顶点,则称 \(V\)\(G\) 的一个闭合子图

\(G\) 中的点有点权,则点权和最大的闭合子图称为有向图 \(G\) 的最大权闭合子图

建立源点 \(S\) 和汇点 \(T\) ,源点 \(S\) 连所有点权为正的点,容量为该点点权;其余点连汇点 \(T\),容量为该点点权的相反数,对于原图中的边 \((x,y)\),连边 \((x,y,inf)\),割从源点出发的边表示不选这个点,割指向汇点的边表示选这个点

最大权闭合图的点权和 \(=\) 所有正权点权值和 \(-\) 最小割

也就是最大收益转化为了最小代价

在残量网络中由源点 \(S\) 能够访问到的点,就构成一个点数最少的最大权闭合图

最大密度子图

一个无向图 \(G=(V,E)\) 的边数 \(|E|\) 与点数 \(|V|\) 的比值 \(D=\frac{|E|}{|V|}\) 称为它的密度

\(G\) 的一个子图 \(G^\prime=(V^\prime,E^\prime)\),使得 \(D^\prime=\frac{|E^\prime|}{|V^\prime|}\) 最大

二分 \(g\leqslant\frac{|E|}{|V|}\),得 \(|E|-|V|×g \geqslant0\)

源点 \(S\) 向所有边连容量为 \(1\) 的边,边向其两端的点连容量为 \(inf\) 的边,点向汇点 \(T\) 连容量为 \(g\) 的边

二分下界:\(\frac{1}{n}\),上界:\(m\),精度:\(\frac{1}{n^2}\)

\(code:\)

bool bfs()
{
    for(int i=s;i<=t;++i) cur[i]=head[i];
    memset(d,0,sizeof(d));
    queue<int> q;
    q.push(s);
    d[s]=1;
    while(!q.empty())
    {
        int x=q.front();
        q.pop();
        for(int i=head[x];i;i=e[i].nxt)
        {
            int y=e[i].to;
            double v=e[i].v;
            if(d[y]||fabs(v)<eps) continue;
            d[y]=d[x]+1;
            q.push(y);
        }
    }
    return d[t];
}
double dfs(int x,double lim)
{
    if(x==t) return lim;
    double res=lim,flow;
    for(int &i=cur[x];i;i=e[i].nxt)
    {
        int y=e[i].to;
        double v=e[i].v;
        if(d[y]!=d[x]+1||fabs(v)<eps) continue;
        if(fabs(flow=dfs(y,min(res,v)))>=eps)
        {
            res-=flow;
            e[i].v-=flow;
            e[i^1].v+=flow;
            if(fabs(res)<eps) break;
        }
    }
    return lim-res;
}
double dinic()
{
    double flow,ans=0;
    while(bfs())
        while(fabs(flow=dfs(s,inf))>=eps)
            ans+=flow;
    return ans;
}
double check(double x)
{
    edge_cnt=1;
    memset(head,0,sizeof(head));
    for(int i=1;i<=n;++i) add(i+m,t,x);
    for(int i=1;i<=m;++i)
    {
        int x=ed[i].x,y=ed[i].y;
        add(s,i,1.0),add(i,x+m,inf),add(i,y+m,inf);
    }
    return m*1.0-dinic();
}
int work()
{
    int ans=0;
    memset(du,0,sizeof(du));
    memset(vis,0,sizeof(vis));
    check(g);
    for(int i=1;i<=m;++i)
    {
        int x=ed[i].x,y=ed[i].y;
        if(d[i])
        {
            if(++du[x]==1) ans++,vis[x]=true;
            if(++du[y]==1) ans++,vis[y]=true;
        }
    }
    return ans;   
}

......

l=0,r=m,g=0;
while(l+1/((double)n*(double)n)<r)
{
    double mid=(l+r)/2.0;
    if(check(mid)>eps) g=l=mid;
    else r=mid;
}
printf("%d\n",work());
for(int i=1;i<=n;++i)
    if(vis[i])
        printf("%d\n",i);

最小割二元关系

二元关系指如选和不选的关系

建立最小割模型,来解决一系列问题,如happiness文理分科人员雇佣

费用流

Edmond—Karp算法

\(code\)

bool spfa()
{
	for(int i=1;i<=n;++i) dis[i]=inf;
	memset(vis,0,sizeof(vis));
	queue<int> q;
	q.push(s);
	vis[s]=true;
	dis[s]=0;
	res[s]=inf;
	while(!q.empty())
	{
		int x=q.front();
		q.pop();
		vis[x]=false;
		for(int i=head[x];i;i=e[i].nxt)
		{
			int y=e[i].to,v=e[i].v,c=e[i].c;
			if(dis[y]>dis[x]+c&&v)
			{
				dis[y]=dis[x]+c;
				res[y]=min(res[x],v);
				pre[y]=i;
				if(!vis[y])
                {
                    vis[y]=true;
                    q.push(y);
                }
			}
		}
	}
	return dis[t]!=inf;
}
void update()
{
	int x=t;
	while(x!=s)
	{
		int i=pre[x];
		e[i].v-=res[t];
		e[i^1].v+=res[t];
		x=e[i^1].to;
	}
	ans+=res[t];
	sum+=res[t]*dis[t];
}

......

while(spfa()) update();

Dinic算法

\(code\)

bool spfa()
{
    queue<int> q;
    for(int i=1;i<=n;++i) dis[i]=inf,vis[i]=false;
    q.push(s),vis[s]=true,dis[s]=0;
    while(!q.empty())
    {
        int x=q.front();
        q.pop(),vis[x]=false;
        for(int i=head[x];i;i=e[i].nxt)
        {
            int y=e[i].to,c=e[i].c;
            if(dis[y]<=dis[x]+c||!e[i].v) continue;
            dis[y]=dis[x]+c;
            if(!vis[y]) q.push(y),vis[y]=true;
        }
    }
    return dis[t]!=inf;
}
int dfs(int x,int lim)
{
    if(x==t) return lim;
    vis[x]=true;
    int flow,res=lim;
    for(int i=head[x];i;i=e[i].nxt)
    {
        int y=e[i].to,v=e[i].v;
        if(dis[y]!=dis[x]+e[i].c||!v||vis[y]) continue;
        if(flow=dfs(y,min(res,v)))
        {
            res-=flow;
            e[i].v-=flow;
            e[i^1].v+=flow;
            if(!res) break;
        }
    }
    return lim-res;
}
void dinic()
{
    int flow;
    while(spfa())
        while(flow=dfs(s,inf))
            ans+=flow,sum+=flow*dis[t];
}

可以去求解二分图带权匹配

有上下界限制的网络流

无源汇有上下界可行流

\(n\)个点,\(m\)条边的网络,求一个可行解,使得边 \((x,y)\) 的流量介于 \([\ low_{x,y},up_{x,y}\ ]\) 之间,并且整个网络满足流量守恒

\(up_{x,y}-low_{x,y}\) 作为容量上界,\(0\) 作为容量下界

\(in_x=\sum\limits_{i\to x} low(i,x)-\sum\limits_{x\to i} low(x,i)\)

\(in_x >0\),则从源点 \(S\)\(x\) 连边,容量为 \(in_x\),反之,则从 \(x\) 向汇点 \(T\) 连边,容量为 \(-in_x\)

在该网络上求最大流,求完后每条边的流量再加上容量下界即为一种可行流

\(code:\)

bool bfs()
{
	memcpy(cur,head,sizeof(head));
	memset(d,0,sizeof(d));
	queue<int> q;
	q.push(s);
	d[s]=1;
	while(!q.empty())
	{
		int x=q.front();
		q.pop();
		for(int i=head[x];i;i=e[i].nxt)
		{
			int y=e[i].to,v=e[i].v;
			if(d[y]||!v) continue;
			d[y]=d[x]+1;
			q.push(y);
		}
	}
	return d[t];
}
int dfs(int x,int lim)
{
	if(x==t) return lim;
	int res=lim,k;
	for(int &i=cur[x];i;i=e[i].nxt)
	{
		int y=e[i].to,v=e[i].v;
		if(d[y]!=d[x]+1||!v) continue;
		if(k=dfs(y,min(res,v)))
		{
			res-=k;
			e[i].v-=k;
			e[i^1].v+=k;
			if(!res) break;
		}
	}
	return lim-res;
}
int dinic()
{
    int flow,ans=0;
    while(bfs())
        while(flow=dfs(s,inf))
            ans+=flow;
    return ans;
}
bool check()
{
    for(int i=head[s];i;i=e[i].nxt)
        if(e[i].v)
            return false;
    return true;
}

......

for(int i=1;i<=m;++i)
{
    int a,b,up;
    read(a),read(b),read(low[i]),read(up);
    in[a]-=low[i],in[b]+=low[i];
    add(a,b,up-low[i]);
}
for(int i=1;i<=n;i++)
{   
    if(in[i]>0) add(s,i,in[i]);
    else add(i,t,-in[i]);
}
dinic();
if(check()) 
{
    puts("YES");
    for(int i=1;i<=m;i++) printf("%d\n",e[(i<<1)^1].v+low[i]);
}
else puts("NO");

有源汇有上下界最大流

\(T\)\(S\) 连一条容量上界为 \(inf\),容量下界为 \(0\) 的边,使有源汇转化为无源汇

在残量网络上再求原源点到原汇点的最大流

\(code:\)

bool bfs()
{
	memcpy(cur,head,sizeof(head));
	memset(d,0,sizeof(d));
	queue<int> q;
	q.push(s);
	d[s]=1;
	while(!q.empty())
	{
		int x=q.front();
		q.pop();
		for(int i=head[x];i;i=e[i].nxt)
		{
			int y=e[i].to,v=e[i].v;
			if(d[y]||!v) continue;
			d[y]=d[x]+1;
			q.push(y);
		}
	}
	return d[t];
}
int dfs(int x,int lim)
{
	if(x==t) return lim;
	int res=lim,k;
	for(int &i=cur[x];i;i=e[i].nxt)
	{
		int y=e[i].to,v=e[i].v;
		if(d[y]!=d[x]+1||!v) continue;
		if(k=dfs(y,min(res,v)))
		{
			res-=k;
			e[i].v-=k;
			e[i^1].v+=k;
			if(!res) break;
		}
	}
	return lim-res;
}
int dinic()
{
    int flow,ans=0;
    while(bfs())
        while(flow=dfs(s,inf))
            ans+=flow;
    return ans;
}
bool check()
{
    for(int i=head[s];i;i=e[i].nxt)
        if(e[i].v)
            return false;
    return true;
}

......

for(int i=1;i<=m;++i)
{
	int a,b,up,low;
	read(a),read(b),read(low),read(up);
	in[a]-=low,in[b]+=low;
	add(a,b,up-low);
}
for(int i=1;i<=n;i++)
{   
    if(in[i]>0) add(s,i,in[i]);
    else add(i,t,-in[i]);
}
add(T,S,inf);
dinic();
ans=e[edge_cnt].v;
e[edge_cnt].v=e[edge_cnt^1].v=0;
if(check()) 
{
    s=S,t=T;
    printf("%d",ans+dinic());
}
else puts("NO");

有源汇有上下界最小流

先不添加 \(T\)\(S\) 的边,求一次超级源到超级汇的最大流。

然后再添加一条从 \(T\)\(S\) 下界为 \(0\) ,上界为 \(inf\) 的边,在残量网络上再求一次超级源到超级汇的最大流

流经 \(T\)\(S\) 的边的流量就是最小流的值

\(code:\)

bool bfs()
{
	memcpy(cur,head,sizeof(head));
	memset(d,0,sizeof(d));
	queue<int> q;
	q.push(s);
	d[s]=1;
	while(!q.empty())
	{
		int x=q.front();
		q.pop();
		for(int i=head[x];i;i=e[i].nxt)
		{
			int y=e[i].to,v=e[i].v;
			if(d[y]||!v) continue;
			q.push(y);
			d[y]=d[x]+1;
		}
	}
	return d[t];
}
int dfs(int x,int lim)
{
	if(x==t) return lim;
	int res=lim,k;
	for(int &i=cur[x];i;i=e[i].nxt)
	{
		int y=e[i].to,v=e[i].v;
		if(d[y]!=d[x]+1||!v) continue;
		if(k=dfs(y,min(res,v)))
		{
			res-=k;
			e[i].v-=k;
			e[i^1].v+=k;
			if(!res) break;
		}
	}
	return lim-res;
}
int dinic()
{
	int k,flow=0;
	while(bfs())
	{
		while(k=dfs(s,inf))
		{
			flow+=k; 
		}
	}
	return flow;
}
bool check()
{
    for(int i=head[s];i;i=e[i].nxt)
        if(e[i].v)
            return false;
    return true;
}

......

for(int i=1;i<=m;++i)
{
	int a,b,up,low;
	read(a),read(b),read(low),read(up);
	in[a]-=low,in[b]+=low;
	add(a,b,up-low);
}
for(int i=1;i<=n;i++)
{   
    if(in[i]>0) add(s,i,in[i]);
    else add(i,t,-in[i]);
}
dinic();
add(T,S,inf);
dinic();
if(!check())
{
    puts("please go home to sleep");
    return 0;
}
printf("%d",e[edge_cnt].v);

循环流

以最大费用循环流为例,对于边 \((x,y,v,c)\),若费用为正,则将其先流满,记录费用总和 \(sum\),通过建立源汇点来实现补流,边正常连。若费用为负,则连边 \((x,y,v,-c)\)

然后跑最小费用最大流得出费用 \(ans\),最终最大费用循环流求解的答案为 \(sum-ans\)

JZOJ Tree

最大费用循环流,树上的边从上向下连,容量为 \(d\),费用为 \(0\),每条路径的边从下向上连,容量为 \(1\),费用为 \(c\)

求解时,先将所有边跑满流,然后增加源汇点来使流量平衡,通过跑最小费用最大流来实现退流,删去不合法的边的贡献

还有一种更强的做法:

转载自JZOJ 100003. 【NOI2017模拟.4.1】 Tree(费用流)

\(code:\)

#include<bits/stdc++.h>
#define maxn 500010
#define maxm 5000010
#define inf 1000000000000000
using namespace std;
typedef long long ll;
template<typename T> inline void read(T &x)
{
    x=0;char c=getchar();bool flag=false;
    while(!isdigit(c)){if(c=='-')flag=true;c=getchar();}
    while(isdigit(c)){x=(x<<1)+(x<<3)+(c^48);c=getchar();}
    if(flag)x=-x;
}
int T,n,m,s,t;
ll ans;
int in[maxn],de[maxn];
ll dis[maxn];
bool vis[maxn];
struct edge
{
    int to,nxt,v;
    ll c;
}e[maxm];
int head[maxn],edge_cnt;
void add(int from,int to,int val,int cost)
{
    e[++edge_cnt]=(edge){to,head[from],val,cost};
	head[from]=edge_cnt;
    e[++edge_cnt]=(edge){from,head[to],0,-cost};
	head[to]=edge_cnt;
}
void Add(int from,int to,int val,ll cost)
{
    in[from]+=val,in[to]-=val,ans+=cost,add(from,to,val,cost);
}
struct Edge
{
    int to,nxt,v;
}ed[maxn];
int hd[maxn],e_cnt;
void link(int from,int to,int val)
{
    ed[++e_cnt]=(Edge){to,hd[from],val};
    hd[from]=e_cnt;
}
void dfs_pre(int x,int fa)
{
	de[x]=de[fa]+1;
	for(int i=hd[x];i;i=ed[i].nxt)
	{
		int y=ed[i].to;
		if(y==fa) continue;
        Add(x,y,ed[i].v,0),dfs_pre(y,x);
	}	
}
bool spfa()
{
    for(int i=s;i<=t;++i) vis[i]=0,dis[i]=inf;
    queue<int> q;
    q.push(s),dis[s]=0,vis[s]=true;
    while(!q.empty())
    {
        int x=q.front();
        q.pop(),vis[x]=false;
        for(int i=head[x];i;i=e[i].nxt)
        {
            int y=e[i].to;
            ll v=e[i].v,c=e[i].c;
            if(dis[y]>dis[x]+c&&v)
            {
                dis[y]=dis[x]+c;
                if(!vis[y])
                {
                    vis[y]=true;
                    q.push(y);
                }
            }
        }
    }
    return dis[t]!=inf;
}
ll dfs(int x,ll lim)
{
    if(x==t) return lim;
    vis[x]=true;
    ll res=lim,flow;
    for(int i=head[x];i;i=e[i].nxt)
    {
        int y=e[i].to;
        ll v=e[i].v,c=e[i].c;
        if(dis[y]!=dis[x]+c||!v||vis[y]) continue;
        if(flow=dfs(y,min(res,v)))
        {
            res-=flow;
            e[i].v-=flow;
            e[i^1].v+=flow;
            if(!res) break;
        }
    }
    return lim-res;
}
ll dinic()
{
    ll flow,sum=0;
    while(spfa())
        while(flow=dfs(s,inf))
            sum+=flow*dis[t];
    return sum;
}
void clear()
{
    e_cnt=ans=0,edge_cnt=1;
    memset(in,0,sizeof(in));
    memset(hd,0,sizeof(hd));
    memset(head,0,sizeof(head));
}
int main()
{
    read(T);
    while(T--)
    {
        clear(),read(n),read(m),t=n+1;
        for(int i=1;i<n;++i)
        {
            int x,y,v;
            read(x),read(y),read(v);
            link(x,y,v),link(y,x,v);
        }
        dfs_pre(1,0);
        for(int i=1;i<=m;++i)
        {
            int x,y,v;
            read(x),read(y),read(v);
            if(de[x]<de[y]) swap(x,y);
            Add(x,y,1,v);
        }
        for(int i=1;i<=n;++i)
        {
            if(in[i]>0) add(s,i,in[i],0);
            else add(i,t,-in[i],0);
        }
        printf("%lld\n",ans-dinic());
    }
    return 0;
}
posted @ 2020-01-22 20:22  lhm_liu  阅读(394)  评论(0编辑  收藏  举报