洛谷P1251 餐巾计划问题、P2765 魔术球问题,POJ3680 Intervals (网络流)

前言

又到了kanon的季节。

还记得我省选前刷了不少网络流的题目,那个时候做完题没有写总结,结果现在再看题,全都懵逼QAQ,故在此记录一下其中三道经典题的建模方法。


洛谷P1251 餐巾计划问题

题目传送门:https://www.luogu.org/problemnew/show/P1251

题目分析:这道题的建模十分巧妙,我看了网上的题解都不是很明白,于是尝试着自己推导了一下。在YY出无数种错误的构图后,我终于把图构对了。

首先题目要求第i天有r[i]块干净的餐巾可以用,于是我们先画出一个最简单的图:

其中逗号左边的数代表流量上限,右边的数代表花费。

接下来题目又说可以快洗和慢洗,于是我们将第i天右边的节点分别向左边第i+m天的节点和第i+n天的节点连对应的边,前提是i+m或i+n小于等于天数:

此时一条流过绿色边的流就代表快洗了一张餐巾,流过红色边就代表慢洗。流向t就代表扔掉这张餐巾。
为什么要右边的点向左边的点连边呢?因为每天快洗+慢洗+扔掉的餐巾要刚好等于r[i],而右边的点本身就刚好受到r[i]的流量限制。

然后我们再观察一下构图,发现还是有问题:某一天用完的餐巾不一定要当天就快洗或慢洗,然后送给第i+m或i+n天用;也不一定用完了就扔掉。它可能留着以后再洗,所以要修改一下构图:

这个图看上去是没问题了,但事实上它有一个很严重的问题。我们的最终目的是要使中间所有r[i],0的边都流满,于是现在来模拟一条流(用下图中的棕色表示):

那么它的含义是:在第一天买进来1条餐巾,然后在第一天结束时送去了慢洗,第二天用完后就一直没再用。

那么这条餐巾实际上用了两天,但它只代表了从s到t的1的流量。也就是说,如果有一些餐巾用了多天,最终的流量就会小于r的总和。而我们知道,最小费用最大流是优先跑最大流的,所以最后解出来的流一定是这样:

因为这样才是最大流量。也就是说,最终答案是p*(r的总和)。这样显然错误。

那有没有什么办法,让一条用了两天的餐巾代表2的流量呢?这就是本题构图的巧妙之处。我们不妨先让每天开始时得到的r[i]条干净的餐巾(左边一列的节点)流向t,然后再在每天结束时从s补回r[i]条脏的餐巾(从s向右边一列的节点连r[i],0的边)

这样,之前的那条棕色的流就被拆成了下面两条流:

然后问题就巧妙地解决啦!(注意流量要开long long)

CODE:

#include<iostream>
#include<string>
#include<cstring>
#include<cmath>
#include<cstdio>
#include<cstdlib>
#include<stdio.h>
#include<algorithm>
using namespace std;

const int maxn=4010;
const int maxm=200000;
const int oo=1000000001;
const long long INF=100000000000000000LL;
typedef long long LL;

struct data
{
    LL cap;
    int obj,cost;
    data *rev,*Next;
} e[maxm];
data *head[maxn];
data *nhead[maxn];
int cur=-1;

int dis[maxn];
bool vis[maxn];
int que[maxn];
int he,ta;

int r[maxn/2];
int N,p,m,f,n,s,t;

void Add(int x,int y,LL flow,int c)
{
    cur++;
    e[cur].obj=y;
    e[cur].cap=flow;
    e[cur].cost=c;
    e[cur].rev=&e[cur+1];
    e[cur].Next=head[x];
    head[x]=e+cur;
    cur++;
    e[cur].obj=x;
    e[cur].cap=0;
    e[cur].cost=-c;
    e[cur].rev=&e[cur-1];
    e[cur].Next=head[y];
    head[y]=e+cur;
}

bool Release(int x,int y,int c)
{
    if (dis[x]+c>=dis[y]) return false;
    dis[y]=dis[x]+c;
    return true;
}

