字符串 (5)--- 后缀数组(倍增思想求解)

字符串下标从 1 开始。
字符串 s 的长度为 n。
" 后缀 i" 代指以第 i 个字符开头的后缀,存储时用 i 代表字符串 s 的后缀 s[i ... n]。

后缀数组(Suffix Array)主要关系到两个数组:sa 和 rk。
后缀数组sa,sa[i] 表示将所有后缀排序后第 i 小的后缀的编号;
排名数组 rk, rk[i] 表示后缀 i 的排名。
这两个数组满足性质:sa[rk[i]]=rk[sa[i]]=i。

/**

 *    倍增的过程是 O(log n),而每次倍增用 sort 对子串进行排序是 O(n\log n),而每次子串的比较花费 2 次字符比较;
 *    这个算法的时间复杂度就是 O(n\log^2n)。
 */
// 常规解法, 倍增快速排序
#include <iostream>
#include <string.h>
#include <algorithm>
using namespace std;

const int MAXN = 1000010;
char str[MAXN];
int sa[MAXN];      // 后缀数组sa[i]表示将所有后缀排序后,第i小的后缀的编号
// 为了防止访问 rk[i+k] 导致数组越界,开两倍数组, 省去越界检测,简化程序。
int rk[MAXN << 1]; // 后缀i的排名,常称为排名数组
int oldrk[MAXN << 1];
int k;
int main()
{
    cin >> str + 1;
    int i = 0, len = strlen(str+1);

    // 初始化后缀树组与排名数组
    for (i = 1; i <= len; ++i)
    {
        sa[i] = i;
        rk[i] = str[i];
    }
    // 倍增排序
    for (k = 1; k < len; k <<= 1)
    {
        /**
         *    每个后缀子串的次序可以表示为一个二元组(x, y), x表示前半段的次序号,y表示后半段的次序号
         *    由于上一次的排序结果已知(即前半段x的排序已知),故只要对后半段进行比较就可以得到当前子串的次序

         *    第n次排序表示对每个后缀子串的前1~2^(n-1)个字符进行排序

         *    比较二元组(x, x+k) 与 (y, y+k)
         *    如果第一关键字相等,则比较第二关键字,关键字小的排名更靠前
         */
        sort(sa+1, sa+len+1, [](int x, int y) { 
                return rk[x] == rk[y] ? rk[x+k] < rk[y+k] : rk[x] < rk[y];    
            }
        );
        // 由于计算 rk 的时候, 原来的 rk 会被覆盖,要先复制一份
        memcpy(oldrk, rk, sizeof(rk));
        int num = 0; // 当前最大的次序号
        // 按照sa从小到大给后缀子串更新次序
        for (i = 1; i <= len; ++i)
        {
            // 如果与前一个二元组不相同,则产生新的次序号
            if (oldrk[sa[i]] == oldrk[sa[i-1]] && oldrk[sa[i]+k] == oldrk[sa[i-1]+k])
                rk[sa[i]] = num;
            else
                rk[sa[i]] = ++num;
        }
        cout << "k: " << k << endl;
        cout << "sa: ";
        for (i = 1; i <= len; ++i)
            cout << sa[i]  << " ";
        cout << endl;
        cout << "rk: ";
        for (i = 1; i <= len; ++i)
            cout << rk[i]  << " ";
        cout << endl;
    }

    for (i = 1; i <= len; ++i)
        cout << sa[i]  << " ";
    cout << endl;

    return 0;
}

