Kruskal算法的各种尝试

Kruskal算法的各种尝试

  • 生成树

在图论的数学领域中,如果连通图 G的一个子图是一棵包含G 的所有顶点的树,则该子图称为G的生成树(SpanningTree)。生成树是连通图的包含图中的所有顶点的极小连通子图。图的生成树不惟一。从不同的顶点出发进行遍历,可以得到不同的生成树。—鲁迅

  • 常用的生成树算法

DFS生成树、BFS生成树、PRIM 最小生成树和Kruskal最小生成树算法。

  • 最小生成树

对于连通的带权图(连通网)G,其生成树也是带权的。生成树T各边的权值总和称为该树的权,记作:

W ( T ) = ∑ ( u , v ) ∈ T , E w ( u , v ) W(T)=\sum_{(u,v)\in T,E}w(u,v) W(T)=(u,v)T,Ew(u,v)

其中,TE表示T的边集, w ( u , v ) w(u,v) w(uv)表示边 ( u , v ) (u,v) (uv)的权。权最小的生成树称为G的最小生成树(Minimum SpannirngTree)。最小生成树可简记为MST。

  • Kruskal算法思路

  1. 把所有的边按照权值先从小到大排序
  2. 按照顺序选取每条边
  3. 利用并查集判断该边的两个端点是否属于同一集合。若不是,则合并,直到所有的点都属于同一个集合为止。

以上可以看出主要可优化的点在与排序和并查集上。而作为学渣,我只会课上给的模板的简化版,这次最多也就改改函数返回值。


以下是做题思考的过程

例题:洛谷P3366 【模板】最小生成树

一、懒人STL法

对于排序我们可以直接用C++STL库自带的sort解决

  • sort版
#include <algorithm>
#include <iostream>
using namespace std;
#define MMAX (int)2e5 + 5
#define NMAX (int)5e3 + 5
// #define MMAX 20
// #define NMAX 20

struct Edge
{
    int u, v, w; //两边结点、长度

    // 排序函数用
    bool operator<(const Edge &temp) const
    {
        return w < temp.w;
    }
} edge[MMAX];
int pa[NMAX];
int N, M, ans, m;

//并查集部分
void Makeset() //初始化
{
    for (int i = 0; i < N; i++) //父结点指向自己
        pa[i] = i;

    return;
}

int Find(int x) //找根结点
{
    int rt = x, temp;

    // 找根结点
    while (pa[rt] != rt)
        rt = pa[rt];

    // 当x不是根结点,沿x向上的结点接到根结点实现路径压缩
    while (pa[x] != rt)
    {
        temp = pa[x];
        pa[x] = rt;
        x = temp;
    }

    return rt;
}

int Union(int x, int y)
{
    x = Find(x), y = Find(y);

    if (x == y)
        return 0;

    if (x < y)
        swap(x, y); //交换操作对象,省代码

    pa[x] = y; //根结点总是值较小的

    return 1;
}

void Kuscal()
{
    // 初始化
    Makeset();

    sort(edge, edge + M);

    // 遍历边
    for (int i = 0; i < M && m < N - 1; i++)
    {
        // 该边两端未联通
        if (Union(edge[i].u, edge[i].v))
        {
            ans += edge[i].w;
            m++;
        }
    }

    return;
}

int main()
{

    // freopen("test.in", "r", stdin);
    ios::sync_with_stdio(false);

    //结点、边
    cin >> N >> M;

    for (int i = 0; i < M; i++)
        cin >> edge[i].u >> edge[i].v >> edge[i].w;

    Kuscal();

    if (m == N - 1)
        cout << ans << endl;
    else
        cout << "orz" << endl;

    // fclose(stdin);

    return 0;
}

评判结果是这样的
sort

同时优先队列也能满足这个要求,于是我也试了一下

  • queue版
#include <iostream>
#include <queue>
using namespace std;
#define MMAX (int)2e5 + 5
#define NMAX (int)5e3 + 5
// #define MMAX 20
// #define NMAX 20

struct Edge
{
    int u, v, w; //两边结点、长度

