BZOJ 3774: 最优选择(最小割建模的一般方法)

题目大意

给定一个矩阵,选择每个元素的代价是aij,如果一个元素被选择了或者上下左右四个元素被选择了,会获得收益bij,要求最大化收益-花费

分析

想不到我也能自己做出最小割啦哈哈哈

首先这道题基本上可以确定使用最小割,第一步就是要把收益累加起来,然后看会用多少花费或者损失多少收益。

然后我们要分析出一个重要的性质就是,一个点的收益,要么选择这个点,要么选择周围一个点,要么都不选,肯定不会都选,因为选了周围一个点,当前点的选择只会带来花费,是毫无意义的,这样建图关系就稍微清晰一点了。

那么就设置一个点代表选择当前结点,设置一个点代表选择周围四个点进去乱搞。

  • 选四周,代价为0
  • 选当前,代价为a
  • 都不选,代价为b

然后你会发现怎么割它都不太对头

于是引入技巧:二分图
一个网络流很常用的技巧,但是在这里使用还是比较巧妙的。
把棋盘黑白染色之后,如果和汇点连通,黑点视为没选,白点视为选了,就很好建图了。

最终我建出来是这个样子的:

  • 源点向所有黑点连边,流量为a
  • 所有白点向汇点连边,流量为a
  • 对于一个黑色结点对应的代表选择了周围四个白色结点的那个点:黑结点向该结点连边b,该结点向对应四个白结点连边inf
  • 对于一个白色结点对应的代表选择了周围四个黑色结点的那个点:该结点向白结点连边b,对应四个黑结点向该节点连边inf

大概谈谈如何分析最小割模型

使用条件

最小割的好处是可以很自然地把元素分割成两个集合来满足题目要求,为了达到最小割这个效果,我们可以前方百计地去使得要求的答案为最小值,比如去个反什么的。

当然这两个集合并不一定是对于任意元素都等价的,比如和S集合连通,对A类元素来说是选择,对B类元素来说可以是不选择。

边和点对象的选择

一般来说点的选择就是看题目有哪些东西是需要被分类的,把需要被分类的元素单独设成一个点。

边的话大概就是把相关的元素全部连进去吧,分析的时候发现流量为0再把它删除。

边的流量的选择

通过该结点属于哪个集合,这时割掉了哪些边,带来了多少代价来计算边的流量, 有时候需要用到解方程的思想来确定边的流量,也要确定边的方向。

至于在分析模型的时候应该把哪些点放在草稿纸上呢?

源点汇点肯定要有,当前结点和相关的所有点也要有,最好是把当前结点相关的结点的
相关结点也放进来(比如这道题周围的四个点)。。。。

还有一种分析就是做一个极小的样例,比如这道题做个1*2的格子出来,并且用a,b来表示各边的值,是可以推出结果的,并且难度很低。

特别的一个:如题:代表四个结点都选的那个结点和四个结点连边必须是inf,因为这些点是不能割断的。

技巧

遇到的技巧应该就只有这个二分图了 (毕竟我才刷两道题)
推广的话就是源和汇仅仅是个分类,对于不同元素的意义可以不相同。

代码

#include<cmath>
#include<queue>
#include<cctype>
#include<cstdio>
#include<vector>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int maxn=55*55*2,maxm=maxn*4,inf=1e9;
int np,first[maxn];
struct edge{
    int from,to,next,cap,flow;
}E[maxm<<1];
void add(int u,int v,int c)
{
    //cout<<u<<" "<<v<<" "<<c<<endl;
    E[++np]=(edge){u,v,first[u],c,0};
    first[u]=np;
    E[++np]=(edge){v,u,first[v],0,0};
    first[v]=np;
}
int dx[]={1,-1,0,0};
int dy[]={0,0,1,-1};
int n,m,s,t,nm,sum;
#define getId(x,y) ((x)-1)*m+(y)
void Init()
{
    //(i+j)&1==1的点为黑色结点 
    np=-1;
    memset(first,-1,sizeof(first));
    int id,a,ni,nj;
    scanf("%d%d",&n,&m);
    nm=n*m,s=2*nm+1,t=s+1;
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=m;j++)
        {
            scanf("%d",&a);
            if((i+j)&1)
                add(s,getId(i,j),a);
            else
                add(getId(i,j),t,a);
        }
    }
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=m;j++)
        {
            scanf("%d",&a);sum+=a;
            id=getId(i,j);

            if((i+j)&1)
                add(id,id+nm,a);
            else
                add(id+nm,id,a);

            id+=nm;
            for(int k=0;k<4;k++)
            {
                ni=i+dx[k],nj=j+dy[k];
                if(ni<1 || nj<1 || ni>n || nj>m)continue;
                if((i+j)&1)
                    add(id,getId(ni,nj),inf);
                else
                    add(getId(ni,nj),id,inf);
            }
        }
    }
}
int dist[maxn],gap[maxn];
int SAP(int i,int lim)
{
    if(i==t)return lim;
    int flow=0,tmp;
    for(int p=first[i];p!=-1;p=E[p].next)if(E[p].cap-E[p].flow)
    {
        int j=E[p].to;
        if(dist[i]==dist[j]+1)
        {
            tmp=SAP(j,min(lim-flow,E[p].cap-E[p].flow));
            E[p].flow+=tmp;
            E[p^1].flow-=tmp;
            flow+=tmp;
            if(dist[s]>=t || lim==flow)return flow;
        }
    }
    if(flow==0)
    {
        if(--gap[dist[i]]==0)dist[s]=t;
        gap[++dist[i]]++;
    }
    return flow;
}
void maxFlow()
{
    memset(gap,0,sizeof(gap));
    memset(dist,0,sizeof(dist));
    gap[0]=t;
    int ret=0;
    while(dist[s]<t)
        ret+=SAP(s,inf);
    printf("%d\n",sum-ret);
}
int main()
{
    //freopen("in.txt","r",stdin);
    Init();
    maxFlow();
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值