后缀数组 + Hash + 二分 or Hash + 二分 + 双指针 求 LCP ---- 2017icpc 青岛 J Suffix (假题!!)_j.suffix 二分-程序员宅基地

技术标签: 算法  c语言  # 后缀数组  哈希算法  

题目链接


题目大意:

就是给你n个串每个串取一个后缀,要求把串拼起来要求字典序最小!!
s u m _ l e n g t h _ o f _ n ≤ 5 e 5 sum\_length\_of\_n\leq 5e5 sum_length_of_n5e5


MY Slove :

  1. 首先我们知道对于最后一个串肯定是取最小后缀的

  2. 那么我们可以把最后一个串的结果接到倒数第二个串上面在求一次最小后缀就可以得倒数第二个串的结果,依次以此类推就好了。

  3. 但是这样我们算算复杂度:假如我们每次都把答案拼到下一个串上面求个后缀数组那么我们假设最坏就是 a + z z z z z . . . . . . a+zzzzz...... a+zzzzz......取均摊的复杂度就是 n \sqrt n n 那么就是每个串长度是 n \sqrt n n ,有 n \sqrt n n 个串,答案最坏的情况是全部取,那么你每次都拼接的话,最后一个串要动 n \sqrt n n 次,倒数第二个要动 n − 1 \sqrt n-1 n 1次…那么最坏就是 O ( n ( 1 + n ) / 2 + n ( ∑ i = 1 n i ∗ l o g ( i ∗ n ) ) ) O(\sqrt n(1+\sqrt n)/2+ \sqrt n(\sum_{i=1}^{\sqrt n}i*log(i*\sqrt n))) O(n (1+n )/2+n (i=1n ilog(in ))) 铁定T飞了(但是实践证明 O ( n 2 ) O(n^2) O(n2)都可以过的假题)

  4. 我们考虑优化:

  5. 我们想想后面那一大坨的问题就是:后缀数组求解拼接后字符串的时间复杂度是很大的 n ( ∑ i = 1 n i ∗ l o g ( i ∗ n ) ) \sqrt n(\sum_{i=1}^{\sqrt n}i*log(i*\sqrt n)) n (i=1n ilog(in )) 那么我们可不可以考虑只对原来的字符求后缀数组,去虚假拼接求呢?

  6. 我们看看后缀排序后的后缀长什么样子:
    eg:aba
    a
    aba
    b

  7. 通过观察我们知道要拼接的答案肯定是以最小后缀为前缀的后缀的其中一个这个贪心看肯定是这样的
    我们看看这个数据就知道了

3
ababac
b
drl
ans:ababacbdrl

那么我们可以顺着这个串的所有后缀比下去,但是这个串所有后缀字符个数是 O ( n 2 ) O(n^2) O(n2)的?肯定不是这样比呀!!
在这里插入图片描述
因为我们知道前面是一样的,实际上我们每次只比较后个串多出来的那部分,那部分的长度是 O ( n ) O(n) O(n)

  1. 但是如果前面都一样怎么办?我们就要看红色重叠的部分了,但是如果暴力比较红色部分复杂度还是将不下来!!因为最坏就是红色的长度了那么有 n n n个后缀就是 O ( n 2 ) O(n^2) O(n2)
  2. 因为后面是答案串我们可以对答案串进行hash,然后二分那个第一个hash值第一个hash值不一样的位置进行判断字典序!
  3. 我们在动态维护答案串的hash,不能正序hash,因为字符是头插入的,那么我们逆着hash

AC code

细节巨多,局难写,写了一晚上

