【做练习】ACM Computer Factory(网络流 Dinic算法)

题目

描述

As you know, all the computers used for ACM contests must be identical, so the participants compete on equal terms. That is why all these computers are historically produced at the same factory.
Every ACM computer consists of P parts. When all these parts are present, the computer is ready and can be shipped to one of the numerous ACM contests.
Computer manufacturing is fully automated by using Nvarious machines. Each machine removes some parts from a half-finished computer and adds some new parts (removing of parts is sometimes necessary as the parts cannot be added to a computer in arbitrary order). Each machine is described by its performance (measured in computers per hour), input and output specification.
Input specification describes which parts must be present in a half-finished computer for the machine to be able to operate on it. The specification is a set of Pnumbers 0, 1 or 2 (one number for each part), where 0 means that corresponding part must not be present, 1 — the part is required, 2 — presence of the part doesn’t matter.
Output specification describes the result of the operation, and is a set of Pnumbers 0 or 1, where 0 means that the part is absent, 1 — the part is present.
The machines are connected by very fast production lines so that delivery time is negligibly small compared to production time.
After many years of operation the overall performance of the ACM Computer Factory became insufficient for satisfying the growing contest needs. That is why ACM directorate decided to upgrade the factory.
As different machines were installed in different time periods, they were often not optimally connected to the existing factory machines. It was noted that the easiest way to upgrade the factory is to rearrange production lines. ACM directorate decided to entrust you with solving this problem.

输入

Input file contains integers P N, then N descriptions of the machines. The description of ith machine is represented as by 2 P + 1 integers Qi Si,1 Si,2…Si,P Di,1 Di,2…Di,P, where Qi specifies performance, Si,j — input specification for part j, Di,k — output specification for part k.
Constraints
1 ≤ P ≤ 10, 1 ≤ N ≤ 50, 1 ≤ Qi ≤ 10000

输出

Output the maximum possible overall performance, then M — number of connections that must be made, then M descriptions of the connections. Each connection between machines A and B must be described by three positive numbers A B W, where W is the number of computers delivered from A to B per hour.
If several solutions exist, output any of them.

样例输入

Sample input 1
3 4
15 0 0 0 0 1 0
10 0 0 0 0 1 1
30 0 1 2 1 1 1
3 0 2 1 1 1 1
Sample input 2
3 5
5 0 0 0 0 1 0
100 0 1 0 1 0 1
3 0 1 0 1 1 0
1 1 0 1 1 1 0
300 1 1 2 1 1 1
Sample input 3
2 2
100 0 0 1 0
200 0 1 1 1

样例输出

Sample output 1
25 2
1 3 15
2 3 10
Sample output 2
4 5
1 3 3
3 5 3
1 2 1
2 4 1
4 5 1
Sample output 3
0 0

翻译一下

工厂要生产电脑,电脑由P个零件组成,进厂时没有零件,出厂时装配好所有零件。
现在工厂里有N台机器。每台机器具有各自的接受某种中间产品,并以指定的效率把输入的中间产品转变成另一种中间产品。输入和输出的产品可以用输入标准输出标准来描述:

  • 输入标准是一个长为P的由{0, 1, 2}组成的串,每个数字对应一个零件。其中0表示输入的中间产品不可具有这个零件,1表示输入的中间产品必须具有这个零件,2则表示有没有这个零件都可以。特别地,如果输入标准全是0和2,则这个机器无需输入而就可以凭空产出。
  • 输出标准是一个长为P的由{0, 1, 2}组成的串,每个数字对应一个零件。其中0表示输出的中间产品没有这个零件,1表示输出的中间产品具有这个零件。特别地,如果输出标准全是1,说明这个机器生产出了完成品。

现在,你需要协调这些机器,恰当地让其中一些机器的产出作为另外一些机器的输入,使得工厂生产出成品电脑的效率最高。你需要输出最高效率和机器之间如何配合。


分析

很明显则是一道网络流题,我们把中间产品作为流量,可以构造网络流。随后使用Dinic算法计算最大流即可。

网络流

如果我们给有向图的边加上“容量”,想象水流从这些边构成的“水管”中流通,边上水流不得大于容量限制,这就构成了所谓的网络流。
在网络流中,存在一个“源点”,可以无限地产生水流;存在一个“汇点”,可以无限地接受水流;而其它节点的输入流量和输出流量相同;网络流地总流量 = 源点输出的流量和 = 汇点输入的流量和。

Ford-Fulkerson算法

这是一种比较基础的计算网络流的算法,它效率稍微低一些,但是能正确算出最大流。

