习题课4-1(hash、回文串问题)

习题课4-1(hash)

矩形

  • 给定两个矩阵,判断第二个矩阵在第一个矩阵哪些位置出现过

  • 输出位置的左上角

  • 有多个答案,按字典序输出

解法1

  • 一行行一列列依次比较
  • 枚举两个矩阵,n的4次方,,50的4次方是1亿以内的

解法2

  • 将一个矩阵转化成一个数字

哈希字符串

  • ebacd

  • hash(ebacd) = (5B^4+2B^3+B^2+3B^1+4B^0) mod mo
    
  • B是大于26的任意整数,最好取质数 mo为一个较大的质数

  • 将这个推广到正常的整数序列中去,假设所给的整数序列是a_i

  • h a s h ( { a i } ) = ( ∑ i = 1 n a i B n − i )    m o d   m o hash(\{a_i\}) = (\sum_{i=1}^na_iB^{n-i}) \space\space mod \space mo hash({ ai})=(i=1naiBni)  mod mo

  • 其中B是任意大于max{a_i}的整数,标程中B取233

  • 解决了一维,拓展到二维

  • 计算一个矩形A_{nXm}的hash值,我们先对每一行求一次hash值,得到n个hash值,然后再对n个hash值再做一遍一维hash

  • 一般来说下一次hash时,B取多少?应该大于等于mo

  • 尽量减少碰撞的可能(密码学里有个碰撞的概念)

  • 取两套B mo,如果算出来的hash1和hash2都相等,则认为这两个矩形相等

代码解析

  • mo1 是1e9+7 mo2是1e9+9 B都是233(大于100(题目数据取值范围)的任意质数)

  • pair是一个二元组

  • for (int i = 1; i <= q; i++) {
           
                    p1 = p1 * pw % mo1;
                    p2 = p2 * pw % mo2;
                }
    
  • 得到B^q次方的值,中间需要对mo取模

  • p1 = (mo1 - p1) % mo1;
    
  • (mo1-p1) = (-p1+mo1) 因为最后要求的是(-B^q)的值

  • for (int i = 1; i <= n; i++) {
           
                    long t1 = 0, t2 = 0;
                    for (int j = 1; j <= m; j++) {
           
                        if (j < q) {
           
    
    
                            t1 = (t1 * pw + a[i][j]) % mo1;
                            t2 = (t2 * pw + a[i][j]) % mo2;
                        } else {
           
                            t1 = h1[0][i][j] = (t1 * pw + a[i][j] + p1 * a[i][j - q]) % mo1;
                            t2 = h2[0][i][j] = (t2 * pw + a[i][j] + p2 * a[i][j - q]) % mo2;
                        }
                    }
                }
    
  • j

  • 而当j>=q时,此时除了新加的a(i)(j),还要减掉前一次计算的头值

  • 按列计算hash值过程类似

  • 最后输出答案时,由于存储hash值的位置在右下角,而题目要求输出左上角,所以结果就是(i-p+1,j-q+1)

