网络流建模(一)

来源:互联网 发布:行知学校 编辑:程序博客网 时间:2024/05/16 11:26

2017.8.13做的两道网络流题,难度5.5左右。
1. 1363餐巾计划问题(YZOJ)
2. 1357魔术球问题(YZOJ)

第一题
这里写图片描述

首先这道题用费用流做,是因为问题需要解决“第 i 天需要 ri 的餐巾”,且 “要使总费用最小”。
接下来考虑建模。
这个问题的难点在于,第 i 天所用的脏餐巾可供 i+m 和 i+n 天以及之后再度使用。于是设流量在通过第 i 天所代表的点之后直接与其他点连边代表再度使用。
但是第 i 天只有用脏的餐巾在可以继续使用,所以此时流量需要一个限制。那么我们可以通过拆点使 i 点本身具有容量。经过 i 点限制的流量流向 i+m 和 i+n 天之后。
图示大概这样:
错误建模
其中S向A连边代表直接购买,Ai向Bi连边代表第 i 天的容量,Bi 向 B 之后连边代表提供洗好的餐巾,边的容量和费用根据意义不说了。目前为止看起来都很正常。
建模完用笔模拟一下发现不对。购买的流量会受到最后一天容量的限制。那不是可以把每一天的B向汇点连边吗?如果这样的话就无法表示继续使用了,所谓鱼和熊掌不可兼。
换个角度看,如果需要流量来同时表示对购买的餐巾计数和提供给之后的,不如设计两份流量? 第 i 天需要的流量是确定的,那提供的脏餐巾状态本来也是确定的。
那就有了如下建图:
正解
A表示提供的脏餐巾,B表示当天需要的餐巾。
1. S往Ai连(容量为 ri,费用为 0 )的边,流量表示一天固定剩下的脏餐巾。
2. Ai 可转移到 Bi+m 之后和 Bi+n 之后,如果与之后的点都连边(inf,f或s)就是O(n2) 的空间复杂度。通过Ai 与Ai+1连边( inf,0),同时Ai 与 Bi+m 和 Bi+n连两条边( inf,f或s),使空间复杂度降为O(n)
3. S与 Bi 连边(inf,p),表示直接购买。
4. Bi 与 T 连边(ri,0),流量表示每天用过的餐巾数之和。

#include<cstdio>#include<algorithm>#include<cstring>#define R register#define inf 1<<30struct Edge{int v,c,f,nex;}edge[10000];int et,full,S,T;int st[1610],vis[1610],dis[1610],prv[1610],pre[1610];int q[1000000],l,r;void read(int &aa){    R char ch;while(ch=getchar(),ch>'9'||ch<'0');aa=ch-'0';    while(ch=getchar(),ch>='0'&&ch<='9')aa=aa*10+ch-'0';}void add(int f,int c,int a,int b){    edge[et].v=b,edge[et].c=c,edge[et].f=f,edge[et].nex=st[a],st[a]=et++;    edge[et].v=a,edge[et].c=-c,edge[et].f=0,edge[et].nex=st[b],st[b]=et++;}bool spfa(){    for(R int i=1;i<=full;i++)dis[i]=inf,vis[i]=0,prv[i]=-1;    dis[S]=0,vis[S]=1,q[0]=S,l=0,r=1;    R int u,v,e,tmp;    while(r>l)    {        u=q[l++],vis[u]=0;        for(e=st[u];e!=-1;e=edge[e].nex)if(edge[e].f>0)        {            v=edge[e].v,tmp=dis[u]+edge[e].c;            if(tmp<dis[v])            {                dis[v]=tmp,prv[v]=u,pre[v]=e;                if(!vis[v])q[r++]=v,vis[v]=1;            }        }    }    return dis[T]!=inf;}void spfa_flow(){    R int cost=0,tmp,now,e;    while(spfa())    {        tmp=inf;        for(now=T;now!=S&&((e=pre[now])|1);now=prv[now])            if(edge[e].f<tmp)tmp=edge[e].f;        for(now=T;now!=S&&((e=pre[now])|1);now=prv[now])            edge[e].f-=tmp,edge[e^1].f+=tmp;        cost+=tmp*dis[T];    }    printf("%d",cost);}int main(){    R int N1,P1,M1,F1,S1,i,j,r;    read(full),read(P1),read(M1),read(F1),read(N1),read(S1);    S=full*2+2,T=full*2+1,et=0;    memset(st,-1,sizeof(st));    for(i=1;i<=full;i++)    {        read(r);        add(r,0,S,i);        add(inf,P1,S,i+full);        add(r,0,i+full,T);        if(i<full)add(inf,0,i,i+1);        if(i+M1<=full)add(inf,F1,i,i+full+M1);        if(i+N1<=full)add(inf,S1,i,i+full+N1);    }    full=full*2+2;    spfa_flow();    return 0;}

