字符串哈希实例 -- 最长重复子串

题目相关

原题链接:1044.最长重复子串
出现形式:原题重现

题解分析

原解

宫水三叶的题解

整体思路

1、通过字符串哈希建立子串的唯一标识;
2、尝试一下以len为最大长度的子串是否重复出现
3、如果len为最大长度可以,那么增加最大长度,继续尝试,否则减小最大长度继续尝试。

步骤解析

1、字符串哈希的构建

概念: 字符串哈希相当于用一个数字去代表一个字符串,并且唯一对应。
字符串哈希的详细介绍

构造方法:
思路:模拟十进制。
例如知道十位是2,个位是3,我们可以通过2 × 10 + 3 = 23,知道构成的值是23。类似的,如果我们知道字符串第一位(从左向右数)是a,第二位b,我们也可以使用同样的思路去构建字符串ab的值。

如果用这种思路,那么有两个关键的值我们需要知道,一个是底数,一个是字母所在的位数。比如十进制中的底数我们知道要用10,位数有十位、百位,底数基本上确定了就不动了,所以只需要确认位数。在10进制中,通过乘以10的次方来判断,比如个位是10的0次,十位是10的1次,以此类推。所以这里也模仿就行。通过乘以底数的次方来确定位置,类似于10进制,左为高位,右为低位。最右边的位数的底数次方为0,从右到左依次递增。那么下面就看下两个关键值都是如何确定的。

Base值的确定

在教程中说的是,底数我们一般 用素数 ,至于取哪个素数自己看情况就行,先随便取一个

比如我在原题解的代码中看到的底数是1313131,我一开始就不知道怎么就突然来一个1313131了,我换成别的是不是也可以,比如24(一开始还不知道只能是素数),看了一下作者在另一篇的解释才知道,他怎么知道是1313131呢?就是试一下,因为取小了没法通过所有用例,至于为什么呢?我还没有弄太清楚。后来我自己试了一下小一点的素数,确实是没有问题的,但是再小一点,确实就出现无法全部通过的情况。

次方值的确定

实现方法: 用数组p[i],保存是底数的i次方,根据前面的解释,最低(右)位p[0] = 1,即底数Base的0次方

例子: 对于字符串abc,取Base为5,则:
p[0] = 50 = 1;
p[1] = 51 = p[0] ×5 = 5;
p[2] = 52 = p[1] ×5 = 25;
p[3] = 53 = p[2] ×5 = 125;

关键点:

  • 次方数组的长度:字符串的长度+1
    因为要用一个p[0]用来保存0次方,后面依次保存字符串对应位的次方,所以需要的长度比字符串的长度多1
  • 元素值的计算:p[i] = p[i - 1] *Base
    因为在计算机执行的时候无法像我们手动计算一样能直接看出应该是1次方还是2次方,还是3次方,所以直接利用他们的递推关系去计算。
哈希值的计算
  • 前i个字符构成的子串的哈希值
    用数组h保存子串的哈希值,也就是h[i]表示由0到i组成的字符串对应的哈希值。
    例子: 对于字符串abc,取Base为5,那么
    h[0] = 0;
    h[1] = h[0]×5 + a = 0×5 + a = a;
    h[2] = h[1] ×5 + b = a×51 + b ;
    h[3] = h[2]× 5 +c = ( a×5 + b)×5 + b = a×52 +b×51 +c ;
    备注:这里对应的字母只是为了便于理解,实际上存的是他们对应的ASCII值

关键点:
这里也是跟数组p一样,需要直到数组长度和元素值的计算,因为我们直到h[0]的值,所以也是用递推去计算。

  • 次方数组的长度:字符串的长度+1
    因为要用一个h[0]用来保存空字符串的哈希值,后面依次保存有i个字符构成串对应位的值,所以需要的长度比字符串的长度多1
  • 元素值的计算:h[i] = h[i - 1] *Base + s.charAt(i)
    这里就是类似于十进制从2边成20,2的位从个位变成了十位,然后通过乘以10实现位的转变。
任意位置i到j的子串的哈希值

前面已经解决了从0到i位置上的哈希值的构建,但是子串的开始不都是从0开始,我们更想要实现的是从任意位置开始,所以关键的问题是我怎么计算从任意位置i到任意位置j的哈希值。
还是用前面的例子, 对于字符串abc,如果取Base为5,h数组的结果如下

h[0] = 0;
h[1] = h[0]×5 + a = 0×5 + a = a;
h[2] = h[1] ×5 + b = a×51 + b ;
h[3] = h[2]× 5 +c = ( a×5 + b)×5 + b = a×52 + b×5 + c