它不断从源点使用深度优先搜索到汇点仍然有剩余容量的路径。计算这条路径上的剩余容量时,应采取木桶原理,取最小剩余容量的边。

这样的仍然具有剩余容量的路径,就称为可增广路径,汇收集到这个路径上的剩余容量,路径上的每条边的容量扣除这个流量。此外,还要给路径的反向路径上增加同等的容量。这两步合在一起就称为增广。残余的流量和增添的反向容量称为残余网络。添加反向容量的目的是,允许后续的搜索通过反向路径达到汇点(通过反向路径的流量,相当于抵消先前找到的正向路径流量),这样才能确保找到的流量最终是最大流。

反复执行以上两步,不断在残余网络基础上进行增广,直到在残余网络中已不存在源到达汇的路径了。这是,原网络流减去残余网络得到的差就是最大流了。

Dinic快速网络流算法

Dinic是一种高效的计算最大流的算法。它通过在残余网络的基础上,通过分层来提高深度优先搜索的效率。

  1. 分层通过BFS来实现。在残余网络上,我们从源点开始进行广度优先搜索,标记每个节点在广度优先搜索树中的深度(标记到了汇点即可停止,剩下还未标记的节点可以视为“无效”,不参与我们下一步的搜索)。将这样的网络称为分层辅助网络
  2. 随后,我们仍然通过深度优先搜索来找可增广路径,但是,我们规定:路径必须具有逐一递增的深度——也就是从一个节点搜索到下一节点时,另一节点必须具有恰巧 加1 的深度。
  3. 当我们找到可增广路径后,进行增广。增广后回溯,回溯到原增广路径上恰巧具有最小剩余容量的那个,继续深度搜索。
  4. 当所有合法路径都被搜索完成后,重新通过BFS分层再次搜索,直到残余网络不存在源到汇的路径了。

(实际上上面只是Dinic算法的一种形式,并且是我认为比较简单的一种形式。Dinic算法还有很多表述方式,但其实背后原理和做法都是一样的)

此题题解

这一题关键在于如何把各种机器之间的配合抽象成一个网络流。

显然,不能把每个机器看成一个节点。因为机器的效率限制的是机器输入边或输出边的总流量和。也就是说,如果我们把每个机器看作一个节点,那么机器效率限制的是节点流量。然而网络流中,容量限制的是边上的流量,这显然是不一样的。

那应该怎么做呢?
其实很简单,我们把每个机器看成两个节点就好了,一个是机器的输入节点,另一个是机器的输出节点机器的装配效率即为输入和输出节点之间具有容量

那不同机器之间的连边该如何构造呢?

  • 如果一个机器的输出产品符合另一机器的输入标准,那么从前一机器的输出节点到另一机器的输出节点存在一条连边,连边的容量为无限大。
  • 再另外增加一个源和汇节点。源到所有接受空输入的机器的输入节点都有无限容量的连边;所有输出完全产品的机器都到汇有无限容量连边。

这样,我们就能使用Dinic网络流算法了。


代码实现

我实现了一个通用的网络流数据结构,并在此基础上实现Dinic算法。大家完全可以把其中这个网络流数据结构用在其它题目上,并可灵活修改应用于网络流的一些变种。

【大家可能觉得我的代码比较冗长,但我是实现了完整的通用的网络流类数据结构。我习惯了面向对象开发,写东西都喜欢先建立一些工具库,对代码风格也比较严格,这样可读性高、结构性好,便于代码合作,大家也能更好看懂Dinic算法,并且已后遇到网络流也可以直接拿来用而不需要重新写东西了。
当然,如果是闭卷上机考试,还是最好依据原理快速实现,没必要像我这么“产品化”。】

#include <iostream>
#include <stdio.h>
#include <vector>
#include <queue>
#include <deque>
#include <map>
#include <memory.h>

# define MAXINT 0x7FFFFFF

#pragma warning(disable: 4996) //make Visual Studio happy

/*
 * 以下定义了一个标准的网络流数据结构,并且实现了Dinic快速网络流算法。
 */


template<typename T>
class NetworkFlow
{
	class Node {
	public:
		std::map<int, T> outs_org; // 原图中的容量
		std::map<int, T> outs_res; // 残余网络中的容量
		int depth;
	};

private:
	// 源
	int src;
	// 汇
	int term;

	// 允许的边的最大容量
	T maxEdgeCapacity;

	std::vector<Node> nodes;