#include <bits/stdc++.h>
#define mid ((l + r) >> 1) 
#define Lson rt << 1, l , mid
#define Rson rt << 1|1, mid + 1, r
#define ms(a,al) memset(a,al,sizeof(a))
#define log2(a) log(a)/log(2)
#define _for(i,a,b) for( int i = (a); i < (b); ++i)
#define _rep(i,a,b) for( int i = (a); i <= (b); ++i)
#define for_(i,a,b) for( int i = (a); i >= (b); -- i)
#define rep_(i,a,b) for( int i = (a); i > (b); -- i)
#define lowbit(x) ((-x) & x)
#define IOS std::ios::sync_with_stdio(0); cin.tie(0); cout.tie(0)
#define INF 0x3f3f3f3f
#define LLF 0x3f3f3f3f3f3f3f3f
#define hash Hash
#define next Next
#define pb push_back
#define f first
#define s second
using namespace std;
const int N = 1e6 + 10, mod = 1e9 + 9;
const int maxn = 5e5+10;
// const long double eps = 1e-5;
const int base = 233;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> PII;
typedef pair<ll,ll> PLL;
typedef pair<double,double> PDD;
template<typename T> void read(T &x) {
    
    x = 0;char ch = getchar();ll f = 1;
    while(!isdigit(ch)){
    if(ch == '-')f*=-1;ch=getchar();}
    while(isdigit(ch)){
    x = x*10+ch-48;ch=getchar();}x*=f;
}
template<typename T, typename... Args> void read(T &first, Args& ... args)  {
    
    read(first);
    read(args...);
}
//......................这里是后缀数组
// sa[l]排序是lth的后缀的开始位置
//ra[l]是起点是l的后缀排名多少
//lcp(suf(i),suf(j)) = min(H(ra[i] + 1),....H(ra[j]));
//区间最小值倍增求
// H(i)是rk[i]和rk[i-1]的lcp
//这个板子下标是从0开始,rank从1开始
//传进来的是int数组但是不能有0 
struct SA {
     
    int sa[maxn], ra[maxn], height[maxn];
    int t1[maxn], t2[maxn], c[maxn];
    int shift[maxn];
    inline void init(const string &s) {
    
        for(int i = 0; i < s.size(); ++ i) shift[i] = (int)(s[i]-'a'+1);
        build(shift,s.size(),30);
    }
    void build(int *str, int n, int m) {
    //字符数组,长度,字符种类 
        str[n] = 0;
        n++;
        int i, j, p, *x = t1, *y = t2;
        for (i = 0; i < m; i++) c[i] = 0;//排序的桶
        for (i = 0; i < n; i++) c[x[i] = str[i]]++;
        for (i = 1; i < m; i++) c[i] += c[i - 1];
        for (i = n - 1; i >= 0; i--) sa[--c[x[i]]] = i;
        for (j = 1; j <= n; j <<= 1) {
    //所有长度为1,2,4,8....的子串的排序
       //长度够长的话就是所有的后缀排序
            p = 0;
            for (i = n - j; i < n; i++) y[p++] = i;
            for (i = 0; i < n; i++) if (sa[i] >= j) y[p++] = sa[i] - j;
            for (i = 0; i < m; i++) c[i] = 0;
            for (i = 0; i < n; i++) c[x[y[i]]]++;
            for (i = 1; i < m; i++) c[i] += c[i - 1];
            for (i = n - 1; i >= 0; i--) sa[--c[x[y[i]]]] = y[i];
            swap(x, y);
            p = 1;
            x[sa[0]] = 0;
            for (i = 1; i < n; i++)
                x[sa[i]] = (y[sa[i - 1]] == y[sa[i]] && y[sa[i - 1] + j] == y[sa[i] + j]) ? p - 1 : p++;
            if (p >= n) break;
            m = p;
        }
        n--;
        for (int i = 0; i <= n; i++) ra[sa[i]] = i;
        for (int i = 0, j = 0, k = 0; i <= n; i++) {
    
            if (k) k--;
            j = sa[ra[i] - 1];
            while (str[i + k] == str[j + k]) k++;
            height[ra[i]] = k;
        }
        st_init(height, n);
    }
    int lg[maxn], table[23][maxn];
    void st_init(int *arr, int n) {
    
        if (!lg[0]) {
    
            lg[0] = -1;
            for (int i = 1; i < maxn; i++)
                lg[i] = lg[i / 2] + 1;
        }
        for (int i = 1; i <= n; ++i)
            table[0][i] = arr[i];
        for (int i = 1; i <= lg[n]; ++i)
            for (int j = 1; j <= n; ++j)
                if (j + (1 << i) - 1 <= n)
                    table[i][j] = min(table[i - 1][j], table[i - 1][j + (1 << (i - 1))]);
                    //从j开始长度是2^i次方得st表
    }
    int lcp(int l, int r) {
    
        l = ra[l], r = ra[r];
        if (l > r) swap(l, r);
        ++l;
        int t = lg[r - l + 1];//区间长度
        return min(table[t][l], table[t][r - (1 << t) + 1]);
    }
} sa;
//............................... 这里是Hash
string s[maxn], ans;
ull pw[maxn];
vector<ull> Hash;

inline void insert(char a) {
    
    ull last = Hash.empty() ? 0ull : Hash.back();
    last = last * base + a - 'a' + 1;
    Hash.push_back(last);
}

