P3294
trie 树好题,题面差评。
题意和思路
P3294 [SCOI2016]背单词
给定 \(n\) 个字符串,要求对其进行排序,使得总贡献最小。
假设插入某个排序后编号为 \(x\) 的字符串 \(s\),产生的贡献规则为:
1.如果在 \(s\) 后的字符串中有 \(s\) 的后缀,贡献为 \(n^2\)。
2.如果在 \(s\) 前的字符串中有 \(s\) 的后缀,且 \(s\) 后无 \(s\) 的后缀,则贡献为 \(x-y\)(\(y\) 是上一个是 \(s\) 的后缀的字符串排序后的编号)。
3.如果 \(s\) 无后缀(单个字符),则贡献为 \(x\)。
对于规则 3,其实可以看出其实就是规则 2 中 \(y=0\) 的特殊情况。
因为无论如何,\(x-y\) 必然远小于 \(n^2\),所以贪心的思路很简单,就是将每个字符串的后缀都放到其之前的前提下,使得最后一个此字符串的后缀与字符串之间的距离最小即可。
做法
将所有字符串翻转,建立 trie 树,并在每个字符串的末尾编号。
因为将所有字符串翻转就能把后缀转变为前缀,从而便于用 trie 树处理前缀问题。
对所有编号的节点建重构树。
遍历原树上每个节点,分情况进行不同的操作,采用并查集判是否属同一集合。
1.如果某个节点的某个子节点没有被编号,就将其与当前节点并查集合并。
2.否则把子节点作为一个新的节点,与当前节点的集合连一条边。
之前编号是为了之后跑出重构树,原先树中没有编号的点与后续操作无关,只需保留根节点和每个字符串的末尾的节点即可。
建重构树的原因是,要统计每个子树的关键节点个数。
统计重构树的每个节点的子树大小后,以此为关键字排序。
比较重构树的每个节点子树的关键节点个数的原因是对于某个节点,其贡献是其前面所有树的节点个数之和,所以要对每个节点的所有子树所含关键节点数为关键字排序。
最后按照题目给出的规则统计和即可。
记得最后统计答案的变量要开 long long。
代码
#include <bits/stdc++.h>
#define int long long
using namespace std;const int N=5e5+1e4;
int n,len[N],size[N],id[N],c,u,cnt,tot,t[N][30],flag[N],f[N],v,ans;string s[N];
vector <int> e[N];int cz(int x){return x==f[x]?x:f[x]=cz(f[x]);}
int R(){
int x=0,f=0; char ch=getchar();while(!isdigit(ch)) f|=(ch==45),ch=getchar();
while(isdigit(ch)) x=(x<<3)+(x<<1)+(ch^48),ch=getchar();return f?-x:x;
}void W(int x){if(x>9)W(x/10);putchar(x%10+'0');}bool cmp(int x,int y){return size[x]<size[y];}
void ins(int x){u=0;for(int i=len[x]-1;i>=0;i--){c=s[x][i]-'a';if(!t[u][c])t[u][c]=++tot;u=t[u][c];}flag[u]=x;}
void pre(int x){for(int i=0;i<26;i++)if(v=t[x][i]){if(!flag[v])f[v]=cz(x);else e[flag[cz(x)]].push_back(flag[v]);pre(v);}}
void dfs(int x){size[x]=1;for(int i=0;i<e[x].size();i++){dfs(e[x][i]);size[x]+=size[e[x][i]];}sort(e[x].begin(),e[x].end(),cmp);}
void ask(int x){id[x]=cnt++;for(int i=0;i<e[x].size();i++){ans+=cnt-id[x];ask(e[x][i]);}}
signed main(){
n=R();for(int i=1;i<=n;i++)cin>>s[i],len[i]=s[i].length();for(int i=1;i<=n;i++)ins(i);
for(int i=1;i<=tot;i++)f[i]=i;pre(0);dfs(0);ask(0);cout<<ans<<endl;return 0;
}