4202 穿过圆(倍增法求解最近公共祖先)

1. 问题描述:

在一个二维平面上有 n 个点和 m 个圆。点的编号为 1∼n。不存在某个点恰好在某个圆的边上的情况。任意两个圆之间没有公共点。现在,请你回答 k 个询问。每个询问给定两个点 ai,bi,并请你回答从点 ai 出发沿任意连续路径到达点 bi,至少需要穿过多少个圆。

输入格式

第一行包含三个整数 n,m,k。接下来 n 行,其中第 i 行包含两个整数 xi,yi,表示点 i 的坐标为 (xi,yi)。注意,点的位置可以重合。再接下来 m 行,其中第 i 行包含三个整数 ri,cxi,cyi,表示第 i 个圆的半径为 ri,圆心坐标为 (cxi,cyi)。最后 k 行,每行包含两个整数 ai,bi,表示一个询问。注意,ai 可以等于 bi。

输出格式

共 k 行,第 i 行输出第 i 个询问的答案,即最少需要穿过的圆的数量。

数据范围

前三个测试点满足 1 ≤ n,m,k ≤ 10。
所有测试点满足 1 ≤ n,m ≤ 1000,1 ≤ k ≤ 10 ^ 5,−10 ^ 9 ≤xi,yi,cxi,cyi ≤ 10 ^ 9,1 ≤ ri ≤ 10 ^ 9,1 ≤ ai,bi ≤ n。

输入样例1:

2 1 1
0 0
3 3
2 0 0
1 2

输出样例1:

1

输入样例2:

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

输出样例2:

3
来源:https://www.acwing.com/problem/content/description/4205/

2. 思路分析:

首先我们需要挖掘一下题目的性质,先将平面内所有区域属于哪一个圆的拓扑关系定义出来,每一块区域隶属于最小的包含它的圆,这样就将所有区域划分为了若干个区域,每一个区域可以使用点来表示,可以发现这样就将空间结构变为了一棵树,并且每一个节点的父节点是唯一的,从圆内到圆外其实相当于是走到了父节点上,从圆外走到圆内相当于是走到子节点上,所以所有穿过圆的路径都可以对应到树上的一条路径,所以从一个点穿过若干个圆走到另外一个点相当于是从树上的某个节点走到另外一个节点,求解最少的穿过圆的数量相当于在树中求解两点之间的最短距离,树上求解任意两点之间的距离属于经典的一类问题,属于最近公共祖先模型,这样我们就将原问题转化为了求解最近公共祖先的问题,因为给出的是一个n个点和m个圆,所以我们需要将其转化为一棵树的形式,也即需要建图才可以求解LCA,因为m个圆将整个平面划分了一棵树的形式,所以我们先预处理一下将n个点隶属于哪一个圆的关系确定出来,每一个点唯一隶属于包含它的半径最小的圆,使用两层循环来确定每一个点属于哪一个圆,将结果存储到bel中;对于m个圆,如果一个点不属于任何一个圆我们将其划分为编号为m + 1的根节点,然后我们确定m个圆的隶属关系(将圆看成是一个点,确定当前圆的父节点),这里是将每一个圆看成是树中的节点,我们是通过圆的包含关系来确定他们的父子关系,对于当前圆的父节点为包含当前圆的所有圆中半径最小的那个:

我们可以使用两层循环枚举当前的圆隶属于哪一个圆,如果当前的圆不属于任何圆那么将其划分到编号为m + 1的根节点上,在枚举的时候判断另外一个圆是否包含当前的圆并且半径更小如果是则更新一下,这样我们就可以通过m个圆创建出棵有向树,对于k个询问我们找到第a,b个点隶属于哪一个圆,然后求解这两个圆的最近公共祖先即可,整体的代码还是比较长的。除了求解最近公共祖先之外我们发现数据范围在1000,所以应该还有更加简单的写法,我们可以统计一下每一个圆包含当前点的情况,可以将其看成是m位二进制状态,如果为1表示包含,不为1表示不包含,这样对于任意两个点我们都可以求解出m位二进制串,二进制串中不同位的数量那么就是答案。

3. 代码如下:

倍增法求解LCA:提交上去还是超时了只过了6个数据,python的运行效率确实比较差:

import collections
from typing import List