/**
 *  input:
 *    aabaaaab
 *    output:
 *    k: 1
 *    sa: 1 4 5 6 2 7 8 3
 *    rk: 1 2 4 1 1 1 2 3
 *    k: 2
 *    sa: 4 5 6 1 7 2 8 3
 *    rk: 4 6 8 1 2 3 5 7
 *    k: 4
 *    sa: 4 5 6 1 7 2 8 3
 *    rk: 4 6 8 1 2 3 5 7
 *    4 5 6 1 7 2 8 3
 */
 
 // 倍增计数排序
 /**
 *    字符串str的下标从1开始,字符串的长度为len
 *    "后缀i"代指以第i个字符开头的后缀,存储时用i代表字符串s的后缀s[i...n]

 *    后缀数组和排名数组满足: sa[rk[i]] == rk[sa[i]] == i
 *    由于计算后缀数组的过程中排序的关键字是排名,值域为 O(n),并且是一个双关键字的排序,可以使用基数排序优化至 O(n)。
 */
#include <iostream>
#include <string.h>
#include <algorithm>
using namespace std;

const int MAXN = 1000010;
char str[MAXN];
int sa[MAXN];      // 后缀数组sa[i]表示将所有后缀排序后,第i小的后缀的编号
// 为了防止访问 rk[i+k] 导致数组越界,开两倍数组, 省去越界检测,简化程序。
int rk[MAXN << 1]; // 后缀i的排名,常称为排名数组
int oldrk[MAXN << 1];
int oldsa[MAXN];
int cnt[MAXN];  // 计数排序用于计数
const int MaxKey = 127;  // the maximum value of ASCII is 127


int main()
{
    cin >> str + 1;
    int i = 0, len = strlen(str+1);
    // 统计str中字符的计数分布
    for (i = 1; i <= len; ++i)
        ++cnt[rk[i] = str[i]];
    // 统计关键字小于等于i的计数分布
    for (i = 1; i <= MaxKey; ++i)
        cnt[i] += cnt[i-1];
    // 升序计数排序sa
    for (i = len; i >= 1; --i)
        sa[cnt[rk[i]]--] = i;

    memcpy(oldrk+1, rk+1, sizeof(int)*len);
    int num = 0; // 当前的最大次序号
    // 第一次排序只有一个关键字
    for (i = 1; i <= len; ++i)
        if (oldrk[sa[i]] == oldrk[sa[i-1]])
            rk[sa[i]] = num;
        else
            rk[sa[i]] = ++num;

    for (int k = 1; k < len; k <<= 1)
    {
        // 对第二关键字:oldsa[i] + k 进行计数排序
        memset(cnt, 0, sizeof(cnt));
        memcpy(oldsa+1, sa+1, sizeof(int)*len);
        for (i = 1; i <= len; ++i)
            ++cnt[rk[oldsa[i]+k]];
        // 首轮排序后,cnt的最大下标不超过len
        for (i = 1; i <= len; ++i)
            cnt[i] += cnt[i-1];
        for (i = len; i >= 1; --i)
            sa[cnt[rk[oldsa[i]+k]]--] = oldsa[i];

        // 对第一关键字:oldsa[i] 进行计数排序
        memset(cnt, 0, sizeof(cnt));
        memcpy(oldsa+1, sa+1, sizeof(int)*len);
        for (i = 1; i <= len; ++i)
            ++cnt[rk[oldsa[i]]];
        // 首轮排序后,cnt的最大下标不超过len
        for (i = 1; i <= len; ++i)
            cnt[i] += cnt[i-1];
        for (i = len; i >= 1; --i)
            sa[cnt[rk[oldsa[i]]]--] = oldsa[i];

        memcpy(oldrk+1, rk+1, sizeof(int)*len);
        num = 0; // 当前的最大次序号
        for (i = 1; i <= len; ++i)
        {
            // 如果与前一个二元组不相同,则产生新的次序号
            if (oldrk[sa[i]] == oldrk[sa[i-1]] && oldrk[sa[i]+k] == oldrk[sa[i-1]+k])
                rk[sa[i]] = num;
            else
                rk[sa[i]] = ++num;
        }
    }

    for (i = 1; i <= len; ++i)
        cout << sa[i]  << " ";
    cout << endl;

    return 0;
}

