Upc 6902 Trie树 这辈子不可能学会的动态规划之状压DP

题目描述

字母(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;
}

猜你喜欢

转载自blog.csdn.net/passer__/article/details/81476127
UPC