贪心法文档

贪心法

引言

在贪婪算法(greedy method) 中,我们要逐步构造一个最优解。每一步,我们都在一定的标准下,做出一个最优决策。做出决策所依据的标准称为贪心准则(greedy criterion)。

贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的是在某种意义上的局部最优解

贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择,选择的贪心策略必须具备无后效性,即某个状态以前的过程不会影响以后的状态,只与当前状态有关。

贪心算法每一步必须满足以下条件:
  1、可行的:即它必须满足问题的约束。
  2、局部最优:他是当前步骤中所有可行选择中最佳的局部选择。
  3、不可更改:即选择一旦做出,在算法的后面步骤就不可改变了。

需求描述

贪心策略适用的前提是:局部最优策略能导致产生全局最优解。

贪心选择是指所求问题的整体最优解可以通过一系列局部最优的选择,即贪心选择来达到。这是贪心算法可行的第一个基本要素,也是贪心算法与动态规划算法的主要区别。

贪心选择是采用从顶向下、以迭代的方法做出相继选择,每做一次贪心选择就将所求问题简化为一个规模更小的子问题。对于一个具体问题,要确定它是否具有贪心选择的性质,我们必须证明每一步所作的贪心选择最终能得到问题的最优解。

通常可以首先证明问题的一个整体最优解,是从贪心选择开始的,而且作了贪心选择后,原问题简化为一个规模更小的类似子问题。然后,用数学归纳法证明,通过每一步贪心选择,最终可得到问题的一个整体最优解。

实际上,贪心算法适用的情况很少。一般,对一个问题分析是否适用于贪心算法,可以先选择该问题下的几个实际数据进行分析,就可做出判断。

结构设计

建立数学模型来描述问题;

把求解的问题分成若干个子问题;

对每一子问题求解,得到子问题的局部最优解;

把子问题的解局部最优解合成原来解问题的一个解。

架构设计

    从问题的某一初始解出发;

    while (能朝给定总目标前进一步)

  {

 利用可行的决策,求出可行解的一个解元素; 

 }

    由所有解元素组合成问题的一个可行解;  

算法设计

最优前缀码

二元前缀码:任何字符的代码不能作为其它字符代码的前缀.eg.Q={001,00,010,01}不是二元前缀代码,如序列0100001会产生歧义

设C={x1,x2,…,xn}是n个字符的集合,f(xi)为xi出现的频率,d(xi)为xi的码长,i=1,2,…,n.

存储一个字符的平均二进制位数(码数):

  B=∑i=1nf(xi)d(xi)B=∑i=1nf(xi)d(xi)

每个二元前缀码对应一棵二叉树,树叶代表码字,树叶的深度表示码长,平均二进制位数相当于这棵树在给定频率下的平均深度,也称为这棵树的权

对同一组频率可以构造出不同的二叉树,对应的平均二进制位数也不同。占用位数越少的压缩效率越高,即每个码字平均使用二进制位数最少的前缀码,称为最优二元前缀码

如果叶片数n=2k,且每个码字的频率是1/n,那么这棵树应是一颗均衡的二叉树

哈夫曼树求得的编码为最优前缀码。每个叶子表示的字符的编码,就是从根到叶子的路径上的标号依次相连所形成的编码,显然这就是该字符的最优前缀码。所谓前缀码是指,对字符集进行编码时,要求字符集中任一字符的编码都不是其它字符的编码的前缀,比如常见的等长编码就是前缀码。所谓最优前缀码是指,平均码长或文件总长最小的前缀编码称为最优的前缀码(这里的平均码长相当于码长的期望值)。

变长编码可能使解码产生二义性,而前缀码的出现很好地解决了这个问题。而平均码长相当于二叉树的加权路径长度,从这个意义上说,由哈夫曼树生成的编码一定是最优前缀码,故通常不加区分的将哈夫曼编码也称作最优前缀码。

需要注意的是,由于哈夫曼树建立过程的不唯一性可知,生成的哈夫曼编码也是不唯一的,并且在本文中,将树中左分支和右分支分别标记为0和1也造成了哈夫曼编码的不唯一性(当然也可以反过来,将左分支记为1,右分支记为0)。

