题目描述
字母(Trie)树是一个表示一个字符串集合中所有字符串的前缀的数据结构,其有如下特征:
1.树的每一条边表示字母表中的一个字母
2.树根表示一个空的前缀
3.树上所有其他的节点都表示一个非空前缀,每一个节点表示的前缀为树
根到该节点的路径上所有字母依次连接而成的字符串。
4.一个节点的所有出边(节点到儿子节点的边)中不存在重复的字母。
单词“A”“to”“tea”“ted”“ten”“i”“in”“inn”对应的Trie树
现在Matej手上有N个英文小写字母组成的单词,他想知道,如果将这N个单词中的字母分别进行重新排列,形成的字母树的节点数最少是多少。
输入
第一行包含一个正整数N(1<=N<=16)
接下来N行每行一个单词,每个单词都由小写字母组成。
单词的总长度不超过1,000,000。
输出
输出仅一个正整数表示N个单词经过重新排列后,字母树的最少节点数。
样例输入
3
a
ab
abc
样例输出
菜鸡LGY的小声BB : ~!@#$%^&*()_+
为什么你们都那么优秀,为什么你们都会DP,为什么你们都会状压DP,为什么你们可以1天学好几个算法,在自闭的道路越走越远。
题意是中文的,就是说让你重新排列几个串,然后建立一颗字典树让节点最少。看到题卧槽不就是贪心+字典树吗,我就写然后就WA,然后就改,然后又挂了,然后小声BB这触及到我的盲区了,结束后说是状压DP,我丢你个小杰瑞,我DP都不会你让我写状压DP,耽搁了好久才来补题,真羡慕什么都会的人,虽然问你的时候你说不出去,但是你好棒棒。
思想:首先从最基础的两个单词看起:当两个单词的相同字母尽量多的时候,此时的Tire树的节点数是最少的。但是可以发现,当单词数目>=3的时候,此结论是不成立的,那么可不可以当单词数>=3时,分成若干个两两相交的状态,从而得到最优解呢?答案是可以的。然后考虑枚举每个状态子集的两个状态,这两种状态无非是将当前状态的每一位的1分到两个子状态里面,但是要确保两个子状态的当前位不同时为1,(对于某个串选则是1不选则是0,如果不太懂可以搜下基础的状压DP学习下,百度一卡车)。最后考虑DP部分,根据一开始的分析,很容易得到状态转移,找到所有串的公共部分sum,用最后枚举得到的dp[i]跟sum做差,得到当前的最优状态。状态需要从小的集合到大的集合过渡,只有子结构最优的前提下,才能得到最优的当前状态,然后考虑dp[i] == sum的时候,说明此时的所有选择的串都相同,也就不存在需要减去的情况,故只有当dp[i] > sum时,才需要dp[i]-sum。(思路来自我太子彬:https://blog.csdn.net/Ever_glow/article/details/81279152)
对于枚举子集需要用到 for(j = i;j;j = (j-1)&i) 这个for循环用于枚举子集很奈斯,下面介绍一点个人对于这个for循环操作的拙见
设S表示一个01状态集,那么它的所有非空子集x可以通过以下代码枚举。
(借鉴 https://blog.csdn.net/waitfor_/article/details/9670833)
for (int x = S; x; x = (x-1)&S)
比如S=1011,则x分别为:1011, 1010, 1001, 1000, 0011, 0010, 0001。 它不断去掉某一位的1(假如存在的话)
对这个过程的时间复杂度是3^n(转自 https://blog.csdn.net/icecab/article/details/80992743)
然后感觉就没啥了,具体看代码吧,代码后边有点拙见
#include<bits/stdc++.h>
using namespace std;
const int INF = 0x3f3f3f3f;
const int maxn = 1e6+5;
char str[maxn];
int vis[maxn][30];
int len[maxn];
int dp[maxn];
int main()
{
int n;
scanf("%d",&n);
for(int i=0;i<n;i++)
{
scanf("%s",str);
int Len=strlen(str);
len[i]=Len;
for(int j=0;j<Len;j++)//记录每个串 每个字符出现的次数
vis[i][str[j]-'a']++;
}
int temp=1<<n;
for(int i=0;i<temp;i++)//枚举所有的情况
{
int ans=0;
for(int j=0;j<n;j++)
if((1<<j)&i)//判断当前这个点是否在集合内 在的话就是1
dp[i]+=len[j];//当前集合所有字符串的总长度
for(int j=0;j<26;j++)//枚举每种字母
{
int Min=INF;
for(int k=0;k<n;k++)
if((1<<k) & i)
Min=min(Min,vis[k][j]);
ans=ans+Min;//计算公共长度的和
}
for(int j=i;j;j=(j-1)&i)//枚举所有状态
dp[i]=min(dp[i],dp[j]+dp[i^j]);//dp[i-j]跟dp[i^j]一样 去除i中j的部分
if(dp[i]>ans)
dp[i]-=ans;
}
printf("%d\n",dp[(1<<n)-1]+1);
return 0;
}