范围搜索 | KD树 | Range Search (KD Tree) | C/C++实现 | 大年三十写CSDN

问题描述

从拥有多个属性的报表集合(数据库)中,寻找具有特定属性且位于指定范围内的元素,这列问题被称为范围搜索。

请编写一个程序,对于二维平面上点的集合,例举出给定范围内的点。另外,给定的点集合无法进行点的添加和删除操作。

输入:
n
x0 y0
x1 y1

xn-1 yn-1
q
sx0 tx0 sy0 ty0
sx1 tx1 sy1 ty1

sxq-1 txq-1 sy1-1 tyq-1
其中第一行的n表示集合里点的数量。接下来的n行输入第 i 个点的2个整数坐标xi、yi。
接下来1行输入区域数q。再接下来q行各输入4个整数 s x i 、 t x i 、 s y i 、 t y i sx_i、tx_i、sy_i、ty_i sxitxisyityi来代表各个区域。
输出:
对于每个区域,按编号升序输出点集合中满足 s x i ≤ x ≤ t x i sx_i≤x≤tx_i sxixtxi s y i ≤ y ≤ t y i sy_i≤y≤ty_i syiytyi的点的编号,每个编号占1行,每个区域输出完毕后空1行(不存在满足条件的点时仅输出1个空行)
限制:
0 ≤ n ≤ 500000
0 ≤ q ≤ 20000
-1000000000 ≤ x, y, sx, tx, sy, ty ≤ 1000000000
sx ≤ tx
sy ≤ ty
各区域范围内的点不超过100个。

输入示例

6
2 1
2 2
4 2
6 2
3 3
5 4
2
2 4 0 4
4 10 2 5

输出示例

0
1
2
4

2
3
5

讲解

在获取点集合后不再进行插入和删除操作,也就是说,在进行查询之前我们已经获得了静态数据集合。一般说来,这类算法需要考虑插入与删除等操作,但我们先处理这个简易的范围搜索问题。

首先我们将问题简化,把点集合从二维空间缩小到一维的x轴上,考虑如何列举出x轴上给定区域(范围)内的点,即一维的范围搜索。

首先通过下述算法将给定点集合制成二叉树:

np = 0//初始化结点编号
make1DTree(0, n)

make1DTree(l, r)
	if !(l < r)
		return NIL

	以x坐标为基准将p中从1到r的点(不包含r)按升序排序

	mid = (l + r) / 2

	t = np++ //设置二叉树的结点编号
	T[t].location = mid //P中的位置
	T[t].l = make1DTree(l, mid) //前半部分生成子树
	T[t].r = make1DTree(mid + 1, r) //后半部分生成子树

	return t

上述算法基于递归函数make1DTree(l, r),可以用点集合P中从 l l l到 r(不包含r)的元素生成子树。算法最初调用make1DTree(0, n)意在将整个点集合作为对象生成二叉树。

make1DTree(l, r)用于确定二叉树的1个结点并返回该结点的编号t。首先我们以x为基准将指定范围内的点按升序排序,然后计算序列中央元素的下标mid,以mid为界将范围内的点集合一分为二。这个mid的值就是结点t在P内的位置。

接下来递归调用make1DTree(l, mid)和make1DTree(mid + 1, t),分别生成结点t的左子树和右子树,这两个函数的返回值就是结点t的左子结点 l l l和右子结点 r。

要例举出指定范围内的点,只需对上述二叉搜索树应用如下算法:

find(v, sx, tx)
	x = P[T[v].location].x
	if sx <= x && x <= tx
		print P[T[v].location]

	if T[v].l != NIL && sx <= x
		find(T[v].l, sx, tx)

	if T[v].r != NIL && x <= tx
		find(T[v].r, sx, tx)

搜索从二叉搜索树的根结点开始,检查当前结点所代表的点是否包含于指定范围(sx与tx之间),如果包含则输出该点(结点编号或坐标)。此外,如果该点大于等于下限sx则递归搜索左子树,小于等于上限tx则递归搜索右子树。

