[NOI2008][bzoj1061] 志愿者招募 [费用流+巧妙的建图]

题面

传送门

思路

引入:网络流?

看到这道题,第一想法是用一个dp来完成决策

但是,显然这道题的数据并不允许我们进行dp,尤其是有10000种志愿者的情况下

那么我们就要想别的办法来解决:

贪心?这道题做的决策除了价值以外还有覆盖面问题,而且更致命的是每一天的人数需求还不一样

那么题目限制如此多元化的题目,我们就一定要想到用那个传说中的万能算法——网络流

想到网络流以后,一个直观的想法就是源点连人,人连可以被雇佣的日子,日子连汇点,然后跑最小费用最大流

但是这样有一个问题:本题中的雇佣不是可以一天用一天不用的,而是你花了那么多钱,他就一次性帮你从Si做到Ti,不能按天购买

而我们的这个网络流模型显然可以让流量从每个“人点”的一部分出边流出,并不正确

“一面对多面”

这里的这个问题,我称之为“一面对多面”,也就是一个决策会影响到多个限制条件的改变,但是网络流中的流量是一对一的,也就是一点流量不能在某一个点“分”成很多流量,所以并不能解决这种“一面对多面”的问题

那我们就要考虑更换思路了

我们之前的想法是以“一个被雇佣的人”为“一点流量”,但是仔细思索,发现出现上述问题的本质在于:不同的日子可能会用同一个人,而代表同一个人的流量只有一

那我们就要想办法让这一个流量,去覆盖所有的Si到Ti的日子

这引导我们想到如下模型:

对于每一中志愿者(si,ti,ci),我们建一条跨过si到ti的所有点的边,费用为ci,来表示“这一个流量一直流完了这些区域”

但是问题在于,只是这样建图的话,并没有把每天的人数限制ai放到图里,也就是这个图缺少信息

补全信息

此时我们需要想办法把人数限制放到图里

我们考虑最大流算法:它会求出最大的流量

那我们既然用一点流量表示一个人,那么为什么我们不把这个“需要用人”的限制,放到另外几条边上呢?

我们在点(i,i+1)之间建边,设流量为-a[i],也就是负的当天需求数,费用自然是零的

然后,令上文中的志愿者(si,ti,ci),建边(si,ti+1),费用ci,流量无限

此时我们相当于是把第i天的决策放到了第i个点和第i+1个点之间的所有边上(就是把所有点排成一排,这两个点之间的那一条位置里的所有边,包括跨过这个区间的志愿者边)

需要志愿者?让它们从志愿者边上流过去,同时让人数限制边满流到-a[i],这样求一个1-n+1的最大流,流量为0的最小费用就是雇佣人的最小费用了

为了让这个限制起效,又因为网络流中流量非负,所以我们建立点SS和TT,连边(SS,1)(n+1,TT),限制为inf,费用为0

同时,我们把之前的人数限制边的流量改成(inf-a[i]),这样最终的SS-TT最大流一定是inf,而且限制依然成立

完成

总结

到这里,我们就完成了本题的建模,可以看到这个过程是复杂而十分深刻的,我们从最开始的暴力、dp,转化到网络流,又从“一面对多面”的问题中跳了出来,转化到了最后的方法,又增加了inf的流量来使整个图合法

可以看到,这道题里主要解决的就是两个矛盾:dp时间复杂度不够,以及“一面对多面”不可行

第一个矛盾是时间上的,我们发现无法进行优化以后,转换了算法

第二个矛盾在于建模之上,我们采取了研究算法本质、重新构建模型的方法,舍弃了常见的建图模型而建立了这道题的独有模型

由此可见,做题的时候思路一定要放开,要大胆;如果发现常用的“套路”不合适,就要果断放弃、选择新方法

同时,研究算法、建立模型要从算法本质出发;一定要先想好算法中的什么东西代表题目中的哪个东西,再放到算法框架中,针对性构建模型;不能生搬硬套旧套路

Code

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define inf 1e9
using namespace std;
inline int read(){
    int re=0,flag=1;char ch=getchar();
    while(ch>'9'||ch<'0'){
        if(ch=='-') flag=-1;
        ch=getchar();
    }
    while(ch>='0'&&ch<='9') re=(re<<1)+(re<<3)+ch-'0',ch=getchar();
    return re*flag;
}
int n,m,cnt=-1,ans,first[10010],dis[10010],vis[10010];
struct edge{
    int to,next,w,cap;
}a[200010];
inline void add(int u,int v,int w,int cap){
    a[++cnt]=(edge){v,first[u],w,cap};first[u]=cnt;
    a[++cnt]=(edge){u,first[v],-w,0};first[v]=cnt;
}
int q[100010];
bool spfa(int s,int t){
    int head=0,tail=1,i,u,v,w;
    memset(dis,-1,sizeof(dis));memset(vis,0,sizeof(vis));
    q[0]=t;vis[t]=1;dis[t]=0;
    while(head<tail){
        u=q[head++];vis[u]=0;
        for(i=first[u];~i;i=a[i].next){
            v=a[i].to;w=a[i].w;
            if(a[i^1].cap&&((dis[v]==-1)||(dis[v]>dis[u]-w))){
                dis[v]=dis[u]-w;
                if(!vis[v]) q[tail++]=v,vis[v]=1;
            }
        }
    }
    return ~dis[s];
}
int dfs(int u,int t,int limit){
    if(u==t||!limit){vis[u]=1;return limit;}
    int i,v,f,flow=0,w;vis[u]=1;
    for(i=first[u];~i;i=a[i].next){
        v=a[i].to;w=a[i].w;
        if(!vis[v]&&(dis[v]==dis[u]-w)&&(a[i].cap)){
            if(!(f=dfs(v,t,min(limit,a[i].cap)))) continue;
            a[i].cap-=f;a[i^1].cap+=f;ans+=f*w;
            flow+=f;limit-=f;
            if(!limit) return flow;
        }
    }
    return flow;
}
int zkw(int s,int t){//zkw费用流
    int re=0;
    while(spfa(s,t)){
        vis[t]=1;
        while(vis[t]){
            memset(vis,0,sizeof(vis));
            re+=dfs(s,t,inf);
        }
    }
    return re;
}
int main(){
    int i,t1,t2,t3;memset(first,-1,sizeof(first));
    n=read();m=read();
    for(i=1;i<=n;i++) t1=read(),add(i,i+1,0,inf-t1);//构建“人数限制边”
    add(0,1,0,inf);add(n+1,n+2,0,inf);
    for(i=1;i<=m;i++){//构建“志愿者边”
        t1=read();t2=read();t3=read();
        add(t1,t2+1,t3,inf);
    }
    zkw(0,n+2);//最大流值一定是inf,费用就是答案
    cout<<ans<<endl;
}
posted @ 2018-04-10 11:11  dedicatus545  阅读(665)  评论(1编辑  收藏  举报