zoukankan      html  css  js  c++  java
  • 字符串算法总结

    前言

    标题是骗你进来的,其实里面全是题目。
    最近一直在搞字符串......
    把一些有代表性或者有一定难度的题放在这里做一个总结。

    [CF666E] Forensic

    给你一个串(S)以及一个字符串数组(T[1..m])(q)次询问,
    每次问(S)的子串(S[p_l..p_r])(T[l..r])中的哪个串里的出现次数最多,并输出出现次数。
    如有多解输出最靠前的那一个。数据范围:(|S|,sum |T|,qleq 10^5)

    (T[1...m])建立广义(SAM),用线段树合并维护出现次数。
    每次查询先倍增到相应结点,然后直接线段树区间查询。

    [NOI2011] 阿狸的打字机

    给定一个打字机,有加字符、删字符、打印三种操作。
    给定操作序列(S),然后有(Q)次询问,每次回答第(x)次打印的串在第(y)次打印的串中出现几次。
    数据范围:(|S|,Qleq 10^5)

    用栈模拟建出所有打印串的(Trie)树,然后构建(AC)自动机与(fail)树。
    每次询问相当于问(x)(Trie)的根这条路径上有多少个点能够跳(fail)跳到(y)
    (Trie)进行(dfs),用树状数组维护(fail)树的子树和。

    [BZOJ3670] 动物园

    给定一个串(S),求每一个前缀的不相交(border)数。 数据范围:(|S|leq 10^7)

    解法一:建出(fail)树,然后(dfs)一遍(fail)树,用单调队列维护合法前缀大小。
    解法二:先预处理出(next)数组,然后类似(next)数组的求法求答案,当不合法时跳(next)数组。

    [SCOI2013] 密码

    给定一个串的每个回文中心的扩展大小,构造满足条件的最小字典序串。数据范围:(nleq 10^5)

    对这个串跑一遍(manacher),直接模拟即可,用并查集维护两个位置的字符是否相同。
    最后使用最小表示法求出答案串。

    [NOI2015] 品酒大会

    给定一个串(S),求(sum_{i} sum_{i eq j} lcp(suf_i,suf_j))。数据范围(|S|leq 10^5)

    使用后缀数组,按照(Height)排序后合并后缀,用带权并查集维护答案。
    或者建出(SAM)(fail)树后,直接(dfs)一遍(fail)树,每次直接合并子树答案。

    [POI2005] SZA-Template

    求一个最短的模板串(T),使得用(T)对长度为(n)的串反复染色后可以得到(S)
    注意:同一个位置可以多次染色,但只能染一种颜色。
    数据范围:(nleq 5*10^5)

    显然(T)(S)的一个(border)
    那么限制条件就是(T)在串(S)中的出现位置的最大间距不能超过(|T|),其中(T)为一个(border)
    注意到(border)匹配的特殊性,当一个前缀(pre)(border)(T)时,(T)就在该点匹配上了一次。
    所以跑(kmp)后建出(fail)树。
    对于一个(T),它合法的充要条件为子树内的点之间的间距不超过(|T|),直接用(set)维护最大间距。

    [BZOJ4310] 跳蚤

    给一个串(S),把它划分为最多(K)段,
    然后挑选出所有段中的最大字典序串,再把这些串取最大字典序串,称得到的串为(T)
    最小化(T)的字典序并输出(T)。 数据范围:(|S|leq 3*10^5)(Kleq 20)

    最大最小化问题,首先二分(T)(S)的所有子串中的排名。
    然后贪心验证。由于我们的后缀数组是以后缀排名的,所以从后往前扫。
    若当前的后缀排名小于等于(T),则直接跳过。
    否则需要考虑割一刀,先求(lcp),然后查看(lcp)长度范围内是否割了一刀,如果没有就割一刀。
    最后比较割的次数与(K)的关系即可。
    需要实现的功能有快速求(lcp),给串求排名,给排名求串,这些都可以用(SA)解决。

    [HihoCoder1413] Rikka-with-String

    给一个串(S),求把每一个位置替换为特殊字符#后本质不同的子串个数。
    数据范围:(|S|leq 10^5)

    先构建(SAM),把原串本质不同的子串个数求出来,然后考虑变化量。
    首先增加量很显然是(i(n-i+1)) ,考虑求减少量。
    对于(SAM)上的每一个点,我们维护其最大(endpos)与最小(endpos)
    然后作图可以发现,这个结点对两个区间的贡献分别为 等差数列 和 一个常数。
    差分即可。

    [BZOJ2384] Match

    给定两个排列(S,T),要求(T)(S)中匹配了多少次。
    这里的匹配定义为:相对大小相同即算匹配上。
    数据范围:(|S|,|T|leq 10^6) 。空间限制:(64MB)

    魔改一下(kmp)算法。
    对于(T),我们可以求出一个(next)数组,表示失配后到达位置,然后在(S)上类似的跑(kmp)即可。
    现在的问题变为:求一个区间内大于某个数的个数,直接想法是主席树,但会(MLE)
    深度发掘一下(kmp)的原理,发现它其实类似一个双指针。
    所以使用树状数组,在跳(next)的时候暴力把删除元素在树状数组中删掉。

    [CF932G] Palindrome

    给定一个偶数长度的串(S),试着把它划分为偶数段。
    设划分为了(k)段,那么需要满足(s_i=s_{k-i+1}),求方案数。数据范围:(|S|leq 10^6)

    首先构建串(S'= s_1s_ns_2s_{n-1}...),问题转化为把(S')划分为若干偶数回文串的方案数。
    对于一个回文串,
    若它的若干回文后缀都不超过其长度的一半,则这些回文后缀一定构成等差数列。
    这样的话,回文树上的某个结点的所有祖先最多只会构成(log)个等差数列。
    我们考虑让同一等差数列中的点一起转移,维护(up)表示最浅的非等差位置。
    对于同一等差数列中的点,由于其长度不超过原串长度的一半,
    故我们对称后刚好只有(up)处的贡献没有加上,所以暴力跳等差数列的同时把该贡献加上即可。

    [NOI2018] 你的名字

    给定一个串(S),有(Q)次询问,每次给定一个串(T),求不在(S[l,r])中出现的(T)的子串个数。
    数据范围:(|S|,Q,sum |T| leq 5 * 10^5),其中(l,r)也是每次询问给定的变量。

    (S)建立后缀自动机,用线段树合并得到其(endpos)集合。
    对于每次询问,先对(T)建立后缀自动机,求的(T)的子串个数,然后再把在(S)中出现的子串减掉。
    对于(T),我们求出其每个前缀的最长匹配后缀(lim)
    那么对于(T)(SAM)上的每个点,利用(lim)把非法贡献减掉即可。
    考虑求(lim),直接把(T)放在(S)(SAM)上做匹配,通过线段树查询(endpos)判断是否出现。
    当失配的时候,不能直接跳(fail),而应该要使长度减一,然后再次检查。
    可以发现这个过程中,(T)和匹配长度(len)的关系类似一个双指针,所以复杂度是没有问题的。

    [NOI2016] 优秀的拆分

    给定一个串(S),求所有子串划分为(AABB)形式的方案数之和。数据范围:(|S|leq 10^5)

    惊现(NOI)史上最良心出题人,白送(95)分简直搞笑。考虑最后(5)分应该怎么拿。
    显然(AABB)是一个障眼法,我们其实只用求(AA)的划分方案就行了。
    开始构造,枚举一下(A)的长度(len)
    然后对于(S),每个长度(len)我们就设置一个顶标点(p_i)
    那么对于任意(|A|=len)(AA)串,它一定恰好经过两个顶标点。
    所以对于相邻两个顶标点,求一下它们的最长公共前、后缀,然后算一下贡献即可。

    [CTSC2010] 珠宝商

    给定一个串(S)和一棵(n)个结点树,其中树上的每一个结点有一个字符。
    求树上每一条有序路径构成的字符串在(S)中的出现次数之和。数据范围:(n,|S|leq 50000)

    树上路径问题考虑点分治,建立点分树。由于(n)范围比较小,所以可以数据均摊分治。
    对于点分子树大小(leq sqrt{n})的点,我们直接暴力做,复杂度(O(sqrt{n}^3) = O(nsqrt{n}))
    对于点分子树大小(> sqrt{n}),这类点显然只有(sqrt{n})个,我们尝试用其它理论解决。
    考虑得到了两条路径,然后把它们拼接在一起。
    那么对应到字符串上,就是一个前缀和一个后缀进行拼接。
    所以我们只需要知道每一个前缀的方案数和每一个后缀的方案数,然后再乘一下就行了。
    我们以求前缀方案数为例,后缀方案数反过来做即可。
    观察一下匹配过程,我们确定了一个(endpos)字符,然后需要向前做匹配。
    这显然是(SAM)(DAG)(fail)树难以做到的。
    所以我们需要将(SAM)生成的(fail)树进一步处理,得到其前缀树,然后匹配就是跳前缀树的子树。
    最后的问题在于如何快速给匹配的点的所有(endpos)加上贡献。
    回顾一下(endpos)集合的得到方法,不难发现只需要把这个过程给逆过来就行了。
    我们先在当前点打上标记,全部匹配完后,顺着(fail)树把标记向下推送。
    那么对于一个前缀,它在(fail)树上对应的结点一定是一个叶子结点,我们直接在该叶子查答案。

    [八省联考2018] 制胡窜

    给定一个串(S)
    (Q)次询问,每次求把串划分为非空三段,且三段中至少包含一个(S[pl,pr])的方案数。
    数据范围:(|S|,|Q|leq 100000),其中(pl,pr)为每次给定的变量。

    此题代码细节贼多,如果要写请务必做好代码调试至少一个晚上的准备。
    正难则反,考虑求不包含(S[pl,pr])的方案数。
    为了书写方便我们令(T = S[pl,pr]),同时定义(T)(S)中的出现次数为(n)
    定义(T)(S)中的出现串为(p_1,p_2...p_n),其中(l_{p_i},r_{p_i})表示它们的左右端点。
    (mn = p_1,mx = p_n),令(len = pr-pl+1)
    我们现在的目标就是用两刀切掉(S)中出现的所有(T)
    大力讨论:

    ( 1 ) 若存在三个不相交的(T),此时显然无解。
    ( 2 ) 否则,若(r_{mn} > l_{mx}),即存在一刀流切法。

    • 若第一刀不是一刀流,枚举第一刀切掉了哪些(T),则有:
      (Ans = sum_{i=1}^{n-1} (r_{p_{i+1}} - r_{p_{i}})(r_{mn} - l_{p_{i+1}}))
      化简有(Ans = (r_{mn}-len+1) sum_{i=1}^{n-1} (r_{p_{i+1}} - r_{p_i}) - sum_{i=1}^{n-1} r_{p_{i+1}} (r_{p_{i+1}} - r_{p_i}))
    • 若第一刀是一刀流,那么第一刀的落刀范围为([l_{mx},r_{mn}]),考虑第二刀的位置。
      若第二刀也落在([l_{mx},r_{mn}]),这种情况的方案数显然为(inom{r_{mn} - l_{mx}}{2})
      否则可以发现第二刀落在([l_{mn},l_{mx}-1))的方案已经算过了,
      故只用算落在其它位置的方案数,这个还是比较好算的。
      若落在右侧,则第一刀落在([l_{mx},r_{mn})),第二刀落在([r_{mn} , n])
      若落在左侧,则第一刀落在((l_mx,r_{mn}]),第二刀落在([1,l_{mn}])
      所以这种情况下的方案数为(Ans = inom{r_{mn} - l_{mx}}{2} + (r_{mn} - l_{mx}) (n-len))

    ( 3 ) 否则,即(r_{mn} leq l_{mx}),即不存在一刀流切法。
    顺着上一种思路,依旧考虑枚举左边那刀切掉了哪些(T)
    那么有:(Ans = sum_{i} (r_{p_{i+1}} - r_{p_i}) (r_{mn} - l_{p_{i+1}}))
    但是由于不存在一刀流,所以一定会有一个(i)(r_{mn})限制,导致切割范围不是完整区间。
    所以我们把左端点最靠近(r_{mn})的那个点先丢掉,设其为(p_t)
    然后就可以得到一个关于(i)的限制条件:(r_{i+1} > l_{mx})(l_i < r_{mn})
    这里需要注意一个天坑(你们就使劲感谢我吧):
    我们找到的是最小的符合条件的(r_{i+1}),而计算区间端点应该是(r_i),所以这里需要找一次前趋。
    然后我们再把之前丢掉的那个点(p_{t})给捡回来,它的答案应该是((r_{mn}-l_{p_t})(r_{p_{t+1}} - l_{mx}))
    QaQ
    累死我了,要是上面哪里写错了麻烦各位吱一声。
    到此为止我们已经讨论了所有情况。
    唯一的问题在于如何实现,
    可以注意到上面所有式子中与(endpos)有关的只有(sum r_i(r_{i+1}-r_i),sum r_i)
    所以完全可以直接使用线段树维护区间最大、最小值,进而使得这两个信息可以合并。
    同时我们发现,维护区间最大、最小值更是一并解决了找前趋、后继的问题。
    所以我们对(S)(SAM),把询问离线,然后按拓扑序逆序进行线段树合并,作出相应回答即可。
    至此问题终于解决。

  • 相关阅读:
    EC600S连接阿里云
    纪念首次使用vscode+platformio完成点灯全过程
    使用EC600S-CN实现短信收发功能
    基于stm32,通过更换数据存储扇区提升w25q128flash芯片使用寿命
    0.96寸OLED模块-简述如何修改OLED_ShowChar()函数达到修改显示字体大小的目的
    stm32定时器初始化后自动进入一次中断问题
    个人PSP升级作业
    第一个微信小项目
    自己设计大学排名-数据库实践
    自己的第一个网页
  • 原文地址:https://www.cnblogs.com/GuessYCB/p/10168949.html
Copyright © 2011-2022 走看看