在实际应用中,我们通常采用下列做法:根据各个字符的权值建立一颗哈夫曼树,求得每个字符的哈夫曼编码,有了每个字符的哈夫曼编码,我们就可以制作一个该字符集的哈夫曼编码表。有了字符集的哈夫曼编码表之后,对数据文件的编码过程是:依次读人文件中的字符c,在哈夫曼编码表H中找到此字符,将字符c转换为对应的哈夫曼编码串。对压缩后的数据文件进行解码则必须借助于哈夫曼树,其过程是:依次读人文件的二进制码,从哈夫曼树的根结点出发,若当前读入0,则走向左孩子,否则走向右孩子。一旦到达某一叶子时便译出相应的字符。然后重新从根出发继续译码,直至文件结束。下面给出制作哈夫曼编码表的过程的代码,通过以上的分析,读者不难写出文件编码过程和解码过程的代码。
 

#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<algorithm>
using namespace std;
 
#define n 6           //叶子数目
#define m 2*n-1       //树中结点总数
typedef struct{       //结点类型
    double weight;    //结点的权值
    int parent,lchild,rchild;//双亲指针及左右孩子
}HTNode;
typedef HTNode HuffmanTree[m];//HuffmanTree是向量类型
 
typedef struct{       //用于SelectMin函数中排序的结点类型
    int id;           //保存根结点在向量中的序号
    double weight;    //保存根结点的权值
}temp;
 
typedef struct{       //编码结点
    char ch;          //存储字符
    char bits[n+1];   //存放编码位串
}CodeNode;
typedef CodeNode HuffmanCode[n];
 
void InitHuffmanTree(HuffmanTree T){
    //初始化哈夫曼树
    //将2n-1个结点里的三个指针均置为空(即置为-1),权值置为0
    for(int i=0;i<m;i++){
        T[i].lchild=-1;
        T[i].rchild=-1;
        T[i].parent=-1;
        T[i].weight=0;
    }
}
 
void InputWeight(HuffmanTree T){
    //输入叶子权值
    //读人n个叶子的权值存于向量的前n个分量中
    for(int i=0;i<n;i++){
        double x;
        scanf("%lf",&x);
        T[i].weight=x;
    }
}
 
bool cmp(temp a,temp b){
    //用于排序的比较函数
    return a.weight<b.weight;
}
 
void SelectMin(HuffmanTree T,int k,int *p1,int *p2){
    //在前k个结点中选择权值最小和次小的根结点,其序号分别为p1和p2
    temp x[m];              //x向量为temp类型的向量
    int i,j;
    for(i=0,j=0;i<=k;i++){  //寻找最小和次小根节点的过程
        if(T[i].parent==-1){//如果是根节点,则进行如下操作
            x[j].id=i;      //将该根节点的序号赋值给x
            x[j].weight=T[i].weight;//将该根节点的权值赋值给x
            j++;            //x向量的指针后移一位
        }
    }
    sort(x,x+j,cmp);        //对x按照权值从小到大排序
    //排序后的x向量的第一和第二个位置中存储的id是所找的根节点的序号值
    *p1=x[0].id;
    *p2=x[1].id;
}
 
void CreateHuffmanTree(HuffmanTree T){
    //构造哈夫曼树,T[m-1]为其根结点
    int i,p1,p2;
    InitHuffmanTree(T);    //将T初始化
    InputWeight(T);        //输入叶子权值
    for(i=n;i<m;i++){
        //在当前森林T[0..i-1]的所有结点中,选取权最小和次小的
        //两个根结点T[p1]和T[p2]作为合并对象
        //共进行n-1次合并,新结点依次存于T[i]中
 
        SelectMin(T,i-1,&p1,&p2);//选择权值最小和次小的根结点,其序号分别为p1和p2
 
        //将根为T[p1]和T[p2]的两棵树作为左右子树合并为一棵新的树
        //新树的根是新结点T[i]
        T[p1].parent=T[p2].parent=i;//T[p1]和T[p2]的两棵树的根结点指向i
        T[i].lchild=p1;             //最小权的根结点是新结点的左孩子
        T[i].rchild=p2;             //次小权的根结点是新结点的右孩子
        T[i].weight=T[p1].weight+T[p2].weight;//新结点的权值是左右子树的权值之和
    }
}
 
