生成树和并查集

文章讲述了三道编程竞赛题目,分别是关于奶酪中的空洞连接问题,猴子跳跃觅食问题以及灾后重建的最短路径问题。解决方案涉及并查集判断球体相交,以及Floyd算法求最短路径。这些问题展示了并查集和最短路径算法在解决实际问题中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

[NOIP2017 提高组] 奶酪

题目背景

NOIP2017 提高组 D2T1

题目描述

现有一块大奶酪,它的高度为 h h h,它的长度和宽度我们可以认为是无限大的,奶酪中间有许多半径相同的球形空洞。我们可以在这块奶酪中建立空间坐标系,在坐标系中,奶酪的下表面为 z = 0 z = 0 z=0,奶酪的上表面为 z = h z = h z=h

现在,奶酪的下表面有一只小老鼠 Jerry,它知道奶酪中所有空洞的球心所在的坐标。如果两个空洞相切或是相交,则 Jerry 可以从其中一个空洞跑到另一个空洞,特别地,如果一个空洞与下表面相切或是相交,Jerry 则可以从奶酪下表面跑进空洞;如果一个空洞与上表面相切或是相交,Jerry 则可以从空洞跑到奶酪上表面。

位于奶酪下表面的 Jerry 想知道,在不破坏奶酪的情况下,能否利用已有的空洞跑 到奶酪的上表面去?

空间内两点 P 1 ( x 1 , y 1 , z 1 ) P_1(x_1,y_1,z_1) P1(x1,y1,z1) P 2 ( x 2 , y 2 , z 2 ) P2(x_2,y_2,z_2) P2(x2,y2,z2) 的距离公式如下:

d i s t ( P 1 , P 2 ) = ( x 1 − x 2 ) 2 + ( y 1 − y 2 ) 2 + ( z 1 − z 2 ) 2 \mathrm{dist}(P_1,P_2)=\sqrt{(x_1-x_2)^2+(y_1-y_2)^2+(z_1-z_2)^2} dist(P1,P2)=(x1x2)2+(y1y2)2+(z1z2)2

输入格式

每个输入文件包含多组数据。

第一行,包含一个正整数 T T T,代表该输入文件中所含的数据组数。

接下来是 T T T 组数据,每组数据的格式如下: 第一行包含三个正整数 n , h , r n,h,r n,h,r,两个数之间以一个空格分开,分别代表奶酪中空洞的数量,奶酪的高度和空洞的半径。

接下来的 n n n 行,每行包含三个整数 x , y , z x,y,z x,y,z,两个数之间以一个空格分开,表示空洞球心坐标为 ( x , y , z ) (x,y,z) (x,y,z)

输出格式

T T T 行,分别对应 T T T 组数据的答案,如果在第 i i i 组数据中,Jerry 能从下表面跑到上表面,则输出 Yes,如果不能,则输出 No

样例 #1

样例输入 #1

3 
2 4 1 
0 0 1 
0 0 3 
2 5 1 
0 0 1 
0 0 4 
2 5 2 
0 0 2 
2 0 4

样例输出 #1

Yes
No
Yes

提示

【输入输出样例 1 1 1 说明】

第一组数据,由奶酪的剖面图可见:

第一个空洞在 ( 0 , 0 , 0 ) (0,0,0) (0,0,0) 与下表面相切;

第二个空洞在 ( 0 , 0 , 4 ) (0,0,4) (0,0,4) 与上表面相切;

两个空洞在 ( 0 , 0 , 2 ) (0,0,2) (0,0,2) 相切。

输出 Yes

第二组数据,由奶酪的剖面图可见:

两个空洞既不相交也不相切。

输出 No

第三组数据,由奶酪的剖面图可见:

两个空洞相交,且与上下表面相切或相交。

输出 Yes

【数据规模与约定】

对于 20 % 20\% 20% 的数据, n = 1 n = 1 n=1 1 ≤ h 1 \le h 1h r ≤ 1 0 4 r \le 10^4 r104,坐标的绝对值不超过 1 0 4 10^4 104

对于 40 % 40\% 40% 的数据, 1 ≤ n ≤ 8 1 \le n \le 8 1n8 1 ≤ h 1 \le h 1h r ≤ 1 0 4 r \le 10^4 r104,坐标的绝对值不超过 1 0 4 10^4 104

对于 80 % 80\% 80% 的数据, 1 ≤ n ≤ 1 0 3 1 \le n \le 10^3 1n103 1 ≤ h , r ≤ 1 0 4 1 \le h , r \le 10^4 1h,r104,坐标的绝对值不超过 1 0 4 10^4 104