class Solution:
    # bfs求解depth和fa的值
    def bfs(self, root: int, depth: List[int], fa: List[List[int]], g: List[List[int]]):
        q = collections.deque([root])
        # 0这个节点相当于是哨兵, 当跳出范围之外那么深度就是0, 这样就可以避免边界上的问题
        depth[0] = 0
        # 根节点的深度为0
        depth[root] = 0
        while q:
            p = q.popleft()
            for next in g[p]:
                j = next
                if depth[next] > depth[p] + 1:
                    # 更新深度
                    depth[next] = depth[p] + 1
                    # 将子节点加入到队列中
                    q.append(j)
                    # 当前节点往上跳1步到节点p
                    fa[j][0] = p
                    # 枚举当前节点往上跳2 ^ k步可以跳到的节点
                    for k in range(1, 11):
                        fa[j][k] = fa[fa[j][k - 1]][k - 1]

    # 倍增法求解a, b的最近公共祖先
    def lca(self, a: int, b: int, depth: List[int], fa: List[List[int]]):
        # 确保节点a的深度大于节点b的深度
        if depth[a] < depth[b]:
            a, b = b, a
        # a跳到节点与节点b相同的深度
        for k in range(10, -1, -1):
            if depth[fa[a][k]] >= depth[b]:
                a = fa[a][k]
        # b是a的父节点返回任意一个都可以
        if a == b: return a
        for k in range(10, -1, -1):
            if fa[a][k] != fa[b][k]:
                a = fa[a][k]
                b = fa[b][k]
        # 节点a往上跳一步就是a, b的最近公共祖先
        return fa[a][0]

    # 判断a是否在b内(可以判断点是否在圆内或者是圆是否在另外一个圆内)
    def check(self, a: tuple, b: tuple):
        px, py = a[0] - b[0], a[1] - b[1]
        if px * px + py * py < b[2] * b[2]: return True
        return False

    def process(self):
        n, m, k = map(int, input().split())
        # python的元组非常适用于存储坐标之类的点
        # points前面加上一个元素这样元素的下标可以从1开始方便处理
        points = [(0, 0)]
        for i in range(n):
            x, y = map(int, input().split())
            points.append((x, y))
        # cir前面加上一个元素这样元素的下标可以从1开始方便处理
        cir = [(0, 0, 0)]
        for i in range(m):
            r, cx, cy = map(int, input().split())
            cir.append((cx, cy, r))
        # 预处理所有点属于哪一个圆
        bel = [-1] * (n + 10)
        # 枚举所有的点
        for i in range(1, n + 1):
            # 枚举所有圆判断当前点属于哪一个圆, 这样可以确定节点之间的父子关系
            for j in range(1, m + 1):
                if self.check(points[i], cir[j]):
                    # 当前这个点不属于任意一个圆或者属于一个半径更小的圆进行更新
                    if bel[i] == -1 or cir[bel[i]][2] > cir[j][2]:
                        bel[i] = j
        # 不属于任何一个点的圆那么将其划分到m + 1的父节点上
        for i in range(1, n + 1):
            if bel[i] == -1: bel[i] = m + 1
        # 有向图(存储的是圆与圆之间的隶属关系), 将圆看成是树中的一个节点
        g = [list() for i in range(n + 10)]
        for i in range(1, m + 1):
            t = -1
            for j in range(1, m + 1):
                # 半径比当前的圆更大并且另外一个圆包含当前的圆
                if cir[j][2] > cir[i][2] and self.check(cir[i], cir[j]):
                    # 之前没有谁包含过它或者找到一个更小半径的那么更新一下
                    if t == -1 or cir[t][2] > cir[j][2]:
                        t = j
            if t == -1:
                # m + 1表示根节点
                g[m + 1].append(i)
            else:
                g[t].append(i)
        INF = 10 ** 10
        depth, fa = [INF] * (n + 10), [[0] * 11 for i in range(n + 10)]
        # 在bfs遍历节点的过程中计算depth和fa的值
        self.bfs(m + 1, depth, fa, g)
        # k个询问
        for i in range(k):
            # a, b表示第a, b个点
            a, b = map(int, input().split())
            # a, b表示第a, b个点属于哪一个圆
            a, b = bel[a], bel[b]
            p = self.lca(a, b, depth, fa)
            # 输出a, b之间的最短距离
            print(depth[a] + depth[b] - 2 * depth[p])


if __name__ == '__main__':
    Solution().process()

技巧:

from typing import List


class Solution:
    # 判断当前点是在圆内还是在圆外
    def check(self, a: int, b: int, points: List[tuple], cir: List[tuple]):
        dx, dy = points[a][0] - cir[b][0], points[a][1] - cir[b][1]
        # 圆内返回1, 否则返回0(反过来也行)
        if dx * dx + dy * dy > cir[b][2] * cir[b][2]:
            return 1
        return 0

    def get(self, x: int):
        res = ""
        while x:
            res += str(x % 2)
            x //= 2
        return res

    def process(self):
        n, m, k = map(int, input().split())
        points = [(0, 0)]
        for i in range(n):
            x, y = map(int, input().split())
            points.append((x, y))
        cir = [(0, 0, 0)]
        for i in range(m):
            r, cx, cy = map(int, input().split())
            cir.append((cx, cy, r))
        st = [0] * (n + 10)
        # 枚举当前点在圆的外面还是里面
        for i in range(1, n + 1):
            for j in range(1, m + 1):
                t = self.check(i, j, points, cir)
                # 1 << (j - 1)表示将第j个位置为1
                if t:  st[i] |= 1 << (j - 1)
        for i in range(k):
            a, b = map(int, input().split())
            # bin方法是将十进制转为二进制字符串并且前面会多出前缀"0b", 计算二进制位有多少个不一样的位即可, 可以使用异或操作
            print(bin(st[a] ^ st[b])[2:].count("1"))
            # print(self.get(st[a] ^ st[b]).count("1"))


if __name__ == '__main__':
    Solution().process()

c++:biset存储二进制状态这样效率会高大概30倍,因为在biset存储的时候可以使用O(1)的时间来存储

#include <iostream>
#include <cstring>
#include <algorithm>
#include <bitset>

#define x first
#define y second

using namespace std;

typedef long long LL;
typedef pair<int, int> PII;

const int N = 1010;

int n, m, Q;
PII p[N], c[N];
int r[N];

bitset<N> st[N];

LL sqr(LL x)
{
    return x * x;
}

int check(int a, int b)
{
    LL dx = p[a].x - c[b].x;
    LL dy = p[a].y - c[b].y;
    if (sqr(dx) + sqr(dy) > sqr(r[b])) return 0;
    return 1;
}

int main()
{
    scanf("%d%d%d", &n, &m, &Q);
    for (int i = 1; i <= n; i ++ ) scanf("%d%d", &p[i].x, &p[i].y);
    for (int i = 0; i < m; i ++ )
        scanf("%d%d%d", &r[i], &c[i].x, &c[i].y);

    for (int i = 1; i <= n; i ++ )
        for (int j = 0; j < m; j ++ )
            st[i][j] = check(i, j);
    while (Q -- )
    {
        int a, b;
        scanf("%d%d", &a, &b);
        printf("%d\n", (st[a] ^ st[b]).count());
    }
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值