void CharSetHuffmanEncoding(HuffmanTree T,HuffmanCode H){
    //根据哈夫曼树T求哈夫曼编码表H
    int c,p;//c和p分别指示T中孩子和双亲的位置
    char cd[n+1];//临时存放编码
    int start;//指示编码在cd中的起始位置
    cd[n]='\0';//编码结束符
    getchar();
    for(int i=0;i<n;i++){//依次求叶子T[i]的编码
        H[i].ch=getchar();//读入叶子T[i]对应的字符
        start=n;//编码起始位置的初值
        c=i;//从叶子T[i]开始上溯
        while((p=T[c].parent)>=0){//直至上溯到T[c]是树根为止
            //若T[c]是T[p]的左孩子,则生成代码0;否则生成代码1
            if(T[p].lchild==c)
                cd[--start]='0';
            else
                cd[--start]='1';
            c=p;//继续上溯
        }
        strcpy(H[i].bits,&cd[start]);//复制编码位串
    }
}
 
//*************************测试函数**********************************
int main(){
    HuffmanTree T;
    HuffmanCode H;
    printf("请输入%d个叶子结点的权值来建立哈夫曼树:\n",n);
    CreateHuffmanTree(T);
    printf("请输入%d个叶子结点所代表的字符:\n",n);
    CharSetHuffmanEncoding(T,H);
    printf("哈夫曼树已经建好,哈夫曼编码已经完成,输出如下:\n");
    printf("哈夫曼树:\n");
    for(int i=0;i<m;i++){
        printf("id:%d  weight:%.1lf   parent:%d",i,T[i].weight,T[i].parent);
        printf("  lchild:%d rchild:%d\n",T[i].lchild,T[i].rchild);
    }
    printf("哈夫曼编码:\n");
    double wpl=0.0;
    for(int i=0;i<n;i++){
        printf("id:%d   ch:%c  code:%s\n",i,H[i].ch,H[i].bits);
        wpl+=strlen(H[i].bits)*T[i].weight;
    }
    printf("平均码长为:%.2lf\n",wpl);
    return 0;
}

 

最小生成树

普里姆算法(Prim算法),图论中的一种算法,可在加权连通图里搜索最小生成树。意即由此算法搜索到的边子集所构成的树中,不但包括了连通图里的所有顶点(英语:Vertex (graph theory)),且其所有边的权值之和亦为最小。该算法于1930年由捷克数学家沃伊捷赫·亚尔尼克(英语:Vojtěch Jarník)发现;并在1957年由美国计算机科学家罗伯特·普里姆(英语:Robert C. Prim)独立发现;1959年,艾兹格·迪科斯彻再次发现了该算法。因此,在某些场合,普里姆算法又被称为DJP算法、亚尔尼克算法或普里姆-亚尔尼克算法。

算法描述

1).输入:一个加权连通图,其中顶点集合为V,边集合为E;

2).初始化:Vnew = {x},其中x为集合V中的任一节点(起始点),Enew = {},为空;

3).重复下列操作,直到Vnew = V:

a.在集合E中选取权值最小的边<u, v>,其中u为集合Vnew中的元素,而v不在Vnew集合当中,并且v∈V(如果存在有多条满足前述条件即具有相同权值的边,则可任意选取其中之一);

b.将v加入集合Vnew中,将<u, v>边加入集合Enew中;

4).输出:使用集合Vnew和Enew来描述所得到的最小生成树

 

下面对算法的图例描述

图例说明不可选可选已选(Vnew)
 

此为原始的加权连通图。每条边一侧的数字代表其权值。---

顶点D被任意选为起始点。顶点ABEF通过单条边与D相连。A是距离D最近的顶点,因此将A及对应边AD以高亮表示。C, GA, B, E, FD
 

下一个顶点为距离DA最近的顶点。BD为9,距A为7,E为15,F为6。因此,FDA最近,因此将顶点F与相应边DF以高亮表示。C, GB, E, FA, D
算法继续重复上面的步骤。距离A为7的顶点B被高亮表示。CB, E, GA, D, F
 

在当前情况下,可以在CEG间进行选择。CB为8,EB为7,GF为11。E最近,因此将顶点E与相应边BE高亮表示。C, E, GA, D, F, B
 

