首页 > 其他分享 >K-D Tree 学习笔记

K-D Tree 学习笔记

时间:2022-11-09 12:01:45浏览次数:71  
标签:return rs int Tree mid 笔记 学习 ls SIZE

K-D Tree 学习笔记

K-D Tree 是一种可以较高效维护高维信息的数据结构,矩形查询的时间复杂度一般是 \(\displaystyle\mathcal O(n^{1-1/n})\) 的(我不会证),OI 中一般用到的都是 2-D Tree,也就是二维的 KDT,时间复杂度最优 \(\mathcal O(\log n)\),最劣 \(\mathcal O(\sqrt n)\)。

下面以 \(k=2\) 为例介绍 KDT,更大的 \(k\) 可以很方便的从 \(k=2\) 拓展过来。

KDT

建树

先搬一张 OI-Wiki 的图过来:

假设有 \(A(2,3),B(4,7),C(5,4),D(7,2),E(9,6),F(8,1)\) 总共 \(6\) 个点。KDT 的构建方式如下:

  • 选择一个维度。
  • 选择一个切割点,将这一维度上值小于切割点的分入左子树,其余分入右子树。
  • 递归处理新分出来的两个子树。

上面的例子构建出来的 KDT 可能长这个样子:

具体说下过程:

  • 选择 \(x\) 轴作为当前维度,取点 \(D\) 作为分割点,将原来的点集划分为两个部分:\([A,B,C,D,E,F]\to [A,B,C],D,[E,F]\)。
  • 对于 \([A,B,C]\),选择 \(y\) 轴作为当前维度,选择点 \(C\) 作为分割点,划分为两部分:\([A],C,[B]\)。
  • 对于 \([E,F]\),选择 \(y\) 轴作为当前维度,选择点 \(E\) 作为当前分割点,划分为两部分:\(E,[F]\)。

但是这样生成出来的 KDT 的树高可能会很不平衡,所以需要人为去确定选择维度和分割点。

对于分割点的选择很好说,直接选择当前维度排序下的中位数即可。对于维度,一种比较不错的选择方法是选择各个维度中点的方差最大的作为选择的维度。这样构建出来的 KDT 树高最多为 \(\log n\) 级别。

现在问题在于如何快速选出中位数。如果使用 sort,那么时间复杂度是 \(\mathcal O(n \log^2 n)\) 的。algorithm 库中有一个函数 nth_element(begin,mid,end,cmp),作用是将 [begin, end] 中按照 cmp 规则小于 mid 放到左侧,大于的放到右侧,用只递归一半的快排实现,期望时间复杂度是 \(\mathcal O(n)\) 的。

构建 KDT 的时间复杂度是 \(\mathcal O(n\log n)\) 的。

插入 / 删除

如果每次插入操作都是对准一棵子树进行插入操作,那么树高将会退化成为 \(\mathcal O(n)\) 的,并且由于 KDT 特殊的构造方式,使得它不能像 Treap 和 Splay 一样通过旋转保证树高。思考一下平衡树的各种实现方式,发现替罪羊树维护平衡的方式比较适合 KDT,即定义一个常数 \(\alpha\),如果一个节点的某个子树的大小超过了这个节点的 \(\alpha\),就将这个子树拍扁重构。重构方式就是上面提到的建树方式。

对于删除操作,也可以采用替罪羊树的操作方式,进行懒惰删除。如果一个子树未删除的节点占不到这棵子树的 \(\alpha\) 就也将这个子树重构。

\(\alpha\) 在替罪羊树上一般取 \(0.7\),在 KDT 上取 \(0.6\) 更为合适,参考这篇博客

例题

领域查询

Luogu P7883 平面最近点对

此题就是一个用 KDT 骗分的很好的例子。

先按照题目要求建出 KDT,然后挨个枚举每一个点,查询距离最近的点,答案就是这些查询的最小值。

显然不能每一次都遍历 KDT,因为这样每次操作的时间复杂度都是满 \(\mathcal O(n)\) 的,所以要加一些搜索剪枝的方法。

记录当前答案为 \(ans\),如果当前节点代表的矩形距离距离当前点的距离已经大于了 \(ans\),就没必要继续搜下去了。另一个剪枝就是如果两个子树的距离都是小于 \(ans\) 的,就选择距离更小的一个矩形进行搜索,这样搜索完了过后可能另一个子树的距离就大于 \(ans\) 了,就可以少去一个子树的搜索量。可以将当前点与矩形的距离理解为这种做法的估价函数。

