网络流建模(一)

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<<30

struct 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<<30

using 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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值