题解 Vjestica

题目简述

有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;
}
原文地址:https://www.cnblogs.com/martian148/p/13887218.html