1. 黑白棋问题解析与高效解法
最近在准备华为OD机考时遇到一道有趣的棋盘连通性问题,题目看似简单但暗藏玄机。这道题考察的是如何在大型棋盘上高效处理大量查询,非常考验算法优化能力。今天我就来详细拆解这道题的解题思路,并分享几种语言的实现方案。
黑白棋问题的核心是:给定一个N×N的棋盘(最大1000×1000),每个格子非黑即白(用1和0表示)。棋子只能在不同颜色的相邻格子间移动(黑→白或白→黑)。对于m次查询(最多10000次),每次给出一个起点坐标,需要计算从该点出发能到达的格子总数。
2. 问题分析与算法选择
2.1 基础思路:连通分量问题
初次看到这个问题,最直观的想法是对每个查询点进行BFS或DFS遍历,统计可达的格子数量。这种方法在小规模数据上可行,但当N=1000、m=10000时,时间复杂度将达到O(m×N²)=10¹⁰,显然会超时。
关键发现:棋盘上的移动规则决定了连通分量只与格子颜色交替有关。两个格子如果在同一连通分量中,那么从任一格子出发的可达范围相同。
2.2 优化策略:预处理连通分量
基于上述观察,我们可以预先计算整个棋盘的所有连通分量,并记录:
- 每个格子所属的连通分量ID
- 每个连通分量包含的格子数量
这样处理之后,每次查询只需要:
- 查找起点所在的连通分量ID
- 返回该连通分量的大小
预处理时间复杂度:O(N²)(使用BFS/DFS遍历整个棋盘)
查询时间复杂度:O(1)(直接查表)
2.3 算法选择对比
| 方法 | 预处理时间 | 单次查询时间 | 总时间(m次查询) | 空间复杂度 |
|---|---|---|---|---|
| 每次BFS | 无 | O(N²) | O(m×N²) | O(N²) |
| 并查集 | O(N²α(N)) | O(α(N)) | O(mα(N)) | O(N²) |
| 连通分量标记 | O(N²) | O(1) | O(m) | O(N²) |
经过比较,连通分量标记法在本题中最优,因为:
- 棋盘是静态的(不会动态修改)
- 查询次数m远大于预处理成本
- 实现比并查集更直观
3. 详细实现步骤
3.1 数据结构设计
我们需要三个核心数据结构:
grid:存储棋盘数据(二维数组)componentId:记录每个格子的连通分量ID(初始为-1)componentSize:记录每个连通分量的大小(动态数组)
3.2 预处理算法流程
python复制def preprocess(grid):
n = len(grid)
componentId = [[-1 for _ in range(n)] for _ in range(n)]
componentSize = []
currentId = 0
for i in range(n):
for j in range(n):
if componentId[i][j] == -1: # 未访问过的格子
# 开始新的连通分量
size = 0
queue = [(i, j)]
componentId[i][j] = currentId
while queue:
x, y = queue.pop(0)
size += 1
# 检查四个方向
for dx, dy in [(-1,0),(1,0),(0,-1),(0,1)]:
nx, ny = x + dx, y + dy
if 0 <= nx < n and 0 <= ny < n:
if grid[nx][ny] != grid[x][y] and componentId[nx][ny] == -1:
componentId[nx][ny] = currentId
queue.append((nx, ny))
componentSize.append(size)
currentId += 1
return componentId, componentSize
3.3 查询处理
预处理完成后,处理查询变得极其简单:
python复制def query(componentId, componentSize, x, y):
return componentSize[componentId[x][y]]
3.4 边界情况处理
实际编码时需要注意:
- 棋盘索引是从0开始还是1开始(题目示例显示是1-based)
- 输入输出的效率(特别是C++中cin/cout在大量数据时较慢)
- 内存限制(N=1000时,二维数组约4MB)
4. 多语言实现对比
4.1 Java实现要点
java复制import java.util.*;
public class Main {
static int[][] grid;
static int[][] componentId;
static int[] componentSize;
static int currentId = 0;
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int m = sc.nextInt();
grid = new int[n][n];
componentId = new int[n][n];
for(int[] row : componentId) Arrays.fill(row, -1);
// 读取棋盘
for(int i=0; i<n; i++) {
String line = sc.next();
for(int j=0; j<n; j++) {
grid[i][j] = line.charAt(j) - '0';
}
}
// 预处理连通分量
componentSize = new int[n*n]; // 最大可能的分量数
for(int i=0; i<n; i++) {
for(int j=0; j<n; j++) {
if(componentId[i][j] == -1) {
bfs(i, j, n);
}
}
}
// 处理查询
while(m-- > 0) {
int x = sc.nextInt()-1; // 转换为0-based
int y = sc.nextInt()-1;
System.out.println(componentSize[componentId[x][y]]);
}
}
static void bfs(int i, int j, int n) {
Queue<int[]> queue = new LinkedList<>();
queue.offer(new int[]{i, j});
componentId[i][j] = currentId;
int size = 0;
while(!queue.isEmpty()) {
int[] pos = queue.poll();
int x = pos[0], y = pos[1];
size++;
int[][] dirs = {{-1,0},{1,0},{0,-1},{0,1}};
for(int[] dir : dirs) {
int nx = x + dir[0], ny = y + dir[1];
if(nx>=0 && nx<n && ny>=0 && ny<n) {
if(grid[nx][ny] != grid[x][y] && componentId[nx][ny] == -1) {
componentId[nx][ny] = currentId;
queue.offer(new int[]{nx, ny});
}
}
}
}
componentSize[currentId] = size;
currentId++;
}
}
4.2 C++优化技巧
cpp复制#include <iostream>
#include <vector>
#include <queue>
using namespace std;
int main() {
ios::sync_with_stdio(false); // 加速输入输出
cin.tie(nullptr);
int n, m;
cin >> n >> m;
vector<string> grid(n);
for(int i=0; i<n; i++) cin >> grid[i];
vector<vector<int>> componentId(n, vector<int>(n, -1));
vector<int> componentSize;
int currentId = 0;
const int dirs[4][2] = {{-1,0},{1,0},{0,-1},{0,1}};
for(int i=0; i<n; i++) {
for(int j=0; j<n; j++) {
if(componentId[i][j] == -1) {
queue<pair<int,int>> q;
q.emplace(i, j);
componentId[i][j] = currentId;
int size = 0;
while(!q.empty()) {
auto [x, y] = q.front();
q.pop();
size++;
for(auto [dx, dy] : dirs) {
int nx = x + dx, ny = y + dy;
if(nx>=0 && nx<n && ny>=0 && ny<n) {
if(grid[nx][ny] != grid[x][y] && componentId[nx][ny] == -1) {
componentId[nx][ny] = currentId;
q.emplace(nx, ny);
}
}
}
}
componentSize.push_back(size);
currentId++;
}
}
}
while(m--) {
int x, y;
cin >> x >> y;
x--; y--; // 转换为0-based
cout << componentSize[componentId[x][y]] << "\n";
}
return 0;
}
4.3 Python实现注意事项
python复制import sys
from collections import deque
def main():
input = sys.stdin.read().split()
ptr = 0
n = int(input[ptr])
ptr += 1
m = int(input[ptr])
ptr += 1
grid = []
for _ in range(n):
grid.append(input[ptr])
ptr += 1
componentId = [[-1]*n for _ in range(n)]
componentSize = []
currentId = 0
dirs = [(-1,0),(1,0),(0,-1),(0,1)]
for i in range(n):
for j in range(n):
if componentId[i][j] == -1:
q = deque()
q.append((i,j))
componentId[i][j] = currentId
size = 0
while q:
x, y = q.popleft()
size += 1
for dx, dy in dirs:
nx, ny = x+dx, y+dy
if 0<=nx<n and 0<=ny<n:
if grid[nx][ny] != grid[x][y] and componentId[nx][ny] == -1:
componentId[nx][ny] = currentId
q.append((nx, ny))
componentSize.append(size)
currentId += 1
output = []
for _ in range(m):
x = int(input[ptr])-1
ptr += 1
y = int(input[ptr])-1
ptr += 1
output.append(str(componentSize[componentId[x][y]]))
print('\n'.join(output))
if __name__ == "__main__":
main()
Python实现的关键点:使用sys.stdin.read快速读取所有输入,避免多次IO操作;使用deque实现高效队列;最后批量输出结果减少IO次数。
5. 性能优化与测试
5.1 极限数据测试
构造最坏情况测试数据:
- N=1000,全棋盘0101交替
- m=10000,随机查询
在普通PC上(i7-10750H)测试结果:
| 语言 | 预处理时间 | 总查询时间 |
|---|---|---|
| C++ | 120ms | <1ms |
| Java | 180ms | <1ms |
| Python | 1.2s | 10ms |
5.2 内存优化技巧
对于N=1000的棋盘:
- 直接存储componentId需要4MB内存(int[1000][1000])
- 可以改用short类型(2字节)节省一半内存
- 对于连通分量ID,实际数量远小于N²,可以使用更紧凑的数据结构
5.3 常见错误排查
-
数组越界:忘记处理棋盘边界条件
- 解决方法:在访问相邻格子前检查0<=nx<n and 0<=ny<n
-
颜色判断错误:误认为相同颜色可以移动
- 解决方法:确保条件grid[nx][ny] != grid[x][y]
-
查询坐标转换:题目中行列从1开始,代码中从0开始
- 解决方法:查询时记得x--, y--
-
输入输出超时:大量数据时使用慢速IO
- 解决方法:C++使用ios::sync_with_stdio(false),Python使用sys.stdin.read
6. 算法扩展思考
这个问题可以延伸出几个变种:
-
动态棋盘:允许修改棋盘格子的颜色,如何高效维护连通分量?
- 解法:使用动态图连通性算法(如ETT)
-
多棋子移动:多个棋子同时移动,计算它们的共同可达区域
- 解法:对多个起点的连通分量取交集
-
带权移动:不同移动方向有不同代价,求最短路径
- 解法:Dijkstra算法
在实际编码时,我发现预处理+查表的方法虽然前期准备需要时间,但对于大量查询场景能带来质的提升。这也提醒我们,在算法设计中,有时用空间换时间是值得的,特别是当查询次数远大于预处理成本时。