哈希前置知识请戳这里-> 哈希绪论
关于哈希的其他例题也可以在我字符串哈希的分类专栏中找到
有了一维哈希的基础知识,很容易就能联想到二维哈希。那具体又该如何实现呢?
前置知识
我们先回顾一下二维前缀和的求解方法,设 p p p为二维差分数组, a a a为原数组,则有 p [ i ] [ j ] = p [ i − 1 ] [ j ] + p [ i ] [ j − 1 ] − p [ i − 1 ] [ j − 1 ] + a [ i ] [ j ] p[i][j]=p[i-1][j]+p[i][j-1]-p[i-1][j-1]+a[i][j] p[i][j]=p[i−1][j]+p[i][j−1]−p[i−1][j−1]+a[i][j],对于这个公式的证明画图即可解决。
用面积类比元素和,则
p
[
i
]
[
j
]
p[i][j]
p[i][j]表示
(
1
,
1
,
i
,
j
)
(1,1,i,j)
(1,1,i,j)所围成矩形的面积,减去重复的即可。于是我们就可以有
O
(
N
∗
M
)
O(N*M)
O(N∗M)预处理二维前缀表的方法。
预处理代码如下:
for (int i = 1; i <= n; ++i)//n行m列的矩阵
for (int j = 1; j <= m; ++j)
p[i][j] = p[i - 1][j] + p[i][j - 1] - p[i - 1][j - 1] + a[i][j];
查询也非常简单,若查询
(
x
1
,
y
1
,
x
2
,
y
2
)
(x_1,y_1,x_2,y_2)
(x1,y1,x2,y2)子矩阵的元素和,则有
a
s
k
(
x
1
,
y
1
,
x
2
,
y
2
)
=
p
[
x
2
]
[
y
2
]
−
p
[
x
1
−
1
]
[
y
2
]
−
p
[
x
2
]
[
y
1
−
1
]
+
p
[
x
1
−
1
]
[
y
1
−
1
]
ask(x_1,y_1,x_2,y_2)=p[x_2][y_2]-p[x_1-1][y_2]-p[x_2][y_1-1]+p[x_1-1][y_1-1]
ask(x1,y1,x2,y2)=p[x2][y2]−p[x1−1][y2]−p[x2][y1−1]+p[x1−1][y1−1]。证明与上述类似,画一个图减去重叠面积答案就出来了,查询复杂度
O
(
1
)
O(1)
O(1)
查询代码:
int ask(int x1, int y1, int x2, int y2)//视情况开long long
{
return p[i][j] - p[i - 1][j] - p[i][j - 1] + p[i - 1][j - 1];
}
二维哈希表的预处理
参考一下一维哈希表的预处理方法,我们对二维哈希表的预处理有如下步骤:
(
h
[
i
]
[
j
]
h[i][j]
h[i][j]表示
(
1
,
1
,
i
,
j
)
(1,1,i,j)
(1,1,i,j)子矩阵的哈希值)
1.先按照一维哈希表的方式处理出每一行的哈希值,即此步处理完成后 h [ i ] [ j ] h[i][j] h[i][j]表示第 i i i行 ( 1 , j ) (1,j) (1,j)子串的哈希值。
2.接下来这步就是获得二维前缀表的关键了。我们把一行缩成一个点,那么
(
1
,
1
,
i
,
j
)
(1,1,i,j)
(1,1,i,j)就被缩成了
j
j
j个在纵向上的点,于是这又转换成了一维哈希表的处理,又因为我们已经提前计算了
h
[
i
−
1
]
[
j
]
、
h
[
i
]
[
j
]
h[i-1][j]、h[i][j]
h[i−1][j]、h[i][j]的值,那么就得到了更新
h
[
i
]
[
j
]
h[i][j]
h[i][j]的公式:
h
[
i
]
[
j
]
+
=
h
[
i
−
1
]
[
j
]
∗
b
a
s
e
h[i][j]+=h[i-1][j]*base
h[i][j]+=h[i−1][j]∗base
下面是预处理二维哈希表的完整代码:
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= m; ++j)
{
scanf("%d", &x);
h[i][j] = ((ull)h[i][j - 1] * 131 + x);
}
//第一步获得每行子串的哈希表
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= m; ++j)
h[i][j] = (h[i][j] + (ull)h[i - 1][j] * 233);
//第二步获得子矩阵的哈希值
这里需要注意两个点,一是我选择了两个进制数131和233,这是为了减少哈希冲突的概率提高我们的AC率 ,二是我这次并没有模大质数而是选择了自然溢出。其实自然溢出相当于电脑自动帮你模了
2
64
2^{64}
264(多用unsingned long long代替long long),这样既有利于代码的书写,也不用再考虑查询的时候出现负数的情况了,还可以在一定程度上节省取模的时间。不过由于自然溢出模的质数是固定的,会被用心良苦的毒瘤出题人卡 一般为了保险会双哈希(即选择两个进制数)。
二维哈希表的查询操作
目标:查询子矩阵 ( x 1 , y 1 , x 2 , y 2 ) (x_1,y_1,x_2,y_2) (x1,y1,x2,y2)的哈希值。
联系一维哈希表的预处理和二维哈希表的预处理以及二维前缀表的知识,我们很容易 得到查询子矩阵哈希值的公式:
a
s
k
(
x
1
,
y
1
,
x
2
,
y
2
)
=
h
[
x
2
]
[
y
2
]
−
h
[
x
2
]
[
y
1
−
1
]
∗
b
a
s
e
1
[
y
2
−
y
1
+
1
]
−
h
[
x
1
−
1
]
[
y
2
]
∗
b
a
s
e
2
[
x
2
−
x
1
+
1
]
−
h
[
x
1
−
1
]
[
y
1
−
1
]
∗
b
a
s
e
1
[
y
2
−
y
1
+
1
]
∗
b
a
s
e
2
[
x
2
−
x
1
+
1
]
ask(x_1,y_1,x_2,y_2)=h[x_2][y_2]-h[x_2][y_1-1]*base_1[y_2-y_1+1]-h[x_1-1][y_2]*base_2[x_2-x_1+1]-h[x_1-1][y_1-1]*base_1[y_2-y_1+1]*base_2[x_2-x_1+1]
ask(x1,y1,x2,y2)=h[x2][y2]−h[x2][y1−1]∗base1[y2−y1+1]−h[x1−1][y2]∗base2[x2−x1+1]−h[x1−1][y1−1]∗base1[y2−y1+1]∗base2[x2−x1+1]
其中
b
a
s
e
1
base_1
base1表示行哈希处理时选用的进制数,也就是步骤1选择的进制数;
b
a
s
e
2
base_2
base2表示列处理选择的进制数,也就是步骤2选择的进制数。
别看这个公式复杂,整体上就是套用了二维前缀表查询的公式再乘上一些
b
a
s
e
base
base,而这些
b
a
s
e
base
base又与一维哈希的查询息息相关,那么整个公式的条理也就清楚了。相关的证明与一维哈希查询类似,但比较繁琐,故从理解的方式简要说明了这个公式。
例题解析
其实就是一道模板题然而我因为单哈希被出题人hack了 ,由于考二维哈希的题目比较少,就只能先拿出这题了。
牛客 Matrix
与一维哈希的查询类似:先预处理所有
A
A
A行
B
B
B列子矩阵的哈希值,最后二分查找即可。时间复杂度
O
(
N
∗
M
+
Q
∗
(
A
∗
B
+
l
o
g
(
N
∗
M
)
)
)
O(N*M+Q*(A*B+log(N*M)))
O(N∗M+Q∗(A∗B+log(N∗M)))
代码如下:
#include<stdio.h>
#include<algorithm>
#define maxn 1005
#define ll long long
int h[maxn][maxn], base1[maxn], ans[maxn * maxn], base2[maxn];
int get(int x1, int y1, int x2, int y2)
{
return h[x2][y2] - (ll)h[x2][y1 - 1] * base1[y2 - y1 + 1] - (ll)h[x1 - 1][y2] * base2[x2 - x1 + 1] + (ll)h[x1 - 1][y1 - 1] * base1[y2 - y1 + 1] * base2[x2 - x1 + 1];
}//查询公式
int main()
{
int n, m, a, b, q, ct = 0; char x;
scanf("%d%d%d%d", &n, &m, &a, &b);
base1[0] = base2[0] = 1;
for (int i = 1; i <= 1000; ++i)
{
base1[i] = (ll)base1[i - 1] * 131;
base2[i] = (ll)base2[i - 1] * 233;
}
getchar();
for (int i = 1; i <= n; ++i, getchar())
for (int j = 1; j <= m; ++j)
{
x = getchar();
h[i][j] = ((ll)h[i][j - 1] * 131 + x);
}
//步骤1
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= m; ++j)
h[i][j] = (h[i][j] + (ll)h[i - 1][j] * 233);
//步骤2
for (int i = 1; i <= n - a + 1; ++i)
for (int j = 1; j <= m - b + 1; ++j)
ans[++ct] = get(i, j, i + a - 1, j + b - 1);
std::sort(ans + 1, ans + ct + 1);//排序二分方便查找
scanf("%d", &q); getchar();
while (q--)
{
for (int i = 1; i <= a; ++i, getchar())
for (int j = 1; j <= b; ++j)
{
x = getchar();
h[i][j] = ((ll)h[i][j - 1] * 131 + x);
}
for (int i = 1; i <= a; ++i)
for (int j = 1; j <= b; ++j)
h[i][j] = (h[i][j] + (ll)h[i - 1][j] * 233);
//与预处理大矩形类似,此时计算询问矩形的哈希值
puts(std::binary_search(ans + 1, ans + ct + 1, h[a][b]) ? "1" : "0");
//二分查询此哈希值是否存在
}
return 0;
}