从K-D tree到……

提到KD Tree就想到陌上花开,提到陌上花开就想到CDQ分治,提到CDQ分治就发现我不会CDQ了……那就先复习一下CDQ吧

陌上花开

三维空间内n个元素,每个元素表示为( a i a_i ai, b i b_i bi, c i c_i ci),求每个点有多少个点在它的左下后方,即( a j ≤ a i a_j \leq a _i ajai, b j ≤ b i b_j \leq b_i bjbi, c j ≤ c i c_j \leq c _i cjci )。

CDQ分治

有两种方法:

  1. CDQ+树状数组
  2. CDQ套CDQ
CDQ 加树状数组
  • 按照x维度进行sort
  • 分治
    • 每次将当前序列S(l, r)分割为A(l, mid)和B(mid + 1, r)
    • 分别递归处理A、B段,再考虑A和B之间的关系
    • 对于两段分别按照y维度进行sort
      • 此时,A段的x均小于(等于)B段,并且A、B两端分别在y上是有序的
      • 接下来就是对于B段的每一个元素b,找到A段中在y维度上小于等于b的最右的元素a,查看a及a的左边有多少个元素在z维度上满足小于b
    • 在B段从左向右枚举b,对应在A段放置一个指针a指向满足在y上小于等于b的最右的元素a。
      • 由于b从左向右移动,所以a必然也是从左向右单向移动的
    • 指针a每次移动时,将对应位置的元素按照z维度放置在树状数组内
    • 对于每一个b,查询当前树状数组内z维度上有多少个比b小的,添加到统计中
    • 【重要】递归回溯时,需要删去这个过程中树状数组内的值,以免对下一层递归产影响
细节
  • 处理重复元素:将重复元素合并,最终记录结果时应当加上(重复数-1),因为重复元素之间互为等于
伪代码
Function CDQ (S, F) 
    # 分割序列S为A、B
	mid = len(S) / 2
    A = S[1...mid]
    B = S[mid+1...len(S)]
    
    # 分别处理 A、B序列
    CDQ(A)
    CDQ(B)
     
	# 按照y维度分别对A、B进行排序
    F = sort(A, cmpy, F)
    F = sort(B, cmpy, F)
        
    # 设定指针a
    a = 1
    # 枚举B中每个b
    for(b=1 to len(B))
     	while (a <= len(A) and A[a].y <= B[b].y)
     		# 插入树状数组,其中cnt表示重复数
     		BiTree.add(A[a].z, A[a].cnt)
     		i++
     	End
     	# 当前树状数组中的值均满足x和y维度上小于b,只需要再查询树状数组中有多少z维度上小于b的就好了
     	F[B[b].id] += BiTree.query(B[b].z)
    End
    for (a=1 to len(A))
    	BiTree.add(A[a].z, -A[a].cnt)
    End
    return F
 End
 
 Function main(S) 
 	# 按照x排序
 	sort(S, cmpx)
 	# 合并重复数
 	S = merge(S)
 	F = CDQ(S)
 	for (i=1 to len(S))
 		ans[F[S[i].id] + S[i].cnt - 1] += S[i].cnt #注意此处需要考虑重复元素
 	return F 
 	
 End
