[NOIP模拟赛]相似字符串

37 篇文章 1 订阅
30 篇文章 0 订阅
题目描述

输入n个字符串s[i],现在需要给它们从0到n-1标号,满足m个限制条件。每个条件形如标号为ai的字符串是标号为bi的字符串的前缀。求标号方案数,答案模10^9+7输出。


输入格式
第1行:1个整数n(n≤50),表示字符串的个数
接下来n行,每行1个字符串s[i](length(s[i])≤50)
接下来1行:1个整数m,表示限制条件的个数

接下来2行:第1行有m个数,描述ai,第2行有m个数,描述bi


输出格式

第1行:1个整数,表示答案


输入样例
4
hideo
hideto
hideki
hide
2
0 0

1 2


输入样例

6


样例说明
只需要把hide编为0号字符串,它是另外3个串的前缀。另3个串的编号可任意排列,故方案数为3!=6



题解:
考虑到前缀限制,容易想到先将给定字符串之间的前缀关系处理出来。添加一个空串为根,所有串之间的前缀关系构成了一个树的结构。
令有特殊限制的字符串总个数为tot,确定了这tot个字符串是哪些字符串后,剩下的字符串可以任意地给予编号,只要最后给答案乘上(n − tot)!即可。
注意到题中的m≤8,也就是tot不超过16个,于是可以在树上进行状压dp。用f (x,S)表示在以x为根的子树中选出|S|个字符串,对应到S所代表的标号集合的方案数。
考虑前缀限制,一个分配标号方案是合法的,当且仅当对于每一对限制(ai, bi),ai在前缀树上是bi 的祖先。由于我们的dp是以子树为状态的,考虑进一步转化这个限制——可以转化为,对于任意一棵子树,任意一个限制(ai, bi)中,或者没有一个编号在其中,或者只有bi在其中,或者ai, bi 均在其中。

根据这个,可以判断一个状态S是否合法。接着,只要在dp过程中,只考虑S合法的f (x,S),就能得到满足前缀限制的标号方案数了。


由于状态的合法性可以预处理出来,dp时可以只枚举合法的状态。

可以证明合法的状态不超过3 ^m个。这是因为,对于一个限制(ai, bi),它在状态S中只有三种合法的存在情况。这样,总的状态数就从n*(2^tot) 降到了n*(3^m)
接下来,只要预处理出所有合法状态S的合法子集A,满足A与S − A均为合法状态,就可以用
O(n *(5^m) )的时间完成dp。
预处理时只要3^tot直接枚举子集判断即可,接下来证明这样做了之后,合法子集的总个数是5^m级别的。
由于A, S-A, S均合法,对于每一对限制(ai, bi),它们在A与S-A中的合法存在情况只有以下5种:
a,b 均在A中;
a ,b 均在S-A中;
a,b 均不在S中;
仅有b在A中;
仅有b在S-A中;

所以这样的组合(S, A)的总个数只有5^m级别。dp时,直接枚举这样的合法组合进行转移。总的复杂度就是O(3^tot+n*(5^m) )了。


#include<cstring>   
#include<cstdio>   
#include<algorithm>   
#include<vector>   
using namespace std; 
  
const int Mod=1e9+7; 
const int N=55; 
const int M=(1<<16)+5; 
  
void Getin( int &shu ) {   
    char c; int f=1; shu=0;   
    for( c=getchar(); c<'0'||c>'9'; c=getchar() ) if( c=='-' ) f=-1;   
    for( ; c>='0'&&c<='9'; c=getchar() ) shu=shu*10+c-'0';   
    shu*=f;   
}  
  
struct node{ char s[N]; }ch[N]; 
bool Cmp( node a, node b ) { return strcmp( a.s, b.s )<0; } 
bool Prefix( node a, node b ) {   
    int len=strlen(a.s);  
    for( int i=0; i<len; i++ ) if( a.s[i]!=b.s[i] ) return 0; 
    return 1; 
} 
  
