【题解】网络流24题 24/24

前言

题目链接:洛谷-网络流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个外国人,给定哪些本国人和外国人可以配对,问最多配多少对,并输出方案。

二分图最大匹配问题,输出答案

  1. 节点
    1. 源点:0
    2. 外国人:1->m
    3. 本国人:m+1->n
    4. 汇点:n+1
    1. (源点,外国人,1)
    2. (外国人,本国人,1)
    3. (本国人,汇点,1)
  2. 最大流
    求出的最大流即为最多的匹配对数。
    外国人到本国人间流量为1的弧即为要输出的匹配对。

2/24_P4016 负载平衡问题

环形道路上有n(100)个仓库,每个仓库都存储着一定量的物资。每个仓库可以往周围的仓库搬运物资,求使得所有仓库存储量相同的最小搬运量。

最小费用最大流

  1. 节点
    1. 源点:0
    2. 仓库:1->n
    3. 汇点:n+1
    1. (源点,仓库,本仓库初始存储量,0)
    2. (仓库,相邻仓库,INF,1)
    3. (仓库,汇点,平均存储量,0)
  2. 费用流
    求出的最小费用即为最小搬运量

3/24_P2761 软件补丁问题

一款软件有n(20)个错误,现在有m(100)个补丁,
每个补丁需要在软件有某些特定错误,没有某些特定错误时才能使用。
每个补丁会给软件修复某些特定错误,会给软件添加某些特定错误。
每个补丁有自己的修复耗时。
问将软件的错误全部修复最少耗时多少,无解输出0.

虚假的网络流。
状压+dijkstra过。

4/24_P2765 魔术球问题

有n(55)根柱子,要依次把编号为1,2,3,…的球放在柱子上,每次只能放在柱子最上面且要保证同一柱子中相邻球编号之和均为完全平方数,问最多能放多少个球,输出方案。

反向思考,拆点,最大流

  1. 节点
    1. 源点:0
    2. 汇点:1
    3. i i i个球:左部分 i ∗ 2 i*2 i2,右部分 i ∗ 2 + 1 i*2+1 i2+1
    1. (源点,左部分,1)
    2. (右部分,汇点,1)
    3. i i i的左部分, j j j的右部分,1)要求 i < j i<j i<j i + j i+j i+j为完全平方数
  2. 最大流
    最大流表示把这些球放上去之后,能省下几根柱子。
    即,设球数为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)处的最少花费,价目表如下:

  1. 网格中存在加油站,驶入加油站时强制加满油,花费为A元。
  2. 汽车往横纵坐标小的方向行驶1单位要花B元。
  3. 可以在格点处添加加油站,花费为C元。

分层图+费用流
参考链接: https://www.luogu.org/blog/I-love-saber/solution-p4009

  1. 层: 0 − > k 0->k 0>k层,第 i i i层表示还剩下多少油
  2. 节点:
    1. 源点: 11 ∗ 101 ∗ 101 + 1 11*101*101+1 11101101+1
    2. 汇点: 11 ∗ 101 ∗ 101 + 2 11*101*101+2 11101101+2
    3. f f f层的 [ r , c ] [r,c] [r,c]: f ∗ 101 ∗ 101 + r ∗ 101 + c f*101*101+r*101+c f101101+r101+c
  3. 弧:
    1. 有加油站:
      第k层: 向下一层正向可达的点连容1费0的边,反向可达的点连容1费B的边.
      非第k层: 向第k层连容1费A的边
    2. 无加油站
      向下一层正向可达的点连容1费0的边,反向可达的点连容1费B的边.
      非第k层: 向第k层连容1费A+C的边
    3. 源点向第k层[1,1]:容1费0
    4. 每层的[n,n]向汇点:容1费0
  4. 最小费用最大流:
    最大流: 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)张可以容纳若干人的餐桌上聚餐,要求同一单位的人餐桌各不相同。输出方案。

最大流

  1. 节点:
    1. 源点: 0
    2. 单位: 1->m
    3. 餐桌: m+1->m+n
    4. 汇点: m+n+1
  2. 弧:
    1. (源点, 单位, 单位人数)
    2. (单位, 餐桌, 1)
    3. (餐桌, 汇点, 餐桌容量)
  3. 最大流:
    1. 最大流为所有单位总人数时, 表示成功安排了这些人
    2. 输出方案