	// 计算每一个节点在广度优先搜索的第几层,返回汇点是否可达
	bool RecalculateLayers()
	{
		for (int i = 0; i < nodes.size(); i++) nodes[i].depth = -1;
		
		std::queue<int> q;
		nodes[src].depth = 0; q.push(src);
		
		while (q.empty() == false) {
			if (q.front() == term) return true;

			Node& front = nodes[q.front()];
			for (auto pair : front.outs_res){
				if (pair.second > 0 && nodes[pair.first].depth == -1) {
					nodes[pair.first].depth = front.depth + 1;
					q.push(pair.first);
				}
			}
			q.pop();
		}

		return false;
	}

	// 设置残余网络中的容量
	void SetResCapacity(int from, int to, T value)
	{
		auto itrt = nodes[from].outs_res.find(to);
		if (itrt == nodes[from].outs_res.end()) {
			if (value != 0) nodes[from].outs_res[to] = value;
		}
		else {
			if (value == 0) nodes[from].outs_res.erase(itrt);
			else itrt->second = value;
		}
	}

	// 取得残余网络中的容量
	T GetResCapacity(int from, int to)
	{
		auto itrt = nodes[from].outs_res.find(to);
		if (itrt == nodes[from].outs_res.end()) return 0;
		else return itrt->second;
	}

	// 取得原本网络中的容量
	T GetOrgCapacity(int from, int to)
	{
		auto itrt = nodes[from].outs_org.find(to);
		if (itrt == nodes[from].outs_org.end()) return 0;
		else return itrt->second;
	}

public:
	NetworkFlow(int nNodes, int s, int t, T maxEdgeCapacity) :
		nodes(nNodes), src(s), term(t), maxEdgeCapacity(maxEdgeCapacity)
	{
		if (s < 0 || s >= nNodes || t < 0 || t >= nNodes) throw "index out of range";
		if (s == t) throw "src equals to terminal";
	}

	// 初始化原图中的容量
	void InitCapacity(int from, int to, T value)
	{
		if(to < 0 || to >= nodes.size() || from < 0 || from >= nodes.size()) 
			throw "index out of range";
		if (value < 0 || value > maxEdgeCapacity) throw "capacity out of range";
		auto itrt = nodes[from].outs_org.find(to);
		if (itrt == nodes[from].outs_org.end()) {
			if (value != 0) nodes[from].outs_org[to] = value;
		}
		else {
			if (value == 0) nodes[from].outs_org.erase(itrt);
			else itrt->second = value;
		}
	}

	void Dinic()
	{
		// 深搜时,记录回溯后从哪一条边继续开始搜
		auto itrts = new typename std::map<int, T>::iterator[nodes.size()];

		// 记录已经走过的点
		auto visited = new bool[nodes.size()];

		// 初始化图为残余网络为原网络
		for (int i = 0; i < nodes.size(); i++)
			nodes[i].outs_res = nodes[i].outs_org;

		// 用于深度优先搜索的队列
		std::deque<int> q;

		// 反复分层,直到不存在源到汇的路径
		while (RecalculateLayers()) {
			// 初始化itrts
			for (int i = 0; i < nodes.size(); i++)
				itrts[i] = nodes[i].outs_res.begin(), visited[i] = false;

			// 深度优先搜索
			q.push_back(src);
			visited[src] = true;
			while (q.empty() == false) {
				auto back = q.back();
				
				// 搜到了汇点,此时序列为路径
				if (back == term) {

					// 寻找最小的可用流量的节点
					int argmin = -1; T min = maxEdgeCapacity;
					for (int i = 0; i + 1 < q.size(); i++) {
						T c = nodes[q[i]].outs_res[q[i + 1]];
						if (c < min) min = c, argmin = i;
					}

					// 增广
					for (int i = 0; i + 1 < q.size(); i++) {
						SetResCapacity(q[i], q[i + 1], GetResCapacity(q[i], q[i + 1]) - min);
						// 反向残余网络
						SetResCapacity(q[i + 1], q[i], GetResCapacity(q[i + 1], q[i]) + min);
					}

					// 出栈并继续搜索
					while (!q.empty() && q.back() != argmin){
						visited[q.back()] = false;
						q.pop_back();
					}

				}

				// 还没搜到汇点,往深处搜
				else {
					auto itrt = itrts[back];
					for(; itrt != nodes[back].outs_res.end(); itrt++)
						// 只搜更深一层、未访问过的节点
						if (nodes[itrt->first].depth == nodes[back].depth + 1 && !visited[itrt->first]
							&& itrt->second > 0) {
							q.push_back(itrt->first);
							visited[itrt->first] = true;
							break;
						}
					// 更深处已无路可走,回溯
					if (itrt == nodes[back].outs_res.end()) q.pop_back();
					// 可以往更深处走,更新itrts[back]
					else itrts[back] = ++itrt;
				}

			}
		}

		delete[] itrts;
		delete[] visited;
	}