/**
 *    input:
 *    aabaaaab
 *    output:
 *    4 5 6 1 7 2 8 3
 */
 
 // 针对大数据做性能优化
 /**
 *    https://www.luogu.com.cn/problem/P3809
 *    字符串str的下标从1开始,字符串的长度为len
 *    "后缀i"代指以第i个字符开头的后缀,存储时用i代表字符串s的后缀s[i...n]

 *    后缀数组和排名数组满足: sa[rk[i]] == rk[sa[i]] == i
 *    由于计算后缀数组的过程中排序的关键字是排名,值域为 O(n),并且是一个双关键字的排序,可以使用基数排序优化至 O(n)。
 *    针对大数据进行优化
 */
#include <iostream>
#include <string.h>
#include <algorithm>
using namespace std;

const int MAXN = 1000010;
char str[MAXN];
int sa[MAXN];      // 后缀数组sa[i]表示将所有后缀排序后,第i小的后缀的编号
// 为了防止访问 rk[i+k] 导致数组越界,开两倍数组, 省去越界检测,简化程序。
int rk[MAXN]; // 后缀i的排名,常称为排名数组
int oldrk[MAXN << 1];
int key1[MAXN]; //    key1[i] = rk[oldsa[i]](作为基数排序的第一关键字数组)
int oldsa[MAXN];
int cnt[MAXN];  // 计数排序用于计数

/**
 *    用函数 cmp 来计算是否重复
 *    同样是减少不连续内存访问,在数据范围较大时效果比较明显。
 */
bool cmp(int x, int y, int k) 
{
    return oldrk[x] == oldrk[y] && oldrk[x + k] == oldrk[y + k];
}

int main()
{
    cin >> str + 1;
    int i = 0, len = strlen(str+1);
    int MaxKey = 127;  // the maximum value of ASCII is 127
    // 统计str中字符的计数分布
    for (i = 1; i <= len; ++i)
        ++cnt[rk[i] = str[i]];
    // 统计关键字小于等于i的计数分布
    for (i = 1; i <= MaxKey; ++i)
        cnt[i] += cnt[i-1];
    // 升序计数排序sa
    for (i = len; i >= 1; --i)
        sa[cnt[rk[i]]--] = i;

    int num = 0; // 最大次序号 

    /**
     *    m=num 就是优化计数排序值域
     *    每次对 rk 进行更新之后,我们都计算了一个 num,这个 num 即是 rk 的值域,将值域改成它即可。
     */
    for (int k = 1 ;; k <<= 1, MaxKey = num)
    {
        /**
         *    第二关键字无需计数排序, 第二关键字排序的实质,
         *    其实就是把超出字符串范围(即 sa[i] + k > len)的 sa[i] 放到 sa 数组头部,然后把剩下的依原顺序放入
         */
        for (num = 0, i = len; i > len - k; --i)
            oldsa[++num] = i;
        for (i = 1; i <= len; ++i)
            if (sa[i] > k)
                oldsa[++num] = sa[i] - k;

        /**
         *    对第一关键字:oldsa[i] 进行计数排序
         *    将 rk[oldsa[i]] 存下来,减少不连续内存访问, 这个优化在数据范围较大时效果非常明显。
         */
        memset(cnt, 0, sizeof(cnt));
        for (i = 1; i <= len; ++i)
            ++cnt[key1[i] = rk[oldsa[i]]];
        for (i = 1; i <= MaxKey; ++i)
            cnt[i] += cnt[i-1];
        for (i = len; i >= 1; --i)
            sa[cnt[key1[i]]--] = oldsa[i];

        memcpy(oldrk+1, rk+1, sizeof(int)*len);
        num = 0; // 当前的最大次序号
        for (i = 1; i <= len; ++i)
        {
            // 如果与前一个二元组不相同,则产生新的次序号
            rk[sa[i]] = cmp(sa[i], sa[i - 1], k) ? num : ++num;
        }
        // 若其值域为 [1,n] 那么每个排名都不同,此时无需再排序
        if (num == len)
            break;
    }

    for (i = 1; i <= len; ++i)
        cout << sa[i]  << " ";
    cout << endl;

    return 0;
}