陌上花开C代码
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;
struct apple{
	int x,y,z,c,id;
	apple(){}
	apple(int a,int b,int m,int n,int p)
	{
		x=a;y=b;z=m;c=n;id=p;
	}
}node[100010],no[100010];
int tree[200010],f[100010],ff[100010];
int m;
bool cmpx(apple a,apple b)
{
	return a.x<b.x||(a.x==b.x&&a.y<b.y)||(a.x==b.x&&a.y==b.y&&a.z<b.z);
}
bool cmpy(apple a,apple b)
{
	return a.y<b.y;
}
int lowbit(int x)
{
	return x&(-x);
}
int query(int x)
{
	int ans=0;
	for(int i=x;i>0;i-=lowbit(i))
		ans+=tree[i];
	return ans;
}
void add(int x,int z)
{
	for(int i=x;i<=m;i+=lowbit(i))
		tree[i]+=z;
} 
void cdq(int l,int r)
{
	if(l==r)
		return;
	int midd=(l+r)>>1;
	cdq(l,midd);
	cdq(midd+1,r);
	sort(no+l,no+midd+1,cmpy);
	sort(no+midd+1,no+r+1,cmpy);
	int i,j,ans;
	i=l;
	for(j=midd+1;j<=r;j++)
	{
		while(i<=midd&&no[i].y<=no[j].y)
		{
			add(no[i].z,no[i].c);
			i++;
		}
		f[no[j].id]+=query(no[j].z);
	 } 
	 for(j=l;j<i;j++)
	 	add(no[j].z,-no[j].c);
}
int main()
{
	int n,i,nn;
	scanf("%d%d",&n,&m);
	for(i=1;i<=n;i++)
		scanf("%d%d%d",&node[i].x,&node[i].y,&node[i].z);
	sort(node+1,node+n+1,cmpx);
	nn=0;
	for(i=1;i<=n;i++)
	{
		if(i>1&&node[i].x==node[i-1].x&&node[i].y==node[i-1].y&&node[i].z==node[i-1].z)
			no[nn].c++;
		else
		{
			nn++;
			no[nn]=apple(node[i].x,node[i].y,node[i].z,1,nn);
		}
	}
	cdq(1,nn);
	for(i=1;i<=nn;i++)
		ff[f[no[i].id]+no[i].c-1]+=no[i].c;
	for(i=0;i<n;i++)
		printf("%d\n",ff[i]);
	return 0;
 } 
CDQ套CDQ
  • 按照x维度进行sort
  • 分治1
    • 每次将当前序列S(l, r)分割为A(l, mid)和B(mid + 1, r)
    • 分别递归处理A、B段,再考虑A和B之间的关系
    • 对于两段按照y进行归并排序得到新的S,但是对于来自A的元素标记flag为1,对于来自b的元素标记flag为0
    • 对新的S进行分治2
  • 分治2
    • 每次将当前序列S(l, r)分割为A(l, mid)和B(mid + 1, r)
    • 分别递归处理A、B段,再考虑A和B之间的关系
    • 对z维度进行归并,归并的过程中,我们可以知道A中的元素在y上是小于等右边的,归并过程中双指针分别指向B中的b和A中比b小的最右端的a
    • 遇到a或者b的flag为0时,需要将目前归并得到的所有flag为1的加入到它的答案中去

相关题解: 题解 P3810 【模板】三维偏序 Shadows的博客

KD Tree

简单题

P4148 简单题

先来一道模板题。给一个N*N棋盘,两种操作:

  1. 将一个格子中的数值加上k
  2. 求一个矩形内所有数的和

强制在线,需要异或前面的答案。

如果不是强制在线,可以将时间作为第三维,进行CDQ分治。

但是强制在线只能KD Tree。

KD Tree的build

与动态线段树的build相同,只不过每次考察的是不同维度。注意每一个点有两组值,分别表示的是原始这个点的坐标,和这个点以下子树做代表的一个矩形的左上和右下。注意向上update,更新父节点的矩形。

KD Tree的query
  1. 当前点所代表的矩形在要求范围内,则整个加入
  2. 当前点所代表的矩形与要求范围没有交集,则回溯
  3. 当前点所代表的矩形与要求范围有交集:
    1. 先判断当前点是否在要求范围内
    2. 递归左右子树
细节

如果失衡,需要把树还原成一个序列,然后重新建树

KD Tree解决陌上花开

首先,直观的思想是将z作为时间,然后利用x和y维度建KD Tree。但是有一个问题,相等的点无法处理。我们举一个情况:

a(1, 2, 3)和b(3, 5, 3)的时候,是可以成功判断出 a ≤ b a\leq b ab的,但是如果a 在b的后面,我们发现就会漏掉这个答案。

解决办法是,在遍历到某一个点的时候,在包含此前所有点的大KD tree之外,将所有和它在z上相等的点捞出来,单独建一棵小KD Tree,同时查询两颗KD tree。然后将z相同的点都处理好后,将这一批z相同的点统一加入到大KD tree中。

