算法学习--贪心算法

任务选择问题

贪心算法又称贪婪算法是指,在对问题求解时,总是做出在当前看来是最好的选择。

当一个问题具有以下的性质时可以用贪心算法求解:每一步的局部最优解,同事也说整个问题的最优解。

如果一个问题可以用贪心算法解决,那么贪心通常是解决这个问题的最好的方法。 贪婪算法一般比其他方法例如动态规划更有效。但是贪婪算法不能总是被应用。例如,部分背包问题可以使用贪心解决,但是不能解决0 – 1背包问题。

下面是使用贪心算法的几个经典算法:

1) Kruskal最小生成树(MST):在Kruskal算法中,我们通过逐个的选取最优边来获得一个MST。每次选择最小权重并且不构成环的边。

2)Prim小生成树算法:在prim算法中,我们也是逐个的选取最优边来获得一个MST。我们维持两组:已经包含在MST的顶点和顶点的集合不包括在内的。贪婪的选择是选择最小的重量边缘连接两组。

3)Dijkstra 最短路径: 迪杰斯特拉算法和prim算法非常相似的。从边缘最短的开始选择。

4) Huffman 编码: 霍夫曼编码是一种无损压缩技术。它分配可变长度编码不同的字符。贪婪的选择是分配一点代码最常见的字符长度。

贪婪算法有时也用用来得到一个近似优化问题。例如,旅行商问题是一个NP难问题。贪婪选择这个问题是选择最近的并且从当前城市每一步。这个解决方案并不总是产生最好的最优解,但可以用来得到一个近似最优解。

让我们考虑一下任务选择的贪婪算法的问题, 作为我们的第一个例子。问题:

给出n个任务和每个任务的开始和结束时间。找出可以完成的任务的最大数量,在同一时刻只能做一个任务

例子:

下面的6个任务:
     start[]  =  {1, 3, 0, 5, 8, 5};
     finish[] =  {2, 4, 6, 7, 9, 9};
最多可完成的任务是:
 {0, 1, 3, 4}

贪婪的选择是总是选择下一个任务的完成时间至少在剩下的任务和开始时间大于或等于以前选择任务的完成时间。我们可以根据他们的任务完成时间,以便我们总是认为下一个任务是最小完成时间的任务。

1)按照完成时间对任务排序

2)选择第一个任务排序数组元素和打印。

3) 继续以下剩余的任务排序数组。

……a)如果这一任务的开始时间大于先前选择任务的完成时间然后选择这个任务和打印。

在接下来的C程序,假设已经根据任务的结束时间排序。

#include<stdio.h>
// 打印可以完成的最大数量的任务
//  n   -->  所有任务的数量
//  s[] -->  开始时间
//  f[] -->  结束时间
void printMaxActivities(int s[], int f[], int n)
{
    int i, j;
    printf ("Following activities are selected \n");
    // 选择第一个任务
    i = 0;
    printf("%d ", i);
    //考虑剩下的任务
    for (j = 1; j < n; j++)
    {
      // 如果当前的任务开始比 前一个选择的任务结束时间大或相等,就选择它
      if (s[j] >= f[i])
      {
          printf ("%d ", j);
          i = j;
      }
    }
}

// driver program to test above function
int main()
{
    int s[] =  {1, 3, 0, 5, 8, 5};
    int f[] =  {2, 4, 6, 7, 9, 9};
    int n = sizeof(s)/sizeof(s[0]);
    printMaxActivities(s, f, n);
    getchar();
    return 0;
}

Kruskal最小生成树

什么是最小生成树?

生成树是相对图来说的,一个图的生成树是一个树并把图的所有顶点连接在一起。一个图可以有许多不同的生成树。一个有 n 个结点的连通图的生成树是原图的极小连通子图,且包含原图中的所有 n 个结点,并且有保持图连通的最少的边。最小生成树其实是最小权重生成树的简称。生成树的权重是考虑到了生成树的每条边的权重的总和。

最小生成树有几条边?

最小生成树有(V – 1)条边,其中V是给定的图的顶点数量。

Kruskal算法

下面是步骤寻找MST使用Kruskal算法

1,按照所有边的权重排序(从小到大)

2,选择最小的边。检查它是否形成与当前生成树形成环。如果没有形成环,讲这条边加入生成树。否则,丢弃它。  

3,重复第2步,直到有生成树(V-1)条边


步骤2使用并查集算法来检测环。如果不熟悉并查集建议阅读下并查集

该算法是一种贪心算法。贪心的选择是选择最小的权重的边,并不会和当前的生成树形成环。

// Kruskal 最小生成树算法
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 带有权重的边
struct Edge
{
    int src, dest, weight;
};

// 无向图
struct Graph
{
    // V-> 顶点个数, E->边的个数
    int V, E;
    // 由于是无向图,从 src 到 dest的边,同时也是 dest到src的边,按一条边计算
    struct Edge* edge;
};

