hihocoder 1398 : 网络流五·最大权闭合子图

来源:互联网 发布:上海红歆财富 骗局知乎 编辑:程序博客网 时间:2024/06/05 15:33

正宗题解:

http://hihocoder.com/problemset/problem/1398

自己的理解:

注意理解题目,题目的意思是当选择一个活动时,一定要选择这个活动所指向的学生。
即:如果有多个活动需要同一个学生,那么这个学生只需要被选中就好了,无需考虑被选了几次
先是按照要求建图,虚拟出源点s汇点t。
对于活动i,直接连s到i,权值为活动的收益。(此点为正权点)
对于学生i,直接连i到t,权值为需要的消耗。(建图的时候权值是正的,但我们知道其实这是负权点。)
对于原来的选活动i必选学生j的关系,我们连一条权值无穷大的边。

首先我们需要两个引理(这里粘自hihocoder):

  1. 最小割一定是简单割
    简单割指得是:割(S,T)中每一条割边都与s或者t关联,这样的割叫做简单割。
    因为在图中将所有与s相连的点放入割集就可以得到一个割,且这个割不为正无穷。而最小割一定小于等于这个割,所以最小割一定不包含无穷大的边。因此最小割一定一个简单割。

  2. 简单割一定和一个闭合子图对应
    闭合子图V和源点s构成S集,其余点和汇点t构成T集。
    首先证明闭合子图是简单割:若闭合子图对应的割(S,T)不是简单割,则存在一条边(u,v),u∈S,v∈T,且c(u,v)=∞。说明u的后续节点v不在S中,产生矛盾。
    接着证明简单割是闭合子图:对于V中任意一个点u,u∈S。u的任意一条出边c(u,v)=∞,不会在简单割的割边集中,因此v不属于T,v∈S。所以V的所有点均在S中,因此S-s是闭合子图。

首先有割的容量C(S,T)=T中所有正权点的权值之和+S中所有负权点的权值绝对值之和。
(由于我们将中间的那些构建成了无穷大的边,所以这时我们的算的一定是简单割的容量,所以这容量一定是由s连到的正权点和负权点连到的t所决定的。)
闭合子图的权值W=S中所有正权点的权值之和-S中所有负权点的权值绝对值之和。
(这里也就是我一开始有所疑惑的地方,后来才发现自己理解错了题意,负权点只需算一遍即可)
则有C(S,T)+W=T中所有正权点的权值之和+S中所有正权点的权值之和=所有正权点的权值之和。
所以W=所有正权点的权值之和-C(S,T)
由于所有正权点的权值之和是一个定值,那么割的容量越小,W也就越大。因此当C(S,T)取最小割时,W也就达到了最大权。

#include <iostream>#include <cstdio>#include <queue>#include <string.h>#include <algorithm>#define inf 0x3f3f3f3ftypedef long long int lli;using namespace std;const lli maxn = 102000;const lli maxq = 1000000;struct edge{   lli to,v,next;}ed[620000];lli head[maxn], cnte;lli d[maxn],cur[maxn],pre[maxn],gap[maxn],s,t,q[maxq];void ae(lli x, lli y, lli v) {    ed[cnte].to = y;    ed[cnte].v = v;    ed[cnte].next = head[x];    head[x] = cnte++;    ed[cnte].to = x;    ed[cnte].v = 0;    ed[cnte].next = head[y];    head[y] = cnte++;}void rbfs (lli s,lli t) {//反向BFS标号    lli fi,se;    memset(gap,0,sizeof(gap));    memset(d,-1,sizeof(d));//没标过号则为-1  补码为 1111 1111    d[t] = 0;//汇点默认为标过号    gap[0] = 1;    fi = se = 0;    q[se++] = t;    while (fi != se) {        lli u = q[fi++];        for (lli i=head[u];~i;i=ed[i].next) {            lli v = ed[i].to;            if (~d[v]) continue;//已经标过号            d[v] = d[u] + 1;//标号            q[se++] = v;            gap[d[v]]++;        }    }}lli isap(lli s,lli t){    memcpy(cur,head,sizeof(head));//复制,当前弧优化    rbfs (s,t);//只用标号一次就够了,重标号在ISAP主函数中进行就行了    lli flow = 0, u = pre[s]=s,i;    while(d[t] < t+1) {//最长也就是一条链,其中最大的标号只会是 t ,如果大于等于t了说明中间已经断层了。        if(u==t) {//如果已经找到了一条增广路,则沿着增广路修改流量            lli f = inf,neck;            for(i= s;i != t;i = ed[cur[i]].to){                if(f > ed[cur[i]].v){                    f = ed[cur[i]].v;//不断更新需要减少的流量                    neck = i;//记录回退点,目的是为了不用再回到起点重新找                }            }            for(i = s;i != t;i = ed[cur[i]].to){//修改流量                ed[cur[i]].v -= f;                ed[cur[i]^1].v += f;            }            flow += f;//更新            u = neck;//回退        }        for(i = cur[u];~i;i=ed[i].next) if(d[ed[i].to]+1 == d[u] && ed[i].v) break;        if(~i) {//如果存在可行增广路,更新            cur[u] = i;//修改当前弧            pre[ed[i].to] = u;            u = ed[i].to;        }        else{//否则回退,重新找增广路            if(0 == (--gap[d[u]])) break;//GAP间隙优化,如果出现断层,可以知道一定不会再有增广路了            lli mind = t+1;            for(i = head[u];~i;i = ed[i].next){                if(ed[i].v && mind > d[ed[i].to]){//寻找可以增广的最小标号                    cur[u] = i;//修改当前弧                    mind = d[ed[i].to];                }            }            d[u] = mind + 1;            gap[d[u]]++;            u = pre[u];//回退        }    }    return flow;}void ini(){    memset(head,-1,sizeof(head));cnte = 0;}int main(){    int cas,n,m,a,b,v,k;    scanf("%d%d",&n,&m);    int s = 0,t = n+m+1;    ini();    lli ans = 0;    for(int i = 1;i <= m;i++){        scanf("%d",&a);        ae(i+n,t,a);    }    for(int i = 1;i <= n;i++){        scanf("%d%d",&a,&k);        ae(s,i,a);ans += a;        for(int j = 1;j <= k;j++){            scanf("%d",&v);            ae(i,v+n,inf);        }    }    printf("%lld\n",ans-isap(s,t));    return 0;}
原创粉丝点击