bool SPFA()
{
    for (int i=0; i<=t; i++)
    {
        nhead[i]=head[i];
        dis[i]=oo;
        vis[i]=false;
    }
    dis[0]=0;
    vis[0]=true;
    que[1]=0;
    he=0,ta=1;
    while (he!=ta)
    {
        he=(he+1)%maxn;
        int node=que[he];
        for (data *p=head[node]; p; p=p->Next)
            if ( p->cap && Release(node,p->obj,p->cost) && !vis[p->obj] )
            {
                ta=(ta+1)%maxn;
                que[ta]=p->obj;
                vis[p->obj]=true;
            }
        vis[node]=false;
        int nhe=(he+1)%maxn;
        if ( dis[ que[ta] ]<dis[ que[nhe] ] ) swap(que[nhe],que[ta]);
    }
    for (int i=0; i<=t; i++) vis[i]=false;
    return dis[t]<oo;
}

LL Dfs(int node,LL mf)
{
    if ( node==t || !mf ) return mf;
    LL nf=0;
    vis[node]=true;
    for (data *&p=nhead[node]; p; p=p->Next)
        if ( p->cap && dis[p->obj]==dis[node]+p->cost && !vis[p->obj] )
        {
            LL d=Dfs(p->obj, min(p->cap,mf) );
            p->cap-=d;
            p->rev->cap+=d;
            mf-=d;
            nf+=d;
            if (!mf) break;
        }
    if (mf) dis[node]=oo;
    vis[node]=false;
    return nf;
}

LL Min_cost_flow()
{
    LL mcf=0;
    while ( SPFA() )
        mcf+=(long long)dis[t]*Dfs(0,INF);
    return mcf;
}

int main()
{
    freopen("c.in","r",stdin);
    freopen("c.out","w",stdout);

    scanf("%d",&N);
    t=(N<<1)+1;
    for (int i=0; i<=t; i++) head[i]=NULL;
    for (int i=1; i<=N; i++) scanf("%d",&r[i]);
    scanf("%d%d%d%d%d",&p,&m,&f,&n,&s);
    for (int i=1; i<=N; i++)
    {
        Add(0,i,INF,p);
        Add(0,N+i,r[i],0);
        Add(i,t,r[i],0);
        if (i<N) Add(N+i,N+i+1,INF,0);
        if (i+m<=N) Add(N+i,i+m,INF,f);
        if (i+n<=N) Add(N+i,i+n,INF,s);
    }

    LL ans=Min_cost_flow();
    cout<<ans<<endl;

    return 0;
}

洛谷P2765 魔术球问题

题目传送门:https://www.luogu.org/problemnew/show/P2765

题目分析:这题的模型主要是DAG的最少路径覆盖。

很明显我们可以二分答案。假设二分出ans,如何判断1~ans中的数要用几根柱子?用 ans2 的时间,判断每两个数是否能组成平方数,然后让小的数向大的数连边,就变成了DAG的最少路径覆盖,可以用二分图匹配解决。

如果原图中x向y有一条边,就让左边的第x个点向右边第y个点连边。匹配完后如果x流向了y,就说明x在最终方案中走向了y。为什么可以这样对应呢?我们注意到在最终方案中,每个点不会超过1条出边,而一个没有出边的点,就意味着一条链的结束,所以我们要使没有出边的点尽可能少,也就是二分图中失配的数量尽可能少,所以就是二分图最大匹配了。

至于打印路径,找到所有失配的点,然后递归打印就好了。

CODE:

#include<iostream>
#include<string>
#include<cstring>
#include<cmath>
#include<cstdio>
#include<cstdlib>
#include<stdio.h>
#include<algorithm>
using namespace std;

const int maxn=100000;
const int maxm=1000000;
const int oo=1000000001;

struct data
{
    int obj,cap;
    data *rev,*Next;
} e[maxm];
data *head[maxn<<1];
data *nhead[maxn<<1];
int cur;

int level[maxn<<1];
int que[maxn<<1];
int he,ta;

bool vis[maxm];
int N,n,t=maxn,match;