//构建一个V个顶点 E条边的图
struct Graph* createGraph(int V, int E)
{
    struct Graph* graph = (struct Graph*) malloc( sizeof(struct Graph) );
    graph->V = V;
    graph->E = E;
    graph->edge = (struct Edge*) malloc( graph->E * sizeof( struct Edge ) );
    return graph;
}

//并查集的结构体
struct subset
{
    int parent;
    int rank;
};

// 使用路径压缩查找元素i
int find(struct subset subsets[], int i)
{
    if (subsets[i].parent != i)
        subsets[i].parent = find(subsets, subsets[i].parent);

    return subsets[i].parent;
}

// 按秩合并 x,y
void Union(struct subset subsets[], int x, int y)
{
    int xroot = find(subsets, x);
    int yroot = find(subsets, y);
    if (subsets[xroot].rank < subsets[yroot].rank)
        subsets[xroot].parent = yroot;
    else if (subsets[xroot].rank > subsets[yroot].rank)
        subsets[yroot].parent = xroot;
    else
    {
        subsets[yroot].parent = xroot;
        subsets[xroot].rank++;
    }
}

// 很据权重比较两条边
int myComp(const void* a, const void* b)
{
    struct Edge* a1 = (struct Edge*)a;
    struct Edge* b1 = (struct Edge*)b;
    return a1->weight > b1->weight;
}

// Kruskal 算法
void KruskalMST(struct Graph* graph)
{
    int V = graph->V;
    struct Edge result[V];  //存储结果
    int e = 0;  //result[] 的index
    int i = 0;  // 已排序的边的 index

    //第一步排序
    qsort(graph->edge, graph->E, sizeof(graph->edge[0]), myComp);

    // 为并查集分配内存
    struct subset *subsets =
        (struct subset*) malloc( V * sizeof(struct subset) );

    // 初始化并查集
    for (int v = 0; v < V; ++v)
    {
        subsets[v].parent = v;
        subsets[v].rank = 0;
    }

    // 边的数量到V-1结束
    while (e < V - 1)
    {
        // Step 2: 先选最小权重的边
        struct Edge next_edge = graph->edge[i++];

        int x = find(subsets, next_edge.src);
        int y = find(subsets, next_edge.dest);

        // 如果此边不会引起环
        if (x != y)
        {
            result[e++] = next_edge;
            Union(subsets, x, y);
        }
        // 否则丢弃,继续
    }

    // 打印result[]
    printf("Following are the edges in the constructed MST\n");
    for (i = 0; i < e; ++i)
        printf("%d -- %d == %d\n", result[i].src, result[i].dest,
                                                   result[i].weight);
    return;
}

// 测试
int main()
{
    /* 创建下面的图:
             10
        0--------1
        |  \     |
       6|   5\   |15
        |      \ |
        2--------3
            4       */
    int V = 4;  // 顶点个数
    int E = 5;  //边的个数
    struct Graph* graph = createGraph(V, E);
    // 添加边 0-1
    graph->edge[0].src = 0;
    graph->edge[0].dest = 1;
    graph->edge[0].weight = 10;

    graph->edge[1].src = 0;
    graph->edge[1].dest = 2;
    graph->edge[1].weight = 6;

    graph->edge[2].src = 0;
    graph->edge[2].dest = 3;
    graph->edge[2].weight = 5;

    graph->edge[3].src = 1;
    graph->edge[3].dest = 3;
    graph->edge[3].weight = 15;

    graph->edge[4].src = 2;
    graph->edge[4].dest = 3;
    graph->edge[4].weight = 4;

    KruskalMST(graph);

    return 0;
}

时间复杂度:

O(ElogE) 或 O(ElogV)。 排序使用 O(ELogE) 的时间,之后我们遍历中使用并查集O(LogV) ,所以总共复杂度是 O(ELogE + ELogV)。E的值最多为V^2,所以

O(LogV) 和 O(LogE) 可以看做是一样的。

霍夫曼编码

霍夫曼编码是一种无损数据压缩算法。在计算机数据处理中,霍夫曼编码使用变长编码表对源符号(如文件中的一个字母)进行编码,其中变长编码表是通过一种评估来源符号出现机率的方法得到的,出现机率高的字母使用较短的编码,反之出现机率低的则使用较长的编码,这便使编码之后的字符串的平均长度、期望值降低,从而达到无损压缩数据的目的。例如,在英文中,e的出现机率最高,而z的出现概率则最低。当利用霍夫曼编码对一篇英文进行压缩时,e极有可能用一个比特来表示,而z则可能花去25个比特(不是26)。用普通的表示方法时,每个英文字母均占用一个字节(byte),即8个比特。二者相比,e使用了一般编码的1/8的长度,z则使用了3倍多。倘若我们能实现对于英文中各个字母出现概率的较准确的估算,就可以大幅度提高无损压缩的比例。

构建霍夫曼编码主要包括两个部分:

1)根据输入的字符串构建霍夫曼树。

2)便利霍夫曼数并给每个字符分配编码。

哈夫曼树(Huffman Tree),又叫最优二叉树,指的是对于一组具有确定权值的叶子结点的具有最小带权路径长度的二叉树。