/**
 *    input:
 *    aabaaaab
 *    output:
 *    4 5 6 1 7 2 8 3
 */

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/591478.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

python基础-01

文章目录 前言一、python中的注释二、变量的数据类型1.Number&#xff08;数字&#xff09;2.Boolean&#xff08;布尔类型&#xff09;—— True 和 False3.String&#xff08;字符串&#xff09;4.List&#xff08;列表&#xff09;5.Tuple&#xff08;元组&#xff09;6.Dic…

详解动态顺序表

&#x1d649;&#x1d65e;&#x1d658;&#x1d65a;!!&#x1f44f;&#x1f3fb;‧✧̣̥̇‧✦&#x1f44f;&#x1f3fb;‧✧̣̥̇‧✦ &#x1f44f;&#x1f3fb;‧✧̣̥̇:Solitary-walk ⸝⋆ ━━━┓ - 个性标签 - &#xff1a;来于“云”的“羽球人”。…

前端:html+css+js实现CSDN首页

提前说一下&#xff0c;只实现了部分片段哈&#xff01;如下&#xff1a; 前端&#xff1a;htmlcssjs实现CSDN首页 1. 实现效果2. 需要了解的前端知识3. 固定定位的使用4. js 监听的使用4. 参考代码和运行结果 1. 实现效果 我的实现效果为&#xff1a; 原界面如下,网址为&…

LLM Agent零微调范式 ReAct Self Ask

前三章我们分别介绍了思维链的使用&#xff0c;原理和在小模型上的使用。这一章我们正式进入应用层面&#xff0c;聊聊如何把思维链和工具使用结合得到人工智能代理。 要回答我们为什么需要AI代理&#xff1f;代理可以解决哪些问题&#xff1f;可以有以下两个视角 首先是我们…

三层架构概述

三层架构就是把整个软件的代码分为三个层次&#xff0c;分层的目的是&#xff1a;规范代码&#xff0c;大型软件需要团队配合的时候问题就来了&#xff0c;由于每个程序员风格不一样&#xff0c;而开发软件大量的代码风格不统一就会造成后期调试和维护出现问题&#xff0c;然而…

Squid 代理服务器

13.1.1缓存代理概述 作为应用层的代理服务软件&#xff0c;Squid主要提供缓存加速、应用层过滤控制的功能。 1.代理的工作机制 当客户机通过代理来请求Web页面时&#xff0c;指定的代理服务器会先检查自己的缓存&#xff0c;如果缓存中已 经有客户机需要的页面&#xff0c;则直…

Udp实现一个小型shell

实现原理 首先我们要有个客户端和一个服务器&#xff0c;客户端向服务器传递命令。而服务器收到命令后创建一个管道&#xff0c;并fork一个子进程。随后子进程解析命令&#xff0c;再把标准输出换成管道文件&#xff0c;因为命令行命令是自动输出到显示器的&#xff0c;所以我…

英飞凌TC3xx之一起认识GTM系列(一)先来认识GTM架构

英飞凌TC3xx之一起认识GTM系列(一)先来认识GTM架构 1 先来认识GTM的通用架构2 概览2.1 架构的简要说明2.2 架构概述1 先来认识GTM的通用架构 GTM系统使用GTM全局时钟fGTM 运行(本文称为SYS_CLK)。 特点如下: GTM模块由两个主要部分组成: 由博世设计的GTM IP v3.1.5.1 …

【Java 数组解析:探索数组的奇妙世界】