对于 100 % 100\% 100% 的数据, 1 ≤ n ≤ 1 × 1 0 3 1 \le n \le 1\times 10^3 1n1×103 1 ≤ h , r ≤ 1 0 9 1 \le h , r \le 10^9 1h,r109 T ≤ 20 T \le 20 T20,坐标的绝对值不超过 1 0 9 10^9 109

思路

刚拿到这道题目的时候,我啥想法都木有呜呜呜呜

(我说怎么哪里见过,这和河北省省赛的一道题目怎么这么像)

用并查集解决这道题真的太妙了.

首先让每一个球都和顶部和底部进行判读,看看是否相交,把顶部和底部作为父节点

然后把每一个球都两两判断,判读是否相交.

最后判断顶部和底部的父节点是否相同即可

(用并查集真的太妙了,感觉用其他的方式真的很难写)

代码实现

#include <algorithm>
#include<iostream>
using namespace std;
typedef long long ll;
ll n,h,r;
int fa[1005]={0};
ll  x[1005]={};
ll  y[1005]={};
ll  z[1005]={};
ll  dis(int u,int v)//求出两点(球心)之间的距离
{
    return (x[u]-x[v])*(x[u]-x[v])+(y[u]-y[v])*(y[u]-y[v])+(z[u]-z[v])*(z[u]-z[v]);
}
int find(int k)//寻找父节点
{
    return k==fa[k]?k:(fa[k]=find(fa[k]));
}
void merge(int i,int j)//合并节点
{
    fa[find(i)]=find(j);
}
int main()
{
    int T;
    cin>>T;
    while(T--)
    {
        cin>>n>>h>>r;
        for(int i=1;i<=n;++i)
        {
            cin>>x[i]>>y[i]>>z[i];
        }
        for(int i=1;i<=n;i++) fa[i]=i;
        fa[1001]=1001,fa[1002]=1002;//设置顶部和底部的父节点
        int cnt=0;
        for(int i=1;i<=n;++i)
        {
            if(z[i]<=r)merge(i, 1001);//把球的父节点设置为底部
            if(z[i]+r>=h)merge(i,1002);//把球的父节点设置为顶部
        }
        bool f1=0;
        for(int i=1;i<=n;++i)
        {
            for(int j=i+1;j<=n;++j)
            {
                if(dis(i,j)<=4*r*r) merge(i,j);//合并球心距离小于2*r的球,并用了路径压缩
            }
        }
        if(find(1001)==find(1002)) cout<<"Yes"<<endl;//如果顶部和底部的父节点相同
        else cout<<"No"<<endl;
    }
    return 0;
}

[HAOI2006]聪明的猴子

题目描述

在一个热带雨林中生存着一群猴子,它们以树上的果子为生。昨天下了一场大雨,现在雨过天晴,但整个雨林的地表还是被大水淹没着,部分植物的树冠露在水面上。猴子不会游泳,但跳跃能力比较强,它们仍然可以在露出水面的不同树冠上来回穿梭,以找到喜欢吃的果实。

现在,在这个地区露出水面的有N棵树,假设每棵树本身的直径都很小,可以忽略不计。我们在这块区域上建立直角坐标系,则每一棵树的位置由其所对应的坐标表示(任意两棵树的坐标都不相同)。

在这个地区住着的猴子有M个,下雨时,它们都躲到了茂密高大的树冠中,没有被大水冲走。由于各个猴子的年龄不同、身体素质不同,它们跳跃的能力不同。有的猴子跳跃的距离比较远(当然也可以跳到较近的树上),而有些猴子跳跃的距离就比较近。这些猴子非常聪明,它们通过目测就可以准确地判断出自己能否跳到对面的树上。

【问题】现已知猴子的数量及每一个猴子的最大跳跃距离,还知道露出水面的每一棵树的坐标,你的任务是统计有多少个猴子可以在这个地区露出水面的所有树冠上觅食。

输入格式

输入文件monkey.in包括:

第1行为一个整数,表示猴子的个数M(2<=M<=500);

第2行为M个整数,依次表示猴子的最大跳跃距离(每个整数值在1–1000之间);

第3行为一个整数表示树的总棵数N(2<=N<=1000);

第4行至第N+3行为N棵树的坐标(横纵坐标均为整数,范围为:-1000–1000)。

(同一行的整数间用空格分开)

输出格式