(1)路劲(Path):从树中的一个结点到另一个结点之间的分支构成两个结点间的路径。
(2)路径长度(Path Length):路径上的分支树。
(3)树的路径长度(Path Length of Tree):从树的根结点到每个结点的路径长度之和。在结点数目相同的二叉树中,完全二叉树的路径长度最短。
(4)结点的权(Weight of  Node):在一些应用中,赋予树中结点的一个有实际意义的树。
(5)结点的带权路径长度(Weight Path Length of Node):从该结点到树的根结点的路径长度与该结点的权的乘积。
(6)树的带权路径长度(WPL):树中所有叶子结点的带权路径长度之和

构建霍夫曼树的步骤:

算法:输入是没有相同元素的字符数组(长度n)以及字符出现的频率,输出是哈夫曼树。

即假设有n个字符,则构造出得哈夫曼树有n个叶子结点。n个字符的权值(频率)分别设为w1,w2,…,wn,则哈夫曼树的构造规则为:

(1)将w1,w2,…,wn看成是有n棵树的森林(每棵树仅有一个结点);
(2)在森林中选出两个根结点的权值最小的树合并,作为一棵新树的左、右子树,且新树的根结点权值为其左、右子树根结点权值之和;
(3)从森林中删除选取的两棵树,并将新树加入森林;
(4)重复(2)、(3)步,直到森林中只剩一棵树为止,该树即为所求得的哈夫曼树。
#include <stdio.h>
#include <stdlib.h>

#define MAX_TREE_HT 100

// 一个霍夫曼树节点
struct MinHeapNode
{
    char data;  // 输入的字符数组中的一个字符
    unsigned freq;  // 字符出现的次数
    struct MinHeapNode *left, *right; 
};

// 最小堆: 作为优先队列使用
struct MinHeap
{
    unsigned size;    // 最小堆元素的个数
    unsigned capacity;   //最大容量
    struct MinHeapNode **array;  
};

//初始化一个最小堆节点
struct MinHeapNode* newNode(char data, unsigned freq)
{
    struct MinHeapNode* temp =
          (struct MinHeapNode*) malloc(sizeof(struct MinHeapNode));
    temp->left = temp->right = NULL;
    temp->data = data;
    temp->freq = freq;
    return temp;
}

// 创建一个容量为capacity 的最小堆
struct MinHeap* createMinHeap(unsigned capacity)
{
    struct MinHeap* minHeap =
         (struct MinHeap*) malloc(sizeof(struct MinHeap));
    minHeap->size = 0;  // current size is 0
    minHeap->capacity = capacity;
    minHeap->array =
     (struct MinHeapNode**)malloc(minHeap->capacity * sizeof(struct MinHeapNode*));
    return minHeap;
}

//  swap 两个堆节点
void swapMinHeapNode(struct MinHeapNode** a, struct MinHeapNode** b)
{
    struct MinHeapNode* t = *a;
    *a = *b;
    *b = t;
}

// 更新最小堆.
void minHeapify(struct MinHeap* minHeap, int idx)
{
    int smallest = idx;
    int left = 2 * idx + 1;
    int right = 2 * idx + 2;

    if (left < minHeap->size &&
        minHeap->array[left]->freq < minHeap->array[smallest]->freq)
      smallest = left;

    if (right < minHeap->size &&
        minHeap->array[right]->freq < minHeap->array[smallest]->freq)
      smallest = right;

    if (smallest != idx)
    {
        swapMinHeapNode(&minHeap->array[smallest], &minHeap->array[idx]);
        minHeapify(minHeap, smallest);
    }
}

//检测堆的大小是否为1
int isSizeOne(struct MinHeap* minHeap)
{
    return (minHeap->size == 1);
}

//取得堆中最小的节点
struct MinHeapNode* extractMin(struct MinHeap* minHeap)
{
    struct MinHeapNode* temp = minHeap->array[0];
    minHeap->array[0] = minHeap->array[minHeap->size - 1];
    --minHeap->size;
    minHeapify(minHeap, 0);
    return temp;
}

// 想最小堆中插入一个节点
void insertMinHeap(struct MinHeap* minHeap, struct MinHeapNode* minHeapNode)
{
    ++minHeap->size;
    int i = minHeap->size - 1;
    while (i && minHeapNode->freq < minHeap->array[(i - 1)/2]->freq)
    {
        minHeap->array[i] = minHeap->array[(i - 1)/2];
        i = (i - 1)/2;
    }
    minHeap->array[i] = minHeapNode;
}

//构建一个最小堆
void buildMinHeap(struct MinHeap* minHeap)
{
    int n = minHeap->size - 1;
    int i;
    for (i = (n - 1) / 2; i >= 0; --i)
        minHeapify(minHeap, i);
}

void printArr(int arr[], int n)
{
    int i;
    for (i = 0; i < n; ++i)
        printf("%d", arr[i]);
    printf("\n");
}

// 检测是否是叶子节点
int isLeaf(struct MinHeapNode* root)
{
    return !(root->left) && !(root->right) ;
}

