Kruscal算法正确性证明

贪心算法:

贪心体现在两点

1.每次的选择一定是当前的最佳选择

2.只看眼前的利益,不把眼光放长远,不像回溯,贪心没有后悔药

所以贪心算法很容易陷入局部最优中

能用贪心算法解决的问题,应具有两个性质

1.最优子结构

官方解释:如果一个问题的最优解包含其子问题的最优解

一直理解不了这句话,举个例子

比如在这个图中,要从左下角到右上角,每次只能向上下左右四个方向移动,要求走过的路径和最大,可以验证得到这个结果的最佳路径中,从左下角的起点到右上角的终点路径中的每个点,其对应的路径和也是最大的

也就是对于从起点到终点的路所要求的最优,对于它的子问题,从起点到路径中的某点也是最优

这就是一个问题的最优解包含了子问题的最优解

2.贪心选择性

官方解释:所求问题的整体最优解可以通过一系列局部最优的选择来到达,即通过贪心选择来达到

贪心算法在图中的应用有最小生成树的Kruscal算法

这个怎么判断有没有贪心选择性有点难,记住常见的题型,比如活动排序、0-1背包、Kruscal算法等就行

Kruscal算法:

用不断加边的方式来得到最小生成树,Kruscal算法就是典型的贪心算法

1、每次选择最小的边

2、如果最小的边加入到已选的边以后不会产生回路,则在剩余的边中继续,否则去掉这条边

如何判断加入的边会产生回路呢?

初始每个顶点都赋予不同的标记,如果某条边被选中,就把其标记设为一样

这样在判断的时候,对于新加进来的边如果两个顶点标记不一样,说明它俩没关系,可以正常加进来,不会有回路

如果一样,则证明这两个点已经都在当前构造的树中,加入就会产生回路

如何证明Kruscal算法的正确性:

找了很多答案,才找到最容易理解的一种说法:

参考链接kruskal求得的生成树是最小生成树的证明_miss_minor的博客-CSDN博客

假如T是用Kruscal算法得到的生成树,U是这个图的最小生成树

如果能证明T的代价和U的代价一样大,那么就证明Kruscal得到的树就是最小生成树

我们用反证法

假如T和U不一样,那么肯定会有T中有,而U中没有的边

假如一共有K个这样的边,而e是这K个边中最小的一条边

我们从这条边e开始探讨

e不在U中,说明把e加到U中一定会形成环(因为U是最小生成树,定义)

我们取出形成的这个环中不在T中的最小的边f,(就是说f在U中,但不在T中,我把它取出来)

(其实环路中,除了e,其他边肯定不在T中,如果在T中,T就加不上e,因为会有回路)

现在分情况讨论e和f的值

如果说e和f代价是一样的,那么最后构造的T和U一定是代价一样的,也就是Kruscal算法得到的是最小生成树

如果说e<f,那么说明这个Kruscal得到的这个树要比最小生成树还要小,这是不可能的,所以这个之前的假设就不成立

如果e>f,那么T在选择的时候肯定不可能先选e啊,肯定捡小的先选

所以综上只有f=e这种情况可行

所以就证明了算法正确性

Kruscal算法实现:

图这部分算法,每次构造图的结构就很麻烦

对于最小生成树的两个算法以及最短路径算法都是用的邻接矩阵法(顺序存储结构)表示,而在拓扑排序和关键路径中用的是邻接表(即链表的形式)

顺序存储结构

关键核心是存储顶点的一维数组和存储边的二维数组,以及顶点数量vexnum和边的数量arcnum,这个顶点不需要重新构造顶点的结构体

邻接表存储结构

邻接表只需要存储顶点数组,但是顶点需要重新构造一个结构体,包括顶点数据以及该顶点链表中的一个指针,除了定义顶点外,由于存在链表,所以还需要定义链表中结点的结构体

平时Java用的较多,所以使用java来实现Kruscal算法

package Graph;

import java.util.Scanner;

public class Kruscal {
    //最小生成树 用邻接表存储
    //构造树时,需要有边的集合和点的集合
    public int arcnum;
    public int vexnum;
    edge[] edges;
    vertex[] vertices;
    //我们用的属于内部类,static+内部类相当于把它变成了外部类
     class edge{
        //三个内容,边的起点值,终点值,权值
        public int start;
        public int end;
        public int weight;

