网络流入门学习笔记

最大流

基本概念

网络流,即网络+

网络就是由许多结点和边组成的图,在这里边权表示允许通过的最大流量

在网络中,有两个特殊的结点,一个叫源点,一个叫汇点

网络流中最大流问题可以看成是:假设在源点注入无限多的水流,最终会流到汇点的最大流量(中间有点类似木桶原理,一条完整路径上的最大流量是最小的边权。

最小割概念:在网络中选取若干条边删除,使得源点到汇点变成不连通的,而且删掉的边权之和最小。

定理:最大流在数值上等于最小割

算法

如果直接找从源点到汇点的一条路径,可能经过了边权不那么优的边
引入反向弧——每次选取 i 到 j 的一条路径中 k 的流量,给它的反向边边权赋予权值 k。
实际的意义是可以反悔。如果某条边被正反走了两次,那么可以认为这条边是没有选取的

EK算法

最基础的、不加优化的网络流算法。

时间复杂度 O(ve2)

每次都从 S 到 T 寻找增广路,如果找到,把答案加进 ans
直到找不到了退出

点击查看代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int inf = 1e9;  //可能的最大流量 
const int N = 205;
int n,m;
ll g[N][N],pre[N];
int S,T; 

ll bfs(int s,int t){  // 求s到t的一条路的流量 
	ll flow[N]={0};
	memset(pre,-1,sizeof(pre));
	flow[s] = inf;
	pre[s] = 0;
	queue<int>q;
	q.push(s);
	while(!q.empty()){
		int u = q.front();
		q.pop();
		for(int i=1;i<=n;i++){
			if(i!=s&&g[u][i]>0&&pre[i]==-1){
				pre[i] = u;
				q.push(i);
				flow[i]=min(flow[u],g[u][i]);
			}
		}
	}
	if(pre[t]==-1) return -1;
	return flow[t];
}

ll maxflow(int s,int t){
	ll Maxflow = 0;
	while(1){
		ll flow = bfs(s,t);
		if(flow==-1) break;
		int cur = t;
		while(cur!=s){  //增加反向弧 
			int father = pre[cur];
			g[father][cur] -= flow;
			g[cur][father] += flow;
			cur = father;
		}
		Maxflow += flow;
	}
	return Maxflow;
}

int main(){
	int t;
//	cin>>t;
	t=1;
	int cnt=0;
	while(t--){  // ind  1 to n    m dugs
		scanf("%d%d",&n,&m);
		scanf("%d%d",&S,&T);  //源点、汇点 
		memset(g,0,sizeof(g));
		ll w;
		for(int i=1,u,v;i<=m;i++){
			scanf("%d%d%lld",&u,&v,&w);
			g[u][v]+=w;
		} 
//		printf("Case %d: ",++cnt);
		printf("%lld\n",maxflow(S,T));
	}
	return 0;
}

ISAP算法

时间复杂度很低的优秀算法。

点击查看代码
#include<stdio.h>
#include<string.h>
#include<queue>
#include<algorithm>
#define ll long long
using namespace std;
#define inf 0x3f3f3f3f
const int maxn = 409;
ll pre[maxn],gap[maxn],h[maxn];//pre记录是从哪一条边到达i的,gap记录在i层中有几个点,h记录i点的层次。
ll firs[maxn];//邻接表建边 
struct node{
    ll  v,flow,nex;
}edge[maxn*maxn];
int N, n, m, S, T;

inline int in(int x) {
	return x;
}

inline int out(int x) {
	return x + n + 1;
}

void build_edge(int u,int v,int flow){
    edge[N]=(node){v,flow,firs[u]};
    firs[u]=N++;
    edge[N]=(node){u,0,firs[v]};
    firs[v]=N++;
}

void bfs(int s,int t)//广搜建层,不过这里是从汇点开始的,别弄错了,
{
    //初始化
    memset(h,-1,sizeof(h));
    memset(gap,0,sizeof(gap));
    queue<int>q;
    while(!q.empty())q.pop();

    q.push(t);
    h[t]=0;//将汇点定义为0层
    gap[0]=1;//0层点的个数加一
    while(!q.empty())
    {
        int u=q.front();
        q.pop();
        for(int i=firs[u];i!=-1;i=edge[i].nex)
        {
            int v=edge[i].v;
            if(h[v]==-1)
            {
                h[v]=h[u]+1;
                gap[h[v]]++;//记录当前层的个数
                q.push(v);
            }
        }
    }
}

ll isap(int s,int t,int n){
    bfs(s,t);
    memset(pre,0,sizeof(pre));//记录该点的前驱是谁(这里的前驱是指边,不是点)
    ll ans=0,d,u=s;//ans记录最大流,d记录当前路径中的最小流,u记录当前查找到哪个点了。
    while(h[s]<n)//如果源点的层小于点的个数就可能还有流
    {
        int flag=1;//判断是否能够走,能否找到流。
        if(u==s) d=inf;//如果再次从源点出发,就初始化d,
        for(int i=firs[u];i!=-1;i=edge[i].nex)
        {
            ll v=edge[i].v,flow=edge[i].flow;
            if(h[v]+1==h[u]&&flow)
            {
                pre[v]=i;//记录该点是由哪条边到达的
                u=v;//记录要去哪个点
                d=min(d,flow);//记录最小流
                flag=0;//能够往前走
                if(v==t)//如果走到了汇点
                {
                    while(u!=s)//原路返回,更新边的容量,与dinic的dfs思路一样
                    {
                        int j=pre[u];
                        edge[j].flow-=d;
                        edge[j^1].flow+=d;
                        u=edge[j^1].v;
                    }
                    ans+=d;//加上当前找到的流
                }
                break;
            }
        }
        if(flag)//如果没有找到下面的路,就更新点的层次
        {
            if(--gap[h[u]]==0) //如果该层上没有了点,说明没法继续查找了,结束。
                break;
            ll min_=n-1;
            for(int i=firs[u];i!=-1;i=edge[i].nex)//查找与u相连的最小层,
            {
                ll v=edge[i].v,flow=edge[i].flow;
                if(flow)
                    min_=min(min_,h[v]);
            }
            h[u]=min_+1;//重新给u建层
            gap[h[u]]++;//更新层的个数
            if(u!=s)//重要一步,如果当前点不是源点就要向后退一步,继续查找。
             u=edge[pre[u]^1].v;
        }
    }
    return ans;
}

int main(){
	int t; scanf("%d", &t);
	while(t--) {
	    scanf("%d%d",&n,&m);
	    memset(firs,-1,sizeof(firs));
	    N=0;
	    for(int i=1,u,v;i<=m;i++){
				scanf("%d%d",&u,&v);
				build_edge(out(u), in(v), 1);
			} 
			for(int i = 0; i <= n; i++) {
				build_edge(in(i), out(i), 1);
			}
		S = out(0);
		T = in(n);
	    n = n * 2 + 2;
	    printf("%lld\n",isap(S,T,n));
    }
	return 0;
}

例题

P2763 试题库问题

题意

现在要从 n 道题目里面抽取 m 道组成试卷,每道题目都涉及 x[i] 种知识点,对于出卷子而言 每道题目只能算作一种知识点

给出构成试卷需要每种知识点的个数 num[i],你需要求出一种合法的组卷方案,如果方案不存在,则输出 -1

思路

关于是否可行:先对题目建模,如果满足存在从源点到汇点的最大流 = 所有知识点的个数之和,则存在合法方案

关于输出方案:在最大流问题中,如果某条边的反向弧流量大于 0 ,则说明这条边中有流量,那就可以输出出来

关于建模:每道题和源点相连,流量为 1 ,表示每道题最多使用一次;每一种知识点和汇点相连,流量为 num[i],表示需求量是这么大;每道题与它包含的知识点相连,

流量为 1,表示每道题只能算作一个知识点

code

点击查看代码
// #pragma GCC optimize(2)
#include<bits/stdc++.h>
#define ll long long
#define ull unsigned long long
#define pii pair<int,int>
#define pb push_back
#define inf 0x3f3f3f3f
using namespace std;
const int maxn = 2209;
ll pre[maxn],gap[maxn],h[maxn];//pre记录是从哪一条边到达i的,gap记录在i层中有几个点,h记录i点的层次。
ll firs[maxn];//邻接表建边 
struct node{
    ll  v,flow,nex;
}edge[maxn*maxn];
int n, k, m;
int num[22];
int S, T, N;
int rec[maxn][maxn];

void add(int u,int v,int flow){
    edge[N]=(node){v,flow,firs[u]};
    firs[u]=N++;
    edge[N]=(node){u,0,firs[v]};
    firs[v]=N++;
}

void bfs(int s,int t)//广搜建层,不过这里是从汇点开始的,别弄错了,
{
    //初始化
    // puts("000");
    memset(h,-1,sizeof(h));
    memset(gap,0,sizeof(gap));
    queue<int>q;
    while(!q.empty())q.pop();

    q.push(t);
    h[t]=0;//将汇点定义为0层
    gap[0]=1;//0层点的个数加一
    while(!q.empty())
    {
        int u=q.front();
        q.pop();
        for(int i=firs[u];i!=-1;i=edge[i].nex)
        {
            int v=edge[i].v;
            if(h[v]==-1)
            {
                h[v]=h[u]+1;
                gap[h[v]]++;//记录当前层的个数
                q.push(v);
            }
        }
    }
    // puts("11");
}

ll isap(int s,int t,int n){
    bfs(s,t);
    memset(pre,0,sizeof(pre));//记录该点的前驱是谁(这里的前驱是指边,不是点)
    ll ans=0,d,u=s;//ans记录最大流,d记录当前路径中的最小流,u记录当前查找到哪个点了。
    while(h[s]<n)//如果源点的层小于点的个数就可能还有流
    {
        int flag=1;//判断是否能够走,能否找到流。
        if(u==s) d=inf;//如果再次从源点出发,就初始化d,
        for(int i=firs[u];i!=-1;i=edge[i].nex)
        {
            ll v=edge[i].v,flow=edge[i].flow;
            if(h[v]+1==h[u]&&flow)
            {
                pre[v]=i;//记录该点是由哪条边到达的
                u=v;//记录要去哪个点
                d=min(d,flow);//记录最小流
                flag=0;//能够往前走
                if(v==t)//如果走到了汇点
                {
                    while(u!=s)//原路返回,更新边的容量,与dinic的dfs思路一样
                    {
                        int j=pre[u];
                        edge[j].flow-=d;
                        edge[j^1].flow+=d;
                        u=edge[j^1].v;
                    }
                    ans+=d;//加上当前找到的流
                }
                break;
            }
        }
        if(flag)//如果没有找到下面的路,就更新点的层次
        {
            if(--gap[h[u]]==0) //如果该层上没有了点,说明没法继续查找了,结束。
                break;
            ll min_=n-1;
            for(int i=firs[u];i!=-1;i=edge[i].nex)//查找与u相连的最小层,
            {
                ll v=edge[i].v,flow=edge[i].flow;
                if(flow)
                    min_=min(min_,h[v]);
            }
            h[u]=min_+1;//重新给u建层
            gap[h[u]]++;//更新层的个数
            if(u!=s)//重要一步,如果当前点不是源点就要向后退一步,继续查找。
             u=edge[pre[u]^1].v;
        }
    }
    return ans;
}

int main(){
    cin >> k >> n;
    S = 0; T = n + k + 1;
    N = 0;
    memset(firs,-1,sizeof(firs));
    for(int i = 1; i <= k; i++) {
        cin >> num[i];
        m += num[i];
        add(n + i, T, num[i]);
    }
    for(int i = 1, x; i <= n; i++) {
        add(S, i, 1);
        scanf("%d", &x);
        for(int j = 1, y; j <= x; j++) {
            scanf("%d", &y);
            add(i, y + n, 1);
            rec[i][y + n] = 1;
        }
    }
    int nn = n;
    n = n + k + 1;
    int ans = isap(S,T,n);
    cout << ans << endl;
    if(ans < m) {
        puts("No Solution!");
    }
    else {
        for(int i = 1; i <= k; i++) {
            printf("%d: ", i);
            for(int j = firs[i + nn]; j != -1; j = edge[j].nex) {
                // cout << edge[j].v << endl;
                if(edge[j].flow > 0 && edge[j].v >= 1 && edge[j].v <= nn) {
                    printf("%d ", edge[j].v);
                }
            }
            // for(int j = 1; j <= nn; j++) {
            //     if(g[i + nn][j] > 0) {
            //         printf("%d ",j);
            //         // cout << j << ' '<< i << endl;
            //     }
            // }
            puts("");
        }
    }
    system("pause");
    return 0;
}

P2764 最小路径覆盖问题

题意

给你一张有向无环图,你需要找到一些路径,使得每个点只出现在一条路径上,同时最小化路径的条数

思路

其实可以用二分图匹配 匈牙利算法做。

每条边上的两个端点之间匹配,match数组就表示了哪些点是相连的,最后输出路径我用了并查集(方法应该很多的吧

code

点击查看代码
//二分图匹配
#include<bits/stdc++.h>
using namespace std;
const int N = 1005;
int n,m,k;  //a方n人 b方m人 k对关系 
int u,v;  
int g[N][N]; //表示a,b之间有关系,赋值为0/1. 
//!若要去掉某一关系,令其=0即可  
int match[N];  //下标是配对的b方  值为对应的a方 
bool reserve_b[N], vis[N];  //标记b方是否已经使用过 
int ans;
int father[N];

bool dfs(int x){
	for(int i=1;i<=m;i++){  //b方 
		if(!reserve_b[i]&&g[x][i]){
			reserve_b[i]=1;
			if(!match[i]||dfs(match[i])){  //b无配对 或者 b的原配可以找到新的配对 
				match[i]=x;  //则令x为b的配对 
				return 1;  //x找到了配对 
			}
		}
	}
	return 0;  //x没有找到配对 
}

inline int f(int x){
    return x == father[x] ? x : (father[x] = f(father[x]));
}

int add(int x,int y){ 
	int fx=f(x);
	int fy=f(y);
	return father[fx]=fy;  //x.y无序,因为不是按树的结构存储,而是按集合 
}

void print(int x) {

}

int main(){
//	while(scanf("%d%d%d",&n,&m,&k)==3){
		scanf("%d%d",&n,&k);
        m = n;
		memset(g,0,sizeof(g)); 
		memset(match,0,sizeof(match));
		for(int i=1;i<=k;i++){
			scanf("%d%d",&u,&v);
			g[u][v]=1;  // 表示a,b之间有关系,也可以表示两点间有一条边 
		}
		for(int i=1;i<=n;i++){  //a方 
			memset(reserve_b,0,sizeof(reserve_b));  //不加会错
			//表示不论bi之前是否有配对,都不会影响它与ai的配对 
			if(dfs(i)) ans++;  //ai配对成功后配对数++,虽然可能更换配对,但是保证ai一定有配对 
		}
		

        for(int i = 1; i <= n; i++) father[i] = i;
        set<int>st;
        for(int i = 1; i <= n; i++) {
            if(match[i]) add(i, match[i]);
        }
        for(int i = 1; i <= n; i++) {
            st.insert(f(i));
        }

        for(auto i : st) {
            for(int j = 1; j <= n; j++) {
                if(f(j) == i) {
                    printf("%d ", j);
                }
            }puts("");
        }
printf("%d\n",n - ans);
//	}
    system("pause");
	return 0;
} 

P1251 餐巾计划问题

题意

有点长,自己看题目吧

思路

是很讲究网络流建模细节的一道题。需要结合现实仔细考虑。

每天使用的餐巾可以分成两部分,一部分是未使用的干净的餐巾,一部分是用过的脏餐巾,他们不能混为一谈的原因是,干净的餐巾用脏之后,不一定都拿去洗掉了,可能有

的在这几天里没有洗。即需要拆点。

用[a, b]表示一条流量为a,单位费用为b的边

干净餐巾的由来:

  • 新买的,由 S 连一条 [inf, p] 到 in(i)
  • 慢洗的,由 out(i - n) 连 [n, s] 到 in(i)
  • 快洗的,由 out(i - m) 连 [m, f] 到 in(i)

脏餐巾的由来:

  • 今天刚用脏的, 从 in(i) 连 [inf, 0] 到 out(i)
  • 昨天(或之前)没洗囤积下来的, 从 out(i - 1) 连 [inf, 0] 到 out(i)

还需要 check 一下当天的餐巾数量是合乎要求的:

  • 干净的餐巾数量是 num[i], 从 in(i) 连 [num[i], 0] 到 T
  • 当天用完的餐巾数量也是 num[i] , 从 S 连 [num[i], 0] 到 out(i)
    注意上面这一点!!必须连它的原因是,可能 流量 从 in(i) 流入,又从 T 流走了,根本没有经过 out(i)

code

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn = 200005, inf = 0x3f3f3f3f;
int need[maxn];
bool vis[maxn];
int p,m,fa,n,sl,day;
int S,T,x,y,z,f,cost[maxn],pre[maxn],last[maxn],flow[maxn],maxflow,mincost;
//cost最小花费;pre每个点的前驱;last每个点的所连的前一条边;flow源点到此处的流量 
struct Edge{
	int to,next,flow,cost;
}edge[maxn];
int head[maxn],num_edge; 
queue <int> q;

void add_edge(int from,int to,int flow,int cost)
{
	edge[++num_edge].next=head[from];
	edge[num_edge].to=to;
	edge[num_edge].flow=flow;
	edge[num_edge].cost=cost;
	head[from]=num_edge;
}

inline void add(int from, int to, int flow, int cost) {
	add_edge(from, to, flow, cost);
	add_edge(to, from, 0, -cost);
}

inline int in(int x) {
    return x;
}

inline int out(int x) {
    return x + day;
}

bool spfa(int s,int t)
{
	memset(cost,0x7f,sizeof(cost));
	memset(flow,0x7f,sizeof(flow));
	memset(vis,0,sizeof(vis));
	q.push(s); vis[s]=1; cost[s]=0; pre[t]=-1;
	
	while (!q.empty())
	{
		int now=q.front();
		q.pop();
		vis[now]=0;
		for (int i=head[now]; i!=-1; i=edge[i].next)
		{
			if (edge[i].flow>0 && cost[edge[i].to]>cost[now]+edge[i].cost)//正边 
			{
				cost[edge[i].to]=cost[now]+edge[i].cost;
				pre[edge[i].to]=now;
				last[edge[i].to]=i;
				flow[edge[i].to]=min(flow[now],edge[i].flow);//
				if (!vis[edge[i].to])
				{
					vis[edge[i].to]=1;
					q.push(edge[i].to);
				}
			}
		}
	}
	return pre[t]!=-1;
}

void MCMF(int s, int t)
{
	while (spfa(s,t))
	{
		int now=t;
		maxflow+=flow[t];
		mincost+=flow[t]*cost[t];
		while (now!=s)
		{//从源点一直回溯到汇点 
			edge[last[now]].flow-=flow[t];
			edge[last[now]^1].flow+=flow[t];
			now=pre[now];
		}
	}
}

signed main() {
	memset(head,-1,sizeof(head)); num_edge=-1;//初始化 
	scanf("%d",&day);
    S = 0; T = 2 * day + 1;
    for(int i = 1; i <= day; i++) {
        scanf("%d", &need[i]);
    }
    cin >> p >> m >> fa >> n >> sl;
    // in: 早上的干净纸巾
    // out: 晚上的用脏的纸巾
    for(int i = 1; i <= day; i++) {
        add(S, in(i), inf, p);

        add(S, out(i), need[i], 0); /////
        
        add(in(i), T, need[i], 0);
        add(in(i), out(i), inf, 0);  ///
        if(i + 1 <= day) add(out(i), out(i + 1), inf, 0);
    }
    for(int i = 1; i + m <= day; i++) {
        add(out(i), in(i + m), inf, fa);
    }
    for(int i = 1; i + n <= day; i++) {
        add(out(i), in(i + n), inf, sl);
    }
	MCMF(S, T);
	// printf("%d %d",maxflow,mincost);
    cout << mincost << endl;
    system("pause");
	return 0;
}

费用流

概念

每条边除了流量的限制外,还有单位价格;要从源点到汇点求最大流的同时,求出最小的总费用(cost[i]flow[i]

思路和贪心有关,求最大流时,首先选择单位费用更小的边

因为边权可能为负,所以用了spfa算法处理负边权

放上板子:

最小费用最大流模板
#include<bits/stdc++.h>
using namespace std;
const int maxn = 20005;

bool vis[maxn];
int n,m,S,T,x,y,z,f,cost[maxn],pre[maxn],last[maxn],flow[maxn],maxflow,mincost;
//cost最小花费;pre每个点的前驱;last每个点的所连的前一条边;flow源点到此处的流量 
struct Edge{
	int to,next,flow,cost;
}edge[maxn];
int head[maxn],num_edge; 
queue <int> q;

void add_edge(int from,int to,int flow,int cost)
{
	edge[++num_edge].next=head[from];
	edge[num_edge].to=to;
	edge[num_edge].flow=flow;
	edge[num_edge].cost=cost;
	head[from]=num_edge;
}

inline void add(int from, int to, int flow, int cost) {
	add_edge(from, to, flow, cost);
	add_edge(to, from, 0, -cost);
}

bool spfa(int s,int t)
{
	memset(cost,0x7f,sizeof(cost));
	memset(flow,0x7f,sizeof(flow));
	memset(vis,0,sizeof(vis));
	q.push(s); vis[s]=1; cost[s]=0; pre[t]=-1;
	
	while (!q.empty())
	{
		int now=q.front();
		q.pop();
		vis[now]=0;
		for (int i=head[now]; i!=-1; i=edge[i].next)
		{
			if (edge[i].flow>0 && cost[edge[i].to]>cost[now]+edge[i].cost)//正边 
			{
				cost[edge[i].to]=cost[now]+edge[i].cost;
				pre[edge[i].to]=now;
				last[edge[i].to]=i;
				flow[edge[i].to]=min(flow[now],edge[i].flow);//
				if (!vis[edge[i].to])
				{
					vis[edge[i].to]=1;
					q.push(edge[i].to);
				}
			}
		}
	}
	return pre[t]!=-1;
}

void MCMF(int s, int t)
{
	while (spfa(s,t))
	{
		int now=t;
		maxflow+=flow[t];
		mincost+=flow[t]*cost[t];
		while (now!=s)
		{//从源点一直回溯到汇点 
			edge[last[now]].flow-=flow[t];
			edge[last[now]^1].flow+=flow[t];
			now=pre[now];
		}
	}
}

int main() {
	memset(head,-1,sizeof(head)); num_edge=-1;//初始化 
	scanf("%d",&n);
    S = 0; T = n + 1;
	for(int i=1; i<=m; i++) {
		scanf("%d%d%d%d",&x,&y,&z,&f);
		add(x,y,z,f);
	}
	MCMF(S, T);
	printf("%d %d",maxflow,mincost);
	return 0;
}
posted @   starlightlmy  阅读(41)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 上周热点回顾(3.3-3.9)
· AI 智能体引爆开源社区「GitHub 热点速览」
· 写一个简单的SQL生成工具
点击右上角即可分享
微信分享提示