题解 P3810 【模板】三维偏序(陌上花开)[demonovo 的博客]

时间复杂度

二维KD Tree

建树: n l o g n nlogn nlogn

增删: l o g n logn logn

超方体查询: n \sqrt n n

kd-tree(KDT) 时间复杂度证明 q779

KNN

K Nearest Neighbors,最近的K个邻居

非参 惰性

对于一个样本,取其最近的K个(K一般为奇数)点,以这K个点中的多数点的类别为这个样本的预测。一般以欧几里得距离作为衡量标准。K值的选择尤为重要。

KNN算法(一) KNN算法原理 风凌天下

朴素版KNN

以下代码来自 KNN和KdTree算法实现 zoukankan

def predict(self, X):
    
    knn_list = []
    # 先取出k的点
    for i in range(self.n):
		# linalg = linear + algebra
    	# linalg.norm相关介绍:https://blog.csdn.net/silent1cat/article/details/120811844
        dist = np.linalg.norm(X - self.X_train[i], ord=self.p)
        knn_list.append((dist, self.y_train[i]))
	# 考虑剩下的点,是否有比已经选中的k个点更近的,如果有,则更新这k个点的序列
    for i in range(self.n, len(self.X_train)):
        # 取出当前k个点中距离最大的一个的位置
        max_index = knn_list.index(max(knn_list, key=lambda x: x[0]))
  		# 计算当前点的距离
        dist = np.linalg.norm(X - self.X_train[i], ord=self.p)
        # 如果当前点比最大的一个点近,则替换掉最大的点
        if knn_list[max_index][0] > dist:
            knn_list[max_index] = (dist, self.y_train[i])

    # 统计
    knn = [k[-1] for k in knn_list]
    return Counter(knn).most_common()[0][0]
KD Tree版KNN

以下代码来自 KNN和KdTree算法实现 zoukankan

# 建立kdtree
def create(self, dataSet, label, depth=0):
    if len(dataSet) > 0:
        m, n = np.shape(dataSet)
        self.n = n
        # 当前层的维度
        axis = depth % self.n
        # 按照当前维度排序
        dataSetcopy = sorted(dataSet, key=lambda x: x[axis])
        # 取中点
        mid = int(m / 2)
        node = Node(dataSetcopy[mid], label[mid], depth)
        # 如果是最顶层,需要记录树根
        if depth == 0:
            self.KdTree = node
        # 递归构建左右子树
        node.lchild = self.create(dataSetcopy[:mid], label, depth+1)
        node.rchild = self.create(dataSetcopy[mid+1:], label, depth+1)
        return node
    return None
# 搜索kdtree的前count个近的点
def search(self, x, count = 1):
    nearest = [] # 存储最近的k个点按照距离递减
    # 初始化k个占位点
    for i in range(count):
        nearest.append([-1, None])
    self.nearest = np.array(nearest)

    def recurve(node):
        if node is not None:
            # 计算当前维度
            axis = node.depth % self.n
            # 计算测试点和当前点在当前维度上的差
            daxis = x[axis] - node.data[axis]
            # 如果小于进左子树,大于进右子树
            if daxis < 0:
                recurve(node.lchild)
            else:
                recurve(node.rchild)
            # 计算预测点x到当前点的距离dist
            dist = np.sqrt(np.sum(np.square(x - node.data)))
            for i, d in enumerate(self.nearest):
                # 如果有比现在最近的n个点更近的点,更新最近的点
                if d[0] < 0 or dist < d[0]:
                    # 插入第i个位置的点
                    self.nearest = np.insert(self.nearest, i, [dist, node], axis=0)
                    # 删除最后一个多出来的点
                    self.nearest = self.nearest[:-1]
                    break

            # 统计距离为-1的个数n
            n = list(self.nearest[:, 0]).count(-1)
            '''
            self.nearest[-n-1, 0]是当前nearest中已经有的最近点中,距离最大的点。
            self.nearest[-n-1, 0] > abs(daxis)代表以x为圆心,self.nearest[-n-1, 0]为半径的圆与axis
            相交,说明在左右子树里面有比self.nearest[-n-1, 0]更近的点
            '''
            if self.nearest[-n-1, 0] > abs(daxis):
                if daxis < 0:
                    recurve(node.rchild)
                else:
                    recurve(node.lchild)

    recurve(self.KdTree)

    # nodeList是最近n个点的
    nodeList = self.nearest[:, 1]

    # knn是n个点的标签
    knn = [node.label for node in nodeList]
    return self.nearest[:, 1], Counter(knn).most_common()[0][0]

