算法设计:实验五分枝限界法

【实验目的】

应用分枝限界法的算法设计思想求解单源最短路径问题。

【实验内容与要求】

采用分支限界法编程求源点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)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值