void Add(int x,int y,int c)
{
    cur++;
    e[cur].obj=y;
    e[cur].cap=c;
    e[cur].rev=&e[cur+1];
    e[cur].Next=head[x];
    head[x]=e+cur;
    cur++;
    e[cur].obj=x;
    e[cur].cap=0;
    e[cur].rev=&e[cur-1];
    e[cur].Next=head[y];
    head[y]=e+cur;
}

int Bfs(int x)
{
    for (int i=0; i<=x; i++)
    {
        nhead[i]=head[i];
        level[i]=0;
    }
    for (int i=t; i<=maxn+x; i++)
    {
        nhead[i]=head[i];
        level[i]=0;
    }
    level[0]=1;
    que[1]=0;
    he=0,ta=1;
    while (he<ta)
    {
        int node=que[++he];
        for (data *p=head[node]; p; p=p->Next)
            if ( p->cap && !level[p->obj] )
            {
                level[p->obj]=level[node]+1;
                que[++ta]=p->obj;
            }
    }
    return level[t];
}

int Dfs(int node,int mf)
{
    if ( node==t || !mf ) return mf;
    int nf=0;
    for (data *&p=nhead[node]; p; p=p->Next)
        if ( p->cap && level[p->obj]==level[node]+1 )
        {
            int d=Dfs(p->obj, min(p->cap,mf) );
            p->cap-=d;
            p->rev->cap+=d;
            mf-=d;
            nf+=d;
            if (!mf) break;
        }
    if (mf) level[node]=-1;
    return nf;
}

int Dinic(int x)
{
    int max_flow=0;
    while ( Bfs(x) )
        max_flow+=Dfs(0,oo);
    return max_flow;
}

int Work(int x)
{
    cur=-1;
    for (int i=0; i<=x; i++) head[i]=NULL;
    for (int i=t; i<=t+x; i++) head[i]=NULL;
    for (int i=1; i<=x; i++)
    {
        Add(0,i,1);
        Add(i+maxn,t,1);
    }
    for (int i=2; i<=x; i++)
        for (int j=1; j<i; j++)
            if (vis[i+j])
                Add(i,maxn+j,1);
    int temp=Dinic(x);
    return x-temp;
}

void Print(int i)
{
    vis[i]=true;
    for (data *p=head[i]; p; p=p->Next)
        if ( p->obj && p->rev->cap )
            Print(p->obj-maxn);
    printf("%d ",i);
}

int Binary()
{
    int L=0,R=10001;
    while (L+1<R)
    {
        int mid=(L+R)>>1;
        if ( Work(mid)<=n ) L=mid;
        else R=mid;
    }
    return L;
}

int main()
{
    freopen("c.in","r",stdin);
    freopen("c.out","w",stdout);

    for (int i=1; i*i<maxm; i++) vis[i*i]=true;
    scanf("%d",&n);
    N=Binary();
    printf("%d\n",N);
    Work(N);
    for (int i=0; i<=N; i++) vis[i]=false;
    for (int i=N; i>=1; i--)
        if (!vis[i])
        {
            Print(i);
            printf("\n");
        }

    return 0;
}

POJ3680 Intervals

题目传送门:http://poj.org/problem?id=3680

题目分析:记得之前APIO的时候,某THU神犇给选手讲关于如何用网络流解决区间选择问题,然后就举了一道类似的例题(或许就是本题)。

这里要求每个数不能被包含于k个区间,那么我们可以将每个数都看成一条边,容量为无穷大,费用为0。出现了一个[a,b]的区间,就将a前面的点向b后面的点连一条流量为1,费用为区间权值的边。跑一次从最左边到最右边,流量为k的最大费用流即可(将边权取反,就是最小费用流):

其中每一条1的流量就是选择了一些不相交的区间。

CODE:

#include<iostream>
#include<string>
#include<cstring>
#include<cmath>
#include<cstdio>
#include<cstdlib>
#include<stdio.h>
#include<algorithm>
using namespace std;

const int maxn=410;
const int maxm=10010;
const int oo=1000000001;