这里,可供选择的顶点只有CGCE为5,GE为9,故选取C,并与边EC一同高亮表示。C, GA, D, F, B, E

顶点G是唯一剩下的顶点,它距F为11,距E为9,E最近,故高亮表示G及相应边EGGA, D, F, B, E, C

现在,所有顶点均已被选取,图中绿色部分即为连通图的最小生成树。在此例中,最小生成树的权值之和为39。A, D, F, B, E, C, G

算法流程:

假设N=(V,{E})是连通网,TE是N上最小生成树中边的集合。

假设 U 是最小生成树中的顶点集合, TE是最小生成树中的边的集合

算法从 U={u0}(u0∈V),TE={}开始,重复执行下述操作:

在所有 u∈U,v∈V−U的边 (u,v)∈E中找一条代价最小的边 (u0,v0)并入集合TE,同时v0并入U,直到 U=V为止。此时TE中必有 n−1
n−1 条边,则T=(V,{TE})为N的最小生成树。

注意,每次在选择最小边 (u0,v0) 时候,需要判断点 v0是否已经并入集合 U,即判断该边的加入是否会产生环路。

简单来说,就是从任意点u0出发,然后不断在所有U中顶点的邻接边中找一个加入不会构成环的顶点并入U直到U=V

Prim算法的思想是贪心算法,其正确性可以通过贪心准则的最优性来证明,常用的有贪心交换和数学归纳法。


数学归纳法:

1.选择第一条边的时,由MST性质,一定是权值最小的边;
2.假设我们选取的前s条边是最小生成树的一部分,这些边连接结点记作n0,n1,...,ns。

接下来,选择下一条边的时候,我们将选择与这s+1个点相连的所有边中最小边,我们可以用反证法证明,这条边一定在最后的生成树中。

假设这条边的一端是ni,i∈[0,s]另一端是nk不属于已选择的集合。我们将这条边加入最后的生成树中会出现一个环,则环的一端是nk ,此时我们能找到与nk相邻的边的权值比这条边大(因为当时未连接nk 时,我们的这条边是最小的),把它去掉我们就能得到一个代价更小的生成树,且是连通的。

所以,我们的贪心选择是最优的。但是,最小生成树不是唯一的,因为在某个步骤会出现多个权值相同的最小边,此时,最小边选择不同,可能导致最小生成树不同。

算法实现:

为了记录从 U 到 V−U具有最小代价的边,我们可以设一个辅助数组closedge。对每一个顶点vi∈V−U,在辅助数组中存在一个相应分量closedge[i-1],它包括两个域:

lowcost存储该最小边上的权值,即
closedge[i−1].lowcost=Min{cost(u,vi)∣u∈U}

vex域存储该边依附的在U中的顶点。
即如下这种数据结构:

有了这个数组,我们可以从某一顶点u出发,然后closedge中其他顶点vi的lowcost初始化为 u到vi 的边上权值,U中附加点的位置就是u在U中的位置。

初始化后,我们就可以利用上述的贪心准则逐步构造我们的最小生成树,即每次从closedge中选择一个不构成环的最小顶点,记住每选择一个v并入U后都要更新数组closedge,因为此时U更新了,而closedge是U中顶点到其他顶点的最小值,是不断更新的。

每当加入一个顶点v到U时,我们将closedge[v-1].cost赋成0,表示已加入顶点集

