[BZOJ1189][HNOI2007]紧急疏散evacuate(二分+网络流)

=== ===

这里放传送门

=== ===

题解

网络流的神建图。。把每个门拆成很多点分别对应每个时刻的情况,bfs出每个门到每个人的距离dis,S向每个人连边流量为1,如果某个人在时限之内能走到某个门,就把这个人连到那个门的对应到达时刻的那个点,流量为1。然后每个门的每一个时刻能够承受的等待时间是不一样的,如果这个门是i时刻的,时限为mid,那么这个门最多还可以承受mid-i+1个单位时间的等待,把这条边从这个点连出去。而如果把这条边直接连到汇点的话会出现超出限定流量的问题,例如时限为4的门却通过了6个人。于是还要为每个门添加一个点限流,把上面提到的那条边连到为每个门设置的限流点,限流点向汇点连边流量为时限。这样建图以后跑最大流,如果最大流量等于总人数那么说明这个方案可行,二分寻找更短的时间;否则二分更长的时间来寻找可行解。而有许多需要注意的细节问题,一个就是对点的编号,这里是把人和门分开编号,每个门的400个时间点预留出来,然后再加上限流点。于是每遇到一个人,总点数就要+1;每遇到一个门,总点数就要至少+401,最后加一来得到汇点编号。读入的同时把地图存储下来。然后bfs的时候以每个门为起点单独bfs,每次清空队列。

代码

#include<cstdio>
#include<cstring>
#include<algorithm>
#define inf 1000000000
#define inc(x)(x=(x%100000)+1)
using namespace std;
int n,m,num[40][40],cnt,tot,S,T,p[50000],mp[40][40],sum,q[100010][3];
int door,dis[500][100],d[50000],cur[50000];
struct edge{
    int to,flw,nxt;
}e[1000010];
char get(){
    char c=getchar();
    while (c!='.'&&c!='X'&&c!='D') c=getchar();
    return c;
}
int getnum(int i,int j){
    return (i-1)*400+j+sum;
}
void add(int from,int to,int flow){
    e[tot].to=to;e[tot].flw=flow;
    e[tot].nxt=p[from];p[from]=tot++;
}
void make_dis(){
    int head,tail,d[4][2]={{0,1},{1,0},{0,-1},{-1,0}};
    memset(dis,-1,sizeof(dis));
    for (int i=1;i<=n;i++)
      for (int j=1;j<=m;j++)
        if (mp[i][j]==2){
            int D=num[i][j];
            head=tail=0;//以每个门为基础开始bfs,求出每个门到每个人的距离
            inc(tail);q[tail][0]=i;q[tail][1]=j;q[tail][2]=0;
            while (head!=tail){
                int u,v;
                inc(head);u=q[head][0];v=q[head][1];
                for (int i=0;i<=3;i++){
                    int x=u+d[i][0],y=v+d[i][1];
                    if (x<=n&&x>=1&&y<=m&&y>=1&&mp[x][y]==1&&dis[num[x][y]][D]==-1){
                        dis[num[x][y]][D]=q[head][2]+1;
                        inc(tail);q[tail][0]=x;q[tail][1]=y;q[tail][2]=dis[num[x][y]][D];
                    }
                }
            }
        }
}
void build(int limit){
    for (int i=1;i<=n;i++)
      for (int j=1;j<=m;j++)
        if (mp[i][j]==1){
            int wer=num[i][j];
            add(S,wer,1);add(wer,S,0);//源点向每个人连边
            for (int k=1;k<=door;k++)
              if (dis[wer][k]<=limit&&dis[wer][k]!=-1){//注意特判dis是否为-1
                  int xcv=getnum(k,dis[wer][k]);//得到这个时刻的这个门的编号
                  add(wer,xcv,1);add(xcv,wer,0);//人到门连边
              }
        }
    for (int i=1;i<=door;i++){
        int wer=getnum(door,400)+i;//得到这个门的限流点的编号
        for (int j=1;j<=limit;j++){
            int xcv=getnum(i,j);
            add(xcv,wer,limit-j+1);add(wer,xcv,0);//每个时刻向限流点连边
        }
        add(wer,T,limit);add(T,wer,0);//限流点向汇点连边
    }
}
bool Bfs(){
    int head,tail;
    memset(d,-1,sizeof(d));
    for (int i=S;i<=T;i++)
      cur[i]=p[i];
    head=0;tail=1;
    q[tail][0]=S;d[S]=0;
    while (head!=tail){
        int u;
        inc(head);u=q[head][0];
        for (int i=p[u];i!=-1;i=e[i].nxt)
          if (e[i].flw>0&&d[e[i].to]==-1){
              int v=e[i].to;
              d[v]=d[u]+1;inc(tail);q[tail][0]=v;
          }
    }
    return (d[T]!=-1);
}
int Dinic(int u,int Min){
    if (u==T||Min==0) return Min;
    for (int i=cur[u];i!=-1;i=e[i].nxt){
        int v=e[i].to,r=0;cur[u]=i;
        if (e[i].flw>0&&d[v]==d[u]+1&&(r=Dinic(v,min(Min,e[i].flw)))){
            e[i].flw-=r;e[i^1].flw+=r;return r;
        }
    }
    return 0;
}
int binary(int l,int r){
    int mid;
    while (l!=r){
        int Flow=0;
        memset(p,-1,sizeof(p));
        tot=0;mid=(l+r)>>1;
        build(mid);
        while (Bfs()){
            int r;
            while (r=Dinic(S,0x7fffffff)){
                Flow+=r;
            }
        }
        if (Flow==sum) r=mid;//如果这个值符合要求的话就搜索更优解
        else l=mid+1;
    }
    return l;
}
int main()
{
    scanf("%d%d",&n,&m);
    S=0;T=1;
    for (int i=1;i<=n;i++)
      for (int j=1;j<=m;j++){
          char c=get();
          switch (c){
              case '.':{
                  ++T;++sum;num[i][j]=++cnt;
                  mp[i][j]=1;break;
              }
              case 'D':{
                  num[i][j]=++door;T+=402;
                  mp[i][j]=2;break;
              }
              case 'X':break;
          }
      }
    make_dis();
    for (int i=1;i<=n;i++)
      for (int j=1;j<=m;j++)
        if (mp[i][j]==1){
            int wer=num[i][j];
            bool flag=false;
            for (int k=1;k<=door;k++)
              if (dis[wer][k]!=-1){
                  flag=true;break;
              }//判断无解
            if (flag==false){
                printf("impossible\n");return 0;
            }
        }
    printf("%d\n",binary(1,400));
    return 0;
}

偏偏在最后出现的补充说明

这个题做的时候可是纠结了好久。。。一开始的思路是费用流,每个人向上下左右连一条费用为1的边,然后增广的时候每次取SPFA找出的增广路的最大费用值就得到了总时间。但这样有一个漏洞就是没有办法处理有好几个人同时到一个门那里然后出现等待时间的情况。另一种思路是二分+最大流,每次二分出一个时限,然后从S向每个人连流量为1的边,如果某个人在时限之内能够走到某个门,就连流量为1的边,每个门向T连流量为时限的边,这样可以为每个门限制等待时间但是就出现了一个问题就是如果这个人走到某个门用了很长时间,已经接近了时限,按理来说他就不能等待很长时间了,但在这个门没有满流的情况下还是会让他过去。。。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值