最小割求方案 & jzoj5537 【2014东莞市选】分组

题面

有n个字符串,给这些字符串分组,使得每个字符串属于且仅属于一个组。
对于一个合法的分组,至少满足以下两个条件种的一个:
1. 所有字符串的k前缀相同(即前k个字母相同)
2. 所有字符串的k后缀相同(即后k个字母相同)
你需要给这些字符串分组,使得所分的组数最少。

100%的数据n<=5000,k<=550

分析

首先不难看出这题其实就是:有若干可选覆盖,要完整覆盖某一对象的模型,字符串只是唬人的。
这种题往网络流、dp想。

其实是经典的最小割模型:
前缀点在T集中为选,在S集中为不选
后缀点在S集中为选,在T集中为不选。

  1. S往所有前缀连1边,所有后缀往T连1边。
  2. 串S的前缀是X,后缀是Y,那么X->Y,INF,因为不能有串前缀没选后缀也没选。 (假如有多个依赖就不能用一条边代替,要建中间点)

然后跑一次最小割就是答案;
接下来就是求方案。一种经典的思路是:
以构造的思想去看残量网络,设s可以走到(只走流量非0边)的点集为割中的S集.
剩下的是T集.
显然S->T的任意一条边都是满流的。而且其流量之和=最大流。
将这些边割掉就得到了一个S-T最小割。(因为可证明最小割=最大流,现在就构出了一个)
如果想看具体一点的证明可以去看这里写图片描述

这题并不需要知道具体割边,只需要求随意一组最小割的S集合即可。于是S遍历一次就可以得出每一个前缀是否选了。

下面是附加内容:

如何判断必然割边(有些地方也叫关键割边)呢?从S,T分别出发(T是用反向边判断当前边是否能走,因为要求的实际上是能走到T的点),可以获得一个S集与T集,可能会有一些两边都没有走到的点。如果一条边连接S,T,那么这是一条必割边。因为只要这条边改变那么最大流也会改变,最小割也会改变,也就是说这条边在最小割上。

判断可能割边也是类似的想法。假如残量网络中存在其他u->v路径,那么将此边删去后再跑最大流流量不会正好减少那么多,也就是说这条边一定不在割中(如果在,那么删掉之后割集会正好减少那么多)。刨除上述两种情况后剩下的就是可能是割边的情况了。 (充分必要只证一边,感性++)

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cstdlib>
#include <map>
using namespace std;
const int N = 5050,mo=1e9+7,SZ='Z'+1,INF=1e9;
int n,k;
char s[N][560];
int len[N],pre[N],suf[N],cnt,S,T;
pair<int,int> pp[N],ss[N];
map<int,int> pm,sm;

int ans,fz[N][N];
int final[N*2],nex[N*6],to[N*6],R[N*6],tot;
int gap[N*2],d[N*2];

int make(char *s,int l,int r) {
    long long ret=0;
    for (int i=l; i<=r; i++) ret=(ret*SZ+s[i])%mo;
    return ret;
}
void link(int x,int y ,int r) {
    R[++tot]=r,to[tot]=y,nex[tot]=final[x],final[x]=tot;
}
int go(int x,int r) {
    if (x==T) return r;
    int used=0;
    for (int i=final[x]; i; i=nex[i]) if (R[i] && d[x]==d[to[i]]+1) {
        int e=go(to[i],min(r,R[i]));
        R[i]-=e,R[i^1]+=e;
        used+=e; if (used==r) return used;
    }
    if (--gap[d[x]++]==0) d[0]=INF;
    gap[d[x]]++;
    return used;
}
int q[2*N],bz[2*N];
void bfs() {
    int l=0,r=0; q[++r]=S;
    while (l<r) {
        int x=q[++l];
        for (int i=final[x]; i; i=nex[i]) if (R[i] && !bz[to[i]]) {
            bz[to[i]]=1;
            q[++r]=to[i];
        }
    }
}

int cx[2*N];
void add(int g,int v) {
    if (!cx[g]) cx[g]=++ans;
    fz[cx[g]][++fz[cx[g]][0]]=v;
}

int main() {
    freopen("group.in","r",stdin);
//  freopen("group.out","w",stdout);
    cin>>n>>k; for (int i=1; i<=n; i++) {
        scanf("%s",s[i]+1);
        len[i]=strlen(s[i]+1);
        pp[i]=make_pair(pre[i]=make(s[i],1,k),i);
        ss[i]=make_pair(suf[i]=make(s[i],len[i]-k+1,len[i]),i);
    }
    sort(ss+1,ss+1+n),sort(pp+1,pp+1+n);
    S=1,T=cnt=2,tot=1;
    for (int i=1; i<=n; i++) 
        if (pp[i].first != pp[i-1].first) pm[pp[i].first]=++cnt,link(S,cnt,1),link(cnt,S,0);
    for (int i=1; i<=n; i++) 
        if (ss[i].first != ss[i-1].first) sm[ss[i].first]=++cnt,link(cnt,T,1),link(T,cnt,0);
    for (int i=1; i<=n; i++) {
        link(pm[pre[i]],sm[suf[i]],INF);
        link(sm[suf[i]],pm[pre[i]],0);
    }
    gap[0]=cnt;

    int sum=0;
    while (d[0]!=INF) sum+=go(1,INF);
    bfs();
    for (int i=1; i<=n; i++) {
        int e=pm[pre[i]];
        if (bz[e]) add(sm[suf[i]],i); else add(e,i);
    }
    cout<<ans<<endl;
    for (int i=1; i<=ans; i++) {
        for (int j=0; j<=fz[i][0]; j++) printf("%d ",fz[i][j]);
        printf("\n");
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值