最小费用最大流 (Dinic + SPFA) + 模板 + 拓展 + 超详细解释

学完 ISAP HLPP 之后 兴高采烈地去学费用流 结果......

居然要用 Dinic!

我真是被气到了 然后 Dinic 打洛谷模板 疯狂 RE 和 TLE

还好满分能去看别人的 然后翻到几十页找了个 Dinic 再修改成常规做法 貌似和 ISAP 差不多 就多了个 bfs

网上概念讲的都很好了 这里懒得批注 =-= 专门学最大流的去翻我的 ISAP 和 HLPP

下放 Dinic 最大流 代码

#include <algorithm>
#include <cstring>
#include <cstdio>
using namespace std;
const int MAXN = 10010;
const int MAXM = 200010;
int s,t,tot = 1;
int dep[MAXN],cur[MAXN],pre[MAXN],first[MAXN];
struct Edge {
	int to,next,w;
} e[MAXM];
void add(int x,int y,int z)
{
	e[++tot].next = first[x],first[x] = tot,e[tot].to = y,e[tot].w = z;
	e[++tot].next = first[y],first[y] = tot,e[tot].to = x;
}
short bfs()
{
	memset(dep,0,sizeof(dep));
	int h = 0,tail = 1;
	dep[s] = 1;
	pre[1] = s;
	while (h < tail)
	{
		int p = pre[++h];
		//for (int a = first[p],b = e[a].to ; a ; a = e[a].next,b = e[a].to) <-要快上许多 
		for (int a = cur[p] = first[p],b = e[a].to ; a ; a = e[a].next,b = e[a].to)
			if (e[a].w && !dep[b])
			{
				pre[++tail] = b;
				dep[b] = dep[p] + 1;
				if (b == t) return 1;
			}
	}
	return dep[t];
}
int dfs(int p,int mx)
{
	if (p == t || !mx) return mx;
	int f = 0;
	//for (int a = cur[p] ? cur[p] : first[p] ; a ; a = e[a].next) <-要快上许多 
	for (int a = cur[p] ; a ; a = e[a].next)
	{
		cur[p] = a;
		if (e[a].w && dep[e[a].to] == dep[p] + 1)
			if (f = dfs(e[a].to,min(mx,e[a].w)))
				return e[a].w -= f,e[a ^ 1].w += f,f;
	}
	return 0;
}
int main()
{
	int n,m,x,y,z;
	scanf("%d%d%d%d",&n,&m,&s,&t); while (m--)
	scanf("%d%d%d",&x,&y,&z),add(x,y,z);
	int f = 0,ans = 0; while (bfs())
	while (f = dfs(s,(1 << 30))) ans += f;
	printf("%d\n",ans);
	return 0;
}

然后费用流是 跑边的时候 权值从 流量 变成 流量 × 单位费用

最小费用最大流 则是在 最大流 的前提下 用最少的费用 学好了肯定能当无良商家2333(题外话 记得某本紫书上讲期望的例子 《惊!某无良老板用期望赚钱》 不仅清空商品 而且还有利润)

改了下原代码 =-= 然后超详细的注释在里面哈 例题仍旧是洛谷的模板 (没办法啊模板只有这里好找)

下放 Dinic 最小费用最大流 代码~

#include <algorithm>
#include <cstring>
#include <cstdio>
using namespace std;
const int MAXN = 5010;
const int MAXM = 100010;
struct Edge {
	int next,to,fl,co;
//邻接表存边 fl是边流量(flow) co是边单位流量的费用(cost)
} e[MAXM];
int first[MAXN],dis[MAXN],ef[MAXN],num[MAXN],pre[MAXN],que[MAXN << 3];//que就是队列
//解释:懒得解释  见子程序   见子程序  见子程序  pre:当前增广路某点是从哪个点来的(前驱)
//Tip:dis存的不是费用×流量! 因为流量每流过一条边都可能变小
short o[MAXN];
int n,s,t,tot = 1,ansf,ansc;
void add(int x,int y,int z,int w)
{
	//存边与普通最大流差不多 就是费用倒过来时取相反数 即可
	e[++tot].next = first[x],first[x] = tot,e[tot].to = y,e[tot].co = w,e[tot].fl = z;
	e[++tot].next = first[y],first[y] = tot,e[tot].to = x,e[tot].co = -w;
}
short spfa()
{ //bfs改成SPFA
	//Tip:队列不用memset可以直接覆盖
	memset(dis,0x7f,sizeof(dis)); //同SPFA里的 此时(增广x次后)流到该点的最小费用 因此要更新
	memset(ef,0x7f,sizeof(ef)); //可改成 ef[s] = INF 反正源点不变然后流可以覆盖
	//此时 流到某点时剩余的流量(和HLPP的概念差不多 就是挤到某点的流量)
	//memset(o,0,sizeof(o)); //判断某点是否在队列里 然而根据SPFA的原理此条可略
	int h = 0,tail = 1; //队列 head 和 tail 初始化
	que[1] = s;//这个可以放外面的说 但为了程序通俗易懂..
	dis[s] = 0;//源点费用初始化
	pre[t] = 0;//前驱初始化 千万别漏
	while (h < tail)
	{ //完全是SPFA的说..
		int p = que[++h];
		o[p] = 0;
		for (int a = first[p],b = e[a].to ; a ; a = e[a].next,b = e[a].to)
			if (e[a].fl && dis[p] + e[a].co < dis[b])
			{
				dis[b] = dis[p] + e[a].co;
				pre[b] = p; //更新当前点的前驱
				num[b] = a; //存当前边的编号 通过前驱找点可以找到该边 然后在主程序里可以更新该边的流量
				ef[b] = min(ef[p],e[a].fl); //挤流量 取小的 然后以此继续推
				if (!o[b]) o[b] = 1,que[++tail] = b; //p点连接的b点如果没在队列里 压进去
			}
	}
	return pre[t];
//返回前驱 为什么不返回流量? 此处原本是Dinic的bfs 是看有无增广路的 流量存到ef里了
//如果前驱没更新到说明没增广路了 这也是pre[t]要初始化的原因
}
int main()
{
	int m,x,y,z,w;
	scanf("%d%d%d%d",&n,&m,&s,&t); while (m--)
	scanf("%d%d%d%d",&x,&y,&z,&w),add(x,y,z,w); //强制对齐的读入
	while (spfa())
	{ //和Dinic一样啦 就是还能找到增广路就更新
		ansf += ef[t]; //答案的流加上
		ansc += ef[t] * dis[t]; //答案的费用乘上
		for (int now = t ; now != s ; now = pre[now])
		{
			//通过前驱找该增广路经过的所有边 然后更新流量 (原路减流量反向弧加流量)
			e[num[now]].fl -= ef[t];
			e[num[now] ^ 1].fl += ef[t];
		}
	}
	printf("%d %d\n",ansf,ansc); //输出
	return 0;
}

