NKOJ4191 Trie树
问题描述
字母(Trie)树是一个表示一个字符串集合中所有字符串的前缀的数据结构,其有如下特征:
1.树的每一条边表示字母表中的一个字母
2.树根表示一个空的前缀
3.树上所有其他的节点都表示一个非空前缀,每一个节点表示的前缀为树根到该节点的路径上所有字母依次连接而成的字符串。
4.一个节点的所有出边(节点到儿子节点的边)中不存在重复的字母。
现在Matej手上有N个英文小写字母组成的单词,他想知道,如果将这N个单词中的字母分别进行重新排列,形成的字母树的节点数最少是多少。
输入格式
第一行包含一个正整数N(1<=N<=16)
接下来N行每行一个单词,每个单词都由小写字母组成。
单词的总长度不超过1,000,000。
输出格式
输出仅一个正整数表示N个单词经过重新排列后,字母树的最少节点数。
样例输入
10
jgda
dbfdjj
hehegdfh
faeejic
acagdgfcjc
jifiigdbif
fdbdii
ch
c
adccdd
样例输出
42
显然,如果我们希望Trie树的节点数尽量少,我们应该先将所有单词公共的字母拿出
来,作为Trie树最上几层的初始链。比如说我们有aaab,baab和cab三个单词,我们会将
ab挑出来,然后剩下的单词就变成了aa,ab,c。
对于剩下的单词,我们将其分成两个子集,(aa,ab)和(c),并分别再计算最长的公
共字母链。显然,当集合中有n个单词时,有2^n种方式将这些单词分成两个子集。
由此,我们可以用状态压缩dp解决这个问题。一个状态由单词的子集来描述,也就是
说我们有2^n个状态,并计算每一种子集形成 Trie树需要的最少节点数,转移时枚举如何将
子集分裂成两个更小的子集,即可解决整个问题。整个算法总的时间复杂度为 O(3^n)
1 #include<iostream> 2 #include<cstdio> 3 #include<cstring> 4 #include<algorithm> 5 #include<cmath> 6 using namespace std; 7 int tmp[26],n,f[1<<17]; 8 int vis[17][26],len[17]; 9 char s[17][1000001]; 10 int cal(int S) 11 {int i,j; 12 memset(tmp,127/2,sizeof(tmp)); 13 for (i=1;i<=n;i++) 14 { 15 if (S&(1<<i-1)) 16 { 17 for (j=0;j<26;j++) 18 { 19 tmp[j]=min(tmp[j],vis[i][j]); 20 } 21 } 22 } 23 int as=0; 24 for (i=0;i<26;i++) 25 as+=tmp[i]; 26 return as; 27 } 28 int dfs(int S) 29 {int i; 30 if (S==0) return 0; 31 if (S==(S&(-S))) return len[S&(-S)]; 32 if (f[S]!=-1) return f[S]; 33 int pre=cal(S),as=1e9; 34 for (i=(S-1)&S;i;i=(i-1)&S) 35 { 36 as=min(as,dfs(i)+dfs(S^i)-pre); 37 } 38 return f[S]=as; 39 } 40 int main() 41 {int i,j; 42 cin>>n; 43 memset(f,-1,sizeof(f)); 44 for (i=1;i<=n;i++) 45 { 46 scanf("%s",s[i]); 47 len[1<<i-1]=strlen(s[i]); 48 for (j=0;j<len[1<<i-1];j++) 49 { 50 vis[i][s[i][j]-'a']++; 51 } 52 } 53 dfs((1<<n)-1); 54 cout<<f[(1<<n)-1]+1; 55 }