模拟费用流的基本模型

Preface

在本蒟蒻学习的过程中参考了这位这位dalao的博客

费用流,是OI中解决最优化最优化问题的一个常用算法。但众所周知费用流的模型虽然很容易构建,但他的时间效率却比较低下

模拟费用流方法是指利用除费用流以外的手段解决一些费用流问题。一般来说,一个问题如果使用模拟费用流算法来解决,你在整个代码中不会见到任何一个与费用流有关的片段。可以说,这个方法非常的抽象

而在这一个算法的学习过程中,也存在着一些比较实用的模型。通过对这些模型的分析让我们可以对模拟费用流的理解步步加深


引子

你现在正在处理一个问题:

在一条数轴上有一些兔子和一些洞,其中兔子的坐标为\(x_i\),洞的坐标为\(y_i\)。兔子只能向左走,现在你要最小化所有兔子行走的距离之和。

对于这个问题,很显然我们按坐标排序后从左到右操作,每次遇到兔子的时候考虑找到它前面的最近的未匹配的洞

用一个就可以维护上面的操作


模型一

在之前的基础上,洞有了附加权值\(v_i\),这意味着如果要选择这个洞那么必须多付出\(v_i\)的代价(相当于打开洞门?)。还是求最小的代价之和。

还是和之前一样,从左往右考虑每个兔子和洞

如果当前这个位置是一个兔子\(a\),那么让它进入之前的一个洞\(b\)的代价就是\(x_a-y_b+v_b\),这也就意味着我们要取\(-y_b+v_b\)最小的洞,可以用一个来存储所有的洞

但我们发现这个贪心的想法只局限于眼下,可能最优的解是另一个兔子\(c\)来和洞\(b\)匹配,然后\(a\)不匹配。那么我们考虑去掉\(a\)对答案的影响,那么就是要减去它的影响\(x_a\)

那么怎么处理呢,很简单,我们选择了\(a\)之后就向堆里扔进去一个权值为\(-x_a\)的洞。这样选择了这个洞就意味这进行了反悔操作

注意这里我们取消\(a,b\)的匹配,改为\(c,b\)的匹配,这个操作是不是很像费用流的退流操作,因此实际上模拟费用流的本质就是用其他的东西来模拟退流,也就是说我们要考虑怎么在贪心的基础上实现反悔

好了知道了这个模型之后你就可以去做一道板题了:BZOJ 4977 跳伞求生,就是把这里的最小值改成最大值,维护的方法完全类似

#include<cstdio>
#include<queue>
#include<algorithm>
#define RI register int
#define CI const int&
using namespace std;
const int N=100005;
struct data
{
    int pos,val;
    friend inline bool operator < (const data& A,const data& B)
    {
        return A.pos!=B.pos?A.pos<B.pos:!~A.val;
    }
}a[N<<1]; int n,m; priority_queue <int> hp; long long ans;
int main()
{
    RI i; for (scanf("%d%d",&n,&m),i=1;i<=n;++i)
    scanf("%d",&a[i].pos),a[i].val=-1;
    for (i=1;i<=m;++i) scanf("%d%d",&a[n+i].pos,&a[n+i].val);
    for (sort(a+1,a+n+m+1),i=1;i<=n+m;++i) if (!~a[i].val)
    {
        if (hp.empty()||hp.top()+a[i].pos<=0) continue;
        ans+=hp.top()+a[i].pos; hp.pop(); hp.push(-a[i].pos);
    } else hp.push(a[i].val-a[i].pos);
    return printf("%lld",ans),0;
}

模型二

在之前(或之后)的任意一种模型中,每个兔子必须找到洞来匹配。

考虑在开始时就让每一个兔子都与一个距离它\(\infty\)的洞匹配,这样这个匹配就无法退流


模型三

兔子洞有额外权值\(s_i\)。兔子可以向左右两边走。

注意,这是所有模型中可以说是最重要的一个了。下面的模型都和它密切相关,一定要掌握透彻

还是和上面的分析类似,我们仍旧是从左向右考虑这个过程,并且还要考虑兔子和后面的洞匹配所带来的一系列情况