第二题
这里写图片描述
这份题解参考了学长(n+e大神)的,讲得很不错。

这道题用暴力的话就是枚举方案数,时间复杂度是指数级的。
考虑如果确定了一个位置放的数,那么它上面可以放什么数?
可以想见,数的数量不确定,有太多方案。这种情况下,通过确定一个上界,使问题转化为判定性问题(判定性算法:“生成问题的一个解通常比验证一个给定的解时间花费要多得多。”—-百度百科)

对于一个数A,向它上面可能放的数B连一条有向边。由于B>A,这张图构成DAG。
我们发现,对于一个图求最小路径覆盖,若覆盖数<=柱子数,则该方案可行。
最小路径覆盖可以通过拆点成为二分图,通过跑最大匹配可以求出(最小路径覆盖=|V|-最大匹配)。

理论上,验证一个一个问题 期望时间 最少的是二分。但这道题跑二分的话每次都要重新建图;而从1开始枚举却满足可持久化,每次只要加入一个点,跑起来实际更快。

后面输出答案常数有点大,不够优秀。

#include<cstdio>#include<algorithm>#include<cstring>#include<cmath>#define dmin(_a,_b)(_a)<(_b)?(_a):(_b)#define R register#define inf 1<<30using namespace std;struct Edge{int to,nex,f,fo;}edge[200000];int M,N,S,T,et,st[5000],aa[5000],t1,vis[5000],to1[5000];int q[10000],l,r,level[10000];void add(int a,int b,int c){    edge[et]=(Edge){b,st[a],0,c},st[a]=et++;    edge[et]=(Edge){a,st[b],0,0},st[b]=et++;}bool bfs(){    memset(level,0,sizeof(level));    l=0,r=1,q[0]=S,level[S]=1;    R int u,v,e;    while(l<r)    {        u=q[l++];        for(e=st[u];e!=-1;e=edge[e].nex)            if(!level[edge[e].to]&&edge[e].f>0)            {                v=edge[e].to;                level[v]=level[u]+1;                q[r++]=v;                if(v==T)return true;            }           }    return false;}int dfs(int u,const int flow){    if(u==T)return flow;    R int v,e,f,ret=0,tmp;    for(e=st[u];e!=-1;e=edge[e].nex)        if(level[edge[e].to]==level[u]+1&&edge[e].f>0)        {            v=edge[e].to;            tmp=dmin(flow-ret,edge[e].f);            f=dfs(v,tmp);            edge[e].f-=f,edge[e^1].f+=f,ret+=f;            if(ret==flow)return ret;        }    return ret;}bool dinic(R int tmp){    for(R int i=0;i<et;i++)edge[i].f=edge[i].fo;    while(bfs())tmp-=dfs(S,inf);    return tmp<=N;}void rec(R int u,R int pre){    if(u==T){aa[t1++]=-1;return;}    aa[t1++]=u>>1;    R int v,e;    for(e=st[u];e!=-1;e=edge[e].nex)        if(edge[e].fo==1&&edge[e].f==0&&edge[e].to!=pre)        {            v=edge[e].to;            rec(v,u);        }}int main(){    R int i,j,k,a,b;    scanf("%d",&N);    memset(st,-1,sizeof(st));    S=0,T=1,et=0;    for(i=1;1;++i)    {        add(S,i<<1,1);add(i<<1|1,T,1);        b=(int)sqrt(i+i-1),a=(int)sqrt(i)+1;        for(j=a;j<=b;j++)            if(j*j-i<i)add((j*j-i)<<1,i<<1|1,1);        if(!dinic(i))break;        t1=1,rec(S,S);    }    M=i-1;printf("%d\n",M);    j=aa[2];    for(i=3;i<t1;i++)    {        if(aa[i]==-1){j=aa[i+1];continue;}        to1[j]=aa[i],j=aa[i];    }    for(i=1;i<=M;++i)if(vis[i]==0)    {        j=i;        while(j!=0)printf("%d ",j),vis[j]=1,j=to1[j];        printf("\n");    }    return 0;}