inline ull gethash(int l,int r) {
    
    if(r < l) return 0; 
    return (ull)Hash[r]-(l - 1 < 0 ? 0ull : Hash[l-1])*pw[r-l+1];
}
//........................
inline void update(int i, int anspos) {
     // 更新答案
    for(int m = s[i].size()-ans.size()-1; m >= anspos; -- m) insert(s[i][m]), ans += s[i][m];
}

int main() {
    
    IOS;
    pw[0] = 1;
    for(int i = 1; i < maxn; ++ i) pw[i] = pw[i-1] * base;
    int _;
    cin >> _;
    while (_--) {
    
        Hash.clear();
        int n;
        cin >> n;
        for(int i = 1; i <= n; ++ i) cin >> s[i];
        sa.init(s[n]);
        ans = s[n].substr(sa.sa[1]); // 先找到最后一个串的最小后缀
        
        for(int i = ans.size()-1; i >= 0; -- i) insert(ans[i]); // 逆着Hash
        reverse(ans.begin(),ans.end()); // 将答案串反过来
        
        for(int i = n - 1; i >= 1; -- i) {
     // 枚举第i个串
            if(s[i].size()==1) {
     insert(s[i][0]); ans += s[i]; continue;}// 特判长度为1的串
            // 对这个串建立后缀数组
            sa.init(s[i]);
            // 把答案串拼到新串后面
            for(int j = (int)ans.size()-1; j >= 0; -- j) s[i] += ans[j];
            
            int anspos = sa.sa[1]; //anspos = 答案后缀的开头位置
            for(int k = 2; k <= s[i].size()-ans.size(); ++ k) {
     // 枚举这个串的每个后缀
                if(sa.lcp(sa.sa[k],sa.sa[k-1]) != s[i].size()-ans.size()-anspos) {
     // 如果不是以前面后缀为前缀那就结束了后面答案不会更优
                    update(i,anspos);
                    break; 
                } 
                int eps = s[i].size() - ans.size() - sa.sa[k-1];
                int is = 0;
                // 只比较突出的部分
                int j;
                for(j = 0; j+anspos+eps < s[i].size() && j+sa.sa[k]+eps<s[i].size()-ans.size(); ++ j) 
                   if(s[i][j+anspos+eps] < s[i][j+sa.sa[k]+eps]) {
    // 如果在黑
                      is = 1;
                      update(i,anspos);
                      break;
                   } else if(s[i][j+anspos+eps] > s[i][j+sa.sa[k]+eps]) {
    
                      anspos = sa.sa[k];
                      if(k == s[i].size()-ans.size()) update(i,anspos);//如果到了最后一个串了可以直接更新了
                      is = 2;
                      break;
                   } else if(j+anspos+eps == s[i].size()-1) {
    
                      is = 1;
                      update(i,anspos);
                      break;                       
                   }
                
                if(is == 1) break; // is == 1 表示可以确定答案了提前退出就好了
                if(is == 2) continue;
                //...................................二分红色部分
                int l = 0, r = s[i].size()-(j+anspos+eps)-1;
                //注意我是把答案串拼到了新串后面
                int tmp = j+anspos+eps-(s[i].size()-ans.size());
                while(l < r) {
    //注意Hash是逆序存的,要从末尾开始看
                    if(gethash(Hash.size()-1-tmp-mid,Hash.size()-1-tmp)==gethash(Hash.size()-1-mid,Hash.size()-1)) l = mid+1;
                    else r = mid;
                }
                if(s[i][j+anspos+eps+l]>s[i][j+sa.sa[k]+eps+l]) anspos = sa.sa[k];
                if(k == s[i].size()-ans.size()) update(i,anspos);//如果到了最后一个串了可以直接更新了
            } 
        }
        for(int i = ans.size()-1;~i;i--) cout << ans[i];
        cout << endl;        
    }
    return 0;
}


Solution 2

贪心从后往前看,最后一个串一定选择字典序最小的后缀,然后把这个后缀拼接到第n - 1个串,重复这个步骤就行了。

具体实现:

从后往前遍历,每次找当前串的最小后缀,这个可以对于当前下标和当前最小后缀下标二分+hash找到lcp,看lcp后一个位置的大小,然后更新最小后缀的下标这里是关键)。然后把最小后缀拼接到前一个串即可。

理论时间复杂度跟后缀数组差不多,但后缀数组常数和编码量更大。