标程

  • static class Task {
           
    
            // 类似于c++里的pair
            class pii {
           
                public int first;
                public int second;
    
                public pii() {
           
                    first = second = 0;
                }
    
                public pii(int first, int second) {
           
                    this.first = first;
                    this.second = second;
                }
            }
    
            final int N = 1005;
            final long mo1 = (long) 1e9 + 7; // 模数最好取质数
            final long mo2 = (long) 1e9 + 9;
            final long pw = 233; // base
    
            // 全局变量
        	// bb:对于b数组,bb[0][i][j]表示从(i,1)到(i,j)的横向hash值(对mo1)取模,bb[1][i][j]表示从(i,1)到(i,j)的横向hash值(对mo2)取模
            long[][][] h1 = new long[2][N][N];
            long[][][] h2 = new long[2][N][N];
            long[][][] bb = new long[2][N][N];
    
            // 为了减少复制开销,我们直接读入信息到全局变量中
            // a, b:题目所述数组,a的大小为(n+1)*(m+1),b的大小为(p+1)*(q+1),下标均从1开始有意义(下标0无意义,你可以直接无视)
            // n, m, p, q:题中所述
       		// n,m,p,q都是实际大小,上面+1是因为数组下标从0开始
            int[][] a = new int[N][N];
            int[][] b = new int[N][N];
            int n, m, p, q;
    
            // 求出a中有哪些位置出现了b
            // 返回值:一个pii的数组,包含所有出现的位置
            List<pii> getAnswer() {
           
                // (a+b) % mo = ((a%mo)+(b%mo))%mo
                // (a-b) % mo = ((a-b)%mo + mo) % mo // 要把范围限制在[0,mo-1]内
                // (a*b) % mo = (a%mo) * (b%mo) %mo
                // 注意,以下所有变量类似于p1,p2的,都表示同一意义,仅仅是取的模数不同(前者是对mo1取模,后者是对mo2),所以下方注释仅给mo1的注释
                
                // p1 = (-pw^q) % mo1
                // pw^q在后面要用
                long p1 = 1, p2 = 1;
                for (int i = 1; i <= q; i++) {
           
                    p1 = p1 * pw % mo1;
                    p2 = p2 * pw % mo2;
                }
                // p1 = ((0-p1)%mo1 + mo1) %mo1
                // 因为p1在上面幂乘的过程中一直对mo1取模,所以是mo1之内的
                // p1 = (mo1-p1) % mo1;
                p1 = (mo1 - p1) % mo1;
                p2 = (mo2 - p2) % mo2;
    			
                // 用a数组计算横向hash值,存储为h1[0]
                // i表示矩阵的行号,是第几行
                for (int i = 1; i <= n; i++) {
           
                    long t1 = 0, t2 = 0;
                    // j表示矩阵的列
                    for (int j = 1; j <= m; j++) {
           
                        // 一直循环到q-1
                        // 假设n,m=4,p,q=2
                        // 以第一行为例
                        // 此处算出[1][1]+[1][2]的hash值,存储到了[1][2]
                        if (j < q) {
           
    						// 之前,t1 = a[i][0] pw^(j-2) + a[i][1] pw^(j-3) + ... + a[i][j-2] pw + a[i][j-1]
                            // 之后,t1 = a[i][0] pw^(j-1) + a[i][1] pw^(j-2) + ... + a[i][j-1] pw + a[i][j]
                            // 所以 之前的t1乘上pw后,再加上a[i][j]就得到了之后的t1
                            t1 = (t1 * pw + a[i][j]) % mo1;
                            t2 = (t2 * pw + a[i][j]) % mo2;
                        } 
                        // 怎么由[1][1]+[1][2]推出[1][2]+[1][3]呢
                        else {
           
                            // 之前,t1 = a[i][j-q] pw^(j-1) + a[i][j-q+1] pw^(j-2) + ... + a[i][j-2] pw + a[i][j-1]
                            // 之后,t1 = a[i][j-q+1] pw^(j-1) + a[i][j-q+2] pw^(j-2) + ... + a[i][j-1] pw + a[i][j]
                            // 所以 之前的t1乘上pw后,再减去a[i][j-q] pw^j,再加上a[i][j]就得到了之后的t1
                            t1 = h1[0][i][j] = (t1 * pw + a[i][j] + p1 * a[i][j - q]) % mo1;
                            t2 = h2[0][i][j] = (t2 * pw + a[i][j] + p2 * a[i][j - q]) % mo2;
                        }
                    }
                }
    
                // p1 = (-pw^q)%mo1
                p1 = 1;
                p2 = 1;
                for (int i = 1; i <= p; i++) {
           
                    p1 = p1 * pw % mo1;
                    p2 = p2 * pw % mo2;
                }
    
                p1 = (mo1 - p1) % mo1;
                p2 = (mo2 - p2) % mo2;
    			
                // 用h1[0]数组计算纵向hash值,存储为h1[1],与上方类似
                // j表示矩阵的列
                for (int j = 1; j <= m; j++) {
           
                    long t1 = 0, t2 = 0;
                    // i表示矩阵的行
                    for (int i = 1; i <= n; i++) {
           
                        if (i < p) {
           
                            t1 = (t1 * pw + h1[0][i][j]) % mo1;
                            t2 = (t2 * pw + h2[0][i][j]) % mo2;
                        } else {
           
                            t1 = h1[1][i][j] = (t1 * pw + h1[0][i][j] + p1 * h1[0][i - p][j]) % mo1;
                            t2 = h2[1][i][j] = (t2 * pw + h2[0][i][j] + p2 * h2[0][i - p][j]) % mo2;
                        }
                    }
                }
    
    			// 计算b数组的横向hash值,存储为bb数组,与上方类似
                // 计算小矩阵的hash值
                // 先按行计算一遍
                for (int i = 1; i <= p; i++) {
           
                    for (int j = 1; j <= q; j++) {
           
                        bb[0][i][j] = (bb[0][i][j - 1] * pw + b[i][j]) % mo1;
                        bb[1][i][j] = (bb[1][i][j - 1] * pw + b[i][j]) % mo2;
                    }
                }
    
                p1 = p2 = 0;
                // 再按列计算一遍
                // 用bb数组的最后一列来计算整个b数组的hash值,存储为p1
                for (int i = 1; i <= p; i++) {
           
                    p1 = (p1*pw+bb[0][i][q])%mo1;
                    p2 = (p2*pw+bb[1][i][q])%mo2;
                }
    			
                // 若值相同,说明匹配到了相同的矩形(右下角),题中要求输出左上角,故得到的坐标是(i-p+1,j-q+1)
                List<pii> ans = new ArrayList<>();
                for (int i = p; i <= n; i++) {
           
                    for (int j = q; j <= m; j++) {
           
                        if (h1[1][i][j] == p1 && h2[1][i][j] == p2) {
           
                            ans.add(new pii(i - p + 1, j - q + 1));
                        }
                    }
                }
    
                return ans;
            }
    }
    