输出文件monkey.out包括一个整数,表示可以在这个地区的所有树冠上觅食的猴子数。

样例 #1

样例输入 #1

4
 1 2 3 4
6
0 0
1 0
1 2
-1 -1
-2 0
2 2

样例输出 #1

3

提示

【数据规模】

对于40%的数据,保证有2<=N <=100,1<=M<=100

对于全部的数据,保证有2<=N <= 1000,1<=M=500

感谢@charlie003 修正数据

思路

刚开始并没有想到~~(学会)~~用并查集写生成树,所以就用了二分+队列写了这道题,也能过(坏笑.jpg)

主要的目标就是求出能跨越每一棵树的最短距离.

代码实现

#include<bits/stdc++.h>
#include <climits>
using namespace std;
const int maxn=100005;
int step[maxn]={};
bool vis[maxn]={},flag=0;
int m,n;
struct loc
{
    int x,y;
}Loc[maxn];
int dis(loc a,loc b)
{
    return (a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y);
}
void bfs(int mid)
{
    int total=1;
    queue<loc> q;
    q.push(Loc[1]);
    vis[1]=1;
    while(q.empty()==0)
    {
        loc TOP=q.front();
        q.pop();
        for(int i=1;i<=n;i++)
        {
            if(vis[i]==1) continue;
            if(dis(TOP,Loc[i])<=mid*mid)
            {
                q.push(Loc[i]);
                vis[i]=1;
                total++;
            }
        }
    }
    if(total==n) flag=1;
}
int main()
{
    cin>>m;
    int l=INT_MAX,r=-INT_MAX;
    for(int i=1;i<=m;i++) 
    {
        cin>>step[i];
        l=min(step[i],l);
        r=max(step[i],r);
    }
    cin>>n;
    for(int i=1;i<=n;i++) cin>>Loc[i].x>>Loc[i].y;
    int mid;
    while(l<=r)
    {
        memset(vis,0,sizeof(vis));
        flag=0;
        mid=(l+r)/2;
        bfs(mid);
        if(flag) r=mid-1;
        else l=mid+1;
    }
    int cnt=0;
    for(int i=1;i<=m;i++) if(l<=step[i])cnt++;
    cout<<cnt<<endl;
    return 0;
}

灾后重建

题目背景

B 地区在地震过后,所有村庄都造成了一定的损毁,而这场地震却没对公路造成什么影响。但是在村庄重建好之前,所有与未重建完成的村庄的公路均无法通车。换句话说,只有连接着两个重建完成的村庄的公路才能通车,只能到达重建完成的村庄。

题目描述

给出 B 地区的村庄数 N N N,村庄编号从 0 0 0 N − 1 N-1 N1,和所有 M M M 条公路的长度,公路是双向的。并给出第 i i i 个村庄重建完成的时间 t i t_i ti,你可以认为是同时开始重建并在第 t i t_i ti 天重建完成,并且在当天即可通车。若 t i t_i ti 0 0 0 则说明地震未对此地区造成损坏,一开始就可以通车。之后有 Q Q Q 个询问 ( x , y , t ) (x,y,t) (x,y,t),对于每个询问你要回答在第 t t t 天,从村庄 x x x 到村庄 y y y 的最短路径长度为多少。如果无法找到从 x x x 村庄到 y y y 村庄的路径,经过若干个已重建完成的村庄,或者村庄 x x x 或村庄 y y y 在第 t t t 天仍未重建完成,则需要返回 -1

输入格式

第一行包含两个正整数 N , M N,M N,M,表示了村庄的数目与公路的数量。

第二行包含 N N N个非负整数 t 0 , t 1 , … , t N − 1 t_0, t_1,…, t_{N-1} t0,t1,,tN1,表示了每个村庄重建完成的时间,数据保证了 t 0 ≤ t 1 ≤ … ≤ t N − 1 t_0 ≤ t_1 ≤ … ≤ t_{N-1} t0t1tN1

接下来 M M M行,每行 3 3 3个非负整数 i , j , w i, j, w i,j,w w w w为不超过 10000 10000 10000的正整数,表示了有一条连接村庄 i i i与村庄 j j j的道路,长度为 w w w,保证 i ≠ j i≠j i=j,且对于任意一对村庄只会存在一条道路。

接下来一行也就是 M + 3 M+3 M+3行包含一个正整数 Q Q Q,表示 Q Q Q个询问。

