【算法】网络最大流问题,三次尝试以失败告终

news2024/11/29 5:34:25

文章目录

  • 开始
  • 基本思路:“反悔”机制
  • 干活
    • 尝试一:深度优先搜索
    • 尝试二:少走弯路
    • 尝试三:最短增广路径,广度优先
    • 还是没ac
  • 记两个小bug
    • 1. 数组越界
    • 2. 写错变量名
  • 小结
  • 最后一个版本的代码(C++)
    • 定义类与函数
    • 主函数
    • 类与函数的调试版本

开始

已多次看到“网络最大流问题”的字眼,一直不知道是什么,后来终于有一次打算仔细了解一下,期间我发现了一篇不错的博客:全面理解网络流中的最大流问题。在这篇博客的帮助下,我成功弄清楚了什么是网络流中的最大流问题,同时也明白了解决这个问题的基本思路。

基本思路:“反悔”机制

这里不仔细介绍思路了,大家可以先看看前面那篇博客

在前面提到的博客中,博主认为“借用”这个描述比“反悔”更为合适。然而在阅读的过程中,我反而理解了“反悔”这个概念,同时觉得还挺合适的。下面简单说说自己关于“反悔”的理解。

首先说说为什么会需要“反悔”。举个有点抽象的例子,在某个网络中,A只有一条路可走,B却率先占用了A唯一的路径,然而其实B还有其它空闲的路径可走,以至于网络的运载能力没有被充分利用,不是“最大流”。于是A告诉B:你挡着道了,B听到后就换了一条路走,A便也得以通过。

我们看看下面这个网络(来自前面提到的博客):