// 创建一个容量为 size的最小堆,并插入 data[] 中的元素到最小堆
struct MinHeap* createAndBuildMinHeap(char data[], int freq[], int size)
{
    struct MinHeap* minHeap = createMinHeap(size);
    for (int i = 0; i < size; ++i)
        minHeap->array[i] = newNode(data[i], freq[i]);
    minHeap->size = size;
    buildMinHeap(minHeap);
    return minHeap;
}

// 构建霍夫曼树
struct MinHeapNode* buildHuffmanTree(char data[], int freq[], int size)
{
    struct MinHeapNode *left, *right, *top;

    // 第 1步 : 创建最小堆.  
    struct MinHeap* minHeap = createAndBuildMinHeap(data, freq, size);

    //知道最小堆只有一个元素
    while (!isSizeOne(minHeap))
    {
        // 第二步: 取到最小的两个元素
        left = extractMin(minHeap);
        right = extractMin(minHeap);

        // Step 3: 根据两个最小的节点,来创建一个新的内部节点
        // '$' 只是对内部节点的一个特殊标记,没有使用
        top = newNode('$', left->freq + right->freq);
        top->left = left;
        top->right = right;
        insertMinHeap(minHeap, top);
    }

    // 第4步: 最后剩下的一个节点即为跟节点
    return extractMin(minHeap);
}

// 打印霍夫曼编码
void printCodes(struct MinHeapNode* root, int arr[], int top)
{
    if (root->left)
    {
        arr[top] = 0;
        printCodes(root->left, arr, top + 1);
    }

    if (root->right)
    {
        arr[top] = 1;
        printCodes(root->right, arr, top + 1);
    }

    // 如果是叶子节点就打印
    if (isLeaf(root))
    {
        printf("%c: ", root->data);
        printArr(arr, top);
    }
}

// 构建霍夫曼树,并遍历打印该霍夫曼树
void HuffmanCodes(char data[], int freq[], int size)
{
   //  构建霍夫曼树
   struct MinHeapNode* root = buildHuffmanTree(data, freq, size);

   // 打印构建好的霍夫曼树
   int arr[MAX_TREE_HT], top = 0;
   printCodes(root, arr, top);
}

// 测试
int main()
{
    char arr[] = {'a', 'b', 'c', 'd', 'e', 'f'};
    int freq[] = {5, 9, 12, 13, 16, 45};
    int size = sizeof(arr)/sizeof(arr[0]);
    HuffmanCodes(arr, freq, size);
    return 0;
}

一个连通图的生成树是一个极小的连通子图,它含有图中全部的顶点,但只有足以构成一棵树的n-1条边。所谓的最小成本,就是n个顶点,用n-1条边把一个连通图连接起来,并且使得权值的和最小。综合以上两个概念,我们可以得出:构造连通网的最小代价生成树,即最小生成树(Minimum Cost Spanning Tree)。找连通图的最小生成树,经典的有两种算法,普里姆算法和克鲁斯卡尔算法,这里介绍普里姆算法。在前面一讲Kruskal最小生成树 中已经介绍了最小生成树的算法。和Kruskal算法类似,Prim算法也是利用贪心算法来解决最小生成树。

最小生成树MST性质:假设N=(V,{E})是一个连通网,U是顶点集V的一个非空子集。若(u,v)是一条具有最小权值(代价)的边,

其中u∈U,v∈V-U,则必存在一颗包含边(u,v)的最小生成树。

prim算法过程为:

假设N=(V,{E})是连通图,TE是N上最小生成树中边的集合。算法从U={u0}(u0∈V),TE={}开始,

重复执行下述操作:

在所有u∈U,v∈V-U的边(u,v)∈E中找一条代价最小的边(u0,v0)并入集合TE,同时v0 并入U,直至U=V为止。

此时TE中必有n-1条边,则T=(V,{TE})为N的最小生成树。

1) 创建一个集合mstSet记录已经包含在MST中的顶点
2)对图中的所有顶点设置一个key值,代表代价,并初始化无穷大。第一个点设置为0,以便总是能第一个取到第一个点
3) While( mstSet没有包含所有的顶点 )
     a) 从mstSet集合中剩下的顶点中,选取一个最小key的顶点u
     b) 把u加入到mstSet
     c) 更新所有的和u相连的那些顶点的key值。

#include <stdio.h>
#include <limits.h>

//图中顶点个数
#define V 5

//未在mstSet中的点的集合中,找出最小key的点
int minKey(int key[], bool mstSet[])
{
   int min = INT_MAX, min_index;

   for (int v = 0; v < V; v++)
     if (mstSet[v] == false && key[v] < min)
         min = key[v], min_index = v;

   return min_index;
}

// 打印MST
int printMST(int parent[], int n, int graph[V][V])
{
   printf("Edge   Weight\n");
   for (int i = 1; i < V; i++)
      printf("%d - %d    %d \n", parent[i], i, graph[i][parent[i]]);
}

