题目

数据范围与提示
节点数量 n≤106n\le 10^6n≤106 。字符集可以大小为 nnn,以数字的方式输出。保证有解(虽然不保证也可以做)。
思路
有一个部分分是,保证 111 为根(空串对应的节点)。想想已知根的情况下,怎么检查答案呢?
考虑 fail\rm failfail 的含义。显然就是某几对节点的字母相同。并查集 可以维护这玩意儿。但是这样需要连 ∑∣fail∣\sum|\rm fail|∑∣fail∣ 条边啊?
然而,fail(x)\mathrm{fail}(x)fail(x) 的父节点,肯定是 fa(x)fa(x)fa(x) 的 fail\rm failfail 链上的点。fail\rm failfail 链上的所有点都相同。也就是说,保证父节点正确时,只需要 xxx 和 fail(x)\mathrm{fail}(x)fail(x) 两个点相同。
这就是充分条件了吗?并不是。可能 fa(x)fa(x)fa(x) 跳 fail\rm failfail 链的时候,先遇到了一个 cxc_xcx,于是 xxx 的 fail\rm failfail 就长度偏大了。(从这一点可以看出,只要不是并查集中的同一连通块,就赋值为一个不同的字符,这样最不容易出锅。)那么我们要检查一下。
一个比较粗暴的做法是,把得到的 trie\tt trietrie 树真的拿去做 AC\Bbb{AC}AC 自动机。但是这样就丢失了 fail\rm failfail 树的信息,并且因为字符集大小的缘故,会劣化到 O(nlogn)\mathcal O(n\log n)O(nlogn) 。有没有什么可以利用 fail\rm failfail 树的方案呢?
想想,其实我们就是要检查,在 已有的 fail\rm failfail 链上,如果下一个字符是 ccc,到底会跳到哪里。那么这个 fail\rm failfail 链其实就是给出的 fail\rm failfail 树的一条到根的链。于是,我们在 fail\rm failfail 树上递归,可以 O(n)\mathcal O(n)O(n) 的维护最晚出现的、某种字符作为子节点,于是就可以线性的检查了。
然而现在并不知道根是谁。根有什么特点吗?肯定是 fail\rm failfail 有特点。根周围的点,其 fail\rm failfail 必然指向根。那么我们找到这种 trie\tt trietrie 与 fail\rm failfail 重合的边,看看它们能怎样帮助我们。
如果这种重合边不与根相连,也有可能,但是它就说明 ∣fail∣=∣s∣−1|\mathrm{fail}|=|s|-1∣fail∣=∣s∣−1,即这个串的每个字符都相同。那么从这个点到根,路径上的每一条边都必须是重合边!同样的,一个点往下,最多只有一条重合边——这说明,一旦有一个点,其周围至少三条边都是重合边,那么必须这个点是根。
如果没有找到三度点,剩下的就是一条链(因为它们必须构成一个连通块,包含根)。看来还需要一个别的点。找到一个到链距离为 111 的点,那么这个点代表的字符串肯定是 aaa⋯axaaa\cdots axaaa⋯ax(其中 aaa 是重合边对应的字符)。那么它的 fail\rm failfail 要么是 a⋯axa\cdots axa⋯ax(长度更短,形式相同,不是链上点),要么是 xxx(显然是链上点),要么是根(链上点)。对于情况一,我们可以递归地使用那个节点,直到它不再是情况一。此时我们得到的这个点,与根的距离不超过 111 。
而在一条链上,与一个点距离不超过 111 的,只有 333 个点。都检查一下,就做完了。时间复杂度 O(n)\mathcal O(n)O(n) 。
代码
#include <cstdio>
#include <iostream>
#include <cstring>
#include <vector>
#include <map>
#include <queue>
using namespace std;
# define rep(i,a,b) for(int i=(a); i<=(b); ++i)
# define drep(i,a,b) for(int i=(a); i>=(b); --i)
typedef long long int_;
inline int readint(){
int a = 0; char c = getchar(), f = 1;
for(; c<'0'||c>'9'; c=getchar())
if(c == '-') f = -f;
for(; '0'<=c&&c<='9'; c=getchar())
a = (a<<3)+(a<<1)+(c^48);
return a*f;
}
const int MaxN = 1000005;
namespace UFS{
int fa[MaxN];
void init(int n){
rep(i,1,n) fa[i] = i;
}
inline int find(int a){
if(fa[a] != a)
fa[a] = find(fa[a]);
return fa[a];
}
void merge(int a,int b){
fa[find(a)] = find(b);
}
}
vector<int> trie[MaxN];
int dep[MaxN], fa[MaxN];
void build(int x){
dep[x] = dep[fa[x]]+1;
for(int y : trie[x])
if(y != fa[x])
fa[y] = x, build(y);
}
vector<int> fail[MaxN];
int mp[MaxN], nxt[MaxN];
bool dfs(int x,int pre){
for(int y : trie[x])
if(dep[y] > dep[x])
if(nxt[y] != mp[UFS::find(y)])
return false; // not match
vector<int> tmp; tmp.clear();
for(int y : trie[x])
if(dep[y] > dep[x]){
int id = UFS::find(y);
if(!(~mp[id])) return 0;
tmp.push_back(mp[id]);
mp[id] = -1; // put tag
}
for(int y : trie[x])
if(dep[y] > dep[x])
mp[UFS::find(y)] = y;
for(int y : fail[x]){
if(y == pre) continue;
if(!dfs(y,x)) return false;
}
auto p = tmp.begin(); // easy access
for(int y : trie[x])
if(dep[y] > dep[x])
mp[UFS::find(y)] = *(p ++);
return true; // no error reported
}
int n;
bool check(int rt){
UFS::init(n), dep[rt] = 0;
for(int x : trie[rt])
fa[x] = rt, build(x);
rep(i,1,n) if(i != rt){
nxt[i] = -1;
for(int x : fail[i])
if(dep[x] < dep[i])
if(~nxt[i]) return 0;
else nxt[i] = x;
else if(dep[x] == dep[i])
return false;
if(!(~nxt[i])) return false;
if(nxt[i] != rt)
UFS::merge(i,nxt[i]);
}
rep(i,1,n) mp[i] = rt; // default
return dfs(rt,-1);
}
pair<int,int> EA[MaxN]; // for output
bool good[MaxN];
# define psbk push_back
int main(){
n = readint(); // global variable
for(int i=1,a,b; i<n; ++i){
a = readint(), b = readint();
trie[a].psbk(b), trie[b].psbk(a);
EA[i] = make_pair(a,b);
}
for(int i=1,a,b; i<n; ++i){
a = readint(), b = readint();
fail[a].psbk(b), fail[b].psbk(a);
}
if(!check(1)){
memset(mp+1,0,n<<2);
int center = 0;
for(int i=1; i<=n; ++i){
for(int x : trie[i])
mp[x] = 1;
int cnt = 0;
for(int x : fail[i])
cnt += mp[x];
if(cnt >= 1){
good[i] = true;
if(cnt >= 3){
check(center = i);
printf("%d\n",i);
break; // found
}
}
for(int x : trie[i])
mp[x] = 0;
}
if(center == 0){
for(int i=1; i<=n; ++i){
if(!good[i]) continue;
for(int j : trie[i]){
if(good[j]) continue;
for(int x : fail[j])
if(good[x])
center = x;
}
}
if(!check(center)){
for(int x : trie[center])
if(good[x] && check(x)){
printf("%d\n",x);
break;
}
} else printf("%d\n",center);
}
}
else puts("1");
for(int j=1; j<n; ++j)
if(dep[EA[j].first] > dep[EA[j].second])
printf("%d ",UFS::find(EA[j].first));
else printf("%d ",UFS::find(EA[j].second));
putchar('\n');
return 0;
}
后记
原题的数据范围是 3×1053\times 10^53×105,因为他的并查集是 logn\log nlogn 个(二进制分解,真的把 ∣fail∣|\rm fail|∣fail∣ 个点相同的信息存了下来),并且是暴力建 AC\Bbb{AC}AC 自动机……
所以是谁,又把出题人吊打?哈哈哈,不言而喻,一目了然。TLY\sf TLYTLY 太阳神 无所不能!
2021/7/22 update\tt 2021/7/22\;update2021/7/22update:忽然想起,给出 trie\tt trietrie 树建 AC\Bbb{AC}AC 自动机的时间复杂度是有问题的!复杂度可达到叶子节点的深度和!扫把形的图就会卡成 Θ(n2)\Theta(n^2)Θ(n2) 了!糟糕的出题人和糟糕的验题人(如果有)。

41

被折叠的 条评论
为什么被折叠?