    // 优先队列用
    bool operator<(const Edge &temp) const
    {
        return w > temp.w;
    }
} edge;
int pa[NMAX];
int N, M, ans, m;
priority_queue<Edge> q;

//并查集部分
void Makeset() //初始化
{
    for (int i = 0; i < N; i++) //父结点指向自己
        pa[i] = i;

    return;
}

int Find(int x) //找根结点
{
    int rt = x, temp;

    // 找根结点
    while (pa[rt] != rt)
        rt = pa[rt];

    // 当x不是根结点,沿x向上的结点接到根结点实现路径压缩
    while (pa[x] != rt)
    {
        temp = pa[x];
        pa[x] = rt;
        x = temp;
    }

    return rt;
}

bool Union(int x, int y)
{
    x = Find(x), y = Find(y);

    if (x == y)
        return 0;

    if (x < y)
        swap(x, y); //交换操作对象,省代码

    pa[x] = y; //根结点总是值较小的

    return 1;
}

void Kuscal()
{
    // 初始化
    Makeset();
    ans = 0;

    while (!q.empty() && m < N - 1)
    {
        // 该边两端未联通
        if (Union(q.top().u, q.top().v))
        {
            ans += q.top().w;
            m++;
        }
        q.pop();
    }

    return;
}

int main()
{

    // freopen("test.in", "r", stdin);
    ios::sync_with_stdio(false);

    //结点、边
    cin >> N >> M;

    for (int i = 0; i < M; i++)
    {
        cin >> edge.u >> edge.v >> edge.w;
        q.push(edge);
    }
    Kuscal();

    if (m == N - 1)
        cout << ans << endl;
    else
        cout << "orz" << endl;

    // fclose(stdin);

    return 0;
}

评判结果:
优先队列比上面还差一些

听说手写可能会更快?好奇的我尝试了一下

二、折腾手写法

#include <cstdlib>
#include <ctime>
#include <iostream>
using namespace std;
#define MMAX (int)2e5 + 5
#define NMAX (int)5e3 + 5
// #define MMAX (int)1e2
// #define NMAX (int)1e2

struct Edge
{
    int u, v, w; //两边结点、长度
} edge[MMAX];
int pa[NMAX];
int N, M, ans, m;

//并查集部分
void Makeset() //初始化
{
    for (int i = 0; i < NMAX; i++) //父结点指向自己
        pa[i] = i;

    return;
}

int Find(int x) //找根结点
{
    int rt = x, temp;

    // 找根结点
    while (pa[rt] != rt)
        rt = pa[rt];

    // 当x的父结点不是根结点,沿x向上的结点接到根结点实现路径压缩
    while (pa[x] != rt)
    {
        temp = pa[x];
        pa[x] = rt;
        x = temp;
    }

    return rt;
}

int Union(int x, int y)
{
    x = Find(x), y = Find(y);

    if (x == y)
        return 0;

    if (x < y)
        swap(x, y); //交换操作对象,省代码

    pa[x] = y; //根结点总是值较小的

    return 1;
}

// 手写快排
void QuickSort(int left, int right)
{
    int i = left, j = right, pivot = rand() % (right - left + 1) + left;
    int piv = edge[pivot].w; //随机化

    while (i < j)
    {
        while (edge[i].w < piv) //从左找大于等于基准值的点
        {
            i++;
        }
        while (edge[j].w > piv) //从右找小于等于基准值的点
        {
            j--;
        }

        if (i <= j) //交换元素
        {
            swap(edge[i], edge[j]);

            i++, j--; //确保i、j不等
        }
    }

    if (left < j)
    {
        QuickSort(left, j);
    }

    if (i < right)
    {
        QuickSort(i, right);
    }

    return;
}

void Kuscal()
{

    // 初始化
    Makeset();

    //结点、边
    cin >> N >> M;

    for (int i = 0; i < M; i++)
        cin >> edge[i].u >> edge[i].v >> edge[i].w;

    QuickSort(0, M - 1);

    // 遍历边
    for (int i = 0; i < M && m < N - 1; i++)
    {
        // 该边两端未联通
        if (Union(edge[i].u, edge[i].v))
        {
            ans += edge[i].w;
            m++;
        }
    }

    return;
}