// Prim算法
void primMST(int graph[V][V])
{
     int parent[V]; // 保持MST信息
     int key[V];   // 所有顶点的代价值
     bool mstSet[V];  //当前包含在MST中点的集合

     // 初始为无穷大
     for (int i = 0; i < V; i++)
        key[i] = INT_MAX, mstSet[i] = false;

     key[0] = 0;     //
     parent[0] = -1; // 第一个作为树的根。

     //  MST 有V的顶点
     for (int count = 0; count < V-1; count++)
     {
        int u = minKey(key, mstSet);
        // 添加u到 MST Set
        mstSet[u] = true;
        //更新和u相连的顶点的代价
        for (int v = 0; v < V; v++)
          if (graph[u][v] && mstSet[v] == false && graph[u][v] <  key[v])
             parent[v]  = u, key[v] = graph[u][v];
     }

     // 打印生成的MST
     printMST(parent, V, graph);
}

int main()
{
   /* 创建以下的图
          2    3
      (0)--(1)--(2)
       |   / \   |
      6| 8/   \5 |7
       | /     \ |
      (3)-------(4)
            9          */
   int graph[V][V] = {{0, 2, 0, 6, 0},
                      {2, 0, 3, 8, 5},
                      {0, 3, 0, 0, 7},
                      {6, 8, 0, 0, 9},
                      {0, 5, 7, 9, 0},
                     };

    // Print the solution
    primMST(graph);

    return 0;
}

时间复杂度:O(V^2).  如果使用 链接表存储的方式并使用堆,复杂度可以为 O(E log V) ,后面会讨论这个算法。

Dijkstra最短路径算法


源最短路径问题
给定一个带权有向图 G=(V,E) ,其中每条边的权是一个非负实数。另外,还给定 V 中的一个顶点,称为源。现在我们要计算从源到所有其他各顶点的最短路径长度。这里的长度是指路上各边权之和。这个问题通常称为单源最短路径问题。

前面Bellman-Ford最短路径算法讲了单源最短路径的Bellman-Ford算法(动态规划算法)。这里介绍另外一个更常见的算法Dijkstra算法。

Dijkstra算法和 最小生成树Prim算法最小生成树算法非常类似,大家可以先熟悉下个算法。两个算法都是基于贪心算法。虽然Dijkstra算法相对来说比Bellman-Ford 算法更快,但是不适用于有负权值边的图,贪心算法决定了它的目光短浅。而Bellman-Ford 算法从全局考虑,可以检测到有负权值的回路。

这里模仿MST(Minimum Spanning Tree)的Prim算法,我们创建一个SPT(最短路径树),最初只包含源点。我们维护两个集合,一组已经包含在SPT(最短路径树)中的顶点S集合,另一组是未包含在SPT内的顶点T集合。每次从T集合中选择到S集合路径最短的那个点,并加入到集合S中,并把这个点从集合T删除。直到T集合为空为止。

#include <iostream>
#include <stdio.h>
#include <limits.h>
using namespace std;
const int V = 9; //定义顶点个数

//从未包含在SPT的集合T中,选取一个到S集合的最短距离的顶点。
int getMinIndex(int dist[V], bool sptSet[V]) {
	   int min = INT_MAX, min_index;
	   for (int v = 0; v < V; v++)
	     if (sptSet[v] == false && dist[v] < min)
	         min = dist[v], min_index = v;
	   return min_index;
}

// 打印结果
void printSolution(int dist[], int n) {
	printf("Vertex   Distance from Source\n");
	for (int i = 0; i < V; i++)
		printf("%d \t\t %d\n", i, dist[i]);
}

//source 代表源点
void dijkstra(int graph[V][V], int source) {
	int dist[V];     // 存储结果,从源点到 i的距离

	bool sptSet[V]; // sptSet[i]=true 如果顶点i包含在SPT中

	// 初始化. 0代表不可达
	for (int i = 0; i < V; i++){
		dist[i] = (graph[source][i] == 0 ? INT_MAX:graph[source][i]);
		sptSet[i] = false;
	}

	// 源点,距离总是为0. 并加入SPT
	dist[source] = 0;
	sptSet[source] = true;

	// 迭代V-1次,因此不用计算源点了,还剩下V-1个需要计算的顶点。
	for (int count = 0; count < V - 1; count++) {
		// u,是T集合中,到S集合距离最小的点
		int u = getMinIndex(dist, sptSet);

		// 加入SPT中
		sptSet[u] = true;

		//更新到V的距离。可以理解为Bellman-Ford中的松弛操作
		for (int v = 0; v < V; v++)
			if (!sptSet[v] && graph[u][v] && dist[u] != INT_MAX
					&& dist[u] + graph[u][v] < dist[v])
				dist[v] = dist[u] + graph[u][v];
	}

	printSolution(dist, V);
}

