我等必将复起,古木已发新枝|

Luckies

园龄:2年粉丝:1关注:1

2024-07-25 12:55阅读: 10评论: 0推荐: 0

P3294 [SCOI2016] 背单词 题解

题意

给你 n 个字符串,让你对其进行排列,使得按以下规则花费最少:

设当前字符串为 sxs 在答案排列中的位置。

  1. 如果 s 存在后缀且 s 的后缀在 s 之后,花费加 n2

  2. 如果 s 不存在后缀则花费加 x

  3. ys 之前离其最近的是 s 的后缀的字符串的位置,s 存在后缀且 s 的后缀在 s 之前,则花费加 xy

解法

对于 1 条件,显然尽量不要满足,因为 1 条件的花费永远比 2、3 条件多。

对于 2 条件,可以视为 3 条件中 y=0,因此和 3 条件合并。

所以可以有一个贪心策略,对于一个字符串 s,将它的若干后缀字符串放在 s 之前。那么此时仅存在 3 条件。

题目需要维护后缀,那么可以考虑将字符串反转,变成前缀,于是也可以想到对于反转的字符串建立 Trie 树。

既然要满足贪心策略,那么在 Trie 中,每一个字符串结尾的结点要在它的祖先结点之后。Trie 树就可以维护排列中的先后关系。

3 条件的花费为 xy,即排列中的距离,那么在 Trie 中,每一个字符串的结尾结点才需要考虑,因此考虑重构 Trie,将不是字符串结尾的结点删去。

考虑另一个贪心策略,由于 3 条件花费为 xy,那么 xy 的距离越近越好,所以最优策略中的 xy 在重构树中一定是父子关系。

由于我们需要 xy 最小,所以我们尽可能地让 xy 放一起,也就是遍历完父亲就遍历儿子。可以发现,这样的遍历顺序就是 dfs 序。

此时我们得到最优的遍历顺序为 dfs 序,对于一个结点 x,可以发现在 dfs 序下,它和父节点的距离为之前遍历过的兄弟结点的子树大小和,也就是花费,因此可以对每一个结点按子树大小排序,即可得到最优解。

AC Code

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e6 + 5;
int n, trie[N][27], cnt, last[N], siz[N], dfn, ans;
bool End[N];
vector<int> e[N];
void insert(string s)
{
int x = 0;
for(int _ = 0; _ < s.size(); _++)
{
char i = s[_];
if(!trie[x][i - 'a'])
trie[x][i - 'a'] = ++cnt;
x = trie[x][i - 'a'];
}
End[x] = true;
return;
}
void rebuild(int x)
{
if(End[x] && x)
{
e[last[x]].push_back(x);
last[x] = x;
}
for(auto y : trie[x])
{
if(!y)
continue;
last[y] = last[x];
rebuild(y);
}
return;
}
void dfs(int x)
{
siz[x] = 1;
for(auto y : e[x])
{
dfs(y);
siz[x] += siz[y];
}
sort(e[x].begin(), e[x].end(), [](int x, int y)
{
return siz[x] < siz[y];
});
return;
}
void dfs1(int x)
{
int k = dfn;
dfn++;
for(auto y : e[x])
{
ans += dfn - k;
dfs1(y);
}
return;
}
signed main()
{
cin >> n;
for(int i = 1; i <= n; i++)
{
string s;
cin >> s;
reverse(s.begin(), s.end());
insert(s);
}
End[0] = true;
rebuild(0), dfs(0), dfs1(0);
cout << ans;
return 0;
}

本文作者:Luckies

本文链接:https://www.cnblogs.com/Luckies/p/18322774/P3294

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   Luckies  阅读(10)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起