随机数据下这种做法的时间是很优秀的,但是不难发现这种做法其实就是优化了搜索顺序的暴搜,最劣的时间复杂度仍然是 \(\mathcal O(n)\) 的。

#include<bits/stdc++.h>
using namespace std;
namespace Hanx16qwq {
constexpr int _SIZE = 4e5;
int n, ls[_SIZE + 5], rs[_SIZE + 5];
double ans = 2e18;
struct Node{
    double x, y;
}s[_SIZE + 5];
double L[_SIZE + 5], R[_SIZE + 5], U[_SIZE + 5], D[_SIZE + 5]; 
double dis(int a, int b) {
    return (s[a].x - s[b].x) * (s[a].x - s[b].x) + (s[a].y - s[b].y) * (s[a].y - s[b].y);
}
void Maintain(int x) {
    L[x] = R[x] = s[x].x;
    D[x] = U[x] = s[x].y;
    if (ls[x])
        L[x] = min(L[x], L[ls[x]]), R[x] = max(R[x], R[ls[x]]),
        D[x] = min(D[x], D[ls[x]]), U[x] = max(U[x], U[ls[x]]);
    if (rs[x])
        L[x] = min(L[x], L[rs[x]]), R[x] = max(R[x], R[rs[x]]),
        D[x] = min(D[x], D[rs[x]]), U[x] = max(U[x], U[rs[x]]);
}
int build(int l, int r) {
    if (l > r) return 0;
    if (l == r) {
        Maintain(l);
        return l;
    }
    int mid = (l + r) >> 1;
    double avx = 0, avy = 0, vax = 0, vay = 0;
    for (int i = l; i <= r; i++) avx += s[i].x, avy += s[i].y;
    avx /= r - l + 1, avy /= r - l + 1;
    for (int i = l; i <= r; i++)
        vax += (s[i].x - avx) * (s[i].x - avx),
        vay += (s[i].y - avy) * (s[i].y - avy);
    if (vax > vay)
        nth_element(s + l, s + mid, s + r + 1, [&](Node x, Node y) {
            return x.x < y.x;
        });
    else 
        nth_element(s + l, s + mid, s + r + 1, [&](Node x, Node y) {
            return x.y < y.y;
        });
    ls[mid] = build(l, mid - 1), rs[mid] = build(mid + 1, r);
    return Maintain(mid), mid;
}
double F(int a, int b) {
    double res = 0;
    if (L[b] > s[a].x) res += (L[b] - s[a].x) * (L[b] - s[a].x);
    if (R[b] < s[a].x) res += (R[b] - s[a].x) * (R[b] - s[a].x);
    if (D[b] > s[a].y) res += (D[b] - s[a].y) * (D[b] - s[a].y);
    if (U[b] < s[a].y) res += (U[b] - s[a].y) * (U[b] - s[a].y);
    return res;
}
void query(int l, int r, int x) {
    if (l > r) return;
    int mid = (l + r) >> 1;
    if (mid != x) ans = min(ans, dis(x, mid));
    if (l == r) return;
    double dist1 = F(x, ls[mid]), dist2 = F(x, rs[mid]);
    if (dist1 < ans && dist2 < ans) {
        if (dist1 < dist2) {
            query(l, mid - 1, x);
            if (dist2 < ans) query(mid + 1, r, x);
        } else {
            query(mid + 1, r, x);
            if (dist1 < ans) query(l, mid - 1, x);
        }
    } else {
        if (dist1 < ans) query(l, mid - 1, x);
        if (dist2 < ans) query(mid + 1, r, x);
    }
}
void main() {
    ios::sync_with_stdio(0);
    cin.tie(0); cout.tie(0);
    cin >> n;
    for (int i = 1; i <= n; i++) cin >> s[i].x >> s[i].y;
    build(1, n);
    for (int i = 1; i <= n; i++) query(1, n, i);
    cout << fixed << setprecision(0) << ans << '\n';
}
}
signed main() {
#ifdef DEBUG
    freopen("../test.in", "r", stdin);
    freopen("../test.out", "w", stdout);
#endif
    Hanx16qwq::main();
    return 0;
}