int n, rot; 
vector<int> son[N]; 
void Build() { 
    Getin(n); rot=n+1;//增加一个虚拟根 
    for( int i=1; i<=n; i++ ) scanf( "%s", ch[i].s ); 
    sort( ch+1, ch+n+1, Cmp );   
    for( int i=1, j=i-1; i<=n; i++, j=i-1 ) { 
        for( ; j && !Prefix( ch[j], ch[i] ); j-- );//找ch[i]的前缀 
        if( !j ) j=rot;//若无前缀, 则连到虚拟根 
        son[j].push_back(i); 
    } 
} 
  
int m, a[10], b[10], p[20], pcnt; 
void Disc() { 
    Getin(m);   
    for( int i=1; i<=m; i++ ) Getin( a[i] ), p[++pcnt]=a[i];  
    for( int i=1; i<=m; i++ ) Getin( b[i] ), p[++pcnt]=b[i]; 
    sort( p+1, p+pcnt+1 ); 
    pcnt=unique( p+1, p+pcnt+1 )-p-1; 
    for( int i=1; i<=m; i++ ) {//离散化编号, 使其在0~15的范围内 
        a[i]=lower_bound( p+1, p+pcnt+1, a[i] )-p-1;  
        b[i]=lower_bound( p+1, p+pcnt+1, b[i] )-p-1;  
    } 
} 
  
int ed; 
vector<int> statu; 
bool ok[M]; 
void Judge() { 
    ed=1<<pcnt; 
    for( int i=0; i<ed; i++ ) {//状态压缩, 用i的二进制数表示状态 
        ok[i]=1;
        for( int j=1; j<=m; j++ )
            if( ( i>>a[j]&1 ) && !( i>>b[j]&1 ) ) {//只有a没有b, i不合法
                ok[i]=0; break;
            }
        if( ok[i] ) statu.push_back(i);
    }
}
  
int siz; 
vector<int> nxt[M]; 
void Prep() {//预处理出所有合法状态S的合法子集A 
	Judge();
    siz=statu.size(); 
    for( int i=0; i<siz; i++ ) 
        for( int t=statu[i]; ; t=(t-1)&statu[i] ) { 
            if( ok[t] && ok[ statu[i]-t ] ) 
                nxt[ statu[i] ].push_back(t); 
            if( !t ) break; 
        } 
} 
  
int dp[N][M], g[M]; 
void Dp( int r ) { 
	Prep();
    dp[r][0]=1; 
    int siz_son=son[r].size(); 
    for( int i=0; i<siz_son; i++ ) { 
        Dp( son[r][i] ); 
        for( int j=0; j<ed; j++ ) g[j]=dp[r][j], dp[r][j]=0; 
        for( int j=0; j<siz; j++ ) { 
            int now=statu[j]; 
            int siz_nxt=nxt[now].size(); 
            for( int k=0; k<siz_nxt; k++ ) 
                ( dp[r][now]+=1ll*g[ nxt[now][k] ]*dp[son[r][i]][ now-nxt[now][k] ]%Mod )%=Mod; 
        } 
    } 
    if( r==rot ) return; 
    for( int i=0; i<ed; i++ ) g[i]=dp[r][i], dp[r][i]=0; 
    for( int i=0; i<siz; i++ ) { 
        dp[r][ statu[i] ]=g[ statu[i] ]; 
        for( int j=statu[i]; j; j-=(j&-j) ) 
            ( dp[r][ statu[i] ]+=g[ statu[i]-(j&-j)] )%=Mod; 
    } 
} 

void Print() { 
    int sum=dp[rot][ ed-1 ]; 
    pcnt=n-pcnt; 
    for( int i=2; i<=pcnt; i++ ) sum=1ll*sum*i%Mod; 
    printf( "%d\n", sum ); 
} 
  
int main() {
    Build();
	Disc();
    Dp( rot ); 
    Print(); 
    return 0;  
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值