int main()
{

    // freopen("test.in", "r", stdin);
    ios::sync_with_stdio(false);

    Kuscal();
    if (m == N - 1)
        cout << ans;
    else
        cout << "orz";

    // fclose(stdin);

    return 0;
}

评判结果
手写快排
比sort函数略逊一筹

三、准备手写又停下来想一想法

众所周知,优先队列是利用堆实现的,于是我打算按照手写快排与sort函数比较的想法,手写堆排来与优先队列比较,在我把以前写好的堆排复制粘贴前, 但是我想起之前调试时发现优先队列并不是把所有的数排好序放到队列里的,而是建一个小根堆,把顶部取出后再调整堆,这样对于当前状况能够得到想要的值,又能为下次的排序减少移动次数,这种大致有序的情况正是优化的地方。于是我就在普通的堆排序中建好小根堆和调整堆操作之间取出需要的边,只要建好最小生成树就可以跳出,不必为了所有的边都有序而浪费时间。

#include <algorithm>
#include <iostream>
using namespace std;
#define NMAX (int)5e3 + 5
#define MMAX (int)2e5 + 5
// #define MMAX 20
// #define NMAX 20

struct Edge
{
    int u, v, w; //两边结点、长度
} edge[MMAX];
int pa[NMAX];
int N, M, ans, m;

//并查集部分
void Makeset() //初始化
{
    for (int i = 0; i < N; i++) //父结点指向自己
        pa[i] = i;

    return;
}

int Find(int x) //找根结点
{
    int rt = x, temp;

    // 找根结点
    while (pa[rt] != rt)
        rt = pa[rt];

    // 当x不是根结点,沿x向上的结点接到根结点实现路径压缩
    while (pa[x] != rt)
    {
        temp = pa[x];
        pa[x] = rt;
        x = temp;
    }

    return rt;
}

int Union(int x, int y)
{
    x = Find(x), y = Find(y);

    if (x == y)
        return 0;

    if (x < y)
        swap(x, y); //交换操作对象,省代码

    pa[x] = y; //根结点总是值较小的

    return 1;
}

// 手写堆排
void HeapAdjust(int start, int end)
{
    int parent = start;

    for (int child = start << 1; child <= end; child = child << 1) //假设左孩子key较小
    {
        if (child < end && edge[child].w > edge[child | 1].w) //存在右孩子且小于左孩子
            ++child;                                          //选右孩子

        if (edge[parent].w <= edge[child].w) //父结点比较大的孩子还小
            break;

        swap(edge[parent], edge[child]);

        parent = child; //准备循环孩子的孩子
    }

    return;
}

void Kuscal()
{
    // 初始化
    Makeset();

    //从最后一个父结点往前调整成为小根堆,根据完全二叉树性质前面都是父结点
    for (int i = M / 2; i > 0; i--)
        HeapAdjust(i, M);

    // 遍历边
    for (int i = M; i > 1 && m < N - 1; i--)
    {
        // 该边两端未联通
        if (Union(edge[1].u, edge[1].v))
        {
            ans += edge[1].w;
            m++;
        }

        swap(edge[1], edge[i]);
        HeapAdjust(1, i - 1);
    }

    return;
}

int main()
{

    // freopen("test.in", "r", stdin);
    ios::sync_with_stdio(false);

    //结点、边
    cin >> N >> M;

    for (int i = 1; i <= M; i++)
        cin >> edge[i].u >> edge[i].v >> edge[i].w;

    Kuscal();

    if (m == N - 1)
        cout << ans << endl;
    else
        cout << "orz" << endl;

    // fclose(stdin);

    return 0;
}

评判结果
堆优化
果不其然,节省了大量移动次数


以下是本人的心路历程

在这里插入图片描述
前面各种TLE是因为并查集Find函数找根结点时打错了一个变量导致死循环,但是题目给的数据又不会出错,所以一直在怀疑人生

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值