如果我现在需要计算2到3位置的字符串的哈希值,也就是计算bc的哈希值,那么应该怎么办呢?根据我们对h数组的构造,如果我们另外对bc字符串计算,那就是:
hbc[0] = 0;
hbc[1] = hbc[0]×5 + b = b;
hbc[2] = hbc[1] ×5 + c = b×5 + c

通过对比数组hbc和h,我们就可以发现,我们需要的结果hbc[2]其实就是h[3]的含有字母b和c后半段。那我们只需要消掉h[3]中a字母的项就可以了,而不是去构建hbc。
要达到这样的目的,只需要通过消元法,就可以借助h[1] 消掉a的项,通过h[3] - h[1] × 52 就可以得到。
对于其他任意的位置,我们也可以通过的计算实现,那么我们只需要通过这里确定以下几个关键的值是如何计算,就可以知道一般情况的计算公式。

  • 怎么知道用h数组中的哪两个相减
    比如例子中,我们要求的开始位置为2,终止位置为3,那么被减数就是终止位置的h,即h[j]。因为最终的结果就是这里的一部分,那减数怎么判断呢,从例子中我们可以到,我们的开始是2,但是用的是1,从式子去看是比较明显的,因为我要之去掉a,所以用到的就是只有a的部分,但是h[2]不仅有a还有我需要的b,所以需要在开始位置的上一个,只能才能保证到我们开始的字母是被保留的。推广到一般的情况,则因为h[i]表示的是从0到i位置的字符,但是i是子串开始的地方,需要消掉的是0到i - 1位置的字符,所以用到h[i - 1];

  • 如何确认要乘以Base的几次方才能消掉无关字母
    从我们的例子来看,我们用到了h[j]和h[i - 1],直接用他们相减就可以了也就是,j - (i - 1) = j - i + 1

  • i到j位置的子串哈希值为
    h[j] - h[i - 1] × p[j - i + 1]

2、最大长度子串重复的尝试

  • 最大长度的选取
    作者在这里使用的是二分法的思想,首次选取整个字符串长度的一半作为最大长度进行尝试。根据当前最大长度的尝试结果对下一次尝试的长度进行调整。

  • 长度调整
    如果上一次尝试可行,那么修改左边界,达到增加最大尝试长度的效果,如果上一次没有不可行,修改右边界达到减小最大尝试长度的效果。

代码实现

class Solution {
    long[] h, p;
    //h:哈希数组;p:次方数组,字符串哈希:一个数字和一个字符串之间的一一对应

    public String longestDupSubstring(String s) {
        int P = 83, n = s.length();
        //P:一般要求是素数,至于要取多少,其实是不要求的
        h = new long[n + 1]; 
        p = new long[n + 1];
        //这里作者原来用的是 + 10,我自己没弄明白,所以改成了自己理解的
        //h[i]:0~i对应字符串哈希值,p[i]:底数的i次方的值
        h[0] = 0;//一个字符都没有时的哈希值
        p[0] = 1;//底数的0次方
        for (int i = 0; i < n; i++) {//预处理P和H数组
            p[i + 1] = p[i] * P;
            //递推的时候我们是用i位置和i - 1位置的关系,这里用的是第i+1和i项的关系式,因为字符串的下标是从0开始,我们需要转换成个数,比如说第一个字符下标是0,但它是有1个字符,应该对应p[1],h[1],一开始我还想那把i的开始位置改成1不就好了,但是这样就遗漏了一个字符。
            //自然溢出法构建哈希公式
            h[i + 1] = h[i] * P + s.charAt(i);//s.charAt(i)相当于用这个位置的ASCII码进行计算
        }


        String ans = "";
        int l = 0, r = n;

        //尝试最大长度子串是否重复
        while (l < r) {
            int mid = (l + r + 1 ) / 2;//也可以通过右移:int mid = (l + r + 1 ) >> 1来实现

            //检查某个长度为最大长度时,是不是有方案
            String t = check(s, mid);

            //二分思想
            if (t.length() != 0) l = mid;
            else r = mid - 1;

            ans = t.length() > ans.length() ? t : ans;//取重复多次中长度最长的子串
        }
        return ans;
    }
    String check(String s, int len) {
        int n = s.length();
        Set<Long> set = new HashSet<>();//记录已经被处理过的子串

        for (int i = 1; i + len - 1 <= n; i++) {
            int j = i + len - 1;//从i开始,向后长度为len的子串

            long cur = h[j] - h[i - 1] * p[j - i + 1];
            //计算i - 1到j位置构成的子串的哈希值

            if (set.contains(cur)) return s.substring(i - 1, j);
            //如果已经出现过1次了,说明这是第二次出现,满足题意.通过字符串哈希让判断变得简单
            set.add(cur);//第一次遇到这个字符串就加到set中
        }

        return "";//如果没有重复那就返回空字符串
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值