Hdu KD-Graph(并查集、Kruscal算法)

1009 KD-Graph

题意分析:

       定义了一种叫KD-Graph的图,它有以下特点,它是无向带权图,n个点可以分成𝐾组,每组至少有一个点,对于分在同一组的𝑝和𝑞两个点,它们之间的路径中至少有一条路径的最大边权是小于等于𝐷的,对于不在同一组的𝑝和𝑞两点,不能找到这样一条路径,使得最大边权小于等于𝐷 。现在要我们求这个最小的𝐷 ,使得这个图成为一张KD图。

最终方法:

       我们使用Kruskal算法,将最小的且权值相等的所有边插入,如果当前的连通块数量等于𝐾,那么当前的𝐷就是答案。如果权值为𝐷的所有边插入前连通块数量大于𝐾而插入后连通块数量小于K,则没有答案,直接输入-1。

知识点:

并查集

主要支持两种操作:

  • 合并(Union):把两个不相交的集合合并为一个集合。
  • 查询(Find):查询两个元素是否在同一个集合中。

最简单版本的并查集代码

初始化:

int fa[MAXN];//用来存储父节点
void init(int n)
{
    for (int i = 1; i <= n; ++i)
        fa[i] = i;
}

查询:

int find(int x)
{
    if(fa[x] == x)
        return x;
    else
        return find(fa[x]);
}

访问到结点的根节点,要判断两个元素是否属于同一个集合,只需要看它们的根节点是否相同即可。

合并:

void merge(int i, int j)
{
    fa[find(i)] = find(j);
}

先找到两个集合的代表元素,然后将前者的父节点设为后者即可。

路径压缩(进阶版并查集查询)

一般并查集的问题:简单的并查集效率较低,合并时候可能会形成一条长链。

方法:只要我们在查询的过程中,把沿途的每个节点的父节点都设为根节点即可。下一次再查询时,我们就可以省很多事。

查询(路径压缩)

int find(int x)
{
    if(x == fa[x])
        return x;
    else{
        fa[x] = find(fa[x]);  //父节点设为根节点
        return fa[x];         //返回父节点
    }
}

按秩合并(进阶版并查集合并)

初始化(按秩合并)

void init(int n)
{
    for (int i = 1; i <= n; ++i)
    {
        fa[i] = i;
        rank[i] = 1;
    }
}

合并(按秩合并)

inline void merge(int i, int j)
{
    int x = find(i), y = find(j);    //先找到两个根节点
    if (rank[x] <= rank[y])
        fa[x] = y;
    else
        fa[y] = x;
    if (rank[x] == rank[y] && x != y)
        rank[y]++;                   //如果深度相同且根节点不同,则新的根节点的深度+1
}

深度相同,新的根节点深度要+1

总结:我们用一个数组rank[]记录每个根节点对应的树的深度(如果不是根节点,其rank相当于以它作为根节点的子树的深度)。一开始,把所有元素的rank(秩)设为1。合并时比较两个根节点,把rank较小者往较大者上合并。路径压缩和按秩合并如果一起使用,时间复杂度接近 O(n). 

Kruscal算法

#include<iostream>
using namespace std;
#define Max 10005
int par[Max];//父节点
int Rank[Max];//秩

typedef struct {
    int a, b, price;
}Node;
Node c[Max];
//并查集初始化
void Init(int n) {
    for (int i = 0; i < n; i++) {
        Rank[i] = 0;
        par[i] = i;
    }
}
int cmp(const void* a, const void* b) {
    return ((Node*)a)->price - ((Node*)b)->price;
}
//查询父节点
int find(int x) {
    if (x == par[x])
        return x;
    else {
        par[x] = find(par[x]);  //父节点设为根节点
        return par[x];         //返回父节点
    }
}
//合并
void Merge(int i, int j)
{
    int x = find(i);
    int y = find(j);
    if (Rank[x] < Rank[y]) {
        par[x] = y;
    }
    else {
        par[y] = x;
        if (Rank[x] == Rank[y]) Rank[x]++;
    }
}
int Kruskal(int n, int m) {
    int nEdge = 0, res = 0;
    //将边按照权值从小到大排序
    qsort(c, n, sizeof(c[0]), cmp);
    for (int i = 0; i < n && nEdge != m - 1; i++) {
        //判断当前这条边的两个端点是否属于同一棵树
        if (find(c[i].a) != find(c[i].b)) //两个结点不在一个并查集中
        {
            Merge(c[i].a, c[i].b);
            res += c[i].price;
            nEdge++;
        }
    }
    //如果加入边的数量小于m - 1,则表明该无向图不连通,等价于不存在最小生成树
    if (nEdge < m - 1) res = -1;
    return res;
}
int main()
{
	int n, m, ans;//n为边数,m为结点数,ans为最小权值
    while (cin >> n >> m&&n)//多个用例,n为0时候停止测试
    {
        Init(m);
        for (int i = 0; i < n; i++) {
            cin >> c[i].a >> c[i].b >> c[i].price;//输入边的两个结点和权值
            //将编号变为0~m-1(非必要)
            c[i].a--;
            c[i].b--;

        }
        ans = Kruskal(n, m);
        if (ans == -1)
            cout << "?" << endl;
        else
            cout << ans << endl;
    }
}