\(v_i\)表示当兔子找到洞\(i\)时需要付出的额外代价(可以看做维护洞的堆的堆顶),同理设\(w_i\)表示洞选择兔子\(i\)付出的额外代价

  • 考虑一只兔子\(b\)如果选择了洞\(a\),贡献就是\(v_a+x_b\)。如果之后的一个洞\(c\)替换了这个洞,总贡献应该加上\(y_c-v_a-2x_b\),因此我们可以加入一个权值为\(-v_a-2x_b\)的兔子
  • 考虑一个洞\(b\)如果找到了兔子\(a\),贡献就是\(w_a+y_b+s_b\)。之后如果有一个兔子\(c\)抢走了这个洞,总贡献应该加上\(w_c+x_c-w_a-2y_b\),对于\(c\)来说,它找到了一个洞,而\(a\)也不会因此无家可归。因为当我们处理\(a\)的时候,我们肯定为它分配好了在它前面的洞,后来\(b\)找到了\(a\)然后\(a\)被赶走了,那么显然\(a\)可以回去找之前分配给它的洞,赶走里面待着的兔子,然后以此类推。这样,一切有回到了\(b\)没有发现\(a\)的样子,因此对于\(c\)来说,我们可以新增一个权值为\(-w_a-2y_b\)的洞
  • 考虑一个洞\(b\)如果找到了兔子\(a\),贡献就是\(w_a+y_b+s_b\)。之后如果有一个洞\(c\)替换了这个洞,总贡献应该加上\(y_c+s_c-y_b-s_b\),对于\(c\)来说,它找到了一个权值为\(-y_b-s_b\)的兔子

然后,我们发现尤其是在第二种情况中,这种\(c\)抢走了\(b\)\(a\)就去匹配它本来匹配过的\(d\),然后现在匹配\(d\)的点又去……的操作,是不是就是费用流的推流操作(前面的只退一次,这里就更复杂了)

这时,兔子已经不在是兔子,洞也已经不再是洞了。洞和兔子因为彼此的利益关系交织在一起,但是无论如何,我们只需要一个堆就可以把它们处理地服服帖帖的

因此,关键的关键还是通过新增了一些“洞” 和 “兔子” 完成了两者的 “反悔” 操作


模型四

我们在以上的任意模型中,兔子和洞都有分身(即一个位置上会有很多个兔子和洞),记为\(c_i,d_i\)。求最小的代价和

我们可以把分身看做有许多个单独的情况。但是当分身的数目很多(到\(10^9\)级别)的时候就不能这么搞了

考虑直接把相同的兔子和洞合并在一起,用一个pair绑起来然后用上面的方法做即可

什么?你说合并的时候产生的新兔子和新洞的数目可能会很多?这个貌似可以用匹配不交叉来证,但是我太弱了不会。不过你只要知道新添加的点都是常数级别的就好了

那么接下来就可以去做一道题了:UOJ#455 雪灾与外卖

#include<cstdio>
#include<queue>
#include<algorithm>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
typedef long long LL;
const int N=100005;
const LL INF=1e12;
struct event
{
	int pos,num,val;
	friend inline bool operator < (const event& A,const event& B)
	{
		return A.pos<B.pos;
	}
}a[N<<1]; int n,m;
struct data
{
	LL val; int num;
	inline data(LL Val=0,CI Num=0)
	{
		val=Val; num=Num;
	}
	friend inline bool operator < (const data& A,const data& B)
	{
		return A.val>B.val;
	}
}; priority_queue <data> A,B; long long sum,ans; //A-Holes B-Rabbits
int main()
{
	//freopen("ex_hole7.in","r",stdin);
	RI i; for (scanf("%d%d",&n,&m),i=1;i<=n;++i)
	scanf("%d",&a[i].pos),a[i].num=1,a[i].val=-1;
	for (i=1;i<=m;++i)
	scanf("%d%d%d",&a[n+i].pos,&a[n+i].val,&a[n+i].num),sum+=a[n+i].num;
	if (sum<n) return puts("-1"),0; sort(a+1,a+n+m+1);
	for (A.push(data(INF,n)),i=1;i<=n+m;++i) if (!~a[i].val)
	{
		data tp=A.top(); A.pop(); ans+=tp.val+a[i].pos; --tp.num;
		if (tp.num) A.push(tp); B.push(data(-tp.val-2LL*a[i].pos,1));
	} else
	{
		int left=a[i].num,cs=0; while (left&&!B.empty())
		{
			data tp=B.top(); if (tp.val+a[i].pos+a[i].val>=0) break;
			B.pop(); int cur=min(left,tp.num); tp.num-=cur; left-=cur;
			cs+=cur; ans+=(tp.val+a[i].pos+a[i].val)*cur;
			if (tp.num) B.push(tp); A.push(data(-tp.val-2LL*a[i].pos,cur)); 
		}
		if (cs) B.push(data(-a[i].pos-a[i].val,cs));
		if (left) A.push(data(a[i].val-a[i].pos,left));
	}
	return printf("%lld",ans),0;
}