K-means

【机器学习】K-means 阿泽

  1. 随机指定K给中心。
  2. 对于每一个点,分别计算这个点到K个中心的距离,并选取最近的中心作为它的聚类中心。
  3. 对于每一个中心,计算属于它的所有点的中心位置,以这个新的位置取代中心。
  4. 重复2、3直到终止条件达成
伪代码
随机生成k个中心a
while(终止条件)
	for(i=1 to n)
		计算i到每个中心距离,并以最近的作为i的聚类中心
	End
	for (j=1 to k)
		用以中心j为聚类中心的点的中心位置取代中心j
	End
End

ball tree

  1. 找到空间中最远的两个点,作为观测点
  2. 将其他点按照距离划归到两个观测点中的一个
  3. 确定两个观测点所代表的子簇的圆心半径
  4. 递归重复上述步骤,直到簇内只有一个点

KNN的核心算法kd-tree和ball-tree 吴智深

LSH 局部敏感哈希

locality-sensetive hashing

哈希:减少冲突

局部敏感哈希:利用哈希冲突加速检索

LSH 的设计思想:如果两点距离较近,它们的哈希值大概率相同;反之则大概率不同。

设小半径 r 1 r_1 r1,大半径 r 2 r_2 r2,近似概率 p 1 p_1 p1,疏远概率 p 2 p_2 p2

p ∈ B ( q , r 1 ) p \isin B (q, r_1) pB(q,r1),则$P_{r_H}[h(q) = h§] \geq p_1 $

p ∉ B ( q , r 2 ) p \notin B (q, r_2) p/B(q,r2),则$P_{r_H}[h(q) = h§] \leq p_2 $

局部敏感哈希(LSH)越前浩波

Locality-Sensitive Hashing: a Primer

PQ (product quantization)

论文Product quantization for nearest neighbor search Herve Jegou, Matthijs Douze, Cordelia Schmid

理解 product quantization 算法 vividfree

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
K-D Tree(K-Dimensional Tree算法是一种基于分治法的数据结构,用于高维空间的搜索和排序。它的基本思想是将多维空间中的点以某种方式分割成更小的子空间,然后在每个子空间中递归地进行搜索。这样可以大大降低搜索的复杂度。 具体来说,K-D Tree算法可以分为以下几步: 1. 选择一个维度,将数据点按照该维度的值进行排序。 2. 找到该维度的中位数,将其作为当前节点,并将数据点分为左右两个子集。 3. 递归地构建左子树和右子树,每次选择一个新的维度进行划分。 4. 最终得到一个K-D Tree。 在搜索时,我们可以从根节点开始,按照一定的规则向下遍历,直到找到目标点或者无法继续向下搜索。具体的规则是: 1. 如果目标点在当前节点的左子树中,则继续向左子树搜索。 2. 如果目标点在当前节点的右子树中,则继续向右子树搜索。 3. 如果目标点和当前节点在选定的维度上的值相等,则说明已经找到目标点。 分治法是一种常见的算法思想,它将一个大规模的问题分解成若干个小规模的子问题,每个子问题独立地求解,然后将这些子问题的解合并起来得到原问题的解。分治法通常包含三个步骤:分解、求解、合并。 具体来说,分治法可以分为以下几步: 1. 分解:将原问题分成若干个子问题,每个子问题规模较小且结构与原问题相同。 2. 求解:递归地求解每个子问题,直到问题规模足够小可以直接求解。 3. 合并:将所有子问题的解合并成原问题的解。 分治法的优点是可以有效地降低算法的时间复杂度。但是它的缺点是需要额外的空间来存储子问题的解,而且分解和合并的过程也需要耗费一定的时间。因此,需要根据实际情况选择合适的算法

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值