在算法的宇宙中,问题如星辰般璀璨各异。有的如迷宫般错综复杂,需巧破路径之谜;有的如精密齿轮,需构建秩序之链。今日,我们将深入两个经典问题:"节点间通路"的图论迷宫与"动物收容所"的队列交响曲。它们看似天壤之别,却在算法设计的琴弦上奏出共鸣——搜索与秩序。我们将以庖丁解牛之姿剖析其内核,用对比图表揭示本质,带您领略算法设计的艺术与哲学。
第一章:图论迷宫——节点间通路的搜索艺术
节点间通路。给定有向图,设计一个算法,找出两个节点之间是否存在一条路径。
问题本质:给定有向图,判断从起点 start
到终点 target
是否存在路径。图中含自环(节点指向自身)与平行边(节点间多条同向边),需应对高达 105105 节点的规模挑战。
算法策略与时空权衡:
-
邻接表建图:
-
使用稀疏矩阵压缩技术,将边集
graph
转化为邻接表(Adjacency List),避免邻接矩阵的 O(n2)O(n2) 空间灾难。 -
处理平行边:邻接表天然支持重边存储,不影响算法正确性。
-
-
搜索算法选型:
-
BFS(广度优先搜索):
-
核心思想:逐层扩散,用队列管理待访问节点。
-
优势:天然找到最短路径(本题无需),无递归栈溢出风险。
-
时间复杂度:O(n+e)O(n+e)(ee 为边数),完美匹配 n≤105n≤105 的约束。
-
-
DFS(深度优先搜索):
-
核心思想:递归深入路径末端,回溯探索分支。
-
风险:图深度过大时易栈溢出,需显式栈实现迭代DFS。
-
-
-
关键优化——访问标记:
-
使用布尔数组
visited
标记已访问节点,避免重复访问与循环陷阱。 -
自环处理:自环边
[u,u]
不影响连通性,但需防止无限循环——visited
数组可拦截此风险。
-
示例解析:
示例1:n=3, graph=[[0,1],[0,2],[1,2],[1,2]], start=0, target=2 邻接表:0→[1,2], 1→[2,2] # 重边不影响 BFS路径:0 → 1 → 2 或 0 → 2,存在通路 → 输出 true
题目程序:
#include <stdio.h> // 标准输入输出头文件
#include <stdlib.h> // 标准库头文件(内存分配、动态数组等)
#include <stdbool.h> // 布尔类型支持
// 定义邻接表节点结构
struct AdjListNode {
int dest; // 目标节点编号
struct AdjListNode* next; // 指向下一个邻接节点的指针
};
// 定义邻接表结构
struct AdjList {
struct AdjListNode* head; // 链表头指针
};
// 定义图结构
struct Graph {
int numVertices; // 图中节点总数
struct AdjList* array; // 邻接表数组(每个元素对应一个节点)
};
// 创建新的邻接表节点
struct AdjListNode* newAdjListNode(int dest) {
// 分配节点内存
struct AdjListNode* newNode = (struct AdjListNode*)malloc(sizeof(struct AdjListNode));
newNode->dest = dest; // 设置目标节点
newNode->next = NULL; // 初始化next指针
return newNode;
}
// 创建图结构
struct Graph* createGraph(int numVertices) {
// 分配图结构内存
struct Graph* graph = (struct Graph*)malloc(sizeof(struct Graph));
graph->numVertices = numVertices; // 设置节点总数
// 分配邻接表数组内存(大小 = 节点数)
graph->array = (struct AdjList*)malloc(numVertices * sizeof(struct AdjList));
// 初始化每个邻接表头指针
for (int i = 0; i < numVertices; ++i)
graph->array[i].head = NULL; // 置空链表头
return graph;
}
// 添加边到图中(有向边)
void addEdge(struct Graph* graph, int src, int dest) {
// 创建新节点(目标为dest)
struct AdjListNode* newNode = newAdjListNode(dest);
// 将新节点插入邻接表头部
newNode->next = graph->array[src].head;
graph->array[src].head = newNode;
}
// BFS搜索路径是否存在
bool BFS(struct Graph* graph, int start, int target) {
// 边界情况:起点等于终点
if (start == target)
return true;
// 创建访问标记数组(初始化为false)
bool* visited = (bool*)calloc(graph->numVertices, sizeof(bool));
// 创建循环队列(大小 = 节点数)
int* queue = (int*)malloc(graph->numVertices * sizeof(int));
int front = 0; // 队列头指针
int rear = 0; // 队列尾指针
// 起点入队并标记
queue[rear++] = start;
visited[start] = true;
while (front < rear) { // 队列非空时循环
int current = queue[front++]; // 出队当前节点
// 遍历当前节点的所有邻接节点
struct AdjListNode* neighbor = graph->array[current].head;
while (neighbor != NULL) {
int adjVertex = neighbor->dest; // 获取邻接节点
// 若找到目标节点则返回成功
if (adjVertex == target) {
free(visited);
free(queue);
return true;
}
// 若邻接节点未访问过
if (!visited[adjVertex]) {
visited[adjVertex] = true; // 标记已访问
queue[rear++] = adjVertex; // 入队
}
neighbor = neighbor->next; // 移动到下一个邻接节点
}
}
// 队列空且未找到目标节点
free(visited);
free(queue);
return false;
}
// 释放图内存
void freeGraph(struct Graph* graph) {
// 释放每个邻接表的节点
for (int v = 0; v < graph->numVertices; ++v) {
struct AdjListNode* current = graph->array[v].head;
while (current != NULL) {
struct AdjListNode* temp = current;
current = current->next;
free(temp);
}
}
// 释放邻接表数组和图结构
free(graph->array);
free(graph);
}
int main() {
int n, m; // n=节点数, m=边数
scanf("%d", &n); // 读取节点数
scanf("%d", &m); // 读取边数
// 创建图结构
struct Graph* graph = createGraph(n);
// 读取所有边
for (int i = 0; i < m; ++i) {
int u, v;
scanf("%d %d", &u, &v); // 读取边的起点和终点
addEdge(graph, u, v); // 添加到图中
}
int start, target;
scanf("%d %d", &start, &target); // 读取起点和终点
// 执行BFS搜索并输出结果
if (BFS(graph, start, target)) {
printf("true\n");
} else {
printf("false\n");
}
// 释放图内存
freeGraph(graph);
return 0;
}
输出结果:
第二章:队列交响曲——动物收容所的秩序法则
动物收容所。有家动物收容所只收容狗与猫,且严格遵守“先进先出”的原则。在收养该收容所的动物时,收养人只能收养所有动物中“最老”(由其进入收容所的时间长短而定)的动物,或者可以挑选猫或狗(同时必须收养此类动物中“最老”的)。换言之,收养人不能自由挑选想收养的对象。请创建适用于这个系统的数据结构,实现各种操作方法,比如enqueue、dequeueAny、dequeueDog和dequeueCat。允许使用Java内置的LinkedList数据结构。
enqueue方法有一个animal参数,animal[0]代表动物编号,animal[1]代表动物种类,其中 0 代表猫,1 代表狗。
dequeue*方法返回一个列表[动物编号, 动物种类],若没有可以收养的动物,则返回[-1,-1]。
问题本质:设计队列系统,实现严格FIFO(先进先出)的动物收养规则。核心操作包括入队(enqueue
)和三种出队方式(dequeueAny
、dequeueDog
、dequeueCat
),需处理最大20000容量的动态序列。
数据结构与算法设计:
-
双队列+时间戳范式:
-
独立猫狗队列:
-
cat_queue
存储猫的入队记录[id, timestamp]
-
dog_queue
存储狗的入队记录[id, timestamp]
-
-
全局时序计数器:每次入队递增,为动物分配唯一时序值,实现跨队列年龄比较。
-
-
操作详解:
-
enqueue(animal):
python
if animal[1] == 0: # 猫 cat_queue.append([animal[0], global_timestamp]) else: # 狗 dog_queue.append([animal[0], global_timestamp]) global_timestamp += 1
-
dequeueAny():
-
比较猫狗队列头部时序值,弹出时序更小者(即更早入队)。
-
-
dequeueDog()/dequeueCat():
-
直接弹出对应队列头部,空队列返回
[-1, -1]
。
-
-
-
时空复杂度:
-
时间:所有操作 O(1)O(1)(队列头部操作均摊常数时间)。
-
空间:O(n)O(n)(双队列存储动物记录)。
-
示例解析:
示例2操作序列: enqueue([0,0]) → 猫队列: [[0,0]] enqueue([1,0]) → 猫队列: [[0,0], [1,1]] enqueue([2,1]) → 狗队列: [[2,2]] dequeueDog() → 狗队列头部 [2,1] → 返回 [2,1] dequeueCat() → 猫队列头部 [0,0] → 返回 [0,0] dequeueAny() → 猫队头时序=1 vs 狗队空 → 弹出猫队 [1,0] → 返回 [1,0]
题目程序:
#include <stdio.h> // 标准输入输出函数库
#include <stdlib.h> // 动态内存管理函数库
#include <string.h> // 字符串操作函数库
// 定义动物节点结构体
typedef struct AnimalNode {
int id; // 动物编号
int species; // 动物种类 (0=猫, 1=狗)
int timestamp; // 入队时间戳
struct AnimalNode* next; // 指向下一个节点的指针
} AnimalNode;
// 定义动物队列结构体
typedef struct {
AnimalNode* head; // 队列头指针
AnimalNode* tail; // 队列尾指针
} AnimalQueue;
// 定义动物收容所结构体
typedef struct {
AnimalQueue* catQueue; // 猫队列
AnimalQueue* dogQueue; // 狗队列
int timestamp; // 全局时间戳计数器
} AnimalShelter;
// 函数声明
AnimalShelter* createAnimalShelter();
void enqueue(AnimalShelter* shelter, int id, int species);
int* dequeueAny(AnimalShelter* shelter);
int* dequeueDog(AnimalShelter* shelter);
int* dequeueCat(AnimalShelter* shelter);
void freeAnimalShelter(AnimalShelter* shelter);
// 主函数:程序入口
int main() {
// 创建动物收容所实例
AnimalShelter* shelter = createAnimalShelter();
// 检查内存分配是否成功
if (shelter == NULL) {
fprintf(stderr, "内存分配失败\n"); // 输出错误信息
return 1; // 异常退出
}
// 示例操作序列
printf("操作序列:\n");
printf("1. enqueue([0,0])\n");
printf("2. enqueue([1,0])\n");
printf("3. enqueue([2,1])\n");
printf("4. dequeueDog()\n");
printf("5. dequeueCat()\n");
printf("6. dequeueAny()\n");
// 执行示例操作
enqueue(shelter, 0, 0); // 入队猫0
enqueue(shelter, 1, 0); // 入队猫1
enqueue(shelter, 2, 1); // 入队狗2
// 执行出队操作并打印结果
int* result;
result = dequeueDog(shelter); // 出队狗
printf("dequeueDog() -> [%d, %d]\n", result[0], result[1]);
free(result); // 释放结果数组
result = dequeueCat(shelter); // 出队猫
printf("dequeueCat() -> [%d, %d]\n", result[0], result[1]);
free(result); // 释放结果数组
result = dequeueAny(shelter); // 出队任意
printf("dequeueAny() -> [%d, %d]\n", result[0], result[1]);
free(result); // 释放结果数组
// 释放收容所内存
freeAnimalShelter(shelter);
return 0; // 程序正常退出
}
// 创建动物收容所
AnimalShelter* createAnimalShelter() {
// 分配收容所内存
AnimalShelter* shelter = (AnimalShelter*)malloc(sizeof(AnimalShelter));
if (shelter == NULL) return NULL; // 内存分配失败
// 分配猫队列内存
shelter->catQueue = (AnimalQueue*)malloc(sizeof(AnimalQueue));
if (shelter->catQueue == NULL) {
free(shelter); // 释放已分配内存
return NULL; // 内存分配失败
}
// 初始化猫队列
shelter->catQueue->head = NULL;
shelter->catQueue->tail = NULL;
// 分配狗队列内存
shelter->dogQueue = (AnimalQueue*)malloc(sizeof(AnimalQueue));
if (shelter->dogQueue == NULL) {
free(shelter->catQueue); // 释放已分配内存
free(shelter); // 释放已分配内存
return NULL; // 内存分配失败
}
// 初始化狗队列
shelter->dogQueue->head = NULL;
shelter->dogQueue->tail = NULL;
// 初始化时间戳
shelter->timestamp = 0;
return shelter; // 返回创建的收容所
}
// 动物入队操作
void enqueue(AnimalShelter* shelter, int id, int species) {
// 创建新动物节点
AnimalNode* newAnimal = (AnimalNode*)malloc(sizeof(AnimalNode));
if (newAnimal == NULL) return; // 内存分配失败
// 设置动物属性
newAnimal->id = id;
newAnimal->species = species;
newAnimal->timestamp = shelter->timestamp; // 分配当前时间戳
newAnimal->next = NULL; // 新节点在队尾,next置空
// 更新全局时间戳
shelter->timestamp++;
// 根据动物种类选择队列
AnimalQueue* targetQueue;
if (species == 0) { // 猫
targetQueue = shelter->catQueue;
} else { // 狗
targetQueue = shelter->dogQueue;
}
// 将动物加入队列
if (targetQueue->tail == NULL) { // 队列为空
targetQueue->head = newAnimal;
targetQueue->tail = newAnimal;
} else { // 队列非空
targetQueue->tail->next = newAnimal; // 当前尾节点指向新节点
targetQueue->tail = newAnimal; // 更新尾指针
}
}
// 出队任意动物
int* dequeueAny(AnimalShelter* shelter) {
// 分配结果数组内存 (2个整数)
int* result = (int*)malloc(2 * sizeof(int));
if (result == NULL) return NULL; // 内存分配失败
// 检查猫队列和狗队列是否都为空
if (shelter->catQueue->head == NULL && shelter->dogQueue->head == NULL) {
result[0] = -1; // 返回-1表示无动物
result[1] = -1;
return result;
}
// 比较两个队列头部的时间戳
int catTime = (shelter->catQueue->head != NULL) ?
shelter->catQueue->head->timestamp : __INT_MAX__; // 猫队列时间戳
int dogTime = (shelter->dogQueue->head != NULL) ?
shelter->dogQueue->head->timestamp : __INT_MAX__; // 狗队列时间戳
// 选择时间戳更小的动物出队(更早入队)
if (catTime <= dogTime) {
return dequeueCat(shelter); // 出队猫
} else {
return dequeueDog(shelter); // 出队狗
}
}
// 出队狗
int* dequeueDog(AnimalShelter* shelter) {
// 分配结果数组内存 (2个整数)
int* result = (int*)malloc(2 * sizeof(int));
if (result == NULL) return NULL; // 内存分配失败
// 检查狗队列是否为空
if (shelter->dogQueue->head == NULL) {
result[0] = -1; // 返回-1表示无狗
result[1] = -1;
return result;
}
// 获取队列头部狗节点
AnimalNode* dog = shelter->dogQueue->head;
// 设置返回结果
result[0] = dog->id;
result[1] = dog->species;
// 更新队列头指针
shelter->dogQueue->head = dog->next;
// 如果队列变为空,更新尾指针
if (shelter->dogQueue->head == NULL) {
shelter->dogQueue->tail = NULL;
}
// 释放出队节点内存
free(dog);
return result; // 返回结果
}
// 出队猫
int* dequeueCat(AnimalShelter* shelter) {
// 分配结果数组内存 (2个整数)
int* result = (int*)malloc(2 * sizeof(int));
if (result == NULL) return NULL; // 内存分配失败
// 检查猫队列是否为空
if (shelter->catQueue->head == NULL) {
result[0] = -1; // 返回-1表示无猫
result[1] = -1;
return result;
}
// 获取队列头部猫节点
AnimalNode* cat = shelter->catQueue->head;
// 设置返回结果
result[0] = cat->id;
result[1] = cat->species;
// 更新队列头指针
shelter->catQueue->head = cat->next;
// 如果队列变为空,更新尾指针
if (shelter->catQueue->head == NULL) {
shelter->catQueue->tail = NULL;
}
// 释放出队节点内存
free(cat);
return result; // 返回结果
}
// 释放动物收容所内存
void freeAnimalShelter(AnimalShelter* shelter) {
// 释放猫队列中的剩余节点
AnimalNode* current = shelter->catQueue->head;
while (current != NULL) {
AnimalNode* next = current->next; // 保存下一个节点
free(current); // 释放当前节点
current = next; // 移动到下一个节点
}
// 释放狗队列中的剩余节点
current = shelter->dogQueue->head;
while (current != NULL) {
AnimalNode* next = current->next; // 保存下一个节点
free(current); // 释放当前节点
current = next; // 移动到下一个节点
}
// 释放队列结构体
free(shelter->catQueue);
free(shelter->dogQueue);
// 释放收容所结构体
free(shelter);
}
输出结果:
第三章:双星对比——搜索与秩序的算法哲学
对比图表:问题本质与解法核心
维度 | 节点间通路(图搜索) | 动物收容所(队列管理) |
---|---|---|
问题类型 | 连通性判定(图论) | 有序资源调度(数据结构) |
核心操作 | 路径搜索(BFS/DFS) | 入队/出队(FIFO策略) |
数据结构 | 邻接表(稀疏图高效存储) | 双队列+时间戳(时序维护) |
时间复杂度 | O(n+e)O(n+e)(遍历节点与边) | O(1)O(1)(队列操作) |
空间复杂度 | O(n+e)O(n+e)(邻接表+访问标记) | O(n)O(n)(双队列存储) |
关键挑战 | 自环/重边处理、大规模图优化 | 跨队列时序比较、空队列处理 |
算法思想 | 状态空间探索(穷举可能路径) | 秩序维护(严格时序优先级) |
本质联系与分岔:
-
共性:二者均依赖高效的数据组织(邻接表 vs 双队列)与状态管理(
visited
数组 vs 时间戳)。 -
分岔:
-
图搜索是横向扩展(BFS的层序遍历)或纵向深入(DFS的路径回溯),目标是覆盖可能性。
-
队列管理是时序维护,通过时间戳将多维选择(猫/狗/任意)压缩为一维时序比较,实现决策简化。
-
第四章:思维升华——从算法到系统设计
-
图搜索的工程映射:
-
社交网络好友关系(节点=用户,边=关注),判断用户A是否间接关注用户B(路径存在性)。
-
优化启示:大规模图需分布式BFS(如Pregel模型)。
-
-
队列管理的现实镜像:
-
多优先级任务调度(如操作系统进程队列),
dequeueAny
对应选择最早提交的任务。 -
扩展思考:若增加动物优先级(如伤病紧急度),可引入优先队列替代FIFO队列。
-
节点间通路,是图论中一曲穿越迷宫的探戈;动物收容所,是队列间一首秩序井然的交响诗。算法之美,正在于将混沌抽象为模型,用数学谱写秩序。当我们以BFS之刃劈开图论荆棘,以双队列之梭编织时序经纬,便是在问题与解的交界处——雕刻出计算机科学的理性丰碑。
后记:每一个算法问题,都是对现实世界的一次隐喻。下一次当你迷失在生活的迷宫中,不妨想想BFS的勇气;当你面对纷杂选择时,请回忆时间戳的公正。这便是算法赠予我们的智慧箴言。