从KD 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 aj≤ai, b j ≤ b i b_j \leq b_i bj≤bi, c j ≤ c i c_j \leq c _i cj≤ci )。
CDQ分治
有两种方法:
- CDQ+树状数组
- 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
简单题
先来一道模板题。给一个N*N棋盘,两种操作:
- 将一个格子中的数值加上k
- 求一个矩形内所有数的和
强制在线,需要异或前面的答案。
如果不是强制在线,可以将时间作为第三维,进行CDQ分治。
但是强制在线只能KD Tree。
KD Tree的build
与动态线段树的build相同,只不过每次考察的是不同维度。注意每一个点有两组值,分别表示的是原始这个点的坐标,和这个点以下子树做代表的一个矩形的左上和右下。注意向上update,更新父节点的矩形。
KD Tree的query
- 当前点所代表的矩形在要求范围内,则整个加入
- 当前点所代表的矩形与要求范围没有交集,则回溯
- 当前点所代表的矩形与要求范围有交集:
- 先判断当前点是否在要求范围内
- 递归左右子树
细节
如果失衡,需要把树还原成一个序列,然后重新建树
KD Tree解决陌上花开
首先,直观的思想是将z作为时间,然后利用x和y维度建KD Tree。但是有一个问题,相等的点无法处理。我们举一个情况:
a(1, 2, 3)和b(3, 5, 3)的时候,是可以成功判断出 a ≤ b a\leq b a≤b的,但是如果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
KNN
K Nearest Neighbors,最近的K个邻居
非参 惰性
对于一个样本,取其最近的K个(K一般为奇数)点,以这K个点中的多数点的类别为这个样本的预测。一般以欧几里得距离作为衡量标准。K值的选择尤为重要。
朴素版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给中心。
- 对于每一个点,分别计算这个点到K个中心的距离,并选取最近的中心作为它的聚类中心。
- 对于每一个中心,计算属于它的所有点的中心位置,以这个新的位置取代中心。
- 重复2、3直到终止条件达成
伪代码
随机生成k个中心a
while(终止条件)
for(i=1 to n)
计算i到每个中心距离,并以最近的作为i的聚类中心
End
for (j=1 to k)
用以中心j为聚类中心的点的中心位置取代中心j
End
End
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) p∈B(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 $
Locality-Sensitive Hashing: a Primer
PQ (product quantization)
论文Product quantization for nearest neighbor search Herve Jegou, Matthijs Douze, Cordelia Schmid