前言
题目链接:洛谷-网络流24题-按难度排序
按在洛谷里的难度排序由易到难去刷,而不是原来的题号。
最大流模板:dinic 算法
struct Dinic
{
struct Edge
{
int from, to, cap, flow;
};
int s, t; //节点数,边数,源点编号,汇点编号
vector<Edge> edges; //边表,edges[e]和edges[e^1]互为反向弧
vector<int> G[M]; //邻接表,G[i][j]表示节点i的第j条边在e中的序号
bool vis[M]; //bfs用
int d[M]; //从起点到i的距离
int cur[M]; //当前弧下标
void addEdge(int from, int to, int cap)
{
edges.push_back({from, to, cap, 0});
edges.push_back({to, from, 0, 0});
G[from].push_back(edges.size() - 2);
G[to].push_back(edges.size() - 1);
}
bool BFS()
{
memset(vis, 0, sizeof(vis));
queue<int> q;
q.push(s);
d[s] = 0;
vis[s] = 1;
while(!q.empty())
{
int u = q.front();
q.pop();
for(int id : G[u])
{
Edge &e = edges[id];
if(!vis[e.to] && e.cap > e.flow)
{
vis[e.to] = 1;
d[e.to] = d[u] + 1;
q.push(e.to);
}
}
}
return vis[t];
}
int DFS(int u, int a)
{
if(u == t || a == 0) return a;
int flow = 0, f;
for(int &i = cur[u]; i < (int)G[u].size(); ++i)
{
Edge &e = edges[G[u][i]];
if(d[u] + 1 == d[e.to] &&
(f = DFS(e.to, min(a, e.cap - e.flow))) > 0)
{
e.flow += f;
edges[G[u][i] ^ 1].flow -= f;
flow += f;
a -= f;
if(a == 0) break;
}
}
return flow;
}
int maxflow(int _s, int _t)
{
s = _s;
t = _t;
int flow = 0;
while(BFS())
{
memset(cur, 0, sizeof(cur));
flow += DFS(s, INF);
}
return flow;
}
}dinic;
最小割
最小割=最大流
最小费用最大流模板:MCMF
struct MCMF
{
struct Edge
{
int from, to, cap, flow, cost;
Edge(int u, int v, int c, int f, int w) : from(u), to(v), cap(c), flow(f), cost(w) {}
};
vector<Edge>edges;
vector<int>G[M];
int inq[M], p[M], a[M], d[M];
void addEdge(int from, int to, int cap, int cost)
{
edges.push_back(Edge(from, to, cap, 0, cost));
edges.push_back(Edge(to, from, 0, 0, -cost));
G[from].push_back(edges.size() - 2);
G[to].push_back(edges.size() - 1);
}
bool spfa(int s, int t, int &flow, ll &cost)
{
memset(d, 127, sizeof(d));
memset(inq, 0, sizeof(inq));
d[s] = 0;
inq[s] = 1;
p[s] = 0;
a[s] = INF;
queue<int>Q;
Q.push(s);
while(!Q.empty())
{
int u = Q.front();
Q.pop();
inq[u] = 0;
for(int i = 0; i < (int)G[u].size(); i++)
{
Edge &e = edges[G[u][i]];
if(e.cap > e.flow && d[e.to] > d[u] + e.cost)
{
d[e.to] = d[u] + e.cost;
p[e.to] = G[u][i];
a[e.to] = min(a[u], e.cap - e.flow);
if(!inq[e.to])
{
Q.push(e.to);
inq[e.to] = 1;
}
}
}
}
if(d[t] >= INF) return false;
flow += a[t];
cost += (ll)d[t] * (ll)a[t];
for(int u = t; u != s; u = edges[p[u]].from)
{
edges[p[u]].flow += a[t];
edges[p[u] ^ 1].flow -= a[t];
}
return true;
}
pair<int,ll> solve(int s, int t)
{
int flow = 0; ll cost = 0;
while(spfa(s, t, flow, cost));
return {flow, cost};
}
}mcmf;
1/24_P2756 飞行员配对方案问题
一共有n(100)个人,其中有m个外国人,给定哪些本国人和外国人可以配对,问最多配多少对,并输出方案。
二分图最大匹配问题,输出答案
- 节点
- 源点:0
- 外国人:1->m
- 本国人:m+1->n
- 汇点:n+1
- 弧
- (源点,外国人,1)
- (外国人,本国人,1)
- (本国人,汇点,1)
- 最大流
求出的最大流即为最多的匹配对数。
外国人到本国人间流量为1的弧即为要输出的匹配对。
2/24_P4016 负载平衡问题
环形道路上有n(100)个仓库,每个仓库都存储着一定量的物资。每个仓库可以往周围的仓库搬运物资,求使得所有仓库存储量相同的最小搬运量。
最小费用最大流
- 节点
- 源点:0
- 仓库:1->n
- 汇点:n+1
- 弧
- (源点,仓库,本仓库初始存储量,0)
- (仓库,相邻仓库,INF,1)
- (仓库,汇点,平均存储量,0)
- 费用流
求出的最小费用即为最小搬运量
3/24_P2761 软件补丁问题
一款软件有n(20)个错误,现在有m(100)个补丁,
每个补丁需要在软件有某些特定错误,没有某些特定错误时才能使用。
每个补丁会给软件修复某些特定错误,会给软件添加某些特定错误。
每个补丁有自己的修复耗时。
问将软件的错误全部修复最少耗时多少,无解输出0.
虚假的网络流。
状压+dijkstra过。
4/24_P2765 魔术球问题
有n(55)根柱子,要依次把编号为1,2,3,…的球放在柱子上,每次只能放在柱子最上面且要保证同一柱子中相邻球编号之和均为完全平方数,问最多能放多少个球,输出方案。
反向思考,拆点,最大流
- 节点
- 源点:0
- 汇点:1
- 第 个球:左部分 ,右部分
- 弧
- (源点,左部分,1)
- (右部分,汇点,1)
- ( 的左部分, 的右部分,1)要求 且 为完全平方数
- 最大流
最大流表示把这些球放上去之后,能省下几根柱子。
即,设球数为b,最大流为mf,则(b-mf)根柱子可以容纳b个球。
依次枚举每个球数对应的柱子数,直到得到n个柱子最多放下的球数为止。
优化: 为了避免每次枚举都需要求网络流带来的时间浪费,每次求网络流前把所有弧的当前流置0,即可接着上一次的图直接插入再求。
输出方案: 从1到最大球数枚举,依次找到每个球的左部分连着哪个球的右部分,即可输出每个柱子上的答案。可以直接对着最后ans+1个球的网络图输出答案,注意判断边界即可。
int vis[2048]={};
for(int i=1; i<=ans; ++i) if(!vis[i])
{
for(int u=i,v; u; u=v)
{
printf("%d ",u );
vis[u] = 1;
v = 0;
for(auto eid : dinic.G[u*2]) if(dinic.edges[eid].flow==1)
{
v = dinic.edges[eid].to/2;
if(v>ans) v = 0;
break;
}
}
printf("\n");
}
5/24 P4011 孤岛营救问题
给一个n*m(10*10)的网格地图,网格之间可以连通、有墙或有门,一共有p(10)种门,对应于放在地图中的10把钥匙,每把钥匙可以开无限次门,问从(1,1)走到(n,m)的最短步数。
虚假的网络流,状压bfs过
6/24 P4009 汽车加油行驶问题
给一张n*n(100)的网格图,一辆汽车处于格点(1,1)处,有着容量为k单位的油箱,每走一步需要消耗1单位的油。问行驶到(n,n)处的最少花费,价目表如下:
- 网格中存在加油站,驶入加油站时强制加满油,花费为A元。
- 汽车往横纵坐标小的方向行驶1单位要花B元。
- 可以在格点处添加加油站,花费为C元。
分层图+费用流
参考链接: https://www.luogu.org/blog/I-love-saber/solution-p4009
- 层: 层,第 层表示还剩下多少油
- 节点:
- 源点:
- 汇点:
- 第 层的 :
- 弧:
- 有加油站:
第k层: 向下一层正向可达的点连容1费0的边,反向可达的点连容1费B的边.
非第k层: 向第k层连容1费A的边 - 无加油站
向下一层正向可达的点连容1费0的边,反向可达的点连容1费B的边.
非第k层: 向第k层连容1费A+C的边 - 源点向第k层[1,1]:容1费0
- 每层的[n,n]向汇点:容1费0
- 有加油站:
- 最小费用最大流:
最大流: 1
最小费用: 答案
MCMF mcmf;
inline int encode(int f, int r, int c)
{
return f*101*101 + r*101 + c;
}
int main(void)
{
int n=read(), k=read(), A=read(), B=read(), C=read();
int src = 11*101*101+1, dst = 11*101*101+2;
mcmf.addEdge(src, encode(k,1,1), 1, 0); //源边
for(int i=0; i<=k; ++i) //汇边
mcmf.addEdge(encode(i,n,n), dst, 1, 0);
const int go[4][2] = {{1,0},{0,1},{-1,0},{0,-1}};
for(int r=1; r<=n; ++r)
for(int c=1; c<=n; ++c)
{
int have = read();
for(int i=0; i<k; ++i) //加油
mcmf.addEdge(encode(i,r,c), encode(k,r,c), 1, have?A:A+C);
for(int dir=0; dir<4; ++dir)
{
int nr=r+go[dir][0], nc=c+go[dir][1];
if(nr>=1&&nr<=n&&nc>=1&&nc<=n)
for(int i=(have?k:1); i<=k; ++i) //走格,要求当前位置满油或者没有加油站
mcmf.addEdge(encode(i,r,c), encode(i-1,nr,nc), 1, dir>>1?B:0);
}
}
cout << mcmf.solve(src,dst).second << endl;
return 0;
}
似乎分层之后按最短路也可以做
7/24 P3254 圆桌问题
m(150)个有若干人的单位要在n(270)张可以容纳若干人的餐桌上聚餐,要求同一单位的人餐桌各不相同。输出方案。
最大流
- 节点:
- 源点: 0
- 单位: 1->m
- 餐桌: m+1->m+n
- 汇点: m+n+1
- 弧:
- (源点, 单位, 单位人数)
- (单位, 餐桌, 1)
- (餐桌, 汇点, 餐桌容量)
- 最大流:
- 最大流为所有单位总人数时, 表示成功安排了这些人
- 输出方案
8/24 P2763 试题库问题
一共有k(20)种试题类型,n道试题(1000),每道试题都属于若干个试题类型。现在要选出m道题且每道题都必须属于给定的一种试题类型,输出方案。
最大流
- 节点:
- 源点: 0
- 种类: 1->k
- 试题: k+1->k+n
- 汇点: k+n+1
- 弧:
- (源点, 种类, 种类所需数量)
- (种类, 对应试题, 1)
- (试题, 汇点, 1)
- 最大流:
- 可选试题数, 如果等于目标题数则成功组卷
- 输出方案
9/24 P2764 最小路径覆盖问题
给定一张有向图,求最小路径覆盖,输出方案。
最小路径覆盖是指一个最小的有向路径集合,且图中的每个节点恰好在其中的一条路径上(路径长度可以为0,即只包含1个点)。
和魔术球问题有些相似,最坏情况的路径数等于节点数,通过将拆点连弧获得的最大流即为最多可以合并几条路径。拿节点数一减就是答案。
拆点最大流
- 节点:
- 源点0, 汇点1
- 图中第i个点: 左部分i2, 右部分i2+1
- 弧:
- (源点,左部分点,1)
- (右部分点,汇点,1)
- (i的左部分,j的右部分,1),前提是原图中i到j有一条有向边
- 最大流:
- (点数-最大流)即为最小路径覆盖
- 输出答案
输出答案时,可以依次遍历未被访问的所有点,首先沿着反向弧找到路径起点,再依次输出并标记访问过即可。
10/24 P4014 分配问题
有n(100)个工作要分配给n个人,已知每个人做每个工作的收益,分别求最小和最大的时间和。
费用流
- 节点:
- 源点: 0
- 人: 1->n
- 工件: n+1->2n
- 汇点: 2n+1
- 弧:
- (源点, 人, 1, 0)
- (人, 工件, 1, 价值(求最大效益则为正,反之则为负))
- (工件, 汇点, 1, 0)
这道题告诉我们,费用是负数时,费用流也可以很好的求出结果。
11/24 P2762 太空飞行计划问题
有m个实验和n个仪器,做实验会有收入,采购仪器需要花钱,仪器可以重复使用。每个实验依赖于若干不同的器材,求做哪些实验可以获得最大净收益。
最小割模型:最大权闭合子图
- 节点:
- 源点: 0
- 实验: 1->m
- 仪器: m+1->m+n
- 汇点: m+n+1
- 弧:
- (源点, 实验, 实验赞助)
- (实验, 仪器, INF)
- (仪器, 汇点, 仪器花费)
- 最小割:
- 最大净收益 = 所有实验收益之和-最小割
- 输出方案
对上述网络跑最小割,显然不会割去中间的INF边,所以此时的S集合即为可用的(实验,仪器)组合。注意最小割的数值里包括了(1.不选择的实验收入,2.选择的仪器花费)。此时拿所有实验的总收入减去最小割的数值,就是(1. 选择的实验收入,2.选择的仪器花费的相反数),即为答案。
输出方案即输出除源点外的S集合,这里有一个待研究的性质:dinic最后一次bfs后,vis为1的节点集合即为最小割的S集合。
这题输入有些烦,列代码如下:
Dinic dinic;
int main(void)
{
int m, n; scanf("%d %d\n", &m, &n);
int src=0, dst=m+n+1, sum=0;
for(int exp=1; exp<=m; ++exp)
{
string line; getline(cin, line);
stringstream sin(line);
int earn; sin>>earn; sum+=earn;
int inses=0, ins;
while(sin >> ins)
{
++inses;
dinic.addEdge(exp, ins+m, INF);
}
dinic.addEdge(src, exp, earn);
}
for(int i=m+1; i<=m+n; ++i)
{
int cost = read();
dinic.addEdge(i, dst, cost);
}
int ans = sum-dinic.maxflow(src, dst);
for(int i=1; i<=m; ++i) if(dinic.vis[i])
printf("%d ",i );
printf("\n");
for(int i=m+1; i<=m+n; ++i) if(dinic.vis[i])
printf("%d ",i-m );
printf("\n");
cout << ans << endl;
return 0;
}