网络流入门学习笔记
最大流
基本概念
网络流,即网络+流
网络就是由许多结点和边组成的图,在这里边权表示允许通过的最大流量
在网络中,有两个特殊的结点,一个叫源点,一个叫汇点
网络流中最大流问题可以看成是:假设在源点注入无限多的水流,最终会流到汇点的最大流量(中间有点类似木桶原理,一条完整路径上的最大流量是最小的边权。
最小割概念:在网络中选取若干条边删除,使得源点到汇点变成不连通的,而且删掉的边权之和最小。
定理:最大流在数值上等于最小割。
算法
如果直接找从源点到汇点的一条路径,可能经过了边权不那么优的边
引入反向弧——每次选取 i 到 j 的一条路径中 k 的流量,给它的反向边边权赋予权值 k。
实际的意义是可以反悔。如果某条边被正反走了两次,那么可以认为这条边是没有选取的
EK算法
最基础的、不加优化的网络流算法。
时间复杂度
每次都从 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;
}
费用流
概念
每条边除了流量的限制外,还有单位价格;要从源点到汇点求最大流的同时,求出最小的总费用()
思路和贪心有关,求最大流时,首先选择单位费用更小的边
因为边权可能为负,所以用了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;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 上周热点回顾(3.3-3.9)
· AI 智能体引爆开源社区「GitHub 热点速览」
· 写一个简单的SQL生成工具