本文介绍如何通过二维前缀和预处理与目标矩阵和值剪枝,显著优化 go 语言中大规模矩阵子图匹配(如 hackerrank “the grid search”)的运行效率,避免暴力遍历超时。
本文介绍如何通过二维前缀和预处理与目标矩阵和值剪枝,显著优化 go 语言中大规模矩阵子图匹配(如 hackerrank “the grid search”)的运行效率,避免暴力遍历超时。
在解决 HackerRank 经典题型《The Grid Search》时,核心任务是:给定一个最大 1000×1000 的大矩阵 grid 和一个较小的 pattern 矩阵(最多 1000×1000),判断 pattern 是否作为连续子块完整出现在 grid 中。原始暴力解法时间复杂度为 $O(R_L \cdot C_L \cdot R_S \cdot C_S)$,在最坏情况下(如 1000×1000 大矩阵匹配 100×100 小矩阵)可能高达 $10^{10}$ 次操作,远超 4 秒时限。
关键瓶颈在于:对每个合法左上角位置 (i, j)(共约 $(R_L - R_S + 1) \times (C_L - C_S + 1)$ 个)都执行全量比对。而实际中绝大多数位置无需比对——我们可通过预计算二维区域和快速排除明显不匹配的位置。
✅ 优化思路:二维前缀和 + 和值剪枝
- 预计算小矩阵 pattern 的元素总和 targetSum;
- 构建大矩阵 grid 的二维前缀和数组 pref,使得 pref[i][j] 表示从 (0,0) 到 (i-1,j-1) 的子矩阵和(标准 1-indexed 定义);
- 对每个可能的左上角 (i, j)(满足 i+R_S ≤ R_L, j+C_S ≤ C_L),用前缀和 $O(1)$ 计算其对应 R_S × C_S 子矩阵和:
sum := pref[i+R_S][j+C_S] - pref[i][j+C_S] - pref[i+R_S][j] + pref[i][j]
- 仅当 sum == targetSum 时,才启动精确比对;否则跳过。该剪枝可将平均比对次数降低 1–2 个数量级(尤其当矩阵含大量 0/1 且 pattern 和值稀疏时效果显著)。
⚠️ 注意:和值相等是必要但不充分条件(如 [[1,0],[0,1]] 和 [[0,1],[1,0]] 和均为 2,但结构不同),因此必须保留最终验证逻辑。
Golang Uber Dig下载使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
? 完整优化实现(Go)
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
scanner := bufio.NewScanner(os.Stdin)
var t int
scanner.Scan()
fmt.Sscanf(scanner.Text(), "%d", &t)
for ; t > 0; t-- {
// 读取大矩阵尺寸
scanner.Scan()
parts := strings.Fields(scanner.Text())
rL, _ := strconv.Atoi(parts[0])
cL, _ := strconv.Atoi(parts[1])
// 构建大矩阵 grid 和前缀和 pref(rL+1 × cL+1)
grid := make([][]int, rL)
pref := make([][]int, rL+1)
for i := range pref {
pref[i] = make([]int, cL+1)
}
for i := 0; i < rL; i++ {
grid[i] = make([]int, cL)
scanner.Scan()
line := scanner.Text()
for j, ch := range line {
grid[i][j] = int(ch - '0')
}
}
// 构建二维前缀和:pref[i][j] = sum of grid[0..i-1][0..j-1]
for i := 1; i <= rL; i++ {
for j := 1; j <= cL; j++ {
pref[i][j] = grid[i-1][j-1] + pref[i-1][j] + pref[i][j-1] - pref[i-1][j-1]
}
}
// 读取小矩阵尺寸
scanner.Scan()
parts = strings.Fields(scanner.Text())
rS, _ := strconv.Atoi(parts[0])
cS, _ := strconv.Atoi(parts[1])
// 读取小矩阵并计算 targetSum
pattern := make([][]int, rS)
targetSum := 0
for i := 0; i < rS; i++ {
pattern[i] = make([]int, cS)
scanner.Scan()
line := scanner.Text()
for j, ch := range line {
val := int(ch - '0')
pattern[i][j] = val
targetSum += val
}
}
// 搜索:仅检查和值匹配的位置
found := false
for i := 0; i <= rL-rS; i++ {
for j := 0; j <= cL-cS; j++ {
// O(1) 计算 grid[i..i+rS-1][j..j+cS-1] 的和
sum := pref[i+rS][j+cS] - pref[i][j+cS] - pref[i+rS][j] + pref[i][j]
if sum != targetSum {
continue
}
// 精确比对
match := true
for x := 0; x < rS; x++ {
for y := 0; y < cS; y++ {
if grid[i+x][j+y] != pattern[x][y] {
match = false
break
}
}
if !match {
break
}
}
if match {
found = true
break
}
}
if found {
break
}
}
if found {
fmt.Println("YES")
} else {
fmt.Println("NO")
}
}
}? 关键优化点说明
- 输入加速:使用 bufio.Scanner 替代 fmt.Scanf,避免格式解析开销;
- 内存局部性:grid 使用一维切片模拟二维,配合连续内存布局,提升缓存命中率;
- 前缀和复用:pref 数组在单次测试用例中只构建一次,后续所有位置查询均为 $O(1)$;
- 剪枝强度:在典型 0/1 矩阵中,若 pattern 和为 k,则合法候选位置比例约为 $\binom{R_S \cdot C_S}{k} / 2^{R_S \cdot C_S}$,远小于 1;
- 边界安全:所有索引严格遵循 0 ≤ i ≤ rL−rS,避免越界访问。
✅ 总结
暴力匹配在大规模矩阵场景下必然超时。引入二维前缀和实现区域和 $O(1)$ 查询,并以“和值相等”作为第一道轻量级过滤器,能将无效比对大幅削减,使算法在最坏情况下仍稳定通过 HackerRank 4 秒时限。该模式也适用于其他子矩阵搜索、滑动窗口统计等高频问题,是 Go 工程中兼顾简洁性与性能的经典实践。
立即学习“go语言免费学习笔记(深入)”;



















