P7469 [NOI Online 2021 提高组] 积木小赛 题解

· · 个人记录

算法:Trie + Dfs

考场心路:

想出这个算法还是经历了一点波折的。 经历的 T1 的折磨之后,脑子非常不好使,当时看大佬用的 子序列自动机(下文自动机都是指此) ,一想也差不多,就写了个二分答案的自动机,想来是后悔(毕竟朴素的完全可以直接过)。洛谷数据 80 pts。
然后过来写代码的时候,发现自动机写其实也怪麻烦的,毕竟判重的 hash 摆在那楞要你用(本人 hash 被卡过无数次)。所以就思考着可不可以用其他的判重,比如 Trie 。写好之后,发现自动机里匹配的步骤和字典树有一点奇妙的联系(自然是朴素自动机)。然后这边思路就出来了。

算法思路

对于自动机思路,我们需要找到可供匹配范围内且顺序最前的相同字符然后转移,转转不已,直到找不到下一个可供匹配的字符(在此题,仅需进行后缀的匹配,将每步转移计入答案即可),然后在 Trie 上,将所得字符串逐个录入树里,然后打上 tag,表示这里是一个完整的串,用以判重。
我们可以发现,在匹配过程中的转移中字符的下标是递增的,而我们字典树的节点的父子关系很明显也暗含着先后关系。
那么我们可以将先后顺序改过来,在 Trie 上将所有可能成为答案的字符串录入,最后再用自动机的思想进行答案合法性的判断。相当于在 Trie 上跑子序列自动机。(?)

## 实现思路 在 `Trie` 上跑自动机不可谓不简单。 我们先思考在自动机里的转移: 1. 枚举 `start` 并使 `at=start` 表示子序列开始匹配的位置。 1. 枚举 `i` 并每次让 `at=nxt[T[i]][at]`,表示 `at` 从 `at` 指向`at`**之后**的**当前匹配字符**的位置。若匹配完 `T` 串则 `T` 为 `S` 的子序列;反正,则 `T` 不为 `S` 子序列。(在此题中,每次转移即代表一个完整的 `T` 串,具体原因可见其他自动机写法的题解) 而在 `Trie` 中,我们也依照此种转移,但因为**从一开头到 `T[i]` 的字符串可表现为其中的根节点到某一节点的路径**而且**其所对应的 `at` 值仅仅会被其父节点的 `at` 值影响**,所以我们**仅需在当前节点找到存储** `at` 的位置,并在子节点继承即可。简略描述如下。 $\texttt{PS:}$ $u$ 即为当前 `Trie` 的节点 1. `u=0` 且 `at=-1` 虚根处无法匹配(第一个字符存于 $0$) 1. 枚举 `Trie[u]` 的子节点使其 `at` 值为当前的 `at+1` 1. 枚举 $i \in [at,n-1]$ 并找到第一个 `i` 使 `S[i]` 与 `Trie[u]` 所表示的字符一致。若找到了,则递归 `2`;反之,则结束该次递归。 这里的优点显而易见,对于 `Trie` 中,其从根到每个叶子节点仅会至多更新 $n$ 次,而易得 `Trie` 上至多仅有 $n$ 个叶子节点。对于这里 $O(n^2)$ 的复杂度,我们完全可以**省略**掉求 $nxt$ 数组的过程。 而且,对于一个 `Trie` 上,**根节点到任意节点的路径能且仅能代表一个本质相同的字符串**,所以可以完美避开判重的问题。 ## 复杂度 其中,`Trie` 的大小至多为 $O(\frac{n^2}{2})$,且其中每个节点仅会经过一个,所以时空复杂度均为 $O(n^2)$。并且注意这里的空间常数很大,所以推荐将字典树改为**儿子及兄弟**的存法。 ## 代码实现 ```cpp #include<bits/stdc++.h> #define LL long long #define N 3010 using namespace std; struct node { int tag; int son,bro; node(){tag=son=bro=0;} }Trie[N*N/2]; //字典树存储(兄弟-儿子存法),tag 即为其表示的字符(儿子-兄弟存法的弊端之一)。 int Tsiz; inline int Add(int tag) { Tsiz++; Trie[Tsiz].tag=tag; return Tsiz; } //在字典树内新开一个 tag 已知的节点,并返回其指针。 void insert(string a)//字典树的插入 { int len=a.size(); for(int i=0,u=0;i<len;i++) { int v=a[i]-'a'; if(Trie[u].son==0) Trie[u].son=Add(v); //若无子节点,则须新建(兄弟-儿子存法弊端之二) for(int j=Trie[u].son;j;j=Trie[j].bro)//找到代表相同字符的子节点 { if(Trie[j].tag==v) {u=j;break;} //若找到,则匹配下一个字符 if(Trie[j].bro==0) Trie[j].bro=Add(v); //若无,则新建一个 } } } int n,ans; string S,T; void dfs(char a,int u,int k)//在字典树上跑自动机的过程。 { for(int i=k;i<n;i++) if(S[i]==a||u==0) { ans+=(u>0); //除了虚根节点以外,所有节点皆代表一个本质不同的字符串 for(int j=Trie[u].son;j;j=Trie[j].bro) dfs(Trie[j].tag+'a',j,i+1); break; } } int main() { cin >> n >> S >> T; for(int i=0;i<n;i++) insert(T.substr(i)); //将 T 的所有子串录入(录入所有后缀等效于录入所有子串) dfs('\0',0,-1); cout << ans; return 0; } ```