【实验目的】
应用分枝限界法的算法设计思想求解单源最短路径问题。
【实验内容与要求】
采用分支限界法编程求源点0到终点6的最短路径及其路径长度。
要求完成:⑴算法描述⑵写出程序代码⑶完成调试⑷进行过程与结果分析。
【实验性质】
在完成的过程中注意与回溯算法思想的比较,重点注意两种算法思想各自的特点以及实现方式比较。此实验的性质为综合性实验。
【算法思想及处理过程】
这段代码实现了一个基于分枝限界法的单源最短路径算法,用于求解给定图中从源点到所有其他节点的最短路径。代码使用了邻接链表来表示图,并使用优先队列来管理待处理的节点。
首先,定义了三个结构体:Edge用于表示图中的边,Node用于表示图中的节点,Element用于表示优先队列中的元素。结构体PriorityQueue则定义了一个优先队列,包含元素数组、当前大小和容量。
在初始化优先队列时,分配内存并设置初始值。swap函数用于交换优先队列中的两个元素,push函数用于向优先队列中插入元素,并确保堆的结构满足最小堆性质,即最小距离的元素在根位置。pop函数则从优先队列中弹出根元素,并调整堆结构。
初始化图时,分配内存并将每个节点的边链表头指针设为空。addEdge函数用于向图中添加边,即将边插入源节点对应的边链表中。
分枝限界法的核心函数branchAndBoundSSSP初始化所有节点的距离为无穷大,前驱节点为-1,并将源点距离设为0。接着初始化优先队列,将源点加入队列。然后在优先队列不为空时,弹出最小距离的节点,遍历其所有邻接节点,更新邻接节点的距离和前驱节点,并将其加入优先队列。
printPath函数用于递归地打印从源点到目标节点的路径,通过前驱节点数组追踪路径。
在主函数中,首先读取顶点数并初始化图。然后通过读取邻接矩阵来添加边。源点默认设为0,分配内存用于存储距离和前驱节点数组,调用branchAndBoundSSSP计算最短路径。最后,打印从源点到每个节点的距离和前驱节点,并打印从源点到最后一个节点的最小路径和距离。
由于C语言没有内置队列,以下是关于队列的初始化、加入、删除:
初始化队列:
此函数分配PriorityQueue结构体的内存,并分配用于存储优先队列元素的数组,设置初始大小为0,容量为输入的参数capacity。
加入队列:
此函数首先检查队列是否已满,如果已满则打印提示并返回。否则,将新元素添加到队列末尾,增加队列大小。然后,通过上浮操作调整堆的结构,确保最小距离的元素在根位置。上浮操作通过比较当前节点与其父节点的距离,如果当前节点的距离较小,则交换两者并继续向上比较。
从队列中删除:
此函数首先检查队列是否为空,如果为空则打印提示并退出。否则,将根元素保存为root,然后将队列末尾的元素移到根位置,并减少队列大小。通过下沉操作调整堆的结构,确保最小距离的元素在根位置。下沉操作通过比较当前节点与其子节点的距离,选择距离最小的子节点进行交换,直到当前节点的距离小于或等于所有子节点的距离,或没有子节点为止。
本程序的核心是如何使用分支限界法来实现最短路径长度,以下是对此函数的分析:
1. 初始化
距离数组distances:初始化所有节点的距离为无穷大(INT_MAX),表示初始时所有节点都不可达。
前驱节点数组predecessors:初始化所有节点的前驱节点为-1,表示初始时所有节点的前驱节点未知。
源点距离:将源点的距离设置为0,表示源点到自身的距离为0。
2. 优先队列初始化
优先队列pq:创建一个容量为图中顶点数的优先队列。
插入源点:将源点及其距离(0)插入优先队列。
3. 主循环
当优先队列不为空:循环处理优先队列中的元素。
弹出元素:从优先队列中弹出距离最小的元素current,获取当前顶点u。
遍历邻接节点:遍历当前顶点u的所有邻接节点v。
计算新距离:计算通过当前顶点u到达邻接节点v的新距离distances[u] + edge->weight。
更新距离和前驱节点:如果新距离小于当前存储的distances[v],则更新distances[v]为新距离,并设置predecessors[v]为当前顶点u。
插入优先队列:将更新后的邻接节点v及其距离插入优先队列。
4. 完成计算
释放优先队列内存:处理完所有节点后,释放优先队列的内存。
分枝限界法和回溯算法虽然都是解决组合优化问题的有效方法,但它们在思路和实现上有显著的不同。分枝限界法主要用于优化问题,目标是找到问题的最优解。它通过构建一个解空间树并利用界限值(bound)来估计当前解的最优值,从而剪枝减少搜索空间。分枝限界法通常使用优先队列存储待处理的节点,以优先搜索当前最优的分支,并动态更新当前最优解和界限值。在每一步中,通过计算当前节点的界限值,如果界限值超出当前最优解,则剪掉这个分支,以此提高搜索效率。
相比之下,回溯算法主要用于搜索和遍历所有可能的解空间,如组合、排列和子集问题。它采用试探法逐步构建解,在每一步尝试所有可能的选择,并在遇到不符合条件的部分解时回溯(撤销最近的一步选择),然后尝试其他选择。回溯算法通常采用递归实现,递归地选择每一步的解,并在每一步检查当前部分解是否满足条件。虽然回溯算法也有剪枝策略,但其剪枝策略主要通过条件检查提前终止不符合条件的搜索分支。
在实现方式上,分枝限界法通过优先队列管理待处理的节点,确保每次处理的是当前已知距离最小的节点。回溯算法则通常采用深度优先搜索(DFS),递归地遍历所有可能的分支,遇到不合适的分支则回溯。分枝限界法适用于需要寻找最优解的问题,如单源最短路径问题、旅行商问题(TSP)和0/1背包问题。回溯算法适用于需要搜索和遍历所有可能解的问题,如八皇后问题、数独解题、图的着色问题和子集和问题。
总结来说,分枝限界法和回溯算法在处理不同类型问题时各有优势。分枝限界法通过优先选择最优分支和剪枝策略来减少搜索空间,提高效率,适用于优化问题。回溯算法通过逐步构建解和回溯策略来遍历所有可能的解空间,适用于搜索和遍历问题。选择哪种算法取决于具体问题的性质和要求。
【程序代码】
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
// 定义一个结构体来表示边
typedef struct Edge {
int dest; // 目标节点
int weight; // 边的权重
struct Edge* next; // 指向下一条边的指针
} Edge;
// 定义一个结构体来表示图中的节点
typedef struct {
Edge* head; // 指向边链表的头指针
} Node;
// 定义一个优先队列中的元素
typedef struct {
int vertex; // 顶点编号
int distance; // 从源点到该顶点的距离
} Element;
// 定义一个优先队列
typedef struct {
Element* elements; // 元素数组,存储优先队列中的所有元素
int size; // 当前优先队列中的元素个数
int capacity; // 优先队列的最大容量
} PriorityQueue;
// 初始化优先队列
PriorityQueue* initPriorityQueue(int capacity) {
PriorityQueue* pq = (PriorityQueue*)malloc(sizeof(PriorityQueue));
pq->elements = (Element*)malloc(sizeof(Element) * capacity);
pq->size = 0;
pq->capacity = capacity;
return pq;
}
// 交换优先队列中的两个元素
void swap(Element* a, Element* b) {
Element temp = *a;
*a = *b;
*b = temp;
}
// 向优先队列中插入一个元素
void push(PriorityQueue* pq, int vertex, int distance) {
if (pq->size == pq->capacity) {
printf("优先队列已满\n");
return;
}
pq->elements[pq->size].vertex = vertex;
pq->elements[pq->size].distance = distance;
int i = pq->size++;
// 调整堆的结构,确保最小距离的元素在根位置
while (i && pq->elements[i].distance < pq->elements[(i - 1) / 2].distance) {
swap(&pq->elements[i], &pq->elements[(i - 1) / 2]);
i = (i - 1) / 2;
}
}
// 从优先队列中弹出一个元素
Element pop(PriorityQueue* pq) {
if (pq->size == 0) {
printf("优先队列为空\n");
exit(1);
}
Element root = pq->elements[0];
pq->elements[0] = pq->elements[--pq->size];
int i = 0;
// 调整堆的结构,确保最小距离的元素在根位置
while (2 * i + 1 < pq->size) {
int left = 2 * i + 1;
int right = 2 * i + 2;
int smallest = i;
if (left < pq->size && pq->elements[left].distance < pq->elements[smallest].distance) {
smallest = left;
}
if (right < pq->size && pq->elements[right].distance < pq->elements[smallest].distance) {
smallest = right;
}
if (smallest == i) {
break;
}
swap(&pq->elements[i], &pq->elements[smallest]);
i = smallest;
}
return root;
}
// 初始化图
Node* initGraph(int vertices) {
Node* graph = (Node*)malloc(vertices * sizeof(Node));
for (int i = 0; i < vertices; i++) {
graph[i].head = NULL;
}
return graph;
}
// 向图中添加一条边
void addEdge(Node* graph, int src, int dest, int weight) {
Edge* newEdge = (Edge*)malloc(sizeof(Edge));
newEdge->dest = dest;
newEdge->weight = weight;
newEdge->next = graph[src].head;
graph[src].head = newEdge;
}
// 分枝限界法求解单源最短路径
void branchAndBoundSSSP(Node* graph, int vertices, int source, int* distances, int* predecessors) {
// 初始化所有节点的距离为无穷大,前驱节点为-1
for (int i = 0; i < vertices; i++) {
distances[i] = INT_MAX;
predecessors[i] = -1;
}
distances[source] = 0; // 源点到源点的距离为0
// 初始化优先队列
PriorityQueue* pq = initPriorityQueue(vertices);
push(pq, source, 0);
// 当优先队列不为空时,继续处理
while (pq->size > 0) {
Element current = pop(pq);
int u = current.vertex;
// 遍历所有与当前节点相邻的节点
Edge* edge = graph[u].head;
while (edge != NULL) {
int v = edge->dest;
int weight = edge->weight;
// 更新距离和前驱节点,如果找到更短的路径
if (distances[u] + weight < distances[v]) {
distances[v] = distances[u] + weight;
predecessors[v] = u;
push(pq, v, distances[v]);
}
edge = edge->next;
}
}
free(pq->elements);
free(pq);
}
// 打印从源点到目标节点的路径
void printPath(int* predecessors, int vertex) {
if (vertex == -1) {
return;
}
printPath(predecessors, predecessors[vertex]);
printf("%d ", vertex);
}
// 主函数
int main() {
int vertices;
printf("请输入顶点数: ");
scanf("%d", &vertices);
Node* graph = initGraph(vertices);
printf("请输入邻接矩阵:\n");
for (int i = 0; i < vertices; i++) {
for (int j = 0; j < vertices; j++) {
int weight;
scanf("%d", &weight);
if (weight != 0 && i != j) {
addEdge(graph, i, j, weight);
}
}
}
int source = 0; // 默认源点为0
int* distances = (int*)malloc(vertices * sizeof(int));//距离
int* predecessors = (int*)malloc(vertices * sizeof(int));//前节点
branchAndBoundSSSP(graph, vertices, source, distances, predecessors);
printf("顶点\t源点的距离\t前一个顶点\n");
for (int i = 0; i < vertices; i++) {
if (distances[i] == INT_MAX) {
printf("%d\t无路径\t\t-\n", i);
} else {
printf("%d\t%d\t\t%d\n", i, distances[i], predecessors[i]);
}
}
printf("\n从源点到最后一个节点的最小路径和距离:\n");
printf("路径: ");
printPath(predecessors, vertices - 1);
printf("\n距离: %d\n", distances[vertices - 1]);
for (int i = 0; i < vertices; i++) {
Edge* edge = graph[i].head;
while (edge != NULL) {
Edge* temp = edge;
edge = edge->next;
free(temp);
}
}
free(graph);
free(distances);
free(predecessors);
return 0;
}
【运行结果】
【算法分析】
branchAndBoundSSSP 函数:
在该函数中,使用了一个优先队列来管理待处理的节点。
主要工作是遍历图中的节点和边,更新节点的距离和前驱节点,并将更新后的节点插入优先队列。
时间复杂度:取决于优先队列的操作和图的规模。
push 函数:
在 push 函数中,需要进行插入操作,并在需要时进行堆调整。
堆调整的时间复杂度为 O(log n),其中 n 是优先队列中的元素个数。
时间复杂度:O(log n)。
pop 函数:
在 pop 函数中,需要进行弹出操作,并在需要时进行堆调整。
堆调整的时间复杂度为 O(log n),其中 n 是优先队列中的元素个数。
时间复杂度:O(log n)。
整体:
节点的数量为 V,边的数量为 E,优先队列的最大容量为 V。
在最坏情况下,每个节点都需要被处理,每个节点都需要入队和出队,因此总的时间复杂度为 O(V log V)。