拓展一下 网络流最常见的 拆点 就是把点拆成边

然后给个费用流点拆成边的例题

本题的 十字路口 就是点 的意思 点不能经过多次 (处源,汇点外) 因此把中间点拆成边 中间通一个单位的流量 就是一次啦

然后源,汇点的话 因为有可能直接相连 因此也要拆 中间流量设 INF (可经过多次) 然后后面如果读入了就只会连一条流量为1的边

然后点编号 网上dalao们都是 第 i 个点 进来是 i 出去是 i + n 本蒟蒻不才 第 i 个点 进来是 i * 2 - 1 出去是 i * 2

嗯进出两点要初始化 就是 某点的进 对应 某点的出

然后读边是 前者的出 对应着 后者的进

其他和费用流无异啦

这里只注释新加的地方 费用流的看上面那篇

下放代码~

#include <cstring>
#include <cstdio>
#define INF 1 << 30
using namespace std;
const int MAXN = 410; //因为点拆边点成了两倍 因此..
const int MAXM = 41000; //同上 然后试过了40100都不行...建议边范围开两倍多一些(不是一点)
struct Edge {
	int next,to,fl,co;
} e[MAXM];
int first[MAXN],dis[MAXN],ef[MAXN],pre[MAXN],que[MAXN << 3];
int s = 1,t,tot = 1,ansf,ansc;
short o[MAXN];
void add(int x,int y,int z,int w)
{
	e[++tot].next = first[x],first[x] = tot,e[tot].to = y,e[tot].co = w,e[tot].fl = z;
	e[++tot].next = first[y],first[y] = tot,e[tot].to = x,e[tot].co = -w;
}
int min(int x,int y)
{
	return x < y ? x : y;
}
short spfa()
{
	memset(dis,0x7f,sizeof(dis));
	ef[1] = INF;
	dis[1] = 0;
	int h = 0,tail = 1;
	while (h < tail)
	{
		int p = que[++h];
		o[p] = 0;
		for (int a = first[p],b = e[a].to ; a ; a = e[a].next,b = e[a].to)
			if (e[a].fl && dis[p] + e[a].co < dis[b])
			{
				dis[b] = dis[p] + e[a].co;
				pre[b] = a;
				ef[b] = min(ef[p],e[a].fl);
				if (!o[b]) o[b] = 1,que[++tail] = b;
			}
	}
	return dis[t] != dis[0]; //判断到t点花费变了没 没变说明增广不到 返回0
}
int main()
{
	int n,m,x,y,w;
	scanf("%d%d",&n,&m);
	t = n * 2; //确定拆点后点最大范围
	for (int a = 2 ; a < n ; ++ a)
	add(a * 2 - 1,a * 2,1,0); //非源,汇点拆点流量设为1 因为这点只能经过一次
	add(1,2,INF,0); //源点可经过多次 流量设为INF
	add(n * 2 - 1,n * 2,INF,0); //汇点也可经过多次 流量设为INF
	while (m--)
	{
		scanf("%d%d%d",&x,&y,&w);
		add(x * 2,y * 2 - 1,1,w);
	}
	que[1] = 1; //队首是源点 init~
	while (spfa())
	{
		ansf += ef[t];
		ansc += ef[t] * dis[t];
		for (int a = t,b = pre[a] ; a != s ; a = e[b ^ 1].to,b = pre[a])
		{/*
			这个循环做的是真好 我翻到的
			开始条件: 从汇点往回 通过pre数组找到经过的边的编号 赋值给b
			循环中: 找到的边除去此次增广路的流量
			判断: 懒得说了=-=
			下一轮循环赋值: 通过反向边找到与a节点相连的上一个节点 然后pre找边赋给b
			就是这么巧妙*/
			  e[b].fl -= ef[t];
			  e[b ^ 1].fl += ef[t];
		}
	}
	printf("%d %d\n",ansf,ansc);
	return 0;
}

猜你喜欢

转载自blog.csdn.net/Frocean/article/details/81543757