在这里插入图片描述
如果选择路径的顺序为:
1、a -> e -> d = 8(有流量8通过这条路径
2、a -> c = 2
那么从路径b过来的流量就被阻塞了。

而如果我们在通过路径e时,建立一条与通过流量相同的反向边 e’ ,表示走这条路径的选择是可以反悔的,最大可反悔流量为8。那么当流量A通过路径b后,发现了可反悔的路径e,就告诉以占用e的流量B:你挡着道了。于是B的两个流量就从 e’ 返回,然后通过c边到达了终点t。而路径d也腾出了2个流量,A就从原先流量B占用的路径通过。于是有:
3、b -> e’ -> c = 2

得到最大流 8 + 2 + 2 = 12

在这里插入图片描述


干活

我对于自己测试代码的能力不太自信,于是从洛谷上找了个题:P3376 【模板】网络最大流

尝试一:深度优先搜索

算法过程采用深度优先搜索,从 源点 开始,到每个点时记录流入该点的流量,然后将这些流量分向其它的点,通过边时就更新边的剩余流量,到达 汇点 时就累加更新最大流。对于有分叉出路的点,通过边的同时建立反向边。

于是我遇到了一些问题:

  • 不应当在搜索过程中刚经过一条边(一个点)时就更新它的剩余流量,因为实际通过流量会受后面的影响。(从源点到汇点一条路径的流量,由路径上流量最小的边决定
  • 解决方案:在遍历时记录走过的点,在达到汇点时再更新整条路径所有边(点)的剩余流量

结果在部分测试集上超时了。

尝试二:少走弯路

我起初并没有去查找其它的算法,而想自己在原来算法过程的基础上优化试试。于是我作出了一个假设:很多时间浪费在已经不可能的方向上

于是我尝试引入一种点死亡机制,在搜索过程中,如果从一个点某次没能成功到达汇点,就判定这个点已经寄了,以后再也不走这里了。

在这里插入图片描述
但我后面发现了问题:

  • 在搜索过程中判定点的死亡并不合适。因为在搜索时,我并不会再走已经走过的点,而一个被判定为寄了的点,可能经过那些已走过的点是可以达到汇点的。
  • 解决方案(没想出来):如果不在搜索路径的中途判定,而是单独判断从某个点到汇点是否联通的话,这个判断本身又会消耗太多的时间。

尝试三:最短增广路径,广度优先

我还是决定看看别人的算法,于是我找到了这篇博客:网络最大流算法,并计划使用Edmonds-Karp算法,即每次寻找一条从源点到汇点的最短路径,以此更新边的剩余流量、最大流。

使用广度优先搜索策略可用来寻找最短路径,从源点开始,在搜索的同时,构建出一个层次网络,到达汇点时停止搜索。然后从汇点逆向一直走,就可以得到一条从源点到汇点的路径。(如果想得到最短路径,可以给层次网络中的每个点标上层号
在这里插入图片描述

图中点2没有指向t是因为有一个点到达t时就可以停止搜索了。此时我们从t往回走,总能得到一条s与t之间的路径,它可能是:
t -> 1 -> s
t -> 1 -> 2 -> s
我们发现,在层次网络中往回走的过程中,每一步要么使所在层数降低,要么层数不变,但层次不会增大。(而如果你给每一点标记了层号,就可以限制每次都是向上一层走

残量网络:原来的网络,每次找到路径后会更新网络中每条边的剩余流量,故称为残量网络。
相比深度优先搜索:即使你不要求最短路径,也总能以不太大的代价找到一条路径,而不会陷入局部的泥沼。

还是没ac

在自己对代码测试一番后,就觉得可以了,然而还是有三个测试点没过。测试数据下载下来发现是200个点,5000条边(数据中有重复的边)的那种,调试感觉无从下手,我至今没有找到问题出在哪
在这里插入图片描述


记两个小bug

1. 数组越界

忘记使用数组下标是从1开始的了,因此数组大小应定义为nNum+1
在这里插入图片描述

2. 写错变量名

不是第一次,也不是第n次,是第n+1次了。初始化列表中应为t(_t)

在这里插入图片描述


小结

在这个问题上花了好多时间,还是没得到一个好的结果。有时会有些后悔感,感觉那些时间花得不值得。我总喜欢了解一下算法大体思路后就自己去琢磨具体的细节和实现,而不愿意去阅读别人的源代码,好几次一个题的bug找很久。有时bug是思路上的,有时是写代码太粗心导致的错误。但是这样太花时间了。

最近从其它方面得到一些感受,多观察是有益的。


最后一个版本的代码(C++)

定义类与函数

#include<iostream>
#include<cstdio>
#include<climits>
#include<cstdlib>
#include<queue>
using namespace std;


const int Len = 201;
class Net
{
	public:
	int net[Len][Len];
	int *road;
	int iR;
	long long sum;
	
	int nNum;
	int s;
	int t;
	
	Net(int _nNum, int _s, int _t);
	void findRoad();
	void upNet();
	void forward();
};
//---------------------------
Net::Net(int _nNum, int _s, int _t)
	:net{}
	,nNum(_nNum)
	,s(_s)
	,t(_t)
{
	iR = 0;
	sum = 0;
	road = new int[nNum]{};
}
//---------------------------
void Net::findRoad()
{
	iR = 0;
	int net2[Len][Len]{};//层次网络 
	bool pass[nNum + 1]{};//走过为true 
	queue<int> q;//辅助层次网络构建 
	q.push(s);
	//pass[s] = 1;
	
	//构建层次网络 
	while(1){
		int front = q.front();
		if(pass[front] == 1){
			q.pop();
			// 可能越界 
			if(q.empty()){
				return;
			} 
			front = q.front();
		}
		pass[front] = 1;
		q.pop();
		
		bool flag = 0;
		
		for(int i = 1; i <= nNum; i++){
			if(net[front][i] > 0 && pass[i] == 0){ 
				net2[front][i] = net[front][i];
				q.push(i);
				if(i == t){
					goto ROAD;
				}
				flag = 1;
			}
		}
		
		if(flag == 0 && q.empty() == true){
			break;
		}
	}
	
	//得到从源点到汇点的路径 
	ROAD:
		for(int i = 1, now = t; i <= nNum; i++){
			if(net2[i][now] > 0){
				road[iR++] = now;
				
				if(i == s){
					road[iR++] = i;
					break;
				}
				
				now = i;
				i = 0;
			}
		}
}
//---------------------------
void Net::upNet()
{
	//1.找road最小值,更新最大流 
	int min(INT_MAX);
	for(int i = 0; i <= iR - 2; i++){
		if(net[road[i + 1]][road[i]] < min){
			min = net[road[i + 1]][road[i]];
		}
	}
	sum += min;
	
	//2.更新net残量,建立反向边 
	for(int i = 0; i <= iR - 2; i++){
		net[road[i + 1]][road[i]] -= min;
		net[road[i]][road[i + 1]] += min;
	} 
}
//---------------------------
void Net::forward()
{
	findRoad();
	while(iR > 0){
		upNet();
		findRoad();
	}
}

主函数

int main(){
	int n = 2;
	int m = 1;
	int s = 1;
	int t = 2;
	cin >> n >> m >> s >> t;
	
	Net netWork(n, s, t);
	
	for(int i = 1; i <= m; i++){
		int a = 0, b = 0, w = 0;
		cin >> a >> b >> w;
		netWork.net[a][b] = w;
	}

	netWork.forward();
	printf("%lld", netWork.sum);
	
	return 0;
}

类与函数的调试版本

IDLE的调试工具我用得不多,比较习惯于通过输出中间结果进行debug,这里记录了含输出点的代码。

#include<iostream>
#include<cstdio>
#include<climits>
#include<cstdlib>
#include<queue>
using namespace std;


const int Len = 201;
class Net
{
	public:
	int net[Len][Len];
	int *road;
	int iR;
	long long sum;
	
	int nNum;
	int s;
	int t;
	
	Net(int _nNum, int _s, int _t);
	void findRoad();
	void upNet();
	void forward();
};
//---------------------------
Net::Net(int _nNum, int _s, int _t)
	:net{}
	,nNum(_nNum)
	,s(_s)
	,t(_t)
{
	iR = 0;
	sum = 0;
	road = new int[nNum]{};
}
//---------------------------
void Net::findRoad()
{
	iR = 0;
	int net2[Len][Len]{};//层次网络 
	bool pass[nNum + 1]{};//走过为true 
	queue<int> q;//辅助层次网络构建 
	q.push(s);
	//pass[s] = 1;
	
	//构建层次网络 
	while(1){
		int front = q.front();
		if(pass[front] == 1){
			// test pop 已过点
//			printf("pop  %d\n", front);
			//---------------------------end
			q.pop();
			// 可能越界 
			if(q.empty()){
				return;
			} 
			front = q.front();
		}
		pass[front] = 1;
		//test pop
//		printf("pop  %4d\n", front);
		//--------------------end
		q.pop();
		
		bool flag = 0;
		
		for(int i = 1; i <= nNum; i++){
			if(net[front][i] > 0 && pass[i] == 0){ 
				net2[front][i] = net[front][i];
				//pass[i] = 1; //改在出队时进行 
				//test push
//				printf("push %4d\n", i);
				//--------------------end
				q.push(i);
				if(i == t){
					goto ROAD;
				}
				flag = 1;
			}
		}
		
		if(flag == 0 && q.empty() == true){
			break;
		}
	}
	
	ROAD:
		for(int i = 1, now = t; i <= nNum; i++){
			//printf("ROAD i:%4d, now:%4d, e:%4d\n", i, now, net[i][now]);
			if(net2[i][now] > 0){
				road[iR++] = now;
				
				//test ROAD
//				printf("road:");
//				for(int i = iR - 1; i >= 0; i--){
//					printf("%4d", road[i]);
//				}
//				printf("\n");
				//--------------------end
				
				if(i == s){
					//test t to road
					//printf("to ROAD\n");
					//------------------end
					road[iR++] = i;
					break;
				}
				
				now = i;
				i = 0;
			}
		}
		
	//test 层次网络
//	printf("层次网络:\n");
//	for(int i = 1; i <= nNum; i++){
//		for(int j = 1; j <= nNum; j++){
//			printf("%4d", net2[i][j]);
//		}
//		printf("\n");
//	} 
	//--------------------end
		
	//test ROAD
//	printf("road:");
//	for(int i = iR - 1; i >= 0; i--){
//		printf("%4d", road[i]);
//	}
//	printf("\n");
	//------------------end
}
//---------------------------
void Net::upNet()
{
	//1.找road最小值,更新最大流 
	int min(INT_MAX);
	for(int i = 0; i <= iR - 2; i++){
		if(net[road[i + 1]][road[i]] < min){
			min = net[road[i + 1]][road[i]];
		}
	}
	sum += min;
	//test min and sum
//	printf("min:%d\n", min);
//	printf("sum:%lld\n", sum);
	//---------------------------end
	
	//2.更新net残量,建立反向边 
	for(int i = 0; i <= iR - 2; i++){
		net[road[i + 1]][road[i]] -= min;
		net[road[i]][road[i + 1]] += min;
	} 
	//test 残量 
//	printf("残量网络:\n");
//	for(int i = 1; i <= nNum; i++){
//		for(int j = 1; j <= nNum; j++){
//			printf("%4d", net[i][j]);
//		}
//		printf("\n");
//	} 
	//---------------------------end
}
//---------------------------
void Net::forward()
{
	findRoad();
	//test iR
//	printf("iR:%4d\n", iR);
	//---------------------------end
	while(iR > 0){
		upNet();
		findRoad();
		//test iR
//		printf("iR:%4d\n", iR);
		//---------------------------end
	}
}


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/3433.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

谷雨妹子要出国

文 / 谷雨&#xff08;微信公众号&#xff1a;王不留&#xff09; 作为出差在外的实施团队中的唯一一位女生&#xff0c;我可以独享一个单间&#xff0c;晚上的备考时候不会受影响&#xff0c;心里倒有点美嗞嗞的。 目前工作状态是 996&#xff08;早上九点到晚上九点&#xf…

常用短信平台一览,记得收藏哦

市面上的短信平台很杂很多&#xff0c;小到几个人的公司、大到腾讯、阿里这样的巨无霸都在做&#xff0c;但常用的就那么几个&#xff0c;因而用户的选择也存在不少的困惑。 在我看来&#xff0c;我觉得选择短信平台、在我看来有这几个需要的注意地方&#xff1a; 1、价格 无论…

Java:Session 会话详解

在介绍本篇的主角之前, 我们先复习一下 Cookie 为了实现在游览器的持久性存储和安全性考虑, 游览器提供了一个机制—— Cookie , Cookie 的储存空间很有限, 不同的游览器Cookie空间上限也不同, 一般总上限是 4k 个字节左右 (例如 Firefox), 其储存也只是按照域名进行分块存储, …

在众多编程语言中,我为什么要学Python?

前言 编程语言排行榜三剑客Java、C、C&#xff0c;长期统治榜首&#xff0c;今日python重回榜首 &#xff08;文末送福利&#xff09; python的前世今生 1、最新动态 TIOBE排行榜是根据互联网上有经验的程序员、课程和第三方厂商的数量&#xff0c;并使用搜索引擎&#xff…

【算法篇-动态规划】手撕各大背包问题 —— 01背包

背包问题1. 最基础的背包 —— 01背包 &#xff08;必看&#xff09;1.1 分析1.2 状态转移方程 和 边界条件1.3 代码1.3.1 代码模拟1.4 空间复杂度的优化1.4.1 错误的优化方式1.4.2 正确的优化方式1.5 终极版优化总结本文章参考自 B站 董晓算法 董晓算法 1. 最基础的背包 ——…

Linux下git和gdb的使用

&#x1f680;每日鸡汤&#xff1a;生活不相信眼泪&#xff0c;即使你把眼泪流成珍珠&#xff0c;灰暗的生活也不会因此而闪光。 目录 一、使用git命令行 1.1安装git、配置仓库 Ⅰ.gitignore Ⅱ.git 1.2git的基本使用 二、Linux调试器-gdb 2.1、gdb的使用 2.2、 debug与…

矩阵求导简记

很多机器学习算法都需要求解最值&#xff0c;比如最小二乘法求解样本空间相对拟合曲线的最短距离&#xff0c;最值的求解往往通过求导来计算&#xff0c;而机器学习中又常用矩阵来处理数据&#xff0c;所以很多时候会涉及到矩阵的求导。矩阵求导就像是线性代数和微积分的结合&a…

熬夜肝出囊括Java后端95%的面试题解析

为大家整理了一版java高频面试题&#xff0c;其实&#xff0c;一直有大佬在面试&#xff0c;不是在面试&#xff0c;就是在面试的路上&#xff0c;2022其实不是个适合跳槽的年份&#xff0c;稳稳当当当然好&#xff0c;但是&#xff0c;也别委屈自己呀&#xff0c;话不多说&…

Kotlin编程实战——与Java互操作(10)

一 概述 Kotlin 中调用 Java 代码Java 中调用 Kotlin 二 Kotlin 中调用 Java 代码 Getter 和 Setter返回 void 的方法将 Kotlin 中是关键字的 Java 标识符进行转义空安全与平台类型Java类型映射kotlin类型Kotlin 中的 Java 泛型Java 可变参数 三 Java 中调用 Kotlin 属性实…

【ELM预测】基于matlab探路者算法优化极限学习机预测(含前后对比)【含Matlab源码 2204期】

一、探路者算法简介 提出的一种新兴的智能优化算法&#xff0c;该算法的思想起源于群体动物的狩猎行为&#xff0c;种群中的个体分为探路者和跟随者两种角色。算法的寻优过程模拟了种群寻找食物的探索过程&#xff0c;利用探路者、跟随者两种角色不同的位置更新方式以及角色间…

NR/5G - PUSCH repetition次数

--- R15 DCI format 0-1 PUSCH 38.214中的描述&#xff0c;DCI format 0-1调度的PUSCH&#xff0c;包括C-RNTI/MCS-C-RNTI动态DCI调度PUSCH以及CS-RNTI&#xff0c;NDI1时候指示的Configured Grant的重传调度PUSCH&#xff0c;通过PUSCH-Config中的pusch-AggregationFactor指示…

谷粒学院——Day02【环境搭建和讲师管理接口开发】

前后端分离概念 传统单体结构 前后端分离结构 前后端分离就是将一个单体应用拆分成两个独立的应用&#xff1a;前端应用和后端应用&#xff0c;以JSON格式进行数据交互。 后台讲师管理模块环境搭建 一、数据库设计 数据库 guli_edu 数据库 guli_edu.sql # # Structure fo…

3.1 Python 字符串类型常用操作及内置方法

文章目录1. 类型转换2. 字符串索引取值3. 遍历字符串4. 统计长度5. 字符串的复制与拼接5.1 字符串的复制5.2 加号拼接5.3 .join 方法拼接字符串6. 字符比较7. 成员运算8. .format9. .split10. .strip11 . .upper 与 .lower12. .isupper 与 .islower13. .startswith 与 .endswit…

15 个机器学习的基本 Python 库

一定有很多次你试图在 Python 中找到一个库来帮助你完成机器学习项目。但是&#xff0c;经常遇到一件事&#xff01;今天有如此多的 Python 库可用&#xff0c;并且许多库在每几年之后都会大量发布&#xff0c;因此选择合适的库并不容易。 有时会花费数小时寻找合适的库&#…

【数据结构基础】之图的介绍,生动形象,通俗易懂,算法入门必看

前言 本文为数据结构基础【图】 相关知识&#xff0c;下边将对图的基本概念&#xff0c;图的存储结构&#xff0c;图的遍历包含广度优先遍历和深度优先遍历&#xff0c;循环遍历数组&#xff0c;最小生成树&#xff0c;拓扑排序等进行详尽介绍~ &#x1f4cc;博主主页&#xf…

spring启动流程(二):包的扫描流程

在applicationContext的创建中&#xff0c;我们分析了applicationContext的创建过程&#xff0c;在本文中&#xff0c;我们将分析spring是如何进行包扫描的。 依旧是AnnotationConfigApplicationContext的构造方法&#xff1a; public AnnotationConfigApplicationContext(St…

自底向上语法分析(bottom-up parsing)

自底向上语法分析&#xff08;bottom-up parsing&#xff09;自底向上分析概述LR分析概述LR(0)分析增广文法点标记项目LR(0)分析表CLOSURE函数GOTO函数LR(0)自动机的状态集LR(0)分析表构造算法LR(0)自动机的形式化定义LR(0)分析的冲突问题SLR分析SLR算法的关键SLR分析的冲突问题…

U3D热更新技术

作者 : SYFStrive 博客首页 : HomePage &#x1f4cc;&#xff1a;个人社区&#xff08;欢迎大佬们加入&#xff09; &#x1f449;&#xff1a;社区链接&#x1f517; &#x1f937;‍♀️&#xff1a;创作不易转发需经作者同意&#x1f608; &#x1f483;&#xff1a;程…

适用于 Windows 的企业级 Subversion 服务器

适用于 Windows 的企业级 Subversion 服务器。 Subversion 的 Windows 身份验证 Windows 身份验证是 VisualSVN 服务器的一个关键特性。此功能专为 Active Directory 域环境设计&#xff0c;允许用户使用其 Windows 凭据访问 VisualSVN 服务器。 VisualSVN Server 支持两种不同…

【Linux】基础IO ——中

&#x1f387;Linux&#xff1a;基础IO 博客主页&#xff1a;一起去看日落吗分享博主的在Linux中学习到的知识和遇到的问题博主的能力有限&#xff0c;出现错误希望大家不吝赐教分享给大家一句我很喜欢的话&#xff1a; 看似不起波澜的日复一日&#xff0c;一定会在某一天让你看…