博客引言
“算法是问题的翻译官,将混沌转化为秩序。”
今日,我们将穿越两个截然不同的领域:
水域大小问题:在数字矩阵中寻找隐藏的湖泊,揭开图论连通性的拓扑秘密;
模式匹配问题:破解字符串与模式间的密码契约,体验组合枚举的逻辑艺术。
二者如莫比乌斯环的两面:一个在空间维度探索连通性,一个在符号维度验证一致性。
我们将用DFS/BFS的解剖刀剖开水域之谜,以枚举剪枝的显微镜解构模式之锁,最后在对比中揭示算法设计的本质矛盾——空间与时间的永恒博弈。
问题一:水域大小——矩阵中的水文测绘学
给定一个二维整数矩阵 land
,其中 0
代表水域,其他值代表陆地。池塘是由垂直、水平或对角线连接的水域组成的区域。要求计算所有池塘的大小,并从小到大排序返回。
1. 问题本质与算法选择
-
核心任务:在离散网格中统计8-连通分量(池塘),输出有序尺寸列表。
-
图论建模:将矩阵转化为无向图,每个单元格为节点,8邻接关系为边,0值节点构成子图。
-
关键挑战:避免重复计数(访问标记)与高效遍历(稀疏矩阵优化)。
2. 算法策略:DFS与BFS的时空博弈
算法 | 空间消耗 | 适用场景 | 优势 |
---|---|---|---|
DFS | 隐式栈(O(max(m,n))) | 池塘结构狭长 | 代码简洁,路径追踪清晰 |
BFS | 显式队列(O(mn)) | 池塘结构宽大 | 避免栈溢出,层序可控 |
-
优化技巧:
-
原地标记:将访问过的0修改为-1,节省O(mn)标记空间。
-
方向向量:预定义
[(-1,-1), (-1,0), ... , (1,1)]
8方向偏移量,简化邻居遍历。 -
剪枝策略:跳过非0单元格,降低无效访问。
-
3. 复杂度与工程陷阱
-
时间复杂度:O(mn)(每个节点仅访问一次)
-
空间复杂度:O(mn)(队列最坏情况)
-
隐藏陷阱:
-
大矩阵递归深度:1000×1000网格下DFS可能导致栈溢出,BFS更安全。
-
对角线连通语义:8连通比4连通多4个方向,需明确定义偏移量。
-
“矩阵是凝固的水域,搜索算法是融冰的暖流。”
详细分析:
- 初始化:创建一个与
land
同大小的访问矩阵visited
,标记每个点是否被访问过。 - 遍历矩阵:对于每一个未访问的
0
点,启动一次 BFS。 - BFS实现:
- 使用队列存储当前需要处理的点。
- 将当前点标记为已访问,并加入队列。
- 处理队列中的每个点,将其所有未访问的
0
邻点加入队列,并标记为已访问。 - 记录当前池塘的大小。
- 结果处理:将所有池塘的大小收集起来,从小到大排序。
验证示例:
- 示例输入:
[
[0,2,1,0],
[0,1,0,1],
[1,1,0,1],
[0,1,0,1]
]- 输出:
[1,2,4]
- 分析:矩阵中有三个池塘,大小分别为
1
、2
和4
。
- 输出:
题目程序:
#include <stdio.h> // 标准输入输出
#include <stdlib.h> // 动态内存分配、排序等函数
// 比较函数,用于qsort排序(升序)
int compare(const void* a, const void* b) {
return (*(int*)a - *(int*)b); // 升序排列
}
// 主函数:计算池塘大小
int* pondSizes(int** land, int landSize, int* landColSize, int* returnSize) {
// 处理空矩阵情况
if (landSize == 0 || landColSize[0] == 0) {
*returnSize = 0; // 返回数组大小为0
return NULL; // 返回空指针
}
int cols = landColSize[0]; // 矩阵列数(假设每列相同)
int total = landSize * cols; // 矩阵总元素数
*returnSize = 0; // 初始化返回数组大小
// 分配visited数组(记录访问状态)
int** visited = (int**)malloc(landSize * sizeof(int*));
for (int i = 0; i < landSize; i++) {
visited[i] = (int*)calloc(cols, sizeof(int)); // 初始化为0
}
// 分配结果数组(最大可能大小)
int* result = (int*)malloc(total * sizeof(int));
// 分配BFS队列(行坐标和列坐标)
int* queue_row = (int*)malloc(total * sizeof(int));
int* queue_col = (int*)malloc(total * sizeof(int));
// 8个方向的偏移量(上、下、左、右、左上、右上、左下、右下)
int dx[] = {-1, -1, -1, 0, 0, 1, 1, 1};
int dy[] = {-1, 0, 1, -1, 1, -1, 0, 1};
// 遍历矩阵中的每个元素
for (int i = 0; i < landSize; i++) {
for (int j = 0; j < cols; j++) {
// 当前元素是水域且未访问
if (land[i][j] == 0 && !visited[i][j]) {
int front = 0; // 队列头指针
int rear = 0; // 队列尾指针
int size = 0; // 当前池塘大小
// 将当前点加入队列
queue_row[rear] = i;
queue_col[rear] = j;
rear++; // 尾指针后移
visited[i][j] = 1; // 标记为已访问
size++; // 池塘大小加1
// BFS遍历
while (front < rear) {
int r = queue_row[front]; // 出队行坐标
int c = queue_col[front]; // 出队列坐标
front++; // 头指针后移
// 检查8个方向
for (int d = 0; d < 8; d++) {
int nr = r + dx[d]; // 新行坐标
int nc = c + dy[d]; // 新列坐标
// 检查新坐标是否有效
if (nr >= 0 && nr < landSize && nc >= 0 && nc < cols) {
// 新坐标是水域且未访问
if (land[nr][nc] == 0 && !visited[nr][nc]) {
queue_row[rear] = nr; // 入队行坐标
queue_col[rear] = nc; // 入队列坐标
rear++; // 尾指针后移
visited[nr][nc] = 1; // 标记为已访问
size++; // 池塘大小加1
}
}
}
}
// 记录当前池塘大小
result[*returnSize] = size;
(*returnSize)++; // 池塘计数加1
}
}
}
// 对结果数组排序(升序)
qsort(result, *returnSize, sizeof(int), compare);
// 释放visited数组内存
for (int i = 0; i < landSize; i++) {
free(visited[i]);
}
free(visited);
// 释放队列内存
free(queue_row);
free(queue_col);
return result; // 返回结果数组
}
// 测试函数
int main() {
// 示例输入矩阵
int row0[] = {0, 2, 1, 0};
int row1[] = {0, 1, 0, 1};
int row2[] = {1, 1, 0, 1};
int row3[] = {0, 1, 0, 1};
int* land[] = {row0, row1, row2, row3};
int landSize = 4; // 矩阵行数
int landColSize[] = {4, 4, 4, 4}; // 每行列数
int returnSize; // 返回数组大小
// 调用函数计算池塘大小
int* sizes = pondSizes(land, landSize, landColSize, &returnSize);
// 打印结果
printf("输出: [");
for (int i = 0; i < returnSize; i++) {
printf("%d", sizes[i]);
if (i < returnSize - 1) printf(",");
}
printf("]\n");
// 释放结果数组
free(sizes);
return 0;
}
输出结果:
问题二:模式匹配——字符串的密码契约论
给定两个字符串 pattern
和 value
,判断 value
是否匹配 pattern
。pattern
由 a
和 b
组成,表示不同的模式。要求 a
和 b
不能同时代表相同的字符串。
1. 问题本质与决策树
-
核心任务:验证是否存在双射映射 φ: {a,b} → Σ⁺,使得φ(pattern)=value。
-
约束条件:
-
一致性:相同字符映射相同子串(φ(a)=s₁, φ(b)=s₂)。
-
互异性:a与b映射子串不同(s₁ ≠ s₂)。
-
2. 算法策略:枚举的哲学与剪枝艺术
步骤全景:
-
模式解析:统计a的数量(countA)、b的数量(countB)。
-
长度方程:设a映射串长lenA,b映射串长lenB,解方程:
text
countA × lenA + countB × lenB = len(value)
-
枚举空间:
-
若countA≠0,lenA ∈ [0, len(value)//countA]
-
若countB≠0,lenB = (len(value) - countA×lenA) / countB(需为整数)
-
-
映射验证:
-
按pattern顺序切分value,验证相同字符映射子串一致。
-
检查a与b映射子串互异。
-
3. 临界场景处理
场景 | 处理策略 | 示例 |
---|---|---|
单字符模式 | 检查整个value是否相同子串重复 | pattern="aaa", value="go!go!go!" |
空字符串映射 | 允许lenA=0或lenB=0 | value="dogdog", a="dogdog", b="" |
整除性过滤 | 跳过非整数lenB的解 | lenB=3.5 → 无效 |
4. 复杂度与优化
-
时间复杂度:O(n²)(n为value长度,lenA最多n种取值,验证O(n))
-
剪枝加速:
-
长度方程过滤:跳过非整数lenB。
-
前缀不匹配提前终止:第一组a/b映射不匹配时跳出。
-
“模式是契约,value是承诺,算法是公正的仲裁人。”
详细分析:
- 模式分析:确定
pattern
中a
和b
的出现顺序和位置。 - 字符串分割:将
value
分割成与pattern
相同数量的子字符串。 - 模式匹配:
- 确保
a
和b
分别对应不同的子字符串。 - 检查子字符串的顺序是否与
pattern
一致。
- 确保
- 递归优化:尝试不同的分割方式,确保匹配的正确性。
验证示例:
- 示例1:
pattern = "abba"
,value = "dogcatcatdog"
- 输出:
true
- 分析:
a = "dog"
,b = "cat"
,满足模式。
- 示例2:
pattern = "abba"
,value = "dogcatcatfish"
- 输出:
false
- 分析:无法分割成满足
abba
模式的子字符串。
- 示例3:
pattern = "aaaa"
,value = "dogcatcatdog"
- 输出:
false
- 分析:无法分割成四个相同的子字符串。
- 示例4:
pattern = "abba"
,value = "dogdogdogdog"
- 输出:
true
- 分析:
a = "dogdog"
,b = ""
,满足模式。
题目程序:
#include <stdio.h> // 标准输入输出
#include <stdlib.h> // 动态内存分配
#include <string.h> // 字符串处理
#include <stdbool.h> // 布尔类型支持
// 模式匹配函数
bool patternMatching(char* pattern, char* value) {
// 处理pattern为空字符串的情况
if (pattern[0] == '\0') {
return value[0] == '\0'; // 当且仅当value也为空时匹配
}
int len_pattern = strlen(pattern); // 计算模式字符串长度
char first = pattern[0]; // 获取模式第一个字符
// 确定第二个模式字符:如果第一个是'a'则第二个是'b',反之亦然
char second = (first == 'a') ? 'b' : 'a';
int countFirst = 0; // 统计第一个模式字符出现的次数
int countSecond = 0; // 统计第二个模式字符出现的次数
// 遍历模式字符串,统计两个字符的出现次数
for (int i = 0; i < len_pattern; i++) {
if (pattern[i] == first) {
countFirst++; // 第一个模式字符计数
} else {
countSecond++; // 第二个模式字符计数
}
}
int len_value = strlen(value); // 计算待匹配字符串长度
// 尝试所有可能的第一个模式字符串长度
for (int len_first = 0; len_first <= len_value; len_first++) {
if (countSecond == 0) { // 模式中只有一种字符的情况
// 检查总长度是否能被模式字符个数整除
if (len_value % countFirst != 0) {
continue; // 不能整除则跳过当前长度
}
int len_per = len_value / countFirst; // 计算每个子串的长度
char* first_str = NULL; // 指向第一个模式字符串的指针
int start = 0; // 当前在value中的位置
int i;
// 遍历模式字符串进行匹配
for (i = 0; i < len_pattern; i++) {
// 检查当前字符是否匹配模式
if (pattern[i] != first) {
break; // 出现非第一个字符(理论上不会发生)
}
// 检查是否超出字符串长度
if (start + len_per > len_value) {
break; // 超出长度则中断
}
if (first_str == NULL) {
first_str = value + start; // 记录第一个模式字符串
} else {
// 比较当前子串与第一个模式字符串
if (strncmp(first_str, value + start, len_per) != 0) {
break; // 不匹配则中断
}
}
start += len_per; // 移动到下一个子串位置
}
// 检查是否完成整个模式匹配且覆盖整个字符串
if (i == len_pattern && start == len_value) {
return true; // 匹配成功
}
} else { // 模式中包含两种字符的情况
int total_second = len_value - countFirst * len_first; // 计算第二个模式字符串总长度
// 检查长度是否有效
if (total_second < 0) {
break; // 长度无效且后续更不可能,直接中断循环
}
// 检查第二个模式字符串总长度是否能被其数量整除
if (total_second % countSecond != 0) {
continue; // 不能整除则跳过
}
int len_second = total_second / countSecond; // 计算每个第二个模式字符串的长度
char* first_str = NULL; // 指向第一个模式字符串的指针
char* second_str = NULL; // 指向第二个模式字符串的指针
int start = 0; // 当前在value中的位置
int i;
// 遍历模式字符串进行匹配
for (i = 0; i < len_pattern; i++) {
if (pattern[i] == first) { // 处理第一个模式字符
// 检查是否超出字符串长度
if (start + len_first > len_value) {
break; // 超出长度则中断
}
if (first_str == NULL) {
first_str = value + start; // 记录第一个模式字符串
} else {
// 比较当前子串与第一个模式字符串
if (strncmp(first_str, value + start, len_first) != 0) {
break; // 不匹配则中断
}
}
start += len_first; // 移动到下一个子串位置
} else { // 处理第二个模式字符
// 检查是否超出字符串长度
if (start + len_second > len_value) {
break; // 超出长度则中断
}
if (second_str == NULL) {
second_str = value + start; // 记录第二个模式字符串
} else {
// 比较当前子串与第二个模式字符串
if (strncmp(second_str, value + start, len_second) != 0) {
break; // 不匹配则中断
}
}
start += len_second; // 移动到下一个子串位置
}
}
// 检查是否完成整个模式匹配且覆盖整个字符串
if (i == len_pattern && start == len_value) {
// 检查两个模式字符串是否不同
if (len_first == 0 && len_second == 0) {
// 两个都是空串,视为相同(不允许)
} else if (len_first == len_second &&
strncmp(first_str, second_str, len_first) == 0) {
// 长度相同且内容相同(不允许)
} else {
return true; // 满足所有条件,匹配成功
}
}
}
}
return false; // 所有尝试均失败
}
// 测试函数
int main() {
// 测试用例1:应返回true
char* pattern1 = "abba";
char* value1 = "dogcatcatdog";
bool result1 = patternMatching(pattern1, value1);
printf("Test1: %s\n", result1 ? "true" : "false");
// 测试用例2:应返回false
char* pattern2 = "abba";
char* value2 = "dogcatcatfish";
bool result2 = patternMatching(pattern2, value2);
printf("Test2: %s\n", result2 ? "true" : "false");
// 测试用例3:应返回false
char* pattern3 = "aaaa";
char* value3 = "dogcatcatdog";
bool result3 = patternMatching(pattern3, value3);
printf("Test3: %s\n", result3 ? "true" : "false");
// 测试用例4:应返回true
char* pattern4 = "abba";
char* value4 = "dogdogdogdog";
bool result4 = patternMatching(pattern4, value4);
printf("Test4: %s\n", result4 ? "true" : "false");
return 0;
}
输出结果:
双问题对比:空间探索 vs 逻辑验证
1. 算法思想对照表
维度 | 水域大小 | 模式匹配 |
---|---|---|
问题领域 | 图论(连通分量) | 组合数学(映射验证) |
核心操作 | 邻域遍历(空间扩展) | 长度枚举(组合生成) |
数据结构 | 队列/栈(BFS/DFS) | 哈希表(存储映射关系) |
最优策略 | BFS(避免栈溢出) | 枚举剪枝(减少无效尝试) |
关键约束 | 8连通性 | 双射一致性 |
2. 复杂度对比雷达图
时间复杂度 ^ | DFS/BFS O(mn) • • • • • • • • • • • • • • • • • • • 模式匹配 O(n²) | 空间复杂度 <-----•-----> 实现复杂度 | (水域:★★☆ 模式:★★★★) | 模式匹配 O(n) • • • • • • • • • • • • • • • • • • 水域 O(mn)
-
空间维度:水域问题需处理矩阵存储(O(mn)),模式匹配仅需线性空间(O(n))。
-
时间维度:水域问题为严格线性(O(mn)),模式匹配受枚举深度影响(O(n²))。
-
实现难度:模式匹配需处理多种边界(空串、单字符),逻辑复杂度更高。
3. 本质矛盾揭示
-
空间 vs 时间:
-
水域问题:空间消耗大(矩阵存储),但时间效率稳定。
-
模式匹配:空间消耗小,但时间随输入规模呈二次增长。
-
-
连通性 vs 组合性:
-
水域:连通性是传递关系(聚类)。
-
模式:映射是等价关系(分类)。
-
“水域问题在空间中编织网络,模式匹配在逻辑中铸造钥匙。”
总结:算法思维的二元性
-
空间型问题(如水域大小):
-
核心:状态传播(传染模型)。
-
武器库:DFS/BFS/并查集。
-
信条:“相邻即相关”。
-
-
逻辑型问题(如模式匹配):
-
核心:约束满足(CSP模型)。
-
武器库:回溯/剪枝/数学推导。
-
信条:“形式即语义”。
-
终极启示:
-
当面对空间结构时,化身拓扑学家——用搜索算法丈量连接。
-
当面对符号逻辑时,化身密码学家——用枚举与验证破解契约。
二者在NP完全问题的疆域交汇(如网格哈密顿路径),那时,空间与逻辑将完成终极统一。
今日的算法之旅如同穿梭于莫比乌斯环:
-
我们从矩阵的湖泊出发,见证DFS如何将凝固的0融为流动的连通域;
-
行至字符串的契约之地,学习枚举如何将混沌的字符锻造成秩序的密钥。
当你下次凝视池塘的涟漪,或观察文字的排列时——
愿你能看见水面下连通分量的脉动,读出字符间模式映射的光谱。
因为算法不仅是代码,更是理解世界的棱镜。
明日预告:《动态规划的禅意:从斐波那契到星际航行的时间折叠术》
点击关注,解锁更多算法宇宙的奥秘!