int main() {
	/* 以例子中的图为例 */
	int graph[V][V] =
			{ { 0, 4, 0, 0, 0, 0, 0, 8, 0 }, { 4, 0, 8, 0, 0, 0, 0, 11, 0 }, {
					0, 8, 0, 7, 0, 4, 0, 0, 2 }, { 0, 0, 7, 0, 9, 14, 0, 0, 0 },
					{ 0, 0, 0, 9, 0, 10, 0, 0, 0 },
					{ 0, 0, 4, 0, 10, 0, 2, 0, 0 },
					{ 0, 0, 0, 14, 0, 2, 0, 1, 6 },
					{ 8, 11, 0, 0, 0, 0, 1, 0, 7 },
					{ 0, 0, 2, 0, 0, 0, 6, 7, 0 } };

	dijkstra(graph, 0);

	return 0;
}

Bellman-Ford最短路径算法

单源最短路径:给定一个图,和一个源顶点src,找到从src到其它所有所有顶点的最短路径,图中可能含有负权值的边。
关于这个问题我们已经讨论了迪杰斯特拉算法。
Dijksra的算法是一个贪婪算法,时间复杂度是O(VLogV)(使用最小堆)。但是迪杰斯特拉算法在有负权值边的图中不适用,
Bellman-Ford适合这样的图。在网络路由中,该算法会被用作距离向量路由算法。
Bellman-Ford也比迪杰斯特拉算法更简单和同时也适用于分布式系统。但Bellman-Ford的时间复杂度是O(VE),E为边的个数,这要比迪杰斯特拉算法慢。
算法描述
输入:图 和 源顶点src
输出:从src到所有顶点的最短距离。如果有负权回路(不是负权值的边),则不计算该最短距离,
没有意义,因为可以穿越负权回路任意次,则最终为负无穷。

算法步骤:

1.初始化:将除源点外的所有顶点的最短距离估计值 d[v] ← +∞, d[s] ←0;
2.迭代求解:反复对边集E中的每条边进行松弛操作,使得顶点集V中的每个顶点v的最短距离估计值逐步逼近其最短距离;(运行|v|-1次)
3.检验负权回路:判断边集E中的每一条边的两个端点是否收敛。如果存在未收敛的顶点,则算法返回false,表明问题无解;否则算法返回true,并且从源点可达的顶点v的最短距离保存在 d[v]中。

该算法是利用动态规划的思想。该算法以自底向上的方式计算最短路径。
它首先计算最多一条边时的最短路径(对于所有顶点)。然后,计算最多两条边时的最短路径。外层循环需要执行|V|-1次。

#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
using namespace std;
//表示一条边
struct Edge
{
    int src, dest, weight;
};

//带权值的有向图
struct Graph
{
    // V 顶点的数量, E 边的数量
    int V, E;

    // 用边的集合 表示一个图
    struct Edge* edge;
};

// 创建图
struct Graph* createGraph(int V, int E)
{
    struct Graph* graph = (struct Graph*) malloc( sizeof(struct Graph) );
    graph->V = V;
    graph->E = E;

    graph->edge = (struct Edge*) malloc( graph->E * sizeof( struct Edge ) );

    return graph;
}

// 打印结果
void printArr(int dist[], int n)
{
    printf("Vertex   Distance from Source\n");
    for (int i = 0; i < n; ++i)
        printf("%d \t\t %d\n", i, dist[i]);
}

// 获得单源最短路径,同时检测 负权回路
void BellmanFord(struct Graph* graph, int src)
{
    int V = graph->V;
    int E = graph->E;
    int dist[V];

    // 第一步初始化
    for (int i = 0; i < V; i++)
        dist[i]   = INT_MAX;
    dist[src] = 0;

    // 第二步:松弛操作
    for (int i = 1; i <= V-1; i++)
    {
        for (int j = 0; j < E; j++)
        {
            int u = graph->edge[j].src;
            int v = graph->edge[j].dest;
            int weight = graph->edge[j].weight;
            if (dist[u] + weight < dist[v])
                dist[v] = dist[u] + weight;
        }
    }

    // 第三步: 检测负权回路.  上面的操作保证没有负权回路的存在,
    // 如果找到了更短的路径,则说明存在负权回路
    for (int i = 0; i < E; i++)
    {
        int u = graph->edge[i].src;
        int v = graph->edge[i].dest;
        int weight = graph->edge[i].weight;
        if (dist[u] + weight < dist[v])
            printf("Graph contains negative weight cycle");
    }

    printArr(dist, V);
    return;
}

// 测试
int main()
{
    /* 创建 例子中的那个图的结构 */
    int V = 5;
    int E = 8;
    struct Graph* graph = createGraph(V, E);

    // add edge 0-1 (or A-B in above figure)
    graph->edge[0].src = 0;
    graph->edge[0].dest = 1;
    graph->edge[0].weight = -1;

    // add edge 0-2 (or A-C in above figure)
    graph->edge[1].src = 0;
    graph->edge[1].dest = 2;
    graph->edge[1].weight = 4;

    // add edge 1-2 (or B-C in above figure)
    graph->edge[2].src = 1;
    graph->edge[2].dest = 2;
    graph->edge[2].weight = 3;

    // add edge 1-3 (or B-D in above figure)
    graph->edge[3].src = 1;
    graph->edge[3].dest = 3;
    graph->edge[3].weight = 2;

    // add edge 1-4 (or A-E in above figure)
    graph->edge[4].src = 1;
    graph->edge[4].dest = 4;
    graph->edge[4].weight = 2;

    // add edge 3-2 (or D-C in above figure)
    graph->edge[5].src = 3;
    graph->edge[5].dest = 2;
    graph->edge[5].weight = 5;

    // add edge 3-1 (or D-B in above figure)
    graph->edge[6].src = 3;
    graph->edge[6].dest = 1;
    graph->edge[6].weight = 1;

    // add edge 4-3 (or E-D in above figure)
    graph->edge[7].src = 4;
    graph->edge[7].dest = 3;
    graph->edge[7].weight = -3;

    BellmanFord(graph, 0);

    return 0;
}