8/24 P2763 试题库问题

一共有k(20)种试题类型,n道试题(1000),每道试题都属于若干个试题类型。现在要选出m道题且每道题都必须属于给定的一种试题类型,输出方案。

最大流

  1. 节点:
    1. 源点: 0
    2. 种类: 1->k
    3. 试题: k+1->k+n
    4. 汇点: k+n+1
  2. 弧:
    1. (源点, 种类, 种类所需数量)
    2. (种类, 对应试题, 1)
    3. (试题, 汇点, 1)
  3. 最大流:
    1. 可选试题数, 如果等于目标题数则成功组卷
    2. 输出方案

9/24 P2764 最小路径覆盖问题

给定一张有向无环图,求最小路径覆盖,输出方案。
最小路径覆盖是指一个最小的有向路径集合,且图中的每个节点恰好在其中的一条路径上(路径长度可以为0,即只包含1个点)。

和魔术球问题有些相似,最坏情况的路径数等于节点数,通过将拆点连弧获得的最大流即为最多可以合并几条路径。拿节点数一减就是答案。
拆点最大流

  1. 节点:
    1. 源点0, 汇点1
    2. 图中第i个点: 左部分i2, 右部分i2+1
  2. 弧:
    1. (源点,左部分点,1)
    2. (右部分点,汇点,1)
    3. (i的左部分,j的右部分,1),前提是原图中i到j有一条有向边
  3. 最大流:
    1. (点数-最大流)即为最小路径覆盖
    2. 输出答案

输出答案时,可以依次遍历未被访问的所有点,首先沿着反向弧找到路径起点,再依次输出并标记访问过即可。

10/24 P4014 分配问题

有n(100)个工作要分配给n个人,已知每个人做每个工作的收益,分别求最小和最大的时间和。

费用流

  1. 节点:
    1. 源点: 0
    2. 人: 1->n
    3. 工件: n+1->2n
    4. 汇点: 2n+1
  2. 弧:
    1. (源点, 人, 1, 0)
    2. (人, 工件, 1, 价值(求最大效益则为正,反之则为负))
    3. (工件, 汇点, 1, 0)

这道题告诉我们,费用是负数时,费用流也可以很好的求出结果。

11/24 P2762 太空飞行计划问题

有m个实验和n个仪器,做实验会有收入,采购仪器需要花钱,仪器可以重复使用。每个实验依赖于若干不同的器材,求做哪些实验可以获得最大净收益。

最小割模型:最大权闭合子图

  1. 节点:
    1. 源点: 0
    2. 实验: 1->m
    3. 仪器: m+1->m+n
    4. 汇点: m+n+1
  2. 弧:
    1. (源点, 实验, 实验赞助)
    2. (实验, 仪器, INF)
    3. (仪器, 汇点, 仪器花费)
  3. 最小割:
    1. 最大净收益 = 所有实验收益之和-最小割
    2. 输出方案

对上述网络跑最小割,显然不会割去中间的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;
}

12/24 P4015 运输问题

有m个存有货物的仓库和n个需要货物的商店,给定每个仓库向每个商店运输的单位花费,求最小的总运输花费。

费用流裸题。

13/24 P2774 方格取数问题

m*n的网格中存放着数字,现在要从方格中取数,使得数字之和最大且任意两个被选中的方格没有相邻边。

最小割模型:二分图最大权独立集
弧:
(源点,偶数点,点权)
(偶数点,相邻奇数点,INF)
(奇数点,汇点,点权)
答案 = 所有点的点权之和 - 最小割。

14/24 P2766 最长不下降子序列问题

给定长为n(500)的数字序列,求

  1. 最长不下降子序列长度s
  2. 原序列可以抽出多少个长为s的不下降子序列
  3. 当x1和xn可以多次使用时,原序列中可以抽出多少个长为s的不下降子序列。

第一问
首先使用O(n^2)或者O(nlogn)方法求得LIS长度,并且记录以每个位置为结尾的最长不下降子序列长度dp[i]。

第二问
建立网络图:
(源点,dp为1的点,1)
(i,i的后继,1),节点 j j j i i i的后继当且仅当 j > i j>i j>i s a v e [ j ] > s a v e [ i ] save[j]>save[i] save[j]>save[i] d p [ j ] = d p [ i ] + 1 dp[j]=dp[i]+1 dp[j]=dp[i]+1
(dp为s的点,汇点,1)
最大流即为答案。