这个算法可以扩展至k为空间。我们只需要构建名为“KD树”的数据结构,就可以搜索指定区域内的点了。

kD树的生成方法多种多样,这类要介绍的是针对二维平面的基本方法。k维算法的基本思路与一维算法的一样,都是需对点进行排序,然后取中间值作为根结点来构建树。只不过,k维算法的排序基准会根据树的深度不同而变化。

处理一维的问题时,我们只以x的值为基准进行了排序,但点分布到二维空间后,就需对x轴和y轴分别排序。至于排序要以哪个轴为基准,则是按照树的深度进行循环。比如深度为偶数时以x轴为基准,为奇数时以y轴为基准,二者交替出现。

下面是用这一方法生成二叉搜索树并进行范围搜索的算法:

make2DTree(l, r, depth)
	if !(l < r)
		return NIL

	mid = (l + r) / 2
	t = np++

	if depth % 2 == 0
		以x坐标为基准,将P中从1至r(不包含r)的点进行升序排列
	else
		以y坐标为基准,将p中从1至r(不包含r)的点进行升序排列

	T[t].location = mid
	T.[t].l = make2DTree(l, mid, depth + 1)
	T.[t].r = make2DTree(mid + 1, r, depth + 1)

	return t

find(v, sx, tx, sy, ty, depth)
	x = p[T[v].location].x;
	y = p[T[v].location].y;

	if sx <= x && x <= tx && sy <= y && y <= ty
		print P[T[v].location]

	if depth & 2 == 0
		if T[v].l != NIL && sx <= x
			find(T[v].l, sx, tx, sy, ty, depth + 1)
		if T[v].r != NIL && x <= tx
			find(T[v].r, sx, tx, sy, ty, depth + 1)
	else
		if T[v].l != NIL && sy <= y
			find(T[v].l, sx, tx, sy, ty, depth + 1)
		if T[v].r != NIL && y <= ty
			find(T[v].r, sx, tx, sy, ty, depth + 1)

make2DTree是make1DTree的扩展,在访问结点时多检查了结点的深度depth,根据深度的奇偶性来变更排序基准(x、y轴)。find函数也同样根据depth来区分两种情况进行搜索。

设点的数量为n,算法在构建树结构时需要进行logn(树高)次 O ( n l o g n ) O(nlogn) O(nlogn)的排序,因此复杂度为 O ( n l o g 2 n ) O(nlog^2n) O(nlog2n)。另外,设指定范围内点的数量为k,KD树的维度为d,那么搜索的算法复杂度为 O ( n 1 − 1 / d + k ) O(n^{1-1/d}+k) O(n11/d+k)

AC代码如下

#include<cstdio>
#include<algorithm>
#include<vector>
using namespace std;

class Node{
	public:
		int location;
		int p, l, r;
		Node() {}
}; 

class Point{
	public:
		int id, x, y;
		Point() {}
		Point(int id, int x, int y): id(id), x(x), y(y) {}
		bool operator < (const Point &p) const {
			return id < p.id;
		}
		
	void print(){
		printf("%d\n", id);//使用比cout更快的printf函数 
	}
};

static const int MAX = 1000000;
static const int NIL = -1;

int N;
Point P[MAX];
Node T[MAX];
int np;

bool lessX(const Point &p1, const Point &p2) { return p1.x < p2.x; }
bool lessY(const Point &p1, const Point &p2) { return p1.y < p2.y; }

int makeKDTree(int l, int r, int depth){
	if(!(l < r) ) return NIL;
	int mid = (l + r) / 2;
	int t = np++;
	if(depth % 2 == 0){
		sort(P + 1, P + r, lessX);
	} else {
		sort(P + 1, P + r, lessY);
	}
	T[t].location = mid;
	T[t].l = makeKDTree(l, mid, depth + 1);
	T[t].r = makeKDTree(mid + 1, r, depth + 1);
	
	return t;
}