Dijkstra算法-邻接表-最小堆实现

前面一篇 Dijkstra最短路径算法 讲了基于邻接矩阵表示的Dijkstra算法。算法的时间复杂度为O(V^2),其中V为顶点的个数。

当一个图为稀疏图时,顶点个数相对比边的个数多。此时用邻接表来存储图,不仅节省存储空间,
在算法实现上也会更高效。这里实现 O((E+V)*LogV) ,近似的 O(ELogV) 复杂度的Dijkstra算法。其中E为边的个数。

正如在前面所讨论的,迪杰斯特拉算法的维护两组顶点集合,
一组已经包含在SPT(最短路径树)中的顶点集合,另一组是未包含在SPT内的顶点集合。

算法的优化在于使用最小堆作为一个优先队列。因为,每次迭代我们都要从
未包含在SPT内的顶点集合中找到一个最小距离的节点,普通作为是遍历一边,复杂度为 O(V),使用最小堆,可以降低到 O(1),之家取出堆顶点即可。关于Dijkstra算法的具体描述不在详细解释。
下面是C++的实现代码:

// C / C++实现的 Dijkstra最短路径,图的邻接表表示
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>

// 邻接表的节点
struct AdjListNode {
	int dest;
	int weight;
	struct AdjListNode* next;
};

// 邻接表 结构体
struct AdjList {
	struct AdjListNode *head;  // 指向头节点
};

// 图结构体,V为顶点个数。array为所有的邻接表
struct Graph {
	int V;
	struct AdjList* array;
};

//创建邻接表的节点
struct AdjListNode* newAdjListNode(int dest, int weight) {
	struct AdjListNode* newNode = (struct AdjListNode*) malloc(
			sizeof(struct AdjListNode));
	newNode->dest = dest;
	newNode->weight = weight;
	newNode->next = NULL;
	return newNode;
}

//创建一个图,包含V的顶点
struct Graph* createGraph(int V) {
	struct Graph* graph = (struct Graph*) malloc(sizeof(struct Graph));
	graph->V = V;

	graph->array = (struct AdjList*) malloc(V * sizeof(struct AdjList));

	for (int i = 0; i < V; ++i)
		graph->array[i].head = NULL;

	return graph;
}

// 添加一个边(无向图)
void addEdge(struct Graph* graph, int src, int dest, int weight) {

	struct AdjListNode* newNode = newAdjListNode(dest, weight);
	newNode->next = graph->array[src].head;
	graph->array[src].head = newNode;

	newNode = newAdjListNode(src, weight);
	newNode->next = graph->array[dest].head;
	graph->array[dest].head = newNode;
}

// 最小堆节点
struct MinHeapNode {
	int v;  //下标
	int dist; //距离
};

// 最小堆
struct MinHeap {
	int size;
	int capacity;
	int *pos;     // pos[i]表示顶点i所在的下标
	struct MinHeapNode **array;
};

// 创建一个最小堆节点
struct MinHeapNode* newMinHeapNode(int v, int dist) {
	struct MinHeapNode* minHeapNode = (struct MinHeapNode*) malloc(
			sizeof(struct MinHeapNode));
	minHeapNode->v = v;
	minHeapNode->dist = dist;
	return minHeapNode;
}

// A utility function to create a Min Heap
struct MinHeap* createMinHeap(int capacity) {
	struct MinHeap* minHeap = (struct MinHeap*) malloc(sizeof(struct MinHeap));
	minHeap->pos = (int *) malloc(capacity * sizeof(int));
	minHeap->size = 0;
	minHeap->capacity = capacity;
	minHeap->array = (struct MinHeapNode**) malloc(
			capacity * sizeof(struct MinHeapNode*));
	return minHeap;
}

// 交换两个最小堆的节点
void swapMinHeapNode(struct MinHeapNode** a, struct MinHeapNode** b) {
	struct MinHeapNode* t = *a;
	*a = *b;
	*b = t;
}