第三问
建立网络图:
(源点,dp为1的点,i==1?INF:1
(i,i的后继,1)
(dp为s的点,汇点,i==n?INF:1
最大流即为答案。

第三问有坑点,需要特判n=1的情况。

15/24 P3358 最长k可重区间集问题

给定一条实直线,n(500)个直线上开端点的线段,一个正整数k。求一个线段集合,使得集合中的线段在直线上的覆盖次数不超过k,且集合中线段长度之和最大。

费用流经典题:流量控制

  1. 将直线上的点离散化成1到2n,从头到尾连接起来,容量为k,费用为0。
  2. 每条线段起止点连接起来,容量为1,费用为线段长度。

此时最大费用即是答案。

这道题中用主干流上的流量来表示还可以被覆盖的次数,每次脱离主干流时流量减一费用加上对应权值,汇入主干流后流量又增加。

16/24 P4012 深海机器人问题

给定一个P*Q的网格,每段网格边有对应的价值,现在在几个选定位置释放机器人,每个位置有固定的释放数量。每个机器人只能向东或向南走,每段价值只能收集一次。有几个选定位置可以收集机器人,有固定的收集数量。问最终收集的最大价值。

多源多汇费用流
有一个经典做法可以处理只能收集一次的价值:建立两组边
(a,b,1,value)
(a,b,0,INF)

17/24 P3355 骑士共存问题

在n*n的棋盘上有m个障碍,计算棋盘上最多可以放多少个骑士(马)使得它们不能互相攻击。(国际象棋不蹩马腿。)

可以证明,棋盘上的格子及互相攻击的关系构成了一张二分图。
最小割模型:二分图最大独立集
因为没有涉及到权,只需要对(源点,集合A),(集合A,相连的集合B),(集合B,汇点)都建立容量为1的边即可。
答案 = 总的可用点数 - 最小割。

18/24 P3357 最长k可重线段集问题

给定一个平面上的n条开线段,一个正整数k。求一个线段集合,使得集合中线段在x轴上的投影覆盖次数不超过k,且集合中线段长度最大。

费用流:流量控制,分点
类似于P3358 最长k可重区间集问题,但是会有垂直于x轴的线段的情况。
所以要把直线上每个点分成左部分和右部分,垂直于x轴的线段从一个点的左部分连到右部分。其它线段从左端点的右部分连道右端点的左部分。

19/24 P1251 餐巾计划问题

餐厅在未来的N天内需要不尽相同的餐巾数量,餐巾使用一次就会脏。
1)在需要时可以购买餐巾,每条花费为p。
2)可以把脏餐巾送去快洗,快洗每条花费为f,需要m天洗好。
3)可以把脏餐巾送去慢洗,慢洗每条花费为s,需要n天洗好。
4)脏餐巾可以暂时不洗。

求总的最小花费。

费用流:分点,反向源汇边(?)
餐巾计划问题-费用流

  1. 节点:
    1. 源点: 0
    2. 汇点: 1
    3. 第i天的早晚: i2, i2+1
  2. 弧:
    1. (源点, 晚上, 当天脏毛巾, 0) //使用
    2. (早上, 汇点, 当天所需毛巾, 0) //需求
    3. (源点, 早上, INF, 购买毛巾所花费用P) //购买
    4. (第i天晚上, 第i+m天早上, INF, f) //快洗
    5. (第i天晚上, 第i+n天早上, INF, s) //慢洗
    6. (第i天晚上, 第i+1天晚上, INF, 0) //不洗
  3. 最小费用最大流:
    最小费用即为答案

这道题分点好想,但是分点之后无法进行建图。
把每一天分成早上和晚上,正确的做法是从源点向晚上连边,表示获得了脏餐巾,从早上向汇点连边,表示消耗了干净餐巾。

一个神奇的模型。

20/24 P4013 数字梯形问题

给定一个n行,第一行有m个数字的数字梯形。现在要从顶部m个数字开始找到m条路径,求分别满足如下规则时的最大m条路径上数字之和。
【题解】网络流24题 24/24_第1张图片
1)m条路径互不相交。
2)数字节点处可以相交,边不能相交。
3)数字节点和边都可以相交。