模型五

在模型四的基础上,分身必须匹配至少一个

我们把分身拆成两种,一种有一个,匹配了可以产生额外价值\(-\infty\),另一种有\(c_i-1\)个,匹配后产生额外价值\(0\)


模型六

把兔子和兔子洞搬到树上。两点间距离定义为树上距离。

还是考虑先定下一种方向,那么我们从底向上匹配,每次合并一个点不同子树的东西,把堆改为可并堆即可

然后又是一道题:LOJ#6405 征服世界

#include<cstdio>
#include<utility>
#include<iostream>
#define RI register int
#define CI const int&
#define CL const LL&
#define mp make_pair
#define fi first
#define se second
using namespace std;
typedef long long LL;
typedef pair <LL,int> pi;
const int N=250005;
const LL INF=1e12;
struct edge
{
	int to,nxt,v;
}e[N<<1]; int n,head[N],u,v,c,x[N],y[N],cnt; LL ans,dis[N];
class Lefty_Tree
{
	private:
		struct data
		{
			int ch[2],dis; pi v;
		}node[N*20]; int rt[N*20],tot;
		#define lc(x) node[x].ch[0]
		#define rc(x) node[x].ch[1]
		#define D(x) node[x].dis
		#define V(x) node[x].v
		inline int merge(int x,int y)
		{
			if (!x||!y) return x|y; if (V(x)>V(y)) swap(x,y);
			rc(x)=merge(rc(x),y); if (D(rc(x))>D(lc(x))) swap(lc(x),rc(x));
			D(x)=D(rc(x))+1; return x;
		}
	public:
		inline void insert(CI pos,CL val,CI num)
		{
			V(rt[pos]=++tot)=mp(val,num); D(tot)=1;
		}
		inline bool non_empty(CI x)
		{
			return rt[x];
		}
		inline void Union(CI x,CI y)
		{
			rt[x]=merge(rt[x],rt[y]);
		}
		inline pi top(CI x)
		{
			return V(rt[x]);
		}
		inline void remove(CI x)
		{
			rt[x]=merge(lc(rt[x]),rc(rt[x]));
		}
		inline void updata(CI x,CI y)
		{
			V(rt[x]).se+=y;
		}
		#undef lc
		#undef rc
		#undef D
		#undef V
}X,Y; int totx,toty;
inline void addedge(CI x,CI y,CI z)
{
	e[++cnt]=(edge){y,head[x],z}; head[x]=cnt;
	e[++cnt]=(edge){x,head[y],z}; head[y]=cnt;
}
inline void Union(CI x,CI y,CL d)
{
	while (X.non_empty(x)&&Y.non_empty(y))
	{
		pi tx=X.top(x),ty=Y.top(y);
		if (tx.fi+ty.fi-(d<<1LL)>=0) break;
		int cur=min(tx.se,ty.se); ans+=(tx.fi+ty.fi-(d<<1LL))*cur;
		X.updata(x,-cur); Y.updata(y,-cur);
		if (!(tx.se-cur)) X.remove(x); if (!(ty.se-cur)) Y.remove(y);
		X.insert(++totx,(d<<1LL)-ty.fi,cur); X.Union(x,totx);
		Y.insert(++toty,(d<<1LL)-tx.fi,cur); Y.Union(y,toty);
	}
}
#define to e[i].to
inline void DFS(CI now=1,CI fa=0)
{
	if (x[now]) X.insert(now,dis[now],x[now]);
	if (y[now]) Y.insert(now,dis[now]-INF,y[now]),ans+=INF*y[now];
	for (RI i=head[now];i;i=e[i].nxt) if (to!=fa)
	{
		dis[to]=dis[now]+e[i].v; DFS(to,now);
		Union(now,to,dis[now]); Union(to,now,dis[now]);
		X.Union(now,to); Y.Union(now,to);
	}
}
#undef to
int main()
{
	//freopen("CODE.in","r",stdin); freopen("CODE.out","w",stdout);
	RI i; for (scanf("%d",&n),i=1;i<n;++i) scanf("%d%d%d",&u,&v,&c),addedge(u,v,c);
	for (i=1;i<=n;++i) scanf("%d%d",&x[i],&y[i]),c=min(x[i],y[i]),x[i]-=c,y[i]-=c;
	return totx=toty=n,DFS(),printf("%lld",ans),0;
}

Postscript

这就完了?其实才刚刚开始呢……

posted @ 2019-10-24 14:57  空気力学の詩  阅读(551)  评论(0编辑  收藏  举报