参考文章:字典树基础知识
参考代码:RepresentativeSampling参考代码来源
【题目描述】
来自ABBYY的小明有一个与“细胞与遗传学研究所”的合作。最近,研究所用一个新的题目考验小明。题目如下。
有由n个细胞组成的一个集合(不一定不同)每个细胞是一个由小写拉丁字母组成的字符串。科学家给小明提出的问题是从给定集合中选出一个大小为k的子集,使得所选子集的代表值最大。
小明做了些研究并得出了一个结论,即一个蛋白质集合的代表制可以用一个方便计算的整数来表示。我们假设当前的集合为{a1, …, ak},包含了k个用以表示蛋白质的字符串。那么蛋白质集合的代表值可以用如下的式子来表示:
其中f(x, y)表示字符串x和y的最长公共前缀的长度,例如:
f(“abc”, “abd”) = 2 , f(“ab”, “bcd”) = 0.
因此,蛋白质集合{“abc”, “abd”, “abe”}的代表值等于6,集合{“aaa”, “ba”, “ba”}的代表值等于2。
在发现了这个之后,小明要求赛事参与者写一个程序选出,给定蛋白质的集合中的大小为k的子集中,能获得最大可能代表性值得一个子集。帮助他解决这个问题吧!
【输入格式】
输入数据第一行包含2个正整数n和k(1≤k≤n),由一个空格隔开。接下来的n行每一行都包含对蛋白质的描述。每个蛋白质都是一个仅有不超过500个小写拉丁字母组成的非空字符串。有些字符串可能是相等的。
输出格式
输出一个整数,表示给定蛋白质集合的大小为k的子集的代表值最大可能是多少。
该问题实际上是在求所有字符串中最长公共子串长度相加之和;
如:蛋白质集合{“abc”, “abd”, “abe”}的代表值等于6,这个6为“abc”中的ab(2)+“abd”中的ab(2)+“abe”中的ab(2)=6
这个问题的有效解决方法就是前缀树,即字典树。
首先,我们通过图片对字典树有一个形象上的初步认识:
如图为两个字典树。
图一包含字符串“aaa”“abba”“abbc”“abbd”
图二包含字符串“aba”“abc”“bzd”
由图可以总结出,字典树的基本性质:
(1)根节点不包含字符,除根节点外每个节点只包含一个字符。
(2)从根节点到某一个节点,路径上经过的字符连接起来,为该节点对应的字符串。
(3)每个节点的所有子节点包含的字符串不相同。
特性:
1)根节点不包含字符,除根节点外每一个节点都只包含一个字符。
2)从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。
3)每个节点的所有子节点包含的字符都不相同。
4)如果字符的种数为n,则每个结点的出度为n,这也是空间换时间的体现,浪费了很多的空间。
5)插入查找的复杂度为O(n),n为字符串长度。
此处为简要摘录,关于字典树的更多具体内容与代码实现可查看顶部链接内博主的介绍。
因链接二中原博只给出了蓝桥杯中上述问题的示范代码,所以我将自己对博主代码的理解与注释写出,方便大家理解,建立字典树与该问题初步的概念。
#include
#include
#include
#include
#include
using namespace std;
#define sqz main
#define ll long long
#define reg register int
#define rep(i, a, b) for (reg i = a; i <= b; i++)
#define per(i, a, b) for (reg i = a; i >= b; i--)
#define travel(i, u) for (reg i = head[u]; i; i = edge[i].next)
const int INF = 1e9, N = 2000, M = N * 500;
const double eps = 1e-6, phi = acos(-1);
ll mod(ll a, ll b)
{
if (a >= b || a < 0)
a %= b;
if (a < 0)
a += b;
return a;
}
ll read() //录入输入的m、n
{
ll x = 0;
int zf = 1;
char ch;
while (ch != '-' && (ch < '0' || ch > '9'))
{
ch = getchar();
// printf("1,%c\n",ch); 验证输出部分,用来帮助理解代码
}
if (ch == '-')
zf = -1, ch = getchar();
while (ch >= '0' && ch <= '9')
{
x = x * 10 + ch - '0', ch = getchar();
// printf("2,x,%d\n",x);
}
return x * zf;
}
void write(ll y)
{
if (y < 0)
putchar('-'), y = -y;
if (y > 9)
write(y / 10);
putchar(y % 10 + '0');
}
int S = 0,t=1,add1=1, num = 0, cnt[M + 5], deep[M + 5], head[M + 5], F[N * 2 + 5][N + 5], Size[M + 5], son[M][26];//cnt[i]=1记录第i个结点为叶子节点,为0表示不是叶子节点
//son为数组表示的前缀树中每个字符的先后顺序,结合edge可还原出前缀树,head帮助在动态规划时找到树的不同入口,完成叶子节点的查找与遍历
char st[N + 5];
struct node
{
int vet, next, val;
}edge[2 * M];//记录叶子节点与分叉结点信息,其中Vet记录该节点对应的cnt[i]中i的值,next记录该节点是否为最后一个叶子节点,val表示从该节点往上数的树高
void add(int u, int v, int w)
{
//printf("add,%d\n",add1);
add1++;
edge[++num].vet = v; //当前是该字符串第几个字符,cnt[v]表示是否为分叉或叶子节点,son[i][j]==v,在整颗前缀树中的序列号
edge[num].next = head[u];
edge[num].val = w; //记录每个字符串长度
head[u] = num;
// printf("edge%d\n vet,%d,next,%d,val,%d\n",num,edge[num].vet,edge[num].next,edge[num].val);
}
void dfs(int u, int fa, int last) //深度优先遍历,u表示当前为son数组即前缀树中哪个字符
{
//printf("dfs,t,%d,u,%d,deep%d,%d,cnt,%d\n",t,u,fa,deep[fa],cnt[u]);
t++;
deep[u] = deep[fa] + 1;
int tot = cnt[u];
rep(i, 0, 25)
if (son[u][i]) tot++;
if (tot != 1 || cnt[u])
add(last, u, deep[u] - deep[last]);//deep记录深度,首末位相减有该节点倒着的高度
rep(i, 0, 25)
if (son[u][i])
if (tot == 1 && !cnt[u])
dfs(son[u][i], u, last);
else dfs(son[u][i], u, u);
}
int dp(int u, int fa, int len)
{
//printf("dp,t,%d,u,%d,fa,%d,cnt%d,%d,len,%d\n",t,u,fa,u,cnt[u],len);
//t++;
int now = ++S;
//printf("dp,now,%d\n",now);
Size[u] = cnt[u];
per(i, cnt[u], 1)
{
F[now][i] = i * (i - 1) / 2 * len;
//printf("now,%d,i,%d,F[now][i],%d\n",now,i,F[now][i]);
}
travel(i, u) //遍历edge结点的入口,每层循环在内部通过递归找到叶子节点,而后在该层循环内循环遍历这一分支的所有叶子节点
{
// printf("travel,i,%d, u,%d\n,headu,%d,edge.next,%d\n",i,u,head[u],edge[i].next);
int v = edge[i].vet;
if (v == u)
{
// printf("v==u, continue\n");
continue;
}
// printf("i,%d,vet,%d,next,%d,val,%d\n",i,edge[i].vet,edge[i].next,edge[i].val);
int pre = dp(v, u, edge[i].val);
// printf("pre %d\n",pre);
Size[u] += Size[v];
// printf("u, %d,Size[u], %d\n",u,Size[u]);
per(j, Size[u], 1)
{
printf("per,j,%d\n",j);
rep(k, 1, min(j, Size[v]))
{
F[now][j] = max(F[now][j], F[now][j - k] + F[pre][k] + len * (j - k) * k + len * k * (k - 1) / 2);
printf("rep,now,%d,i,%d,j,%d,k,%d,pre,%d,len,%d,F[now][j],%d\n",now,i,j,k,pre,len,F[now][j]);
}
}
}
return now;
}
int sqz()
{
memset(head, 0, sizeof head);
int n = read(), m = read(), point = 0;//n录入字符串个数,m录入前m个字符相同
rep(i, 1, n) //循环从1到n,将n个字符串录入
{
scanf("%s", st + 1);
int len = strlen(st + 1), now = 0;//录入第i个字符串,并得到字符串长度
rep(j, 1, len)
{
if (!son[now][st[j] - 'a']) //now表示当前字符串的第now+1个字符,0-25列表示a-z ,数组中的数表示该字符为第几个字符
son[now][st[j] - 'a'] = ++point; //son[now][st[j] - 'a']为空时执行 ,有值则表明前缀树中已经存在以该字符为前缀的字符串
now = son[now][st[j] - 'a'];
// printf("%d,point,%d,now\n",point,now);
}
cnt[now]++;//到达叶子节点或分叉,记录标记
}//利用数组建好了前缀树
/* rep(i,0,11)
{
rep(j,0,25)
printf("%d ",son[i][j]);
printf("\n");
} */
dfs(0, -1, 0);//深度优先,遍历前缀树,将空的根节点、分叉点、叶子节点的相关信息写入edge结构体中;
dp(0, -1, 0);//动态规划遍历到所有edge结点,判断叶子节点,分叉点并计算相关的前缀长度
/* printf("F[n][m]\n"); 各个数组的输出显示,方便理解代码
rep(i,0,9)
{
rep(j,0,9)
printf("%d ",F[i][j]);
printf("\n");
}
printf("cnt\n");
rep(i,0,15)
{
printf("%d ",cnt[i]);
}
printf("head\n");
rep(i,0,15)
{
printf("%d ",head[i]);
}*/
printf("%d\n", F[1][m]);//输出
return 0;
}