费用流:分点,多模型
因为数字上有价值,自然考虑分点,上部分与上层节点相连,下部分与下层节点相连,均无费用,称为外部弧。数字内部上部分与下部分节点相连,称为内部弧,有费用(价值)。

  1. 问题一:外部弧与内部弧容量均为1.
  2. 问题二:外部弧容量为1,内部弧容量INF。
  3. 问题三:内部弧与外部弧容量均为INF。

注意,在三个子问题中源点向顶层节点上部分的弧容量总是1.
在问题1中,底层节点下部分向汇点的弧容量是1,问题2和3中是INF。

分别求最大费用即可。

放一下自认为写的不错的代码:

MinCostMaxFlow pro1, pro2, pro3;
inline int upp(int i, int j){return (i*64+j)*2;}
inline int dow(int i, int j){return (i*64+j)*2+1;}
int main(void)
{
	#ifdef _LITTLEFALL_
	freopen("in.txt","r",stdin);
    #endif

	int m=read(), n=read();

	int src=0, dst=1;
	for(int i=1; i<=n; ++i) for(int j=1; j<=m+i-1; ++j)
	{
		if(i==1) //源边
		{
			pro1.addEdge(src, upp(i,j), 1, 0);
			pro2.addEdge(src, upp(i,j), 1, 0);
			pro3.addEdge(src, upp(i,j), 1, 0);
		}

		int v=read(); //拆点边
		pro1.addEdge(upp(i,j), dow(i,j), 1, -v);
		pro2.addEdge(upp(i,j), dow(i,j), INF, -v);
		pro3.addEdge(upp(i,j), dow(i,j), INF, -v);

		for(int nj:{j,j+1})	//下层边
		{
			pro1.addEdge(dow(i,j), upp(i+1, nj), 1, 0);
			pro2.addEdge(dow(i,j), upp(i+1, nj), 1, 0);
			pro3.addEdge(dow(i,j), upp(i+1, nj), INF, 0);
		}

		if(i==n) //汇边
		{
			pro1.addEdge(dow(i,j), dst, 1, 0);
			pro2.addEdge(dow(i,j), dst, INF, 0);
			pro3.addEdge(dow(i,j), dst, INF, 0);
		}
	}
	cout << -pro1.solve(src,dst).second << endl;
	cout << -pro2.solve(src,dst).second << endl;
	cout << -pro3.solve(src,dst).second << endl;
    return 0;
}

21/24 P3356 火星探险问题

在n*m的网格中,每个网格要么是空地,要么是障碍,要么是具有价值的资源。现在在(1,1)放下P个机器人,每个机器人只能朝南或东移动,机器人可以在(n,m)被回收。要求在机器人回收数量最多的前提下收集到的资源价值最大。输出所有机器人的路径。

费用流:分点,一次价值,输出路径
因为是节点有价值,所以要分点。
因为价值只能收集一次,所以建立两条边(1,v)和(INF,0)。
输出路径可以考虑dfs,每次走一步就给边上流量减一。

22/24 P2770 航空路线问题

给定一张图,找到一条从最左侧点到最右侧点再回到最左侧点的经过节点最多的路径。输出答案。

费用流:分点,输出路径
模型类似于[P4013 数字梯形问题]的问题一。

当最大流大于等于2时有解,输出路径也可采用dfs。
注意处理1->n->1的情况。

这道题还涉及字符串与数字的转化,码量较大。

MinCostMaxFlow mcmf;