struct edge
{
    int obj,cap,cost;
    edge *rev,*Next;
} e[maxm];
edge *head[maxn];
edge *nhead[maxn];
int cur;

int dis[maxn];
bool vis[maxn];
int que[maxn];
int he,ta;

int a[maxn];
int b[maxn];
int w[maxn];

struct data
{
    int P,id,val;
} bot[maxn];

int T,n,k,t;
int ans;

bool Comp(data x,data y)
{
    return x.val<y.val;
}

void Add(int x,int y,int f,int c)
{
    cur++;
    e[cur].obj=y;
    e[cur].cap=f;
    e[cur].cost=c;
    e[cur].rev=&e[cur+1];
    e[cur].Next=head[x];
    head[x]=e+cur;
    cur++;
    e[cur].obj=x;
    e[cur].cap=0;
    e[cur].cost=-c;
    e[cur].rev=&e[cur-1];
    e[cur].Next=head[y];
    head[y]=e+cur;
}

bool Release(int x,int y,int c)
{
    if (dis[x]+c>=dis[y]) return false;
    dis[y]=dis[x]+c;
    return true;
}

bool SPFA()
{
    for (int i=0; i<=t; i++)
    {
        nhead[i]=head[i];
        dis[i]=oo;
        vis[i]=false;
    }
    dis[0]=0;
    vis[0]=true;
    he=0,ta=1;
    que[1]=0;
    while (he!=ta)
    {
        he=(he+1)%maxn;
        int node=que[he];
        for (edge *p=head[node]; p; p=p->Next)
            if ( p->cap && Release(node,p->obj,p->cost) && !vis[p->obj] )
            {
                ta=(ta+1)%maxn;
                que[ta]=p->obj;
                vis[p->obj]=true;
            }
        vis[node]=false;
        int nhe=(he+1)%maxn;
        if ( dis[ que[nhe] ]>dis[ que[ta] ] ) swap(que[nhe],que[ta]);
    }
    return dis[t]<0;
}

int Dfs(int node,int mf)
{
    if ( node==t || !mf ) return mf;
    vis[node]=true;
    int nf=0;
    for (edge *&p=nhead[node]; p; p=p->Next)
        if ( p->cap && dis[p->obj]==dis[node]+p->cost && !vis[p->obj] )
        {
            int d=Dfs(p->obj, min(p->cap,mf) );
            p->cap-=d;
            p->rev->cap+=d;
            mf-=d;
            nf+=d;
            if (!mf) break;
        }
    if (mf) dis[node]=oo;
    vis[node]=false;
    return nf;
}

int Min_cost_flow()
{
    int mcf=0;
    while ( SPFA() )
        mcf+=(dis[t]*Dfs(0,oo));
    return mcf;
}

int main()
{
    freopen("c.in","r",stdin);
    freopen("c.out","w",stdout);

    scanf("%d",&T);
    while (T--)
    {
        scanf("%d%d",&n,&k);
        for (int i=1; i<=n; i++)
        {
            int x=(i<<1)-1;
            bot[x].P=0;
            bot[x].id=i;
            scanf("%d",&bot[x].val);
            x++;
            bot[x].P=1;
            bot[x].id=i;
            scanf("%d%d",&bot[x].val,&w[i]);
        }

        sort(bot+1,bot+(n<<1)+1,Comp);
        int temp=0;
        for (int i=1; i<=(n<<1); i++)
        {
            if (bot[i-1].val<bot[i].val) temp++;
            if (bot[i].P) b[ bot[i].id ]=temp;
            else a[ bot[i].id ]=temp;
        }

        t=temp+1,cur=-1;
        for (int i=0; i<=t; i++) head[i]=NULL;
        for (int i=1; i<t; i++) Add(i,i+1,oo,0);
        Add(0,1,k,0);
        for (int i=1; i<=n; i++) Add(a[i],b[i],1,-w[i]);

        ans=Min_cost_flow();
        ans=-ans;
        printf("%d\n",ans);
    }

    return 0;
}
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值