习题课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=1∑naiBn−i) 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<q,是为了目标矩形的大小,计算出存储到h1()()()数组中的值是跟目标矩阵的大小相等的
-
而当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; } }