struct Discretization
{
	vector<string> save;
	map<string, int> dcter;
	inline int get(string name) 
	{
		if(dcter.count(name))
			return dcter[name];
		save.push_back(name);
		return dcter[name] = save.size();
	}
	inline string get(int id) {return save[id-1];}
}dct; //将城市名映射为1-n的数字
/*
费用流
节点: 
	源点: 城市1的左部分
	汇点: 城市n的右部分
	城市i: 左部分i*2, 右部分i*2+1
弧:
	(城市左部分, 城市右部分, 1, -1), 城市为1或n时容量为2
	(左边城市右部分, 右边城市左部分, INF, 0)
最小费用最大流:
	最大流>=2, 表示有解.
	输出路径
*/
inline int lef(int i){return i*2;}
inline int rig(int i){return i*2+1;}
int main(void)
{
	#ifdef _LITTLEFALL_
	freopen("in.txt","r",stdin);
    #endif

	int n=read(), v=read();

	for(int i=1; i<=n; ++i)
	{
		string name; cin>>name; dct.get(name);
		mcmf.addEdge(lef(i), rig(i), i==1||i==n?2:1, -1);
	}
	while(v--)
	{
		string city1, city2;
		cin >> city1 >> city2;
		int c1 = dct.get(city1), c2=dct.get(city2);
		if(c1>c2) swap(c1, c2);
		mcmf.addEdge(rig(c1), lef(c2), INF, 0);
	}
	auto res = mcmf.solve(lef(1), rig(n));
	if(res.first<2)
	{
		printf("No Solution!\n" );
		return 0;
	}

	vector<int> ans[2];
	for(int i=0; i<2; ++i)
	{
		int now = 1;
		while(now!=n)
		{
			for(int eid : mcmf.G[rig(now)]) if(mcmf.edges[eid].flow>0)				
			{
				--mcmf.edges[eid].flow;
				ans[i].push_back(now);
				now = mcmf.edges[eid].to/2;
				break;
			}	
		}
	}
	ans[0].push_back(n);
	ans[0].insert(ans[0].end(),ans[1].rbegin(), ans[1].rend());
	cout << ((int)ans[0].size()-1) << endl;
	for(int c : ans[0])
	{
		cout << dct.get(c) << endl;
	}



    return 0;
}

23/24 P2754 [CTSC1999]家园

有n(13)个空间站,编号为1到n。此外,地球的编号为0,月球编号为-1。现在有m(20)个宇宙飞船,每个都有一定容量,且在某几个空间站或星球之间周期性移动。现在有k(50)个人在地球上,需要被送到月球上。求最小的时间。

按时间分层的最大流算法(zht算法)
整体思路是使用并查集判断是否有解,再按时间分层建图,然后枚举时间在残量网络中跑最大流看什么时候最大流大于等于人数,当前时间就是答案。

  1. 层: 0 0 0 t t t层,表示时间,每层有 ( n + 2 ) (n+2) (n+2)个点数.
  2. 节点:
    • 源点: 0 0 0
    • 汇点: ( n + 2 ) ∗ t + n + 1 (n+2)*t+n+1 (n+2)t+n+1(第t层的月球)
    • 第f层地球: ( n + 2 ) ∗ f + 0 (n+2)*f+0 (n+2)f+0
    • 第f层空间站i: ( n + 2 ) ∗ f + i (n+2)*f+i (n+2)f+i
    • 第f层月球: ( n + 2 ) ∗ f + n + 1 (n+2)*f+n+1 (n+2)f+n+1.
  3. 弧:
    • ( t t t层节点, t + 1 t+1 t+1层对应节点, INF)
    • ( t t t层节点, t t t时刻飞船路径到达的节点, 飞船容量)
  4. 最大流:
    • 满足最大流大于等于初始人数的最小时间 t t t即为答案

需要注意的是,因为没有单独建源点和汇点,跑出的最大流可能会恰大于人数k,如果以等于人数k去判断,会TLE到爆炸(血泪教训)。

因为是实际意义上的最后一篇,放一下代码。


需要C++11

1. dinic板子

可以调用的函数一共有两个,分别是添加边的addEdge和求最大流的maxflow。
addEdge会自动添加反向弧。

struct Dinic
{
    void addEdge(int from, int to, int cap);
    int maxflow(int _s, int _t);
}dinic;

需要两个全局常数 const int M = 100016, INF = 1000000007;

2. 飞船

get_path可以接收天数,返回某条弧的起始点。

struct SpaceShip
{
	int limit; //人数限制
	int cycle; //周期
	vector<int> path; //路径
	pair<int,int> get_path(int day)
	{
		return {path[day%cycle], path[(day+1)%cycle]};
	}
}ship[32];

3. 并查集

为了减少全局变量和全局函数强行struct。

struct UnionFindSet
{
	int fa[20]={};
	void init()
	{
		for(int i=1;i<20;++i)
			fa[i] = i;
	}
	int find(int a)
	{
		return fa[a]==a ? a : fa[a]=find(fa[a]);
	}
	void add(int a, int b)
	{
		fa[find(a)] = find(b);
	}
	bool check(int a, int b)
	{
		return find(a) == find(b);
	}
}ufs;