Luogu P4357 [CQOI2016]K 远点对

与上面一道题类似,此时的估价函数应该改成距离矩形最远点的距离,然后用堆维护前 \(k\) 大的距离即可。

具体做法就是维护一个小根堆,先往堆内加入 \(k\) 个 \(0\),然后对于每个子树如果当前子树估价小于了堆顶元素,那么就证明这个子树不会更新堆内元素了,所以就不需要搜索这个子树。当进入一个新节点的时候,如果这个节点大于堆顶元素,就弹出堆顶,加入当前节点。其它细节与上一题很类似。

#include<bits/stdc++.h>
#define int long long

using namespace std;

namespace Hanx16qwq {
constexpr int _SIZE = 1e5;
int n, k;

struct Node {
	int x, y;
}s[_SIZE + 5];

int ls[_SIZE + 5], rs[_SIZE + 5];
int U[_SIZE + 5], D[_SIZE + 5], L[_SIZE + 5], R[_SIZE + 5];
priority_queue<int, vector<int>, greater<int>> q;

void Maintain(int x) {
	L[x] = R[x] = s[x].x;
	U[x] = D[x] = s[x].y;

	if (ls[x])
		L[x] = min(L[x], L[ls[x]]), R[x] = max(R[x], R[ls[x]]),
		D[x] = min(D[x], D[ls[x]]), U[x] = max(U[x], U[ls[x]]);

    if (rs[x])
        L[x] = min(L[x], L[rs[x]]), R[x] = max(R[x], R[rs[x]]),
        D[x] = min(D[x], D[rs[x]]), U[x] = max(U[x], U[rs[x]]);
}

int sq(int x) {return x * x;}

int Build(int l, int r) {
    if (l > r) return 0;

    int mid = (l + r) >> 1;
    double avx = 0, avy = 0, vax = 0, vay = 0;
    
    for (int i = l; i <= r; i++)
        avx += s[i].x, avy += s[i].y;

    avx /= (r - l + 1);
    avy /= (r - l + 1);

    for (int i = l; i <= r; i++)
        vax += sq(s[i].x - avx), vay += sq(s[i].y - avy);
    
    if (vax > vay)
        nth_element(s + l, s + mid, s + r + 1, [](Node x, Node y) {
            return x.x < y.x;
        });
    else
        nth_element(s + l, s + mid, s + r + 1, [](Node x, Node y) {
            return x.y < y.y;
        });

    ls[mid] = Build(l, mid - 1), rs[mid] = Build(mid + 1, r);
    return Maintain(mid), mid;
}

int calc(int a, int b) {
    return max(sq(s[a].x - L[b]), sq(s[a].x - R[b])) + 
           max(sq(s[a].y - U[b]), sq(s[a].y - D[b]));
}

void query(int l, int r, int x) {
    if (l > r) return;

    int mid = (l + r) >> 1;
    int res = sq(s[x].x  - s[mid].x) + sq(s[x].y - s[mid].y);

    if (res > q.top()) q.pop(), q.emplace(res);

    int dist1 = calc(x, ls[mid]), dist2 = calc(x, rs[mid]);

    if (dist1 > q.top() && dist2 > q.top()) {
        if (dist1 > dist2) {
            query(l, mid - 1, x);

            if (dist2 > q.top()) query(mid + 1, r, x);
        } else {
            query(mid + 1, r, x);

            if (dist1 > q.top()) query(l, mid - 1, x);
        }
    } else {
        if (dist1 > q.top()) query(l, mid - 1, x);

        if (dist2 > q.top()) query(mid + 1, r, x);
    }
}

void main() {
    ios::sync_with_stdio(0);
    cin.tie(0); cout.tie(0);
    cin >> n >> k;
    k <<= 1;

    for (int i = 1; i <= k; i++) q.emplace(0);

    for (int i = 1; i <= n; i++) cin >> s[i].x >> s[i].y;

    Build(1, n);

    for (int i = 1; i <= n; i++) query(1, n, i);

    cout << q.top() << '\n';
}
}

signed main() {
#ifdef DEBUG
    freopen("../test.in", "r", stdin);
    freopen("../test.out", "w", stdout);
#endif
    Hanx16qwq::main();
    return 0;
}

矩形查询

