贪心算法

引言

所谓贪心算法是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的仅是在某种意义上的局部最优解。
贪心算法没有固定的算法框架,算法设计的关键是贪心策略的选择。必须注意的是,贪心算法不是对所有问题都能得到整体最优解,选择的贪心策略必须具备无后效性,即某个状态以后的过程不会影响以前的状态,只与当前状态有关。
所以对所采用的贪心策略一定要仔细分析其是否满足无后效性。

最短路径问题

问题描述

给定一个有向图G=(V,E),s为V中一点,以s为起点,要确定从s出发到V中每一个其他定点的距离(距离的定义是最短路径对应的长度).

Dijkstra算法

算法步骤

  • 初始时,S只包含源点,即S={v},v的距离为0。U包含除v外的其他顶点,即:U={其余顶点},若v与U中顶点u有边,则 < u,v >正常有权值,若u不是v的出边邻接点,则 < u,v > 权值为∞

  • 从U中选取一个距离v最小的顶点k,把k,加入S中(该选定的距离就是v到k的最短路径长度)

  • 以k为新考虑的中间点,修改U中各顶点的距离;若从源点v到顶点u的距离(经过顶点k)比原来距离(不经过顶点k)短,则修改顶点u的距离值,修改后的距离值的顶点k的距离加上边上的权

  • 重复步骤b和c直到所有顶点都包含在S中

代码

const int  MAXINT = 32767;
const int MAXNUM = 10;
int dist[MAXNUM];//表示距离
int prev[MAXNUM];//记录每个顶点的前驱顶点(因为最短路径的唯一性,所以每个点的前驱元素都是唯一的)

int A[MAXUNM][MAXNUM];