	// 获得某一边上的流量
	T GetFlow(int from, int to)
	{
		if (to < 0 || to >= nodes.size() || from < 0 || from >= nodes.size())
			throw "index out of range";

		auto itrt = nodes[from].outs_org.find(to);
		if (itrt == nodes[from].outs_org.end()) return 0;
		else return itrt->second - GetResCapacity(from, to);
	}

	// 获得最大流
	T MaxFlow()
	{
		T sum = 0;
		for (auto pair : nodes[src].outs_org)
			sum += GetFlow(src, pair.first);
		return sum;

	}

	// 打印信息, DEBUG用
	void Print()
	{
		std::cout << "src: " << src << "\tterm: " << term << "\n";
		std::cout << "max flow: " << MaxFlow() << "\n";
		for (int i = 0; i < nodes.size(); i++) {
			if (nodes[i].outs_org.empty()) continue;
			std::cout << "NODE[" << i << "]:\n";
			for (auto pair : nodes[i].outs_org)
				std::cout << "\t to NODE [" << pair.first << "]: " 
				<< GetFlow(i, pair.first) << "/" << GetOrgCapacity(i, pair.first) << "\n";
		}
	}

};


/**
 * 以下部分解此题
 */


// 描述“机器”的数据结构定义
class Machine
{
public:
	static int numParts;
	int performance;
	int* in;
	int* out;
	Machine()
	{
		in = new int[numParts];
		out = new int[numParts];
	}
	~Machine()
	{
		delete[] in;
		delete[] out;
	}
	bool AcceptEmpty()
	{
		for (int i = 0; i < numParts; i++)
			if (in[i] == 1) return false;
		return true;
	}

	bool ProduceFull()
	{
		for (int i = 0; i < numParts; i++)
			if (out[i] != 1) return false;
		return true;
	}

	bool AcceptFrom(const Machine & other)
	{
		for (int i = 0; i < numParts; i++)
			if (in[i] != 2 && in[i] != other.out[i]) return false;
		return true;
	}
};
int Machine::numParts = 0;

std::istream & operator >> (std::istream & in, Machine& m)
{
	in >> m.performance;
	for (int i = 0; i < Machine::numParts; i++)
		in >> m.in[i];
	for (int i = 0; i < Machine::numParts; i++)
		in >> m.out[i];
	return in;
}

// 每个机器对应两个节点:2*i - 1, 2 * i,分别是进和出。以下函数用于转换序号
inline int InNode(int iMachine) {return 2 * iMachine - 1; };
inline int OutNode(int iMachine) {return 2 * iMachine; };
inline int MachineId(int iNode) { return (iNode + 1) / 2; };

int main()
{
	int N;
	std::cin >> Machine::numParts >> N;
	Machine* machines = new Machine[N + 1];
	for (int i = 1; i <= N; i++)
		std::cin >> machines[i];


	// 增加两个节点分别是源和汇
	int s = 0, t = 2 * N + 1;  
	auto flow = NetworkFlow<int>(2 * N + 2, s, t, MAXINT);

	// 设置节点之间的相连关系
	for (int i = 1; i <= N; i++) {
		// 一个节点的出口到另一节点的入口
		for (int j = 1; j <= N; j++)
			if (i != j && machines[j].AcceptFrom(machines[i]))
				flow.InitCapacity(OutNode(i), InNode(j), MAXINT);
		// 节点自己的入口到自己的出口
		flow.InitCapacity(InNode(i), OutNode(i), machines[i].performance);
		// 和源和汇连接
		if (machines[i].AcceptEmpty()) flow.InitCapacity(s, InNode(i), MAXINT);
		if (machines[i].ProduceFull()) flow.InitCapacity(OutNode(i), t, MAXINT);
	}

	flow.Dinic();

	// 计算有哪些联系
	struct Connection { int from, to, num; };
	auto connections = std::vector<Connection>();
	for (int i = 1; i <= N; i++) 
		for (int j = 1; j <= N; j++) {
			int num;
			if ((num = flow.GetFlow(OutNode(i), InNode(j))) > 0) {
				Connection c;
				c.from = i, c.to = j, c.num = num;
				connections.push_back(c);
			}
		}

	//输出
	std::cout << flow.MaxFlow() << ' ' << connections.size() << '\n';
	for (auto c : connections)
		std::cout << c.from << ' ' << c.to << ' ' << c.num << '\n';

	delete[] machines;
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值