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()函数读入否则会超时