Preface
费用流,是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)的洞匹配,这样这个匹配就无法退流
模型三
兔子洞有额外权值(v_i),且兔子也有额外权值(w_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
这就完了?其实才刚刚开始呢……