zoukankan      html  css  js  c++  java
  • 模拟费用流

    模拟费用流有2种:第一种是考虑图的特殊性,使用合适的数据结构维护这个图。第二种是考虑这个费用流的实际意义,使用其他算法代替费用流。

    先说第一种。模拟的有2种:最短增广路,消圈

    增广路费用流算法的3条重要性质:

    1.每次增广后,若流量为k,则现在的解是流量为k的最优解。

    2.和源,汇点相邻的边不会被退流

    3.如果不是每次选一条全局最短路进行增广,而是选择任意一条和源点相连的,以后必然会流的边进行增广(假设这条边指向a,且每次选择a~汇点的边进行增广),则也能得到最小费用最大流。

    不会证明。

    如果老鼠只能向右走,则显然可以构建图:

    1.s->每个老鼠,费用0,流量1

    2.离散化,每个老鼠/洞连向它右边的第一个关键点,费用距离,流量无限(老鼠或洞)

    3.每个洞->t,费用0,流量1

    从右往左扫,每次给老鼠分配当前最近且未被占用的洞,则发现这样子模拟了“每次取任意一条以后必然会流的边进行增广",由性质2,这样子是正确的。用一个栈维护,每次遇到洞插入进入栈,遇到老鼠弹出栈顶统计答案。

    实际上,从右->左增广的好处是不用考虑反向边。

    ctsc2010 产品销售运用到了这些性质。

    显然可以构建图:

    1.s->i 容量d[i],费用0

    2.i->i+1,容量inf,费用c[i]

    3.i+1->i,容量inf,费用m[i]

    4.i->t 容量u[i],费用p[i]

    考虑以i小->大扫描每条s->i边增广。每次流一条边i,尝试向左/右走,并取代价最小者。

    实际上,这样子增广的好处在于不需要考虑向右的反向边。

    在向右增广

    jzoj5461 也运用到了这些性质

    有一个费用流模型:

    1.s->每个点i连边,费用为0,流量1

    2.每个点i向t连边,费用p[i],流量1

    3.每个点向新点v连边,费用q[i],流量1

    4.新点v向t连边,费用0,流量k

    有一个显然的做法:每次增广,然后动态维护现在的费用流图。如果费用太大则跳出循环。可以用堆来维护现在这个图。

    发现增广路径只有3种:

    1.从起点->i->t,前提是s->i,i->t的边未被占用。

    2.从起点->i->v->j->t,前提是i->v被占用,且s->i,j->t的边未被占用,费用为p[i]-q[j]

    3.从起点->i->v->t,前提是s->i,i->v的边未被占用。

    其他的路径都会有环,不优秀。

    发现这样子的加边不会让某条s->i的边解除占用,可以使用一个vis数组存储s->i的边的占用情况。

    维护2个堆,每个堆存储优惠券的边(i->v的边),普通的边(i->t的边),

    如果现在的元素被占用,则从堆中删除。

    实际上,在程序中,2,3情况可以再一起考虑。

    额外使用一个堆,这个堆存储如果沿着反向边走,获得的代价。

    取2者较小者增广即可。

    #include<bits/stdc++.h>
    using namespace std;
    priority_queue<pair<int,int>,vector<pair<int,int> >,greater<pair<int,int> > > q1,q2;
    priority_queue<int,vector<int>,greater<int> > q3;
    int n,k,vis[50010],r;
    long long m,p[50010],q[50010];
    int main(){
        freopen("shopping.in","r",stdin);
        freopen("shopping.out","w",stdout);
        scanf("%d%d%lld",&n,&k,&m);
        for(int i=1;i<=n;i++)
            scanf("%d%d",&p[i],&q[i]);
        for(int i=1;i<=k;i++)
            q3.push(0);
        for(int i=1;i<=n;i++){
            q1.push(make_pair(p[i],i));
            q2.push(make_pair(q[i],i));
        } 
        while(r<n){
            while(vis[q1.top().second])
                q1.pop();
            while(vis[q2.top().second])
                q2.pop();
            if(q2.top().first+q3.top()<q1.top().first){
                m-=q2.top().first+q3.top();
                if(m<0)break;
                q3.pop();
                q3.push(p[q2.top().second]-q[q2.top().second]);
                vis[q2.top().second]=1;
                q2.pop();
            }
            else{
                m-=q1.top().first;
                if(m<0)break;
                vis[q1.top().second]=1;
                q1.pop();
            }
            r++;
        }
        printf("%d",r);
    }
    View Code

    NOI2019 序列

    这道题的费用流模型不是很显然。且比上一道题更复杂。

    对于每个点新建i',再新建a,b

    1.s->每个点i连边,费用为a[i],流量1

    2.每个点i'向t连边,费用b[i],流量1

    3.每个i向新点a连边,费用0,流量1

    4.新点a向b连边,费用0,流量l

    5.b向所有点i'连边,费用0,流量1

    考虑每次增加1点流量。

    实际上,边只能这么走,如果走其他边会有环:

    1.s->i->i'->t,表示选出一对下标相同的对,前提是i,i'都未被占用

    2.s->i->a->b->j'->t 表示选出一对下标不同的对,前提是a->b的选择次数<l,i->a,b->j'都未被占用。

    3.s->i->i'->b->a->j->j'->t 则把i,j都选上,并且调整一下答案让一个相同下标的边都匹配。

    4.s->i->i'->b->j'->t

    由费用流的重要性质1,被选出的元素不会被退。

    实际上,还可能有s->i->a->b->i'->t,但是可以把它调整为s->i->i'->t,这样子也符合要求。且后面的流会更“自由”

    使用一个变量cnt表示现在a->b还有多少容量。

    1操作就是使用一个堆e存储a[i]+b[i]最大的,选出

    2操作要使用堆a,b维护a,b最大值,并取出。当下标相同时不用减少cnt,但是当下标不同时要减。

    用2个数组维护左边,右边的点被选择的情况。用另外2个堆c,d维护一个点被选后,左边/右边的点被选的情况。

    对于3操作,

    如果老鼠既可以向右,也可以向左走,则显然可以构建图:

    1.s->每个老鼠,费用0,流量1

    2.离散化,每个老鼠/洞连向它右/左边的第一个关键点,费用距离,流量无限(老鼠或洞)

    3.每个洞->t,费用0,流量1

    实际上,一些模拟费用流的题可以使用带权二分做。如种树。

  • 相关阅读:
    openmediavault 5.5.23 安装插件失败的解决方案
    qt下载地址
    qt 5.12 增加 mysql驱动
    选基金标准
    关注几个基金
    调仓的几个问题
    要读的书
    ubuntu 20.04 LTS 安装webmin
    set的常见用法
    斜率优化dp([HNOI2008]玩具装箱)
  • 原文地址:https://www.cnblogs.com/cszmc2004/p/12572812.html
Copyright © 2011-2022 走看看