Luogu P4148 简单题

此题不仅卡空间,而且强制在线,就是摆明了让你写 KDT。

加入节点的方式上面有说到,不再说了。这里来说说怎么查询。

很明显,KDT 上每个节点都代表了一个矩形,如果这个矩形被查询区间完全覆盖,那么就将这个节点维护好的子树和直接贡献进入答案。如果完全无交,就没有继续搜索的必要了。否则就递归进入子树,每进入一个新节点就判断是否在查询区间内,是就贡献进入答案(有点像线段树)。

具体可以结合代码(封装的非常严实)。

#include<bits/stdc++.h>

using namespace std;

namespace Hanx16qwq {
class KDT {
private:
    static const int _SIZE = 5e5;
    int ls[_SIZE + 5], rs[_SIZE + 5], d[_SIZE + 5];
    int L[_SIZE + 5], R[_SIZE + 5], D[_SIZE + 5], U[_SIZE + 5];
    int siz[_SIZE + 5], sum[_SIZE + 5];
    int ldt[_SIZE + 5];
    int root, cnt;
    struct Node{
        int x, y, v;
    }s[_SIZE + 5];
    const double alpha = 0.6;

    void Maintain(int x) {
        siz[x] = siz[ls[x]] + siz[rs[x]] + 1;
        sum[x] = sum[ls[x]] + sum[rs[x]] + s[x].v;
        L[x] = R[x] = s[x].x;
        D[x] = U[x] = s[x].y;

        if (ls[x])
            L[x] = min(L[x], L[ls[x]]), R[x] = max(R[x], R[ls[x]]),
            D[x] = min(D[x], D[ls[x]]), U[x] = max(U[x], U[ls[x]]);

        if (rs[x])
            L[x] = min(L[x], L[rs[x]]), R[x] = max(R[x], R[rs[x]]),
            D[x] = min(D[x], D[rs[x]]), U[x] = max(U[x], U[rs[x]]);
    }

    bool CanRbd(int x) {
        return siz[x] * alpha <= (double)max(siz[ls[x]], siz[rs[x]]);
    }

    void Flatten(int x, int &ldc) {
        if (ls[x]) Flatten(ls[x], ldc);

        ldt[++ldc] = x;

        if (rs[x]) Flatten(rs[x], ldc);
    }

    int sq(int x) {return x * x;}

    int Build(int l, int r) {
        if (l > r) return 0;

        int mid = (l + r) >> 1;
        double avx = 0, avy = 0, vax = 0, vay = 0;
        
        for (int i = l; i <= r; i++) avx += s[ldt[i]].x, avy += s[ldt[i]].y;

        avx /= r - l + 1, avy /= r - l + 1;

        for (int i = l; i <= r; i++)
            vax += sq(s[ldt[i]].x - avx), vay += sq(s[ldt[i]].y - avy);

        if (vax > vay)
            d[ldt[mid]] = 1, nth_element(ldt + l, ldt + mid, ldt + r + 1, [&](int x, int y) {
                return s[x].x < s[y].x;
            });
        else
            d[ldt[mid]] = 2, nth_element(ldt + l, ldt + mid, ldt + r + 1, [&](int x, int y) {
                return s[x].y < s[y].y;
            });

        ls[ldt[mid]] = Build(l, mid - 1), rs[ldt[mid]] = Build(mid + 1, r);
        return Maintain(ldt[mid]), ldt[mid];
    }

    void Rebuild(int &x) {
        int ldc = 0;
        Flatten(x, ldc);
        x = Build(1, ldc);
    }
    
    int NewNode(int x, int y, int v) {
        s[++cnt] = {x, y, v};
        return cnt;
    }

    void Insert(int &x, int a, int b, int w) {
        if (!x) {
            x = NewNode(a, b, w);
            return Maintain(x);
        }

        if (d[x] == 1) {
            if (a <= s[x].x) Insert(ls[x], a, b, w);
            else Insert(rs[x], a, b, w);
        } else {
            if (b <= s[x].y) Insert(ls[x], a, b, w);
            else Insert(rs[x], a, b, w);
        }

        Maintain(x);

        if (CanRbd(x)) Rebuild(x);
    }