// 无向带权图的普里姆算法
void MST_Prim(Graph G, string v) {
	// 用普里姆算法从顶点v出发构造网G的最小生成树T,并输出T的各条边
	// 记录顶点集U到V-U的代价最小的边的辅助数组定义
	struct arcnode{
		string adjvex; // U中的尾点
		int lowcost;   // 对应的最小代价
	}closedge[MAX_VERTEX_NUM];
	int k = LocateVex(G, v); //v在G中的位置
	int i;
	for (i = 0; i < G.vexnum; i++) { //辅助数组初始化
		if (i != k) {
			closedge[i] = {v,G.arcs[k][i].adj};
		} 
	}
	closedge[k].lowcost = 0;  // 初始U={vk},即先并入顶点v
	for (i = 1; i < G.vexnum; i++) { // 选择其余G.vexnum-1个顶点
		// 在数组closedge中找到数组最小的元素对应的下标,且其对应元素值不为0
		int min = 0;
		for (int j = 0; j < G.vexnum; j++) {
			if ((closedge[j].lowcost < closedge[min].lowcost && closedge[j].lowcost) || closedge[min].lowcost == 0) min = j;
		}
		cout << "(" << closedge[min].adjvex << "," << G.vexs[min] << "),";
		closedge[min].lowcost = 0;  // 将vmin并入U
		for (int j = 0; j < G.vexnum; j++) { // 更新辅助矩阵
			if (G.arcs[min][j].adj < closedge[j].lowcost) {
				closedge[j] = { G.vexs[min],G.arcs[min][j].adj };
			}
		}
	}
}

 

 

克鲁斯卡尔算法(kruskal)

 

Kruskal算法是一种用来查找最小生成树的算法,由Joseph Kruskal在1956年发表。用来解决同样问题的还有Prim算法和Boruvka算法等。三种算法都是贪心算法的应用。和Boruvka算法不同的地方是,Kruskal算法在图中存在相同权值的边时也有效。

2.算法简单描述

1).记Graph中有v个顶点,e个边

2).新建图Graphnew,Graphnew中拥有原图中相同的e个顶点,但没有边

3).将原图Graph中所有e个边按权值从小到大排序

4).循环:从权值最小的边开始遍历每条边 直至图Graph中所有的节点都在同一个连通分量中

                if 这条边连接的两个节点于图Graphnew中不在同一个连通分量中

                                         添加这条边到图Graphnew中

 

图例描述:

图例描述:

首先第一步,我们有一张图Graph,有若干点和边 

将所有的边的长度排序,用排序的结果作为我们选择边的依据。这里再次体现了贪心算法的思想。资源排序,对局部最优的资源进行选择,排序完成后,我们率先选择了边AD。这样我们的图就变成了右图

在剩下的变中寻找。我们找到了CE。这里边的权重也是5

依次类推我们找到了6,7,7,即DF,AB,BE。

下面继续选择, BC或者EF尽管现在长度为8的边是最小的未选择的边。但是现在他们已经连通了(对于BC可以通过CE,EB来连接,类似的EF可以通过EB,BA,AD,DF来接连)。所以不需要选择他们。类似的BD也已经连通了(这里上图的连通线用红色表示了)。

最后就剩下EG和FG了。当然我们选择了EG。最后成功的图就是右

 

3.简单证明Kruskal算法

对图的顶点数n做归纳,证明Kruskal算法对任意n阶图适用。

归纳基础:

n=1,显然能够找到最小生成树。

归纳过程:

假设Kruskal算法对n≤k阶图适用,那么,在k+1阶图G中,我们把最短边的两个端点a和b做一个合并操作,即把u与v合为一个点v',把原来接在u和v的边都接到v'上去,这样就能够得到一个k阶图G'(u,v的合并是k+1少一条边),G'最小生成树T'可以用Kruskal算法得到。

我们证明T'+{<u,v>}是G的最小生成树。

用反证法,如果T'+{<u,v>}不是最小生成树,最小生成树是T,即W(T)<W(T'+{<u,v>})。显然T应该包含<u,v>,否则,可以用<u,v>加入到T中,形成一个环,删除环上原有的任意一条边,形成一棵更小权值的生成树。而T-{<u,v>},是G'的生成树。所以W(T-{<u,v>})<=W(T'),也就是W(T)<=W(T')+W(<u,v>)=W(T'+{<u,v>}),产生了矛盾。于是假设不成立,T'+{<u,v>}是G的最小生成树,Kruskal算法对k+1阶图也适用。

由数学归纳法,Kruskal算法得证。

#include <iostream>
#include <string>
#include <algorithm>

using namespace std;

