当你试图把一个设计成O(n²)解答的题优化到O(n)

本文介绍了一种算法,用于判断一个单词是否为优美单词,并提供了一种高效的解决方案。优美单词定义为:在一个单词中,若能将其划分为三部分,使得第一部分与第二部分不同,而第二部分与第三部分相同,则该单词为优美单词。文章详细解释了两种解法:一种使用KMP算法的O(n)复杂度解法;另一种尝试使用SAM(Suffix Automaton)的解法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Description
叉叉哥哥最近要考CET-2048,俗称大学英语2048级。为了备考CET-2048,叉叉哥哥每天都跑学校图书馆学习,哦不,复习。今天,叉叉哥哥拿出了一本厚厚的单词本,打算背单词。在背单词的过程中,叉叉哥哥发现,某些单词看起来就很优美,某些则丑到忧伤。

这些优美的单词有这样一个特征:在不打乱单词中字母顺序的前提下,将单词切成三部分,如果第一部分跟第二部分不同,同时第二部分跟第三部分是相同的话,那么这个单词就是优美的。

现在叉叉哥哥已经分辩出一批单词了,你的任务是写代码来检查下叉叉哥哥的分辨结果是否正确。

没错,这题其实跟2048没什么关系……
Input
第一行输入正整数T,表示有T个单词要判断。T <= 40。
接下来T行,每行一个单词,单词仅由26个小写英文字母组成。每个单词长度不超过500。
Output
对于每个单词,如果不优美,则输出一行“SAD”(不含引号)。
如果优美,则按顺序输出分割后的三个单词,单词与单词之间用一个空格隔开。如果有多种情况符合,要求第一个单词最短。
Sample Input
4
abb
niceword
xxxyyyyyy
ppt

Sample Output
a b b
SAD
xxx yyy yyy
SAD

大师兄出的题,这里直接引用ccgg的题解,这题的设计解法是这样的:

模拟题。暴力枚举端点即可,但是不能用 N^3 的复杂度。第二个端点其实不用枚举,直接取中点即可,优化到了 N^2.

注意A 不能和B相等。像aaa这种输入就得输出SAD而不是a a a。

显然是一个O(n²)的解法。

但是大师兄说:
在这里插入图片描述

是的…用字符串倒序跑kmp就可以了,首先把字符串倒叙序,这个时候问题就变成了找一个可以被平均分成两个完全一样部分的前缀,同时如果整个字符串可以被平均分成三份相等的,所选取的这一前缀不能是这三份相等串中的前两份。

是不是很绕,其实就是一个循环节问题。
对于每个位置,如果存在循环节,且能被当前长度除以2整除,则可以分割成相等的两部分。
这样就可以快速判断当前位置是不是满足要求的前缀,而剩下的部分题目要求不能和这两部分相等,如上所述,这种情况仅当字符串可以被分成三份相等时才有可能存在,在三等分时再用循环节判一下就可以了,复杂度O(n)。

#include<bits/stdc++.h>

using namespace std;

const int maxn = 1e6 + 5;
int t;
string s, ans;

struct KMP {
    int next[maxn];

    void getNext() {
        int j = 0, k = -1;
        next[0] = -1;
        while (j < s.length()) {
            if (k == -1 || s[j] == s[k]) {
                j++;
                k++;
                next[j] = k;
            } else
                k = next[k];
        }
    }
    //vvvvvvv
    //bba

    void solve() {
        reverse(s.begin(), s.end());
        getNext();
        for (int i = s.length() - 1; i >= 1; --i) {
            if (i % 2 == 1) {
                continue;
            }
            int len = i / 2;
            bool canGet = 0;
            canGet |= next[i] >= len && len % (i - next[i]) == 0;
            canGet &= !(s.length() - 2 * len == len && (next[s.length()] >= len && len % (s.length() - next[s.length()]) == 0));
            if(canGet){
                ans = s.substr(0, len) + ' '
                      + s.substr(len, len) + ' '
                      + s.substr(2 * len);
                return;
            }
        }
        ans = "DAS";
    }

} kmp;

int main() {
//    freopen("33.in", "r", stdin);
//    freopen("test.out", "w", stdout);
    cin >> t;
    while (t--) {
        cin >> s;
        kmp.solve();
        reverse(ans.begin(),ans.end());
        cout << ans << '\n';
    }
    return 0;
}

上面这份代码的maxn是1e6,是因为我放了一个40 * 1e6版本的数据上去,这份代码跑了773ms。

然后我还拍脑袋想出了一个sam的解法,500数据的版本可以ac,1e6数据版本先爆栈,改成stack模拟dfs之后超时(只给了1秒,放宽到3秒也非常可惜的没有ac)。

我仔细计算了复杂度,即使是最坏情况(整个字符串都是相同字符),也只需要跑数遍自动机和后缀树,sam的节点数最坏不超过2 * maxn个,sam的这个解法确实应该也是O(n)的,但是常数可能大的超乎想象,一直在想怎么优化,但是还没有这个本事。

不管怎么说,sam解法比暴力快了不止十万倍(狗头)。

sam的思路和kmp完全不同,不需要倒序字符串,我用sam求解的东西是该字符串每个前缀与自身的最长公共后缀。
如果该前缀的结束位置到最后的长度小于等于其与自身的最长公共后缀长度,则证明这里满足题目要求的后两部分的要求,剩下的部分进行同样的判断,如果不满足上述条件,则不等,ok,输出。

“前缀的结束位置到最后的长度小于等于其与自身的最长公共后缀长度”这东西,很显然是主轴parent树上每个节点与字符串最后一个位置节点的lca。

对于怎么求lca,说实话我搞了很多种实现,有上面的建后缀树dfs,也有排序后更新的,但是速度都不太令人满意。

之间的区别就是在规定的时间内跑出的结果不同(排序那个代码比dfs代码跑出来多了3个“SAD”才tle)。本来想贴代码,但是估计没多少人看而且没ac有点惨,就先坑着了。

看我哪天有没有脑洞把常数优化小一点。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值