二维哈希匹配:行内用位串移位精确取 B 位窗口,竖向按 2^B 进制折叠对 2^61-1 取模 O(1) 滑动,询问指纹入集合,O(MN+QAB) 判定。
OJ: roj
题目 ID: 3050
难度:普及+/提高-
标签:字符串hash哈希二维匹配python
创建: 2026-10-01 12:10
更新: 2026-10-07 13:50
形式化题目
给定 1,否则输出 0。
样例中询问矩阵为 1。下面用这个
原矩阵每行可以看成一个二进制数,“行内长度为
| 行(二进制) | 窗口 |
窗口 |
|---|---|---|
111 = 7 |
11 = 3 |
11 = 3 |
000 = 0 |
00 = 0 |
00 = 0 |
111 = 7 |
11 = 3 |
11 = 3 |
把每个
暴力解法
思路
对每个询问,枚举原矩阵所有
代码
暴力层不单独给出代码(本题按 Python 教学流程只保留正解 main.py,暴力仅作推导起点);完整实现见下方正解。
复杂度
时间
瓶颈
单次比较
正解
思路
第一步:把矩阵匹配变成整数匹配。 一个
第二步:行内窗口用精确位串。 Python 里一行 0/1 字符串 int(s, 2) 就是一个大整数(左端在高位)。第
代码里写成 (v >> (n - c - b)) & ((1 << b) - 1)。这是精确值,行内不引入任何哈希碰撞。
第三步:竖向
对 Mersenne 素数
下面用样例(
| 子块 | 各行窗口值 | 按行主序拼出的位串 | |
|---|---|---|---|
1100 = 12 |
|||
0011 = 3 |
观察重点:折叠值(第 3 列)与真实位串值(第 4 列)在不取模时完全相等——这正是"折叠 = 行主序拼接"的含义;取模后只损失极小概率的等值关系(碰撞概率
第四步:查询。 逐列拿 1。询问之间可能出现重复矩阵,按"矩阵原文"分桶去重,一次判定回填多个下标。
两个特判:0;
代码
Python 版:
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-01 12:10
# update_at: 2026-10-01 12:10
import sys
MOD = (1 << 61) - 1 # Mersenne 素数:竖向折叠在 2^b 进制下对它取模(数据随机,单哈希足够)
def row_hashes(s: str, w: int) -> list[int]:
"""一行里所有长度为 w 的窗口,各拼成 w 位二进制整数指纹(精确值)。"""
bits = int(s, 2) # 整行读成一个二进制大整数,左端在高位
full = (1 << w) - 1
n = len(s)
# 第 c 列的窗口 = (整行 >> (n - c - w)) 的低 w 位
return [(bits >> (n - c - w)) & full for c in range(n - w + 1)]
def solve() -> None:
it = iter(sys.stdin.buffer.read().split())
m, n, a, b = (int(next(it)) for _ in range(4))
rows = [next(it).decode() for _ in range(m)] # 原矩阵 M 行字符串
q = int(next(it))
# 每个询问读 A 行、每行 B 列,按行主序拼成一个位串;重复矩阵去重后共享一次判定
queries: dict[str, list[int]] = {}
for i in range(q):
queries.setdefault(''.join(next(it).decode() for _ in range(a)), []).append(i)
q_hashes = {int(s, 2) % MOD: s for s in queries} # 指纹 → 询问矩阵原文
ans = [0] * q
if a <= m and b <= n: # 询问比原矩阵还大时必然找不到
fold = [[h % MOD for h in row_hashes(s, b)] for s in rows] # 每行窗口指纹(mod 后)
B = (1 << b) % MOD # 竖向折叠的进制 2^b
if a == 1: # 单行没有竖向折叠,直接查行内窗口
table = set()
for s in rows:
table.update(h % MOD for h in row_hashes(s, b))
for h, s in q_hashes.items():
if h in table:
for i in queries[s]:
ans[i] = 1
else:
ncol = n - b + 1
# H[c] = 以当前行为底的 a 行窗口在列 c 的指纹:
# sum(第 r-k 行窗口指纹 * B^k),顶行权 B^(a-1)(与询问位串的高位对齐)
H = [0] * ncol
for i in range(a - 1, -1, -1):
pw = pow(B, a - 1 - i, MOD)
Hi = fold[i]
H = [(h + r * pw) % MOD for h, r in zip(H, Hi)]
top_pow = pow(B, a - 1, MOD) # 滑出行指纹的权
for r in range(a - 1, m):
if r > a - 1: # 底行滑入第 r 行、顶行滑出第 r-a 行
out_row, in_row = fold[r - a], fold[r]
H = [((h - o * top_pow) * B + i_) % MOD
for h, o, i_ in zip(H, out_row, in_row)]
for c, h in enumerate(H): # 逐列查指纹集合
s = q_hashes.get(h)
if s is not None:
for i in queries[s]:
ans[i] = 1
print('\n'.join(map(str, ans)))
if __name__ == "__main__":
solve()C++ 版(同一算法):
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-06 16:31
* update_at: 2026-10-06 16:32
*/
#include <cstdio>
#include <string>
#include <vector>
#include <map>
#include <cstddef>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const int MAXN = 1005; // M、N 的上限
const ull MOD = (1ULL << 61) - 1; // Mersenne 素数 2^61-1,二维哈希取模
const int BUFSZ = 1 << 20; // 快速读入缓冲区大小
ll m, n, a, b, q; // 原矩阵行数、列数,询问矩阵行数、列数,询问个数
string grid[MAXN]; // 原矩阵每一行(01 串)
ull rowhash[MAXN][MAXN]; // rowhash[i][j]:第 i 行第 j 列起长度为 b 的窗口指纹
ull pw2[2 * MAXN]; // pw2[k] = 2^k mod MOD
ull H[MAXN]; // H[j]:以当前行为底的 a 行窗口在第 j 列的指纹
map<ull, vector<int> > query_group; // 询问指纹 -> 具有该指纹的询问下标列表(去重)
int ans[MAXN]; // ans[i]:第 i 个询问的答案
char inbuf[BUFSZ]; // 输入缓冲区
int inpos = 0, inlen = 0;
// 从缓冲区读入一个字符,缓冲区空时补读一块
inline char next_char() {
if (inpos == inlen) {
inlen = (int)fread(inbuf, 1, BUFSZ, stdin);
inpos = 0;
if (inlen == 0) return '\0';
}
return inbuf[inpos++];
}
// 读入一个非负整数
ll read_int() {
char c = next_char();
while (c < '0' || c > '9') c = next_char();
ll x = 0;
while (c >= '0' && c <= '9') {
x = x * 10 + (c - '0');
c = next_char();
}
return x;
}
// 2^61-1 下的乘法,用 __int128 承接中间结果防止溢出
ull mul_mod(ull x, ull y) {
return (ull)((unsigned __int128)x * y % MOD);
}
// (x - y) mod MOD
ull sub_mod(ull x, ull y) {
return x >= y ? x - y : x + MOD - y;
}
// 读入长度为 len 的 01 串(跳过空白),返回其按位拼成的整数 mod MOD
ull read_row_hash(ll len) {
ull h = 0;
ll done = 0;
while (done < len) {
ll take = len - done;
if (take > 30) take = 30; // 每 30 位成块,减少取模次数
ull block = 0;
for (ll k = 0; k < take; k++) {
char c = next_char();
while (c < '0' || c > '1') c = next_char();
block = block * 2 + (c - '0');
}
h = (mul_mod(h, pw2[take]) + block) % MOD;
done += take;
}
return h;
}
// 读入 q 个询问矩阵,各自按行主序折叠成一个指纹,相同指纹的询问合并到一组
void read_queries() {
q = read_int();
for (int i = 1; i <= q; i++) {
ull h = 0;
for (ll r = 1; r <= a; r++) {
ull rowval = read_row_hash(b); // 这一行 b 个字符拼成的 b 位整数
h = (mul_mod(h, pw2[b]) + rowval) % MOD;
}
query_group[h].push_back(i);
}
}
int main() {
m = read_int();
n = read_int();
a = read_int();
b = read_int();
for (ll i = 1; i <= m; i++) {
grid[i].reserve(n);
for (ll j = 0; j < n; j++) {
char c = next_char();
while (c < '0' || c > '1') c = next_char();
grid[i].push_back(c);
}
}
pw2[0] = 1;
for (int k = 1; k < 2 * MAXN; k++) pw2[k] = pw2[k - 1] * 2 % MOD;
read_queries();
if (a > m || b > n) { // 询问矩阵比原矩阵还大,任何位置都放不下
for (int i = 1; i <= q; i++) printf("0\n");
return 0;
}
ll ncol = n - b + 1; // 每行横向长度为 b 的窗口个数
// 第一步:每行用滑动窗口求出所有 b 位窗口指纹
for (ll i = 1; i <= m; i++) {
ull val = 0;
for (ll c = 0; c < b; c++) val = (val * 2 + (grid[i][c] - '0')) % MOD;
rowhash[i][1] = val;
for (ll j = 1; j < ncol; j++) {
ull del = (grid[i][j - 1] == '1') ? pw2[b - 1] : 0; // 左端最高位
ull mid = sub_mod(rowhash[i][j], del);
rowhash[i][j + 1] = (mid * 2 + (grid[i][j + b - 1] - '0')) % MOD;
}
}
// 第二步:竖向按 2^b 进制折叠,顶行权最大;滑动时每列 O(1) 维护
ull pwB = pw2[b];
ull pwTop = 1; // 顶行的权 pwB^(a-1)
for (ll k = 1; k <= a - 1; k++) pwTop = mul_mod(pwTop, pwB);
for (ll j = 1; j <= ncol; j++) {
ull h = 0;
for (ll i = 1; i <= a; i++) h = (mul_mod(h, pwB) + rowhash[i][j]) % MOD;
H[j] = h;
}
for (ll r = a; r <= m; r++) { // r 为当前 a×b 子矩阵的底行
if (r > a) {
ll out = r - a; // 滑出的行
ll in = r; // 滑入的行
for (ll j = 1; j <= ncol; j++) {
ull outv = mul_mod(rowhash[out][j], pwTop);
ull mid = sub_mod(H[j], outv);
H[j] = (mul_mod(mid, pwB) + rowhash[in][j]) % MOD;
}
}
for (ll j = 1; j <= ncol; j++) {
map<ull, vector<int> >::iterator it = query_group.find(H[j]);
if (it != query_group.end()) {
for (size_t t = 0; t < it->second.size(); t++) ans[it->second[t]] = 1;
query_group.erase(it); // 该指纹已命中,后续无需再查
}
}
if (query_group.empty()) break; // 所有询问都已找到
}
for (int i = 1; i <= q; i++) printf("%d\n", ans[i]);
return 0;
}复杂度
- 时间:预处理
,滑动枚举 ,询问读入 位串操作,总计 ,与 C++ 二维哈希同阶。实测 的极限数据单点 0.35s(Python 侧宽裕;大整数移位/取模按机器字并行是 Python 的加分项)。 - 空间:
fold存个指纹, ;实测峰值约 58MB,在 128MB 限内。
总结
- 二维匹配的标准思路:把二维模式压成一维指纹,"文本侧枚举 + 哈希查询"替代逐格比较。
- 本题的实现亮点是不对称哈希:横向窗口在 Python 里用大整数移位精确取出(零碰撞),只有竖向拼接需要多项式哈希(模
滑动维护)。 - 询问侧先算指纹入集合,把"每个询问扫一遍原矩阵"反转成"原矩阵子块查一次表",
倍的开销被合并进一个哈希表。
图示解析
一趟滑动的完整视图
以样例前两行为底行(
| 步骤 | 状态(列 |
|---|---|
| 读完第 1 行 | |
| 读完第 2 行 | |
| 查表 | 1 |
| 底行滑到第 3 行 |
观察重点:第二行初始化后