无中生有之突击NOIP(8)——更多算法(一)

又开始了哈:

  1. 最小生成树(1)

    何为最小生成树呢?我们有很多路可以选,可有有很多种方案,然而如何达到一个最低值,而取消掉那些多余的路呢?这就是它的作用所在!
    

    算法分析:

    首先我们要让n个顶点的图连通,那么至少需要n-1条边。既然我们想让边的总长度之和最短,我们自然可以首先选择最短的边,直到选择了n-1条边为止,此时有n个图的边才得以连通。然后中间肯定会有重复连接的,比如1->2,2->3,1->3此时就会形成回路,就不是树了,所以,我们要保证:若两边已连通,且因为我们是从最小边开始找起的,为了防止回路,我们要跳过与其形成回路的边。回顾算法:我们发现想要判断两个顶点是否已经连通,这一点我们可以使用DFS和BFS来解决。但这样效率会很低,所以,我们可以利用上一张所学,将所有顶点放大品牌一个并查集里面,判断两个顶点是否连通,只需判断两个顶点是否在同一个集合即可,这样的时间复杂度仅为logN。这个算法的名字叫做Kruskal,音译过来是克鲁斯卡尔,我们总结一下该算法:首先按照边的权值进行从小到大排序,每次从剩余的边中选择权值较小且边的两个顶点不在同一个集合内的边。
    代码如下:
    
#include<stdio.h>
struct edge
{
    int u;
    int v;
    int w;
    };//为了方便排序这里创建了一个结构体来判断边的关系