// ---- 树的双亲表存储表示 ----
#define MAX_NODE_NUM 20
#define MAX_EDGE_NUM 20
typedef string ElemType;
typedef struct PTNode{ // 结点结构
	ElemType data;
	int parent; // 双亲位置域
}PTnode;
typedef struct { // 树结构
	PTnode nodes[MAX_NODE_NUM];
	int n;       // 结点数
}PTree;
//---- MFSet的树的双亲存储表示 ----
typedef PTree MFSet;
// ---- 树的结构定义 ----
typedef struct edge {
	int begin;
	int end;
	int cost;
}Edge;  // 边结点定义
typedef struct MGraph {
	Edge edges[MAX_EDGE_NUM];  // 边数组
	string vexs[MAX_NODE_NUM]; // 结点数组
	int arcnum, vexnum;        // 当前结点和边数
}MGraph;
int Locate(MGraph G, string u); // 返回u在G中的位置
void createGraph(MGraph& G, MFSet& S); // 创建图G,同时初始化并查集s
int mix_merge(MFSet& s, int i, int j); // 合并i和j所在子树
int mix_find(MFSet& s, int i); // 查找i所在子树的根结点
int Locate_node(MFSet s, string u); //包含信息u的结点的位置
int cmp(Edge e1, Edge e2) {
	return e1.cost < e2.cost;
}
void MST_Kruscal(MGraph G, MFSet s); //最小生成树的克鲁斯卡尔算法
int main() {
	MGraph G;
	MFSet S;
	createGraph(G, S);
	MST_Kruscal(G, S);

	system("pause");
	return 0;
}
void MST_Kruscal(MGraph G, MFSet s) {
	int vexn = G.vexnum;
	int arcn = G.arcnum;
	sort(G.edges, &G.edges[arcn - 1], cmp); // 先对边结点进行排序
	int i;
	for (i = 0; i < arcn; i++) {
		int v1 = G.edges[i].begin;
		int v2 = G.edges[i].end;
		int r1 = mix_find(s, v1);
		int r2 = mix_find(s, v2);
		if (r1 != r2 ) { // 两个顶点和合并
			cout << "(" << G.vexs[v1] << "," << G.vexs[v2] << ") " << G.edges[i].cost << endl;;
			mix_merge(s, r1, r2); // 注意合并的一定是根节点
		}
	}
}
int Locate_node(MFSet s, string u) {
	int i;
	for (i = 0; i < s.n && s.nodes[i].data != u; i++);
	if (i == s.n) return -1;
	else return i;
}
int Locate(MGraph G, string u) {
	int i;
	for (i = 0; i < G.vexnum && G.vexs[i] != u; i++);
	if (i == G.vexnum) return -1;
	else return i;
}
void createGraph(MGraph& G,MFSet& S) {
	printf("输入图的顶点数和边数:\n");
	cin >> G.vexnum >> G.arcnum;
	S.n = G.vexnum; // 结点数目
	printf("输入图的顶点信息:\n");
	int i;
	for (i = 0; i < G.vexnum; i++) {
		cin >> G.vexs[i];
		S.nodes[i].data = G.vexs[i];
		S.nodes[i].parent = -1;
	} 
	printf("以vi vj cost的形式输入边:\n");
	for (i = 0; i < G.arcnum; i++) {
		string v1, v2;
		int weight;
		cin >> v1 >> v2 >> weight;
		int l1 = Locate(G, v1);
		int l2 = Locate(G, v2);
		G.edges[i].begin = l1;
		G.edges[i].end = l2;
		G.edges[i].cost = weight;
	}
}

int mix_merge(MFSet& s, int i, int j) {
	// i和j分别是两个子集si和sj的根节点
	if (i < 0 || i>s.n || j<0 || j>s.n) return 0; //输入有误
	if (s.nodes[i].parent > s.nodes[j].parent) { // i的成员少
		s.nodes[j].parent += s.nodes[i].parent;
		s.nodes[i].parent = j;
	}
	else {
		s.nodes[i].parent += s.nodes[j].parent;
		s.nodes[j].parent = i;
	}
	return 1;
}
int mix_find(MFSet& s, int i) {
	// 确定i所在子集,并将从到根路径上的所有结点变成根的孩子结点
	if (i < 0 || i > s.n) return -1; // 根结点编号0到n-1
	int j;
	// 查找i的根结点j
	for (j = i; s.nodes[j].parent >= 0; j = s.nodes[j].parent); // 到-1停止
	int k;
	int t;
	for (k = i; k != j; k = t) {
		t = s.nodes[k].parent;
		s.nodes[k].parent = j;
	}
	return j;
}

 

 