数组的引入 我们先通过一段简单的代码引入数组的概念。 import java.util.Scanner; public class TestArray01{public static void main(String[] args){//功能&#xff1a;键盘录入十个学生的成绩&#xff0c;求和&#xff0c;求平均数&#xff1a;//定义一个求和的变量&…

【我与CSDN的128天】相识相知相守

目录: 相识相知相守 相识 为什么选择写博客? 写博客的目的,我觉得是因为想要记录。记录学习的过程,整理学过的知识,方便今后的复习。 更重要的是热爱分享,分享给别人知识也是一种快乐。 在某一瞬间教会某一个你不认识的人,难道不是一个很酷的事情吗? 为什么选择CSDN? 作…

企业签名分发对移动应用开发者有什么影响

企业签名分发是移动应用开发者在应用程序发布前测试、内部分发和特定的受众群体分发等方面比较常用的一种工具。那对于应用商城分发有啥区别&#xff0c;下面简单的探讨一下。 独立分发能力 通过企业签名分发开发者可以自己决定应用程序的发布时间和方式&#xff0c;不用受应用…

[2024区块链开发入门指引] - 比特币运行原理

一份为小白用户准备的免费区块链基础教程 工欲善其事,必先利其器 Web3开发中&#xff0c;各种工具、教程、社区、语言框架.。。。 种类繁多&#xff0c;是否有一个包罗万象的工具专注与Web3开发和相关资讯能毕其功于一役&#xff1f; 参见另一篇博文&#x1f449; 2024最全面…

Android14之禁掉Selinux的两种方式(一百七十四)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 优质专栏&#xff1a;多媒…

uniapp的分包使用记录

UniApp的分包是一种将应用代码划分为多个包的技术。分包的核心思想是将不同部分的代码划分为不同的包&#xff0c;按需加载&#xff0c;从而提高应用性能。使用UniApp的条件编译功能&#xff0c;开发人员可以根据需要将代码划分为多个包。每个包都包含一组页面和组件&#xff0…

大数据Doris(四十五):物化视图选择最优

文章目录 物化视图选择最优 物化视图选择最优 下面详细解释一下第一步最优物化视图是被如何选择出来的。 这里分为两个步骤: 对候选集合进行一个过滤。只要是查询的结果能从物化视图数据计算(取部分行,部分列,或部分行列的聚合)出都可以留在候选集中,过滤完成后候选集合…

RocketMQ源码解析-主从同步原理(HA)

1、关键组件 主从同步的实现逻辑主要在HAService中&#xff0c;在它的构造函数中实例化了几个对象同时在start()方法内执行启动&#xff1a; public class HAService {public HAService(final DefaultMessageStore defaultMessageStore) throws IOException {this.defaultMes…

李宏毅机器学习第二十三周周报 Flow-based model

文章目录 week 23 Flow-based model摘要Abstract一、李宏毅机器学习1.引言2.数学背景2.1Jacobian2.2Determinant2.3Change of Variable Theorem 3.Flow-based Model4.GLOW 二、文献阅读1. 题目2. abstract3. 网络架构3.1 change of variable formula3.2 Coupling layers3.3Prop…

阿里云域名外部入库流程

注册商是阿里云&#xff0c;且在阿里云管理的&#xff0c;请使用此教程外部入库。 如您的域名注册商是阿里云但在聚名管理&#xff0c;请参考教程&#xff1a;https://www.west.cn/faq/list.asp?unid2539 在外部入库操作之前&#xff0c;请先登录阿里云获取账号ID。详细的账…

软件测试方法分类-按照是否手工执行划分

接上一篇,下来我们再细讲,第二个维度的分类, 软件测试方法分类-按照是否手工执行划分 按是否手工执行划分 1,手工测试(manualTesting) 手工测试是由人一个一个的输入用例,然后观察结果,和机器测试相对应,属于比较原始但是必须的一种。 2,自动化测试(automationTestin…

【刷题日志】深度理解除(/)与取模(%)附水仙花数以及变种水仙花数题解

文章目录 &#x1f680;前言&#x1f680;除与取模&#x1f680;水仙花数&#x1f680;变种水仙花数 &#x1f680;前言 本专栏文章都直奔刷题主题&#xff0c;阿辉都不会在废话了&#xff0c;加油&#xff0c;少年&#xff01;&#xff01;&#xff01; &#x1f680;除与取…