题目简述
有n个字符合集,求其前缀树的最少结点个数。
Solve
首先(N)最大才是(16),自然想到的就是压位(D)P,定义(F[s])表示状态(s)下最短的(Trie)大小(-1)(除去根节点)
(F[s]=?),转移方程并不好推,但是我们有一个贪心的想法
因为单词可以任意排序,所以第一步就是将字符串转化为哈希表
我们将问题简化,如果两个字符串当作(Trie)合并,最小大小是多少?
自然可以想到(Union(String1,String2)=Length(String1)+Length(String2)-String1)和(String2)的最大可重部分
于是针对两个(Trie)的合并,我们也可以采取同样的方法
(Union(Trie1,Trie2)=Size(Trie1)+Size(Trie2)-Trie1)和(Trie2)的最大可重部分
由于一个(Trie)由多个(String)合并而来,于是我们可以用(L[s])表示合并状态为s的最大可重部分
自然的(L[s])很好求,我们将每种字符在每一个s状态包含字符串中出现次数最小值的加和即是(L[s])
记(cnt[s][j])表示状态s的串中字符j出现的次数
我们可以用如下代码段实现:
for (int i=0;i<N;i++){
scanf("%s",S);
for (int j=0;S[j];j++) cnt[1<<i][S[j]-'a']++;
//初始化,仅有1个字符串
}
for (int i=0;i<26;i++) cnt[0][i]=INF;
//初始化
for (int s=1;s<1<<N;s++){
int x=s&-s;
for (int i=0;i<26;i++) cnt[s][i]=min(cnt[s-x][i],cnt[x][i]);
//s&-s表示s最末尾的那一个的元素的单独的状态值,我们这么推算便能从上一状态推算而来,快了许多
for (int i=0;i<26;i++) L[s]+=cnt[s][i];
}
这样的话接下来的转移方程就容易得多了
for (int s=1;s<1<<N;s++){
F[s]=L[s];
for (int i=0;i<N;i++) if (s&(1<<i)) F[s]+=L[1<<i]-L[s];
//先把所有包含的String合并,这种情况一定可行
for (int x=(s-1)&s;x;x=(x-1)&s) F[s]=min(F[s],F[x]+F[s-x]-L[s]);
//然后试着将包含的两个Trie合并,提示:(s-1)&s=s-(s&-s),也就是每次多抠掉一个元素之后留下的状态值
}
Code
#include<bits/stdc++.h>
using namespace std;
int N,cnt[1<<16][26],F[1<<16],L[1<<16];
char S[1000005];
int main(){
freopen("vjestica.in","r",stdin);
freopen("vjestica.out","w",stdout);
scanf("%d",&N);
for (int i=0;i<N;i++){
scanf("%s",S);
int len=strlen(S);
for (int j=0;j<len;j++) cnt[1<<i][S[j]-'a']++;
}
for (int i=0;i<26;i++) cnt[0][i]=1<<30;
for (int s=1;s<1<<N;s++){
int x=s&-s;
for (int i=0;i<26;i++) cnt[s][i]=min(cnt[s-x][i],cnt[x][i]);
for (int i=0;i<26;i++) L[s]+=cnt[s][i];
}
for (int s=1;s<1<<N;s++){
F[s]=L[s];//每个串都要切掉最长的公共的部分,所以初始的时候应该单独+1个
for (int i=0;i<N;i++) if (s&1<<i) F[s]+=L[1<<i]-L[s];//先切掉每个串的公共部分,获得初始
for (int x=s-1&s;x;x=x-1&s) F[s]=min(F[s],F[x]+F[s-x]-L[s]);//转移方程也很好理解的,枚举每个串单独拎出来而已
}
printf("%d
",F[(1<<N)-1]+1);
return 0;
}