    int Query(int x, int al, int ar, int au, int ad) {
        if (!x || R[x] < al || L[x] > ar || U[x] < ad || D[x] > au) return 0;

        if (L[x] >= al && R[x] <= ar && U[x] <= au && D[x] >= ad) return sum[x];

        int res = 0;

        if (s[x].x >= al && s[x].x <= ar && s[x].y <= au && s[x].y >= ad) res = s[x].v;

        return res + Query(ls[x], al, ar, au, ad) + Query(rs[x], al, ar, au, ad);
    }
public:
    KDT() {root = 0, cnt = 0;}

    void Insert(int a, int b, int w) {Insert(root, a, b, w);}

    int Query(int al, int ar, int au, int ad) {return Query(root, al, ar, au, ad);}
};

KDT t;
int n, last;

void main() {
    ios::sync_with_stdio(0);
    cin.tie(0); cout.tie(0);
    cin >> n;
    int opt, x, y, a, b;

    while (cin >> opt, opt != 3) {
        cin >> x >> y >> a;
        x ^= last, y ^= last, a ^= last;
        
        if (opt == 1) t.Insert(x, y, a);
        else {
            cin >> b; b ^= last;
            cout << (last = t.Query(x, a, b, y)) << '\n';
        }
    }
}
}
signed main() {
#ifdef DEBUG
    freopen("../test.in", "r", stdin);
    freopen("../test.out", "w", stdout);
#endif
    Hanx16qwq::main();
    return 0;
}

标签:return,rs,int,Tree,mid,笔记,学习,ls,SIZE
From: https://www.cnblogs.com/hanx16msgr/p/16873158.html

相关文章

  • Linux课上笔记
    Linux课上笔记Linux版本说明CentOS7提供的ISO镜像文件:DVDISO标准安装版,推荐使用.EverythingISO对完整版安装盘的软件进行补充,集成所有软件MinimalISO......
  • JavaSE补遗学习
    1简述Java1.1Java数据类型Java是一种强类型语言,所有变量必须先定义,再使用。Java的数据类型分为两种,一种是基本类型,一种是引用类型。基本类型就例如int,byte,float......
  • 神经网络与深度学习(三):循环神经网络网络
    给神经网络增加记忆能力前馈神经网络:相邻两层之间存在单向连接,层内无连接有向无环图输入和输出的维数都是固定的,不能任意改变(全连接前馈网络)无法处理变长的序列数据......
  • 尚硅谷java零基础入门从221p开始的笔记
    221***面向对象上四种权限修饰的理解封装性的体现需要权限修饰符来配合1.java规定的四种权限(从小到大排列)private,缺省,protected,publicpublic类可以在任意地方被访问pr......
  • 【W3学习】CSS View Transitions Module Level 1(CSS 视图转换模块)
    该模块定义了视图转换API,以及相关的属性和伪元素。html{page-transition-tag:root;}html::page-transition{position:fixed;inset:0;}html::page-t......
  • 【W3学习】CSS Box Model Module Level 4(CSS 盒子模型模块)
    这个规范描述了margin和padding属性,它们在CSS框内和周围创建间距。以后可能会扩展到包括边框(目前在[css-backgrounds-3]中描述)。  Eachboxhasacontentar......
  • 【W3学习】CSS Box Model Module Level 3(CSS 盒子模型模块)
    这个规范描述了margin和padding属性,它们在CSS框内和周围创建间距。   content-boxReferstothecontentboxorcontentedge.(InanSVGcontext,treate......
  • 好好生活、好好工作、好好学习
    ——这是第62篇原创文章禅语有云:“吃饭穿衣皆是修行。”一个人如何可以做到终身成长?就是好好生活、好好工作、好好学习,努力过好每一个当下。迈克尔-柯蒂斯导演的电影《......
  • 学习笔记-ThinkPHP 之 SQLI审计分析(三)
    ThinkPHP之SQLI审计分析(三)Time:9-23影响版本:ThinkPHP=5.1.22Payload:/public/index.php/index/index?orderby[id`|updatexml(1,concat(0x7,user(),0x7e),1)%23]=1......
  • 学习笔记-ThinkPHP5之SQLI审计分析(二)
    ThinkPHP之SQLI审计分析(二)Time:9-3影响版本:ThinkPHP=5.0.10Payload:/public/index.php/index/index?username[0]=notlike&username[1][0]=&username[1][1]=&userna......