回文串

  • 给定一个字符串,求出该字符串中有多少子串是回文串
  • 子串是字符串连续的一段
  • 回文串是原字符串倒序写出来和该字符串相同

解法1

  • 把字符串倒过来写
  • 枚举子串是O(n2),判断回文串是O(n),整体是O(n3)

解法2

  • 枚举子串
  • 每一个子串正序算一遍hash值,反序算一遍哈希值,结果相等则认为相等
  • 时间复杂度是O(n^2)

解法3(manacher算法)

  • 线性时间求出
  • 格式化+对称性+单调性
  • 回文串长度有奇偶怎么办?
  • 格式化:在字符串中间插入一个相同字符
  • 对称性,回文串是对称的,每次记录最远的位置,根据对称性算出当前所在位置的回文串长度(至少长多少)
  • 单调性,每一次最远的位置是单调上升的
0 1 2 3 4 5 6 7 8 9 10 11 12
s % # a # a # b # a # a # $
len 0 0 1 2 1 0 5 0 1 2 1 0 0
cur 0 0 2 3 3 3 6 6 6 6 6 6 0

代码解析

  • cursor是指针的意思

  • cur作为中心点

  • i是移动点

  • pos = cur*2-i 就是关于中心对称的右端点位置,

  • int pos = (cur<<1) - i;
                    int now = Math.max(Math.min(len[pos],cur+len[cur]-i),0);
    
  • cur+len(cur)-i求出i这个点通过对称性得出的至少的回文串长度

  • while(s[i-now-1] == s[i+now+1]) {
           
                        ++now;
                    }
    
  • 尝试往两边扩张

  • if (i+now > cur + len[cur]){
           
                        cur = i;
                    }
    
  • 更新cur,因为之前i+l(i)总是小于等于cur+l(cur)

  • 最后得到的答案就是最长回文串除以2向上取整,就是这个回文串及其子串的所有回文串数目

标程

  • static class Task {
           
    
            /* 全局变量 */
            final int N = 500005;
    
            char[] s = new char[N*2];
            int[] len = new int[N*2];
    
            // 计算str中有多少个回文子串
            // 返回值:子串的数目
            long getAnswer(String str) {
           
                
                int n = str.length();
                for (int i = n; i != 0 ; --i) {
           
                    s[i<<1] = str.charAt(i-1);
                    s[i<<1 | 1] = 0;
                }
    
                n = n << 1 | 1;
                s[1] = 0;
                s[0] = 1;
                s[n+1] = 2;
    			
                // manacher算法
                int cur = 1;
                long ans = 0;
                for (int i = 2; i <= n; i++) {
           
                    // cursor是对称中心
                    // i和pos是关于cursor对称的对称点
                    int pos = (cur<<1) - i;
                    // now是pos到0的距离和i到2n的距离的最小者,如果越界了就是0
                    // now就是i能往两侧至少能拓展多长
                    int now = Math.max(Math.min(len[pos],cur+len[cur]-i),0);
                    while(s[i-now-1] == s[i+now+1]) {
           
                        ++now;
                    }
                    // cur+len[cur]要保证是最大的
                    // 找到一个比当前对称中心所处的回文串要大的对称中心点,更新对称中心
                    if (i+now > cur + len[cur]){
           
                        cur = i;
                    }
                    // 相当于now/2取上界
                    // 一个字母也算回文串
                    ans += Math.ceil(now/2.0);
                    // 更新目前的回文串长度
                    len[i] = now;
    
                }
    
                return ans;
            }
    
           
    }
    

你可能感兴趣的:(数据结构,字符串)