AC code

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef  unsigned long long ull;
const ull base=131;
#define mod
#define maxn 605000
ull p[maxn];ull h[maxn];
ull get_hash(ull l,ull mid)
{
    
    return h[l]-h[l-mid]*p[mid];
}
string str[maxn];char ans[maxn];
int len[maxn];
ull check(ull x,ull y)
{
    
    ull l=1,r=y;
    while(l<=r)
    {
    
 
        ull mid=(l+r)/2;
        //cout<<"l="<<l<<" r="<<r<<" mid="<<mid<<'\n';
        if(get_hash(x,mid)==get_hash(y,mid))
        {
    
            l=mid+1;
        }
        else r=mid-1;
    }
    if(l==y+1) return 0;
    return ans[x-r]<ans[y-r];
}
int main()
{
    
    ull T;
    p[0]=1;
    for(ull i=1;i<=maxn-100;i++)
    {
    
        p[i]=p[i-1]*base;
    }
    cin>>T;
    while(T--)
    {
    
        ull n;
        cin>>n;
        for(ull i=1;i<=n;i++)
        {
    
            cin>>str[i];
            len[i]=str[i].size();
        }
        ull id=0;
        for(ull i=n;i>=1;i--)
        {
    
            //cout<<"i="<<i<<" id="<<id<<'\n';
           ans[++id]=str[i][len[i]-1];
           h[id]=h[id-1]*base+str[i][len[i]-1];
           ull to=1;
           ull t=id;
           for(int j=(int)(len[i])-2;j>=0;j--)
           {
    
               //cout<<"i="<<i<<" j="<<j<<'\n';
               ans[id+to]=str[i][j];
               h[id+to]=h[id+to-1]*base+str[i][j];
               if(check(id+to,t))
               {
    
                   t=id+to;
               }
               to++;
           }
           id=t;
        }
        for(ull i=id;i>=1;i--) cout<<ans[i];
        cout<<'\n';
    }
}
/*
3
3
bbb
aaa
ccc
*/
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_45630722/article/details/121007642

智能推荐

MFC3 基本对话框的使用(三) 滑块与进度条_setticfreq-程序员宅基地

文章浏览阅读1.5k次,点赞3次,收藏16次。要求:将滑块与编辑框、进度条相连接。调整滑块位置同时显示滑块当前对应数值,达到设定要求时改变进度条的进度。一、界面设计滑块是slider control,进度条是progress control对于三个滑块,修改属性:对于三个示例编辑框,修改属性:二、添加变量三、初始化滑块和进度条在Dlg.cpp中找到初始化函数BOOL COOPEx3Dlg::OnI..._setticfreq

240320俄罗斯方块java,JAVA游戏编程之三----j2me 手机游戏入门开发--俄罗斯方块_2-程序员宅基地

文章浏览阅读202次。packagecode;//importjava.awt.*;//importjava.awt.Canvas;//importjava.awt.event.*;//importjavax.swing.*;importjava.util.Random;importjavax.microedition.lcdui.*;//写界面所需要的包/***//***俄罗斯方块*高雷*2007年1..._240×320java游戏

在线电影院售票平台(源码+开题报告)-程序员宅基地

文章浏览阅读779次,点赞14次,收藏19次。然后,实现系统的数据管理和服务功能,包括用户的注册与登录、电影的分类与展示、电影信息的查询与推荐、座位的选择与预订、在线支付与电子票生成等。此外,随着在线视频平台的兴起,越来越多的人选择在线观看电影,这对传统电影院产生了巨大的冲击。研究意义: 开发在线电影院售票平台对于提升用户的观影体验、优化电影院的运营效率、促进电影产业的发展具有重要的意义。该系统旨在通过技术手段解决传统电影院售票中的问题,提供一个集成化的电影信息展示、座位选择、在线支付和用户评价平台,同时也为电影院和电影制作方提供有效的工具。

程序员熬夜写代码,用C/C++打造一个安全的即时聊天系统!_基于c++的即时聊天系统设计-程序员宅基地

文章浏览阅读509次。保护我们剩下的人的通话信息安全,使用TOX可以让你在和家人,朋友,爱人交流时保护你的隐私不受政府无孔不入的的偷窥.关于TOX:其他牛逼的软件因为一些细化服务问你要钱的时候, TOX分文不取 . 你用了TOX, 想干嘛就干嘛.网友评论:项目源码展示:源码测试效果:最后,如果你学C/C++编程有什么不懂的,可以来问问我哦,或许我能够..._基于c++的即时聊天系统设计