        public edge() {
        }

        public edge(int start, int end, int weight) {
            this.start = start;
            this.end = end;
            this.weight = weight;
        }
    }
    //每个顶点也用一个类来表示
     class vertex{
        //两个内容,边的值以及边所属集合
        //边所属集合是为了后期去判断加入该边是否会造成环
        int value;
        int sign;

        public vertex() {
        }

        public vertex(int value, int sign) {
            this.value = value;
            this.sign = sign;
        }
    }
//    构造无向网UDN
    public void createUDN()
    {
        Scanner in=new Scanner(System.in);
        this.vexnum=in.nextInt();
        this.vertices=new vertex[this.vexnum];
        this.arcnum=in.nextInt();
        this.edges=new edge[this.arcnum];
        for(int i=0;i<this.vexnum;i++)
        {
            vertex v=new vertex(in.nextInt(),i);
            vertices[i]=v;
        }
        for(int j=0;j<this.arcnum;j++)
        {
            edge e=new edge(in.nextInt(),in.nextInt(),in.nextInt());
            edges[j]=e;
        }
    }
    public void sort(edge[] edges)
    {
        //按照权值对这个数组进行排序,用最基本的冒泡
        for(int i=0;i<this.arcnum-1;i++)
        {
            for(int j=0;j+1<this.arcnum-i;j++)
            {
                if(edges[j].weight>edges[j+1].weight)
                {
                    //调换顺序,排序不仅要把权值对调,而是把整个对象对调
                    edge t=edges[j];
                    edges[j]=edges[j+1];
                    edges[j+1]=t;
                }
            }
        }
    }
    public edge[] kruscal()
    {
        //构造最小生成树,第一步对边进行排序
        //最小生成树的下标
        int index=0;
        sort(edges);
        edge[] minTree=new edge[vexnum-1];
        for(int i=0;i<this.arcnum;i++)
        {
            //选择最小的边,先判断这条边的两个顶点的标志一样不一样
            //注意存边时顶点值不是下标,而是真实值,我们要根据值找到它在顶点数组中的下标,从而看起始点的标志是不是一样
            int start=LocateVex(edges[i].start,vertices);
            int end=LocateVex(edges[i].end,vertices);
            int sign1=vertices[start].sign;
            int sign2=vertices[end].sign;
            //说明顶点存在,且加入这条边不会产生回路
            if(start!=-1&&end!=-1&&sign1!=sign2)
            {
                //将这条边加入到最小生成树中
                minTree[index++]=edges[i];
                //将与新加入的边的end顶点标记一样的全部改为和start一样的
                for(vertex v:vertices)
                {
                    if(v.sign==sign2)
                    {
                        v.sign=sign1;
                    }
                }
            }
            if(index==arcnum-1)
            {
                break;
            }
        }
        return minTree;

    }

    public int LocateVex(int start, vertex[] vertices) {
        for(int i=0;i<this.vexnum;i++)
        {
            if(vertices[i].value==start)
            {
                return i;
            }
        }
        return -1;
    }

    public void print(edge[] edges)
    {
        for(edge e:edges)
        {
            System.out.println(e.start+"->"+e.end+","+e.weight);
        }
    }


    public static void main(String[] args) {
        Kruscal k=new Kruscal();
        k.createUDN();
        edge[] minTree=k.kruscal();
        k.print(minTree);
    }

}

实现过程中需要注意,想要在main方法中新建内部类对象,必须把内部类改为static修饰

另外就是注意在根据对象的某个属性对对象排序时,不能只交换这个属性值,要把整个对象进行交换

如果在方法中传入数组,结果在方法中又重新定义了数组,这样无法实现修改数组的目的

比如说:

edge[] e=new edge[0];
test(e);

void test(edge[] e)
{
   e=new edge[5];
}

这样对e修改是不起作用的

输入:(原图)

 

6 10
1
2
3
4
5
6
1 2 6
1 3 1
1 4 5
2 3 5
2 5 3
3 4 5
3 5 6
3 6 4
4 6 2
5 6 6

输出:

1->3,1
4->6,2
2->5,3
3->6,4
2->3,5

 

得到的最小生成树如上

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值