[JZOJ3302] 【集训队互测2013】供电网络

本文深入解析了一道复杂的上下界最小费用流问题,通过巧妙地拆分二次函数成本,采用动态加边策略,实现了高效的算法设计。文章详细介绍了如何处理图中节点的初始水量和边的流量成本,以及如何通过动态调整边来优化流的计算。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

题目

题目大意

给你一个有向图,每个点开始有一定的水量(可能为负数),可以通过边流到其它点。
每条边的流量是有上下界的。
每个点的水量可以增加或减少(从外界补充或泄出到外界),但是需要费用,和增加(减少)流量呈正比例函数关系。
每条边的流量也需要费用,费用和流量呈二次函数关系(常数项为 0 0 0)。
问将所有水流完的最小花费。


思考历程

这显然是一道上下界最小费用可行流嘛!
有源有汇的上下界可行流的做法:建立超级源 s s ss ss和超级汇 t t tt tt(和 S S S T T T),对于 u u u v v v的容量为 [ l o w , u p ] [low,up] [low,up]的边。这时从 s s ss ss v v v连一条容量为 l o w low low的边, u u u t t tt tt连一条容量为 l o w low low的边, u u u v v v连一条 u p − l o w up-low uplow的边。并且要从 T T T S S S连一条容量为无限大的边。
具体原因不再赘述。
对于每个点的初始水量,如果为正数,就从 S S S向它连一条容量上下界都为水量的边,费用为 0 0 0
如果为负数,就从 T T T向它连一条容量上下界都为水量的绝对值的边,费用为 0 0 0
对于每个点和外界之间的关系,可以从 S S S到它连一条上限为无限大的边,它到 T T T连一条上限为无限大的边,费用由题目给定。
可是最麻烦的来了,这个二次函数该怎么处理呢?
想不出来……
最终交了个错误的程序上去,成功爆0……


正解

前面的都差不多了,就只有二次函数的那一部分。
二次函数为 y = a x 2 + b x y=ax^2+bx y=ax2+bx,直接搞似乎不行,那就试着将它们拆开,变成 a + b a+b a+b 3 a + b 3a+b 3a+b 5 a + b 5a+b 5a+b……这些边。每条边的容量为 1 1 1
具体来说,当 x x x变成 x + 1 x+1 x+1时,费用就会新增 a ( 2 x + 1 ) + b a(2x+1)+b a(2x+1)+b
由于我们跑的是最小费用可行流,一定会先跑更小的边。所以这种方法是不可能WA的。
但是如果直接这样拆,边会很多啊!想一想,要拆成最多 100 100 100条边……
于是就有了动态加边大法!
一开始只加入 a + b a+b a+b的边,如果这条边被流满,那就加一条 3 a + b 3a+b 3a+b的边,以此类推……
当然,由于上下界的问题,一开始加的并不是 a + b a+b a+b。先将最低费用 a ∗ l o w 2 + b ∗ l o w a*low^2+b*low alow2+blow加入答案。为了使费用不重复计算, s s ss ss v v v u u u t t tt tt的边就不需要费用了,只是中间那条 u u u v v v的边初始费用变成了 a ( 2 l o w + 1 ) + b a(2low+1)+b a(2low+1)+b
至此整道题就解决了。


代码

独爱zkw费用流……

using namespace std;
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cassert>
#define min(a,b) ((a)<(b)?(a):(b))
#define INF 1000000000
#define N 1000
int n,m;
struct EDGE{
	int to,c,w;
	EDGE *las;
	int sp;
} e[100000];
int ne;
EDGE *last[N];
#define rev(ei) (e+(int((ei)-e)^1))
inline void link(int u,int v,int c,int w,int sp=0){
	e[ne]={v,c,w,last[u],sp};
	last[u]=e+ne++;
	e[ne]={u,0,-w,last[v],0};
	last[v]=e+ne++;
}
int nsp;
int mx[100000],ad[100000];
int ex[100000];
inline void link2(int u,int v,int low,int up,int a,int b){
	nsp++;
	mx[nsp]=up,ad[nsp]=a*2,ex[nsp]=low+1;
	e[ne]={v,1,a*(2*low+1)+b,last[u],nsp};
	last[u]=e+ne++;
	e[ne]={u,0,-(a*(2*low+1)+b),last[v],0};
	last[v]=e+ne++;
}
int s,t,ss,tt;
int mincost;
int dis[N];
int vis[N],BZ;
int dfs(int x,int s){
	if (x==tt){
		mincost+=dis[ss]*s;
		return s;
	}
	int have=s;
	vis[x]=BZ;
	for (EDGE *ei=last[x];ei;ei=ei->las)
		if (vis[ei->to]!=BZ && ei->c && dis[x]==dis[ei->to]+ei->w){
			int t=dfs(ei->to,min(have,ei->c));
			ei->c-=t,rev(ei)->c+=t,have-=t;
			if (ei->sp && ex[ei->sp]<mx[ei->sp]){
				link(x,ei->to,1,ei->w+ad[ei->sp],ei->sp);
				ex[ei->sp]++;
				ei->sp=0;
			}
			if (!have)
				return s;
		}
	return s-have;
}
inline bool change(){
	int d=INF;
	for (int i=1;i<=n+4;++i)
		if (vis[i]==BZ)
			for (EDGE *ei=last[i];ei;ei=ei->las)
				if (vis[ei->to]!=BZ && ei->c)
					d=min(d,dis[ei->to]+ei->w-dis[i]);
	if (d==INF)
		return 0;
	for (int i=1;i<=n+4;++i)
		if (vis[i]==BZ)
			dis[i]+=d;
	return 1;
}
inline void zkw(){
	do
		do
			BZ++;
		while (dfs(ss,INF));
	while (change());
}
int main(){
	scanf("%d%d",&n,&m);
	s=n+1,t=n+2,ss=n+3,tt=n+4;
	for (int i=1;i<=n;++i){
		int left,in,out;
		scanf("%d%d%d",&left,&in,&out);
		if (left>0)
			link(ss,i,left,0),link(s,tt,left,0);
		else if (left<0)
			link(ss,t,-left,0),link(i,tt,-left,0);
		link(s,i,INF,in);
		link(i,t,INF,out);
	}
	link(t,s,INF,0);
	for (int i=1;i<=m;++i){
		int u,v,a,b,low,up;
		scanf("%d%d%d%d%d%d",&u,&v,&a,&b,&low,&up);
		mincost+=a*low*low+b*low;
		if (a){
			if (low)
				link(ss,v,low,0),link(u,tt,low,0);
			if (up-low)
				link2(u,v,low,up,a,b);
		}
		else{
			if (low)
				link(ss,v,low,b),link(u,tt,low,0);
			if (up-low)
				link(u,v,up-low,b);
		}
	}
	zkw();
	printf("%d\n",mincost);
	return 0;
}

总结

题目的难点主要在拆边,事实上,这似乎有点套路啊……
所以当直接做不方便时,可以试着作差,然后就出来一些东西……

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值