linux Java服务swap分区被占用内存泄露问题故障及解决方法_linux swap占用很高-程序员宅基地

文章浏览阅读584次。鱼弦:CSDN内容合伙人、CSDN新星导师、全栈领域创作新星创作者 、51CTO(Top红人+专家博主) 、github开源爱好者(go-zero源码二次开发、游戏后端架构 https://github.com/Peakchen)当Java服务在Linux系统中运行时,可能会出现swap分区被占用的内存泄露问题,导致系统性能下降或者崩溃。下面是该问题的故障及解决方法、底层结构、架构图、工作原理、使用场景详解和实际应用方式、原理详细描述、相关命令使用示例以及文献材料链接。_linux swap占用很高

word中利用宏替换标点标点全角与半角-程序员宅基地

文章浏览阅读662次。Alt+F11,然后插入-模块:复制下面代码到编辑窗口:Sub 半角标点符号转换为全角标点符号()'中英互译文档中将中文段落中的英文标点符号替换为中文标点符号 Dim i As Paragraph, ChineseInterpunction() As Variant, EnglishInterpunction() As Variant Dim MyRange..._替换半角宏

随便推点

用什么软件测试内存条稳定,使用内存条检测工具监测内存稳定性,内存条检测工具有哪些...-程序员宅基地

文章浏览阅读1.2w次。内存从某种意义上来说对于电脑的运行会产生着基础性的影响,所以我们需要经常的检测一下我们电脑的稳定性,那么下载一款内存条监测工具对我们来说就非常的有必要了,现在的内存条检测工具有很多,那么我们应该如何选择一款合适的内存条监测工具呢,接下来就为大家介绍一下。【鲁大师】这是一款综合的电脑检测软件,不仅仅可以对我们电脑内存当中的内存进行检测,还可以对我们的电脑系统的方方面面都进行监测,比如说我们的内存占比..._怎么测试内存稳定性

Harmonyos 自定义下拉列表框(select)_harmonyos 下拉列表-程序员宅基地

文章浏览阅读7.8k次。自定义一个下拉列表框,当这个功能有效时,点击可弹出下拉框,选中某个选项后,在左边功能名称下面显示选项值,右边的箭头替换成自定义图标,例如手法功能;当功能无效时,置灰,如力度功能;具体示例如下:代码如下:index.hml<!--手法无效时--><div class="fun-grid-item" if="{{manualInvalid}}"> <div class="grid-item-parent-ver._harmonyos 下拉列表

VBA入门到进阶常用知识代码总结44_msofalse-程序员宅基地

文章浏览阅读1k次。第44集 图片与图形处理198、 Shape对象的类型和属性该对象代表工作表或图形工作表上的所有图形,它是sheets和chart的子对象(属性)。Sheet1.ShapesSub t2()On Error Resume NextDim ms As Shapek = 1For Each ms In Sheet1.Shapesk = k + 1Cells(k, 1) = ms.Na..._msofalse

公司个人年终工作总结【10篇】_csdn 公司 年终终结-程序员宅基地

文章浏览阅读1.2k次。公司个人年终工作总结1 20__年即将过去,在公司领导的悉心关怀下和同事们的帮助指导下,结合我自身的努力,在工作、学习等各方面都取得了长足的进步,尤其是在保险理赔专业知识和技能培养方面的成熟,使我成为一名合格的车险查勘定损员。随着工作岗位的调整,我已经成长为为一名能够独立工作、业务熟练的前台工作人员。现将一年来的工作情况向公司领导总结汇报如下: 一、加强理论学习,注重个人素质提高 加强自身业务学习,争做理赔标兵。在日常的工作学习中,我坚持学习更多的保险知识和业务技能,在老同志的“传帮带”下,不断加强个_csdn 公司 年终终结

bitcoin 调试环境搭建-程序员宅基地

文章浏览阅读1.6k次。_bitcoin 调试环境搭建

曲线生成 | 图解B样条曲线生成原理(基本概念与节点生成算法)-程序员宅基地

文章浏览阅读4.3k次,点赞93次,收藏94次。为了解决贝塞尔曲线无法局部修正、控制性减弱、曲线次数过高、不易拼接的缺陷,引入B样条曲线(B-Spline)。本文介绍B样条曲线的基本概念:节点向量、支撑性、次数阶数、加权性质、节点生成算法等,为后续曲线计算打下基础。_样条曲线生成

推荐文章

热门文章

相关标签