struct edfe e[10];//数组大小根据实际情况来设置,要比m的最大值大1
int n,m;
int f[7]={0},sum=0;count=0;
//这是并查集需要用到的变量,f数组的大小根据实际情况来设置,要比n的最大值大1
void quicksort(int left,int right)
{
    int i,j;
    struct edge t;
    if(lefe>right)
    return;
    i=left;
    j=right;
    while(i!=j)
    {
        while(e[j].w>e[left].w && i<j)
            j--;
        while(e[j].w<e[left.w &&i<j)
            i++;
        if(i<j)
        {
            t=e[i];
            e[i]=e[j];
            e[j]=t;
        }
    }
    //最终将基准数归为,将left和i互换
    //t=e[left];
    e[left]=e[i];
    e[j]=t;
    quicksort(left,i-1);//继续吧处理左边的,这里是一个递归的过程
    quicksort(i=1,right);//继续处理右边的,这里是一个递归的过程
    return;
}

//并查集寻找祖先的函数
int getf(int v)
{
    if(f[v]==v)
        return; v;
    else
    {
        //这里是路径压缩;
        f[v]=getf(f[v]);
        return f[v];
    }
}
//并查集合并两个子集合的函数
int merge(int v,int u)
{
    int t1,t2;
    t1=getf(v);
    t2=getf(u);
    if( t1!=t2 )//判断两个点是否在同一个集合中
    {
        f[t2]=t1;
        return 1;
    }
    return 0;
}

//从此处阅读哦:)
int main()
{
    int i;
    //读入n和m,n表示顶点的个数,m表示边的条数
    scanf("%d %d",&n,&m);
    //读入边,这里用一个结构体存储边的关系
    for(i=1;i<=m;i++)
        scanf("%d %d %d",&e[i].u,&e[i].v,&e[i].w);
    quicksort(1,m);//根据权值从小到大对边进行快速排序

    //并查集初始化
    for(i=1;i<=n;i++)
        f[i]=i;
    //Kruskal算法核心部分
    for(i=1;i<=m;i++)
    {
        //判断一条边的两个顶点是否已经连通,即判断是否已经在同一个集合中
        if( merge(e[i].u,e[i].v))//如果目前尚未连通,则选用这条边
        {
            count++;
            sum=sum+e[i].w;
        }
        if( count == n-1 )//直到选用了n-1条边之后退出循环
            break;
    }
    printf("%d",sum);
    getchar();getchar();
    return 0;
}

最终时间复杂度为O(MlogM)

  1. 最小生成树(2)
    上节说过,最小生成树里,要用n-1条边把n个顶点连接起来,那么每个顶点都必须至少有一条边与它相连,(要不然这个店就是一个孤立的点了)。那我随便选一个顶点开始,反正最终每个顶点都是要选到的,看看这个顶点有哪些边,在它的边中找一个最短的,并且使其相连,然后我们再找其他的顶点,离着它们越近越好,然后我们枚举他们这两个顶点所有的边,看看哪些边可以连接到没有被选中的顶点,并且边越短越好,通过这条边就可以把其他的一个点连在了一起了,相信你已经看出来了眉目,不错,其实就是继续采用刚才的方法去查看一条最短边可以连接到没有被选中的顶点,照此方法,一共重复操作n-1次,直到所有的顶点都选中,算法结束这个方法就像一个生成树在慢慢长大一样,从一个顶点长到了n个顶点。

总结一下这个算法,我们将图中所有的顶点分为两类;树顶点,(已被选入生成树的顶点)和非树顶点(还未被选入生成树的顶点)。首先选择任意一个顶点加入生成树(你可以理解为是生成树的根)。接下来找到一条边添加到生成树,这需要枚举每一个数顶点到每一个非树顶点所有的边,然后找到一个最短边加入到生成树,照此方法重复操作n-1次,直到所有顶点都加入生成树中。
z在实现这个算法的时候,比较麻烦和费事的是,如何找到下一个添加到生成树的边,但我们学习了迪杰斯特拉算法,该算法的思想是用一个dis数组记录各个点到原点的距离,然后每次扫描数组dis,从中选出离顶点最近的顶点,(假设这个顶点为j),看看该顶点的所有边能否更新源点到各个顶点的距离,即如果dis[k]>dis[j]+e[j]k则更新dis[k]=dis[j]+e[j][k]。在这里我们也可以使用类似的方法,但有一点小小的变化。用数组dis来记录生成树到各个顶点的距离,也就是说现在记录的最短距离,不是每个顶点到1号顶点的最短距离,而是每个顶点到任意一个树顶点(已被选入生成树的顶点)的最短距离,即如果dis[k]>e[j]k则更新dis[k]=e[j][k]。因此在计算更新最短路径的时候不再需要加上dis[j]了,因为我们的目标并不是非得靠近1号顶点,而是靠近生成树就可以。也就是说只需要靠近“生成树”中任意一个树顶点就可以,也就是说只需要靠近生成树·中任意一个树顶点就行。
这种方法是不是很巧妙,我们再来整理一下这个算法的流程
1、从任意一个顶点开始构造生成树,假设从1号顶点开始,首先将1号顶点加入生成树中,然后用一个一维数组book来记录那些顶点加入了生成树。
2、用数组dis记录生成树到各顶点的距离。最初生成树只有1号顶点,有直连边时,数组dis中存储的就是1号顶点到该顶点的边的权值,没有直连边时就是无穷大,即初始化dis数组。
3、从数组dis中选出离生成树最近的顶点,(假设这个顶点为j)加入到生成树中(即在数组dis中找到最小值)。再以j为中间点,更新生成树到每一个非树顶点的距离(就是松弛啦)即如果dis[k]>e[j][k]则更新dis[k]=e[j][k]。
4、重复第3步,直到生成树中有n个顶点为止。

#include<stdio.h>
int main()
{
    int n,m,i,j,k,min,t1,t2,t3;
    int e[7][7],dis[7],book[7]={0};//对book数组进行了初始化
    int inf=99999999;//用inf存储一个认为无穷大的值。
    int count=0,sum=0;//count用来记录生成树中顶点的个数,sum用来存储路径之和。
    //读入n和m,n表示顶点个数,m表示边的条数。
    scanf("%d %d",&n,&m);
    //初始化
    for(i=1;i<=n;i++)
        for(j=1;j<=n;j++)
            if(i==j) e[i][j]=0;
                else e[i][j]=inf;
    //开始读入边
    for(i=1;i<=m;i++)
    {
        scanf("%d %d %d",&t1,&t2,&t3);
        //注意这里是无向图,所以我们要反向再存储一遍。
        e[t1][t2]=t3;
        e[t2][t1]=t3;
    }
    //初始化dis数组,这里是1号ID那个I顶点到各个顶点的初始距离,因为当前的生成树中只有一个1号顶点。
    for(i=1;i<=n;i++)
        dis[i]=e[1][i];
    //Prim核心部分开始
    //将1号顶点加入生成树
    book[1]=1;//这里用book来标记一个顶点是否已经加入了生成树
    count++;
    while(count<n)
    {
        min=inf;
        for(i=1;i<=n;i++)
        {
            if(book[i] == 0 && dis[i]<min)
            {min=dis[i];j=i;}
        }
        book[j]=1;count++;sum=sum+dis[j];
        //扫描当前顶点j所有的边,再以j为中间点,更新生成树到每一个非树顶点的距离
        for(k=1;k<=n;k++)
        {
            if(book[k]==0 && dis[k]>e[j][k])
                dis[k]=e[j][k];
        }
    }
    printf("%d",sum);
    getchar();getchar();
    return 0;
}       
该算法时间复杂度为O(N²)。如果借助堆,每次选边的时间复杂度是O(longM),然后使用邻接表存储图的话,整个算法的时间复杂度会降低到O(MlogN)。那么如何使用堆来优化呢?
数组dis用来记录生成树到各个顶点的距离,数组h是一个最小堆,堆里面存储的是顶点编号。请注意这里并不是按照顶点编号的大小来建立最小堆的,而是按照顶点在数组dis中所对应的值来建立最小堆。此外还需要要一个数组pos来记录每个顶点在最小堆中的位置。例如上图中,左边最小堆的圆圈中存储的是顶点编号,圆圈右下角的数是该顶点(圆圈里面的数)到生成树的最短距离,即数组dis中存储的值,下面代码来了。
#include<stdio.h>
int dis[7],book[7]={0};//book数组用来记录哪些项点已经放入生成树
int h[7],pos[7],size;//h用来保存堆,pos用来存储每个顶点在堆中的位置,size为堆的大小
void swap(int x,int y)
{
    int t;
    t=h[x];
    h[x]=h[y];
    h[y]=t;
    //同步更新pos
    t=pos[h[x]];
    pos[h[x]]=pos[h[y]];
    pos[h[y]]=t;
    return;
}
//向下调整函数
void siftdown (int i)//传入一个需要向下调整的结点编号
{
    int t,flag=0;//flag用来标记是否需要继续向下调整
    while( i*2<=size && flag==0)
    {
        //比较i和它左儿子i*2在dis中的值,并用t记录值较小的结点编号
        if( dis[h[i]]>dis[h[i*2]])
            t=i*2;
        else
            t=i;
        //如果它有右儿子,再对右儿子进行讨论
        if(i*2+1<=size)
        {
            //如果右儿子的值更小,更新较小的结点编号
            if(dis[h[t]] > dis[h[i*2+1]])
                t=i*2+1;
        }
        //如果发现最小的结点编号不是自己,说明子节点中有比父节点跟小的
        if(t!=i)
        {
            swap(t,i);
            i=t;
        }
        else
            flat=1;
    }
    return;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值