本题解

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <algorithm>
#include <cmath>
using namespace std;
const int maxn = 1e6 + 10;
int T, n, m, k, fa[maxn], now, ans;
struct da { int u, v, w; }q[maxn];
bool cmp(da aa, da bb) { return aa.w < bb.w; }
int find(int x)
{
	if (fa[x] == x) return x;
	fa[x] = find(fa[x]);
	return fa[x];
}
void solve()
{
	scanf_s("%d%d%d", &n, &m, &k);
	now = n; //当前有几个类
	ans = 0;
	//并查集初始化
	for (int i = 1; i <= n; i++) fa[i] = i;
	//输入边的左右结点和权值
	for (int i = 1; i <= m; i++) scanf_s("%d%d%d", &q[i].u, &q[i].v, &q[i].w);
	//将边按照权值排序
	sort(q + 1, q + m + 1, cmp);
	for (int i = 1; i <= m; i++)
	{
		if (q[i].w != q[i - 1].w) 
		{ 
			if (now == k)
		     { 
			    printf("%d\n", ans); 
		        return; 
		      } 
		}
		if (find(q[i].u) == find(q[i].v)) //左右结点属于同一个并查集
			continue;
		now--; //当前类的个数减一
		fa[find(q[i].u)] = find(q[i].v); //将u,v所属的集合合并为同一并查集
		ans = q[i].w;
	}
	printf("%d\n", now == k ? ans : -1);
}
int main()
{
	scanf_s("%d", &T);
	while (T--) solve();
	return 0;
}

附一道类似的题

CF632F Magic Matrix

#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
#define maxn 2505
int n;
int a[maxn][maxn];
int fa[maxn];

inline int read() {
	int a = 0; char c = getchar(), f = 1;
	for (; c < '0' || c>'9'; c = getchar())
		if (c == '-') f = -f;
	for (; '0' <= c && c <= '9'; c = getchar())
		a = (a << 3) + (a << 1) + (c ^ 48);
	return a * f;
}

struct node
{
	int u;
	int v;
	int w;
} q[maxn*maxn+5];

int find(int x)
{
	if (x == fa[x])
		return x;
	else {
		fa[x] = find(fa[x]);  //父节点设为根节点
		return fa[x];         //返回父节点
	}
}
void merge(int i, int j)
{
	fa[find(i)] = find(j);
}

bool cmp(node a, node b)
{
	return a.w < b.w;
}
void Kruscal()
{
	sort(q + 1, q + n * n + 1, cmp);
	for (int i = 1; i <= n; i++)
		fa[i] = i;
	for (int i = 1; i <= n * n;)//前n条边权值都为0(这句很重要不能省)
	{
		int r=0;//记录有几条相同的边
		while (q[i+r+1].w == q[i].w)
			++r;
		if(q[i].w!=0)//权值为0的边不用考虑
		for (int j = i; j <= i + r; j++)
		{
			if (find(q[j].u) == find(q[j].v))
			{
				printf("NOT MAGIC\n");
				return;
			}
		}
		for (int j = i; j <= i + r; j++)
		{
			merge(q[j].u, q[j].v);//先全部检查完再合并否则有影响
		}
		i +=r + 1;
	}
	printf("MAGIC\n");
}
int main() 
{
	n = read();
	int cnt = 1;
	for(int i=1;i<=n;i++)
		for (int j = 1; j <= n; j++)
		{
			a[i][j] = read();
			q[cnt].w = a[i][j];
			q[cnt].u = i;
			q[cnt].v = j;
			cnt++;
		}
	int flag = 0;
	for (int i = 1; i <= n; i++)
		for (int j = i; j <= n; j++)
		{
			if (i == j)
			{
				if (a[i][j] != 0)
					flag = 1;
				break;
			}
			else
			{
				if (a[i][j] != a[j][i])
					flag = 1;
				break;
			}
		}
	if(flag)
		printf("NOT MAGIC\n");
	else
	{		
		Kruscal();		
	}
	return 0;
}

注:这题一定要自己写个read()函数读入否则会超时

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值