void find(int v, int sx, int tx, int sy, int ty, int depth, vector<Point> &ans){
	int x = P[T[v].location].x;
	int y = P[T[v].location].y;
	
	if(sx <= x && x <= tx && sy <= y && y <= ty){
		ans.push_back(P[T[v].location]);
	}
	
	if(depth % 2 == 0){
		if(T[v].l != NIL){
			if(sx <= x) find(T[v].l, sx, tx, sy, ty, depth + 1, ans);
		}
		if(T[v].r != NIL){
			if(x <= tx) find(T[v].r, sx, tx, sy, ty, depth + 1, ans);
		}
	} else {
		if(T[v].l != NIL){
			if(sy <= y) find(T[v].l, sx, tx, sy, ty, depth + 1, ans);
		}
		if(T[v].r != NIL){
			if(y <= ty) find(T[v].r, sx, tx, sy, ty, depth + 1, ans);
		}
	}
}

int main(){
	int x, y;
	scanf("%d", &N);
	for(int i = 0; i < N; i++){
		scanf("%d %d", &x, &y);
		P[i] = Point(i, x, y);
		T[i].l = T[i].r = T[i].p = NIL;
	}
	
	np = 0;
	
	int root = makeKDTree(0, N, 0);
	
	int q;
	scanf("%d", &q);
	int sx, tx, sy, ty;
	vector<Point> ans;
	for(int i = 0; i < q; i++){
		scanf("%d %d %d %d", &sx, &tx, &sy, &ty);
		ans.clear();
		find(root, sx, tx, sy, ty, 0, ans);
		sort(ans.begin(), ans.end());
		for(int j = 0; j < ans.size(); j++){
			ans[j].print();
		}
		printf("\n");
	}
	
	return 0;
}
  • 11
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是一个简单的示例代码,演示如何使用PCL库实现kd树划分和遍历叶子节点。 ```c++ #include <iostream> #include <pcl/point_types.h> #include <pcl/kdtree/kdtree_flann.h> int main() { // 创建点云数据 pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>); cloud->width = 5; cloud->height = 1; cloud->points.resize(cloud->width * cloud->height); for (size_t i = 0; i < cloud->points.size(); ++i) { cloud->points[i].x = 1024 * rand() / (RAND_MAX + 1.0f); cloud->points[i].y = 1024 * rand() / (RAND_MAX + 1.0f); cloud->points[i].z = 1024 * rand() / (RAND_MAX + 1.0f); } // 创建kd树对象 pcl::KdTreeFLANN<pcl::PointXYZ> kdtree; kdtree.setInputCloud(cloud); // 查询点 pcl::PointXYZ searchPoint; searchPoint.x = 1024 * rand() / (RAND_MAX + 1.0f); searchPoint.y = 1024 * rand() / (RAND_MAX + 1.0f); searchPoint.z = 1024 * rand() / (RAND_MAX + 1.0f); // K近邻搜索 std::vector<int> pointIdxKNNSearch; std::vector<float> pointKNNSquaredDistance; int K = 3; kdtree.nearestKSearch(searchPoint, K, pointIdxKNNSearch, pointKNNSquaredDistance); // 遍历叶子节点 std::vector<int> pointIdxRadiusSearch; float radius = 256.0f * rand() / (RAND_MAX + 1.0f); if (kdtree.radiusSearch(searchPoint, radius, pointIdxRadiusSearch) > 0) { for (size_t i = 0; i < pointIdxRadiusSearch.size(); ++i) std::cout << " " << cloud->points[pointIdxRadiusSearch[i]].x << " " << cloud->points[pointIdxRadiusSearch[i]].y << " " << cloud->points[pointIdxRadiusSearch[i]].z << std::endl; } return 0; } ``` 以上代码实现了以下操作: 1. 创建一个点云数据,包含5个点。 2. 使用KdTreeFLANN类创建kd树对象,并将点云数据设置为其输入。 3. 随机生成一个查询点,并执行K近邻搜索操作,找到距离该点最近的3个点。 4. 随机生成一个半径,并执行半径搜索操作,找到距离该查询点在该半径内的所有点。 代码中包含基本注释,可以帮助您更好地理解每个操作的含义和实现方式。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值