接下来 Q Q Q行,每行 3 3 3个非负整数 x , y , t x, y, t x,y,t,询问在第 t t t天,从村庄 x x x到村庄 y y y的最短路径长度为多少,数据保证了 t t t是不下降的。

输出格式

Q Q Q行,对每一个询问 ( x , y , t ) (x, y, t) (x,y,t)输出对应的答案,即在第 t t t天,从村庄 x x x到村庄 y y y的最短路径长度为多少。如果在第t天无法找到从 x x x村庄到 y y y村庄的路径,经过若干个已重建完成的村庄,或者村庄x或村庄 y y y在第 t t t天仍未修复完成,则输出 − 1 -1 1

样例 #1

样例输入 #1

4 5
1 2 3 4
0 2 1
2 3 1
3 1 2
2 1 4
0 3 5
4
2 0 2
0 1 2
0 1 3
0 1 4

样例输出 #1

-1
-1
5
4

提示

对于 30 % 30\% 30%的数据,有 N ≤ 50 N≤50 N50

对于 30 % 30\% 30%的数据,有 t i = 0 t_i= 0 ti=0,其中有 20 % 20\% 20%的数据有 t i = 0 t_i = 0 ti=0 N > 50 N>50 N>50

对于 50 % 50\% 50%的数据,有 Q ≤ 100 Q≤100 Q100

对于 100 % 100\% 100%的数据,有 N ≤ 200 N≤200 N200 M ≤ N × ( N − 1 ) / 2 M≤N \times (N-1)/2 MN×(N1)/2 Q ≤ 50000 Q≤50000 Q50000,所有输入数据涉及整数均不超过 100000 100000 100000

思路

看完题解之后恍然大悟

其实就是用Floyd算法,每次时间增加后,把可以走的村庄标记下来

刚开始想在每次询问中都用一次Floyd算法,带着侥幸心理交了一发,30分……

(这时间不爆炸,怎么想的啊)

然后我考虑到,每次都从头进行Floyd算法,确实存在很多重复,其实可以每加入一个新的可以到达的地区,就把它当作中转点进行更新.

大概一想,感觉可行,但是有一点细节要注意.先看如下核心代码

for(int b=1;b<=n;++b)
{
  for(int e=1;e<=n;++e)
  {
    f[b][e] = f[e][b] = min(f[b][e],f[b][j]+f[j][e]);
  }
}

这里无论是否判断点b和点e是否可以走,都能得到最终的正确答案.因为如果加上了限制,最后在点b可以走的时候,会再来一次,如果没加上限制,因为没有标记,所以并没有把那个没有标记的地区作为中转点更新值,因此也不会影响两点之间的最短距离.~~(呜呜呜,我花了好久才搞懂为什么(╥﹏╥))~~但是加上判断肯定是可以优化时间的,所以我们加上.

代码实现

#include<bits/stdc++.h>
using namespace std;
const int maxn=205;
int n,m;
int u,v,w;
int t[maxn]={};
int times;
int f[maxn][maxn]={};
bool vis[maxn]={};
int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)
    {
        cin>>t[i];
    }
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=n;j++)
        {
            f[i][j]=0x3fffffff;
        }
    }
    for(int i=1;i<=n;i++)f[i][i]=0;
    for(int i=1;i<=m;i++)
    {
        cin>>u>>v>>w;
        u++,v++;
        f[u][v]=f[v][u]=w;
    }
    int q;
    cin>>q;
    int now=1;
    for(int i=1;i<=q;++i)
    {
        cin>>u>>v>>times;
        u++,v++;
        for(int j=now;j<=n;++j)
        {
            if(t[j]<=times)
            {
                vis[j]=1;
                now=j+1;
                for(int b=1;b<=n;++b)
                {
                    for(int e=1;e<=n;++e)
                    {
                        if((vis[b]&& vis[e]==0))continue;
                        f[b][e] = f[e][b] = min(f[b][e],f[b][j]+f[j][e]);
                    }
                }
            }
            else break;
        }
        if(f[u][v]!=0x3fffffff && vis[u] && vis[v])cout<<f[u][v]<<endl;
        else cout<<-1<<endl;
    }
    return 0;
}

总结

  1. 这次学了并查集和最短生成树,他们两简直就是绝配(用于判断环的生成),和在一起使用非常方便

  2. 并查集出现了从属关系,可以解决那种溯源求父节点的问题

  3. 最短生成树更多的是作为一种工具,很多题目可以转换为求最短生成树来解决.

    最近的题目真的是越来越难了,要继续加油啊啊啊啊(╥﹏╥)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值