单源最短路径

问题描述

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

Dijkstra算法的解决方案

Dijkstra提出按各顶点与源点v间的路径长度的递增次序,生成到各顶点的最短路径的算法。既先求出长度最短的一条最短路径,再参照它求出长度次短的一条最短路径,依次类推,直到从源点v 到其它各顶点的最短路径全部求出为止。

Dijkstra算法的解题思想

将图G中所有的顶点V分成两个顶点集合S和T。以v为源点已经确定了最短路径的终点并入S集合中,S初始时只含顶点v,T则是尚未确定到源点v最短路径的顶点集合。然后每次从T集合中选择S集合点中到T路径最短的那个点,并加入到集合S中,并把这个点从集合T删除。直到T集合为空为止。

具体步骤

1、选一顶点v为源点,并视从源点v出发的所有边为到各顶点的最短路径(确定数据结构:因为求的是最短路径,所以①就要用一个记录从源点v到其它各顶点的路径长度数组dist[],开始时,dist是源点v到顶点i的直接边长度,即dist中记录的是邻接阵的第v行。②设一个用来记录从源点到其它顶点的路径数组path[],path中存放路径上第i个顶点的前驱顶点)。

2、在上述的最短路径dist[]中选一条最短的,并将其终点(即<v,k>)k加入到集合s中。

3、调整T中各顶点到源点v的最短路径。 因为当顶点k加入到集合s中后,源点v到T中剩余的其它顶点j就又增加了经过顶点k到达j的路径,这条路径可能要比源点v到j原来的最短的还要短。调整方法是比较dist[k]+g[k,j]与dist[j],取其中的较小者。

4、再选出一个到源点v路径长度最小的顶点k,从T中删去后加入S中,再回去到第三步,如此重复,直到集合S中的包含图G的所有顶点。

算法过程描述:

表格中默认选取的起始顶点为1顶点,所以本问题就转化为求解1顶点到2, 3, 4, 5这几个顶点的最短路径。首先初始条件列出1顶点到2, 3, 4, 5各个顶点的距离,这个距离直接在图的存储邻接矩阵中得到,选取距离最近的一个也就是2顶点加入集合S,下面要进行的是比较关键的一步,这个时候应该去获取3, 4, 5三个顶点到集合S的最短距离(从1顶点出发,可以经过S中的任意顶点):将1到2顶点的距离加上2到各个点的距离,然后用这个距离来同1到各个顶点的距离相比较,谁小就取谁,以此类推,然后每次取Distance[]最小的值进入集合S。

 这样下去,Distance[]中存放的就是每个顶点到集合S的最短距离,比如当前的集合只有1, 2,按照规则顶点4应该入选进集合S,因为Distance[3]没有入选集合的顶点中对应的Distance[]最小的顶点。现在需要计算3和5到新集合S={1, 2, 4}的最短距离,这个时候就只需要将Distance[2]和Distance[4]中的值(现在这里面的值表示集合S={1, 2}到顶点3和5顶点的最短距离),但是现在集合中加入了顶点4,计算方法如下:

Distance[3] + 邻接矩阵中顶点4到顶点3的距离 < Distance[2] ?
Distance[3]:(顶点4到S={1, 2}的最短距离)
Distance[2]: (顶点3到S={1, 2}的最短距离)
如果这个小于成立,那么很明显新的集合到顶点3的最小距离应该是先从S={1, 2}到顶点4的最短距离,然后再从顶点4到顶点3。
由于每一次的比较都是在上一次集合的最优结果中计算的,所以新计算出来的顶点3到集合S={1, 2, 4}的最短距离也是全局最优的。

对应的C语言代码如下:

#include <stdio.h>
#define M    65535 //无穷大
#define N    5 //顶点数
//Dijkstra算法函数,求给定顶点到其余各点的最短路径
//参数:邻接矩阵、出发点的下标、结果数组、路径前一点记录
void Dijkstra(int Cost[][N], int v0, int Distance[], int prev[])
{
    int s[N];
    int mindis,dis;
    int i, j, u;
    //初始化
    for(i=0; i<N; i++)
    {
        Distance[i] = Cost[v0][i];
        s[i] = 0;
        if(Distance[i] == M)
            prev[i] = -1;
        else
            prev[i] = v0;
    }
    Distance[v0] = 0;
    s[v0] = 1; //标记v0
    //在当前还未找到最短路径的顶点中,
    //寻找具有最短距离的顶点
    for(i=1; i < N; i++)
    {//每循环一次,求得一个最短路径
        mindis = M;
        u = v0;
        for (j=0; j < N; j++) //求离出发点最近的顶点
            if(s[j]==0 && Distance[j]<mindis)
            {
                mindis = Distance [j];
                u = j;
            } // if语句体结束,j循环结束
        s[u] = 1;
        for(j=0; j<N; j++) //修改递增路径序列(集合)
        if(s[j]==0 && Cost[u][j]<M)
        { //对还未求得最短路径的顶点
            //求出由最近的顶点 直达各顶点的距离
            dis = Distance[u] +Cost[u][j];
            // 如果新的路径更短,就替换掉原路径
 
            if(Distance[j] > dis)
            {
                Distance[j] = dis;
                prev[j] = u;
            }
        } // if 语句体结束,j循环结束
    } // i循环结束
}
// 输出最短路径
// 参数:路径前一点记录、出发点的下标、到达点下标
void PrintPrev(int prev[],int v0,int vn)
{
    int tmp = vn;
    int i, j;
    //临时存路径
    int tmpprv[N];
    //初始化数组
    for(i=0; i < N; i++)
        tmpprv[i] = 0;
 
    //记录到达点下标
    tmpprv[0] = vn+1;
    //中间点用循环记录
    for(i =0, j=1; j < N ;j++)
    {
        if(prev[tmp]!=-1 && tmp!=0)
        {
            tmpprv[i] = prev[tmp]+1;
            tmp = prev[tmp];
            i++;
        }
        else break;
    }
 
    //输出路径,数组逆向输出
    for(i=N-1; i >= 0; i--)
    {
        if(tmpprv[i] != 0)
        { //排除0元素
            printf("V%d", tmpprv[i]);
            if(i)  //不是最后一个输出符号 
                printf("-->");
        }
    }
    printf("-->V%d", vn+1);
}
//主函数
int main()
{
    //给出有向网的顶点数组
    char *Vertex[N]={"V1", "V2", "V3", "V4", "V5"};
    //给出有向网的邻接矩阵
    int Cost[N][N]={
        {0, 10, M, 30, 100},
        {M, 0, 50, M, M},
        {M, M, 0, M, 10},
        {M, M, 20, 0, 60},
        {M, M, M, M, 0},
    };
    int Distance[N]; //存放求得的最短路径长度
    int prev[N];  //存放求得的最短路径
    int i;
    //调用Dijkstra算法函数,求顶点V1到其余各点的最短路径
    //参数:邻接矩阵、顶点数、出发点的下标、 结果数组
    Dijkstra(Cost, 0, Distance, prev);
    for(i=0; i < N; i++)
    {
        //输出最短路径长度
        printf("%s-->%s:%d\t", Vertex[0], Vertex[i], Distance[i]);
        //输出最短路径
        PrintPrev(prev, 0, i);
        printf("\n");
    }
 
    return 0;
}

测试评价

优点:O(N*N),加堆优化:O(N*logN)
缺点:    在单源最短路径问题的某些实例中,可能存在权为负的边。
如果图G=(V,E)不包含从源s可达的负权回路,
则对所有v∈V,最短路径的权定义d(s,v)依然正确,
即使它是一个负值也是如此。但如果存在一从s可达的负回路,
最短路径的权的定义就不能成立。S到该回路上的结点就不存在最短路径。
当有向图中出现负权时,则Dijkstra算法失效。当不存在源s可达的负回路时,
我们可用Bellman-Ford算法实现。 

结论

 prim,kruskal和dijkstra算法有贪心策略,他们贪在哪啊?

  1. prim:每次执行都选择轻边
  2. kruskal:每次执行都选择权值最小的边,同时合并两个不相交的子集
  3. dijkstra:每次执行都选择路径最短d(s,t),并将顶点t加入到集合S中,同时对边进行松弛
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值