void Dijkstra(int v0)
{
    bool S[MAXNUM];                                  // 判断是否已存入该点到S集合中
      int n=MAXNUM;
    for(int i=1; i<=n;   i)
    {
        dist[i] = A[v0][i];
        S[i] = false;                                // 初始都未用过该点
        if(dist[i] == MAXINT)   //如果该点与源点之间无边 
              prev[i] = -1;
        else 
              prev[i] = v0;
     }
     dist[v0] = 0;
     S[v0] = true;   
    for(int i=2; i<=n; i  )
    {
         int mindist = MAXINT;
         int u = v0;                               // 找出当前未使用的点j的dist[j]最小值
         for(int j=1; j<=n;   j)
            if((!S[j]) && dist[j]//如果j点没有被用过而且dist[j]小于mindist
            {
                  u = j;                             // u保存当前邻接点中距离最小的点的号码 
                  mindist = dist[j];
            }
         S[u] = true; //将u置为已用
         for(int j=1; j<=n; j  )//更新u加入已用集合后的未用顶点集合中点到已用集合中点的距离
             if((!S[j]) && A[u][j]if(dist[u]   A[u][j] < dist[j])     //在通过新加入的u点路径找到离v0点更短的路径  
                 {
                     dist[j] = dist[u]   A[u][j];    //更新dist 
                     prev[j] = u;                    //记录前驱顶点 
                  }
              }
     }
}

算法复杂度

算法的时间复杂度是senta(n^2)

稠图的线性时间算法

最小耗费生成树

问题描述

设G = (V,E)是无向连通带权图,即一个网络。E中的每一条边(v,w)的权为c[v][w]。如果G的子图G’是一棵包含G的所有顶点的树,则称G’为G的生成树。生成树上各边权的总和称为生成树的耗费。在G的所有生成树中,耗费最小的生成树称为G的最小生成树。
最小生成树最常见的题就是求解n个城市之间的修路问题.

Kruskal算法

算法描述

给定无向连同带权图G = (V,E),V = {1,2,…,n}。Kruskal算法构造G的最小生成树的基本思想是:

  • 将G的n个顶点看成n个孤立的连通分支,将所有的边按权从小大排序。

  • 从第一条边开始,依边权递增的顺序检查每一条边。并按照下述方法连接两个不同的连通分支:当查看到第k条边(v,w)时,如果端点v和w分别是当前两个不同的连通分支T1和T2的端点时,就用边(v,w)将T1和T2连接成一个连通分支,然后继续查看第k 1条边;如果端点v和w在当前的同一个连通分支中,就直接再查看k 1条边。这个过程一个进行到只剩下一个连通分支时为止。

此时,已构成G的一棵最小生成树。

代码实现

#include 
#include 
#include 
#include 
using namespace std;

#define maxn 110    //最多点个数
int n, m;   //点个数,边数
int parent[maxn];   //父亲节点,当值为-1时表示根节点
int ans;    //存放最小生成树权值
struct eage     //边的结构体,u、v为两端点,w为边权值
{
    int u, v, w;
}EG[5010];

bool cmp(eage a, eage b)    //排序调用
{
    return a.w < b.w;
}

int Find(int x)     //寻找根节点,判断是否在同一棵树中的依据
{
    if(parent[x] == -1) return x;
    return Find(parent[x]);
}

void Kruskal()      //Kruskal算法,parent能够还原一棵生成树,或者森林
{
    memset(parent, -1, sizeof(parent));
    sort(EG 1, EG m 1, cmp);    //按权值将边从小到大排序
    ans = 0;
    for(int i = 1; i <= m; i  )     //按权值从小到大选择边
    {
        int t1 = Find(EG[i].u), t2 = Find(EG[i].v);
        if(t1 != t2)    //若不在同一棵树种则选择该边,合并两棵树
        {
            ans  = EG[i].w;
            parent[t1] = t2;
        }
    }
}

复杂度分析

当图的边数为e时,Kruskal算法所需的时间是O(eloge).

Prim算法

算法描述

设G = (V,E)是连通带权图,V = {1,2,…,n}。构造G的最小生成树,Prim算法的基本思想是:首先置S = {1},然后,只要S是V的真子集,就进行如下的贪心选择:选取满足条件i ∈S,j ∈V – S,且c[i][j]最小的边,将顶点j添加到S中。这个过程一直进行到S = V时为止。在这个过程中选取到的所有边恰好构成G的一棵最小生成树。

代码实现

/* Prim算法生成最小生成树  */
void MiniSpanTree_Prim(MGraph MG)
{
    int min, i, j, k;
    int adjvex[MAXVEX];/* 保存相关顶点的父亲节点 */
    int lowcost[MAXVEX];/* 保存相关顶点间边的权值 */
    lowcost[0] = 0;/* 初始化第一个权值为0,即v0加入生成树 */
    /* lowcost的值为0,在这里就是此下标的顶点已经加入生成树 */
    adjvex[0] = 0;/* 初始化第一个顶点下标为0 */
    cout << "最小生成树的边为:" << endl;
    for (i = 1; i < MG.numVertexes; i  )
    {
        lowcost[i] = MG.arc[0][i];/* 将v0顶点与之有边的权值存入数组 */
        adjvex[i] = 0;/* 初始化都为v0的下标 */
    }

    for (i = 1; i < MG.numVertexes; i  )
    {
        min = INFINITY; /* 初始化最小权值为∞, */

        j = 1;
        k = 0;

        while (j < MG.numVertexes)/* 循环全部顶点 */
        {
            if (lowcost[j] != 0 && lowcost[j] < min)/* 如果权值不为0且权值小于min */
            {
                min = lowcost[j];/* 则让当前权值成为最小值 */
                k = j;/* 将当前最小值的下标存入k */
            }

            j  ;
        }

        cout << "(" << adjvex[k] << ", " << k << ")" << "  "; /* 打印当前顶点边中权值最小的边 */
        lowcost[k] = 0;/* 将当前顶点的权值设置为0,表示此顶点已经完成任务 */

        for (j = 1; j < MG.numVertexes; j  )/* 循环所有顶点 */
        {
            /* 如果下标为k顶点各边权值小于此前这些顶点未被加入生成树权值 */
            if (lowcost[j] != 0 && MG.arc[k][j] < lowcost[j])
            {
                lowcost[j] = MG.arc[k][j];/* 将较小的权值存入lowcost相应位置 */
                adjvex[j] = k;/* 将k设为下标j的父亲节点 */
            }
        }
    }
    cout << endl;
}

复杂度分析

此算法的时间复杂度为O(n^2)

总结

当e = Ω(n^2)时,Kruskal算法比Prim算法差;
但当e = o(n^2)时,Kruskal算法比Prim算法好得多。

你可能感兴趣的:(算法,算法与数据结构)