4. 主干代码: 读入与判断无解

	int n=read(), m=read(), k=read(); //太空站数,太空船数,人数
	ufs.init();
	for(int i=0; i<m; ++i)
	{
		ship[i].limit = read();
		ship[i].cycle = read();
		for(int j=0; j<ship[i].cycle; ++j)
		{
			int star=read();
			if(star==-1) star=n+1; //将月球的-1变成n+1
			ship[i].path.push_back(star);
			ufs.add(star, ship[i].path[0]);
		}
	}
	if(!ufs.check(0,n+1))
	{
		printf("0\n");
		return 0;
	}

5. 主干代码:枚举答案

	for(int day=1, maxflow=0; ; ++day)
	{
		int off1 = (day-1)*(n+2), off2 = day*(n+2); //上一层和本层的偏移量
		for(int i=0; i<=n+1; ++i)
			dinic.addEdge(off1+i, off2+i, INF); //不转移
		for(int i=0, s, t; i<m; ++i)
		{
			tie(s,t) = ship[i].get_path(day-1);;
			dinic.addEdge(off1+s, off2+t, ship[i].limit);  //转移
		}
		maxflow += dinic.maxflow(0, off2+n+1);
		if(maxflow>=k)
		{
			printf("%d\n",day );
			return 0;
		}
	}

除了这些之外,还有一个快读板子,因为和题目关系不大,就不单独列出了。

完整代码

/* LittleFall : Hello! */
#include 
using namespace std; typedef long long ll; inline int read();
const int M = 100016, INF = 1000000007;

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;

/* 按时间分层的最大流算法
层: 表示时间,从0开始,每层有(n+2)的偏移.
节点: 
	源点: 0 
	汇点: (n+2)*t+n+1
	第f层地球: (n+2)*f+0
	第f层空间站i: (n+2)*f+i
	第f层月球: (n+2)*f+n+1.
弧:
	(t层节点, t+1层对应节点, INF)
	(t层节点, t时刻飞船路径到达的节点, 飞船容量)
最大流:
	满足最大流等于初始人数的最小时间t即为答案
*/
struct SpaceShip
{
	int limit;
	int cycle;
	vector<int> path;
	pair<int,int> get_path(int day)
	{
		return {path[day%cycle], path[(day+1)%cycle]};
	}
}ship[32];
struct UnionFindSet
{
	int fa[20]={};
	void init()
	{
		for(int i=1;i<20;++i)
			fa[i] = i;
	}
	int find(int a)
	{
		return fa[a]==a ? a : fa[a]=find(fa[a]);
	}
	void add(int a, int b)
	{
		fa[find(a)] = find(b);
	}
	bool check(int a, int b)
	{
		return find(a) == find(b);
	}
}ufs;
int main(void)
{
	#ifdef _LITTLEFALL_
	freopen("in.txt","r",stdin);
    #endif

	int n=read(), m=read(), k=read(); //太空站数,太空船数,人数
	ufs.init();
	for(int i=0; i<m; ++i)
	{
		ship[i].limit = read();
		ship[i].cycle = read();
		for(int j=0; j<ship[i].cycle; ++j)
		{
			int star=read();
			if(star==-1) star=n+1;
			ship[i].path.push_back(star);
			ufs.add(star, ship[i].path[0]);
		}
	}
	if(!ufs.check(0,n+1))
	{
		printf("0\n");
		return 0;
	}

	for(int day=1, maxflow=0; ; ++day)
	{
		int off1 = (day-1)*(n+2), off2 = day*(n+2); //上一层和本层的偏移量
		for(int i=0; i<=n+1; ++i)
			dinic.addEdge(off1+i, off2+i, INF); //不转移
		for(int i=0, s, t; i<m; ++i)
		{
			tie(s,t) = ship[i].get_path(day-1);;
			dinic.addEdge(off1+s, off2+t, ship[i].limit);  //转移
		}
		maxflow += dinic.maxflow(0, off2+n+1);
		if(maxflow>=k)
		{
			printf("%d\n",day );
			return 0;
		}
	}
    return 0;
}


inline int read(){
    int x=0,f=1;char ch=getchar();
    while(ch<'0'||ch>'9') {if(ch=='-')f=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
    return x*f;
}

24/24 P2775 机器人路径规划问题

假题,过。
【题解】网络流24题 24/24_第2张图片

你可能感兴趣的:(题解,精选)