//在位置 idx 调整堆
void minHeapify(struct MinHeap* minHeap, int idx) {
	int smallest, left, right;
	smallest = idx;
	left = 2 * idx + 1;
	right = 2 * idx + 2;

	if (left < minHeap->size
			&& minHeap->array[left]->dist < minHeap->array[smallest]->dist)
		smallest = left;

	if (right < minHeap->size
			&& minHeap->array[right]->dist < minHeap->array[smallest]->dist)
		smallest = right;

	if (smallest != idx) {
		// 需要交换的节点
		MinHeapNode *smallestNode = minHeap->array[smallest];
		MinHeapNode *idxNode = minHeap->array[idx];

		//交换下标
		minHeap->pos[smallestNode->v] = idx;
		minHeap->pos[idxNode->v] = smallest;

		//交换节点
		swapMinHeapNode(&minHeap->array[smallest], &minHeap->array[idx]);

		minHeapify(minHeap, smallest);
	}
}

// 推是否为空
int isEmpty(struct MinHeap* minHeap) {
	return minHeap->size == 0;
}

// 弹出堆顶的节点(即最小的节点)
struct MinHeapNode* extractMin(struct MinHeap* minHeap) {
	if (isEmpty(minHeap))
		return NULL;

	struct MinHeapNode* root = minHeap->array[0];

	struct MinHeapNode* lastNode = minHeap->array[minHeap->size - 1];
	minHeap->array[0] = lastNode;

	// 更新下标
	minHeap->pos[root->v] = minHeap->size - 1;
	minHeap->pos[lastNode->v] = 0;

	// 记得减少堆的大小
	--minHeap->size;
	minHeapify(minHeap, 0);

	return root;
}

// 当节点v的距离更新后(变小了)调整堆
void decreaseKey(struct MinHeap* minHeap, int v, int dist) {
	//获取节点 v 在 堆中的下标
	int i = minHeap->pos[v];

	minHeap->array[i]->dist = dist;

	// 因为是变小了,自下向上调整堆即可。 O(Logn)
	while (i && minHeap->array[i]->dist < minHeap->array[(i - 1) / 2]->dist) {
		minHeap->pos[minHeap->array[i]->v] = (i - 1) / 2;
		minHeap->pos[minHeap->array[(i - 1) / 2]->v] = i;
		swapMinHeapNode(&minHeap->array[i], &minHeap->array[(i - 1) / 2]);

		i = (i - 1) / 2;
	}
}

// 判断节点v是否在堆中
bool isInMinHeap(struct MinHeap *minHeap, int v) {
	if (minHeap->pos[v] < minHeap->size)
		return true;
	return false;
}

// 打印结果
void printArr(int dist[], int n) {
	printf("Vertex   Distance from Source\n");
	for (int i = 0; i < n; ++i)
		printf("%d \t\t %d\n", i, dist[i]);
}

void dijkstra(struct Graph* graph, int src) {
	int V = graph->V;
	int dist[V];

	struct MinHeap* minHeap = createMinHeap(V);

	// 初始化堆包含所有的顶点
	for (int v = 0; v < V; ++v) {
		dist[v] = INT_MAX;
		minHeap->array[v] = newMinHeapNode(v, dist[v]);
		minHeap->pos[v] = v;
	}

	// 把 源点 src 的距离设置为0,第一个取出的点即为源点
	minHeap->array[src] = newMinHeapNode(src, dist[src]);
	minHeap->pos[src] = src;
	dist[src] = 0;
	decreaseKey(minHeap, src, dist[src]);

	minHeap->size = V;

	// 这个循环中,minHeap包含的是所有未在SPT中的顶点
	while (!isEmpty(minHeap)) {
		// 取得堆顶节点,即最小距离的顶点
		struct MinHeapNode* minHeapNode = extractMin(minHeap);
		int u = minHeapNode->v;

		// 只需要遍历和u相邻的顶点进行更新
		struct AdjListNode* pCrawl = graph->array[u].head;
		while (pCrawl != NULL) {
			int v = pCrawl->dest;
			// 松弛操作,更新距离
			if (isInMinHeap(minHeap, v) && dist[u] != INT_MAX
					&& pCrawl->weight + dist[u] < dist[v]) {
				dist[v] = dist[u] + pCrawl->weight;
				//距离更新了之后,要调整最小堆
				decreaseKey(minHeap, v, dist[v]);
			}
			pCrawl = pCrawl->next;
		}
	}

	// 打印
	printArr(dist, V);
}

// 测试
int main() {
	// 创建上一讲:http://www.acmerblog.com/dijkstra-shortest-path-algorithm-5876.html 例子中的图
	int V = 9;
	struct Graph* graph = createGraph(V);
	addEdge(graph, 0, 1, 4);
	addEdge(graph, 0, 7, 8);
	addEdge(graph, 1, 2, 8);
	addEdge(graph, 1, 7, 11);
	addEdge(graph, 2, 3, 7);
	addEdge(graph, 2, 8, 2);
	addEdge(graph, 2, 5, 4);
	addEdge(graph, 3, 4, 9);
	addEdge(graph, 3, 5, 14);
	addEdge(graph, 4, 5, 10);
	addEdge(graph, 5, 6, 2);
	addEdge(graph, 6, 7, 1);
	addEdge(graph, 6, 8, 6);
	addEdge(graph, 7, 8, 7);

	dijkstra(graph, 0);

	return 0;
}




  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值