画卷 發表於 2025-12-1 12:01:00

K-D Tree 相关

<p><strong>部分发表于洛谷。</strong></p>
<h3 id="简介">简介:</h3>
<p>K-D Tree 是一种适用于 <span class="math inline">\(k\)</span> 维空间信息处理的数据结构,一般是维护 <span class="math inline">\(n\)</span> 个点的信息,建出平衡二叉树;在 <span class="math inline">\(k\)</span> 比较小的</p>
<h3 id="建树">建树:</h3>
<p>一般使用交替建树,递归的分为以下三个步骤:</p>
<ul>
<li>
<p><strong>交替</strong>选择一个维度切割(即 <span class="math inline">\(x, y, z, \cdots\)</span> 依次切一遍,最后回到 <span class="math inline">\(x\)</span> 继续切)。</p>
</li>
<li>
<p>选择一个切割点将这个维度切割了。</p>
</li>
<li>
<p>然后<strong>递归</strong>到被切割点切开的两个超立方体继续切割,直到区域内没有点。</p>
</li>
</ul>
<p>一个切割点的左右儿子是其切开的两个超立方体的切割点。</p>
<p>为了维持二叉树的平衡,要左右子树尽量均匀,所以一般选择<strong>这个切割维度的中位数</strong>作为切割点。</p>
<p>此时得到的树高显然是 <span class="math inline">\(\log n + O(1)\)</span> 级别的。</p>
<p>为了方便理解,给定一个在二维平面的例子:</p>
<p><img src="https://cdn.luogu.com.cn/upload/image_hosting/k5pe0v3n.png" alt="选自 b 站董晓算法,若侵权联系可删" loading="lazy"></p>
<p>此时建出的 K-D Tree 就是:</p>
<p><img src="https://cdn.luogu.com.cn/upload/image_hosting/wef1mepb.png" alt="" loading="lazy"></p>
<p>可以使用 <code>nth_element</code> 辅助建树,时间复杂度为 <span class="math inline">\(O(n\log n)\)</span>。</p>
<p>为了方便操作,对于每个点,可以维护其被切割时的超立方体,即可以记录其子树内每个维度的最大最小值。</p>
<h3 id="最近点对">最近点对:</h3>
<p>即对于每个点,求出到其它点的最短距离。</p>
<p>设查询点是 <span class="math inline">\(a\)</span>,依然是递归的形式的从 <span class="math inline">\(rt\)</span> 进入(设当前到了 <span class="math inline">\(p\)</span>):</p>
<ul>
<li>
<p>先用 <span class="math inline">\(\operatorname{dis}(a, p)\)</span> 更新答案 <span class="math inline">\(ans\)</span>。</p>
</li>
<li>
<p>然后求出 <span class="math inline">\(p\)</span> 到左右子树所代表的超立方体的最短距离 <span class="math inline">\(disl, disr\)</span>,如果大于 <span class="math inline">\(ans\)</span>,则直接剪枝。</p>
</li>
<li>
<p>否则进入 <span class="math inline">\(disl, disr\)</span> 更小的那个子树先更新,出来时再判断是否比另外一个更优;这是估价型剪枝。</p>
</li>
</ul>
<blockquote>
<p><strong>提示:</strong> 使用 K-D Tree 单次查询最坏是 <span class="math inline">\(O(N)\)</span> 的,但是如果没有特意卡的情况下,还是可以骗到很多分的。</p>
</blockquote>
<h3 id="操作">操作:</h3>
<p>对于一个高维矩形 <span class="math inline">\(Q\)</span> 内的点的查询,可以递归式的从 <span class="math inline">\(rt\)</span> 开始判断(设当前到了 <span class="math inline">\(p\)</span>):</p>
<ul>
<li>
<p>若包含 <span class="math inline">\(p\)</span> 子树,则直接返回所有点的信息。</p>
</li>
<li>
<p>若与 <span class="math inline">\(p\)</span> 子树所在超立方体有交,先考虑 <span class="math inline">\(p\)</span> 本身的贡献,然后递归到左右子树处理。</p>
</li>
<li>
<p>否则无交,直接退出。</p>
</li>
</ul>
<p>考虑时间复杂度分析,先考虑二维情况,根据递归,显然时间复杂度是跟与 <span class="math inline">\(Q\)</span> 相交的点(且没有被 <span class="math inline">\(Q\)</span> 包含)的数量,将这些点分为两类:</p>
<ul>
<li>
<p>完全包含 <span class="math inline">\(Q\)</span>:即树上一条到根的链,点数是 <span class="math inline">\(O(h) = O(\log n)\)</span> 的。</p>
</li>
<li>
<p>部分与 <span class="math inline">\(Q\)</span> 相交:显然这些点所代表的矩形互不相交。</p>
</li>
</ul>
<p>考虑求与 <span class="math inline">\(Q\)</span> 相交的矩形的数量,显然这些与 <span class="math inline">\(Q\)</span> 相交的矩形必然至少与一条 <span class="math inline">\(Q\)</span> 的边相交;于是可以转化为与一条边相交的矩形的数量:</p>
<ul>
<li>考虑一个点 <span class="math inline">\(p\)</span> 所代表的矩形,通过两次切割将这个矩形分为了四个部分,每个部分为 <span class="math inline">\(p\)</span> 的孙子来表示,而一条平行于坐标轴的直线显然最多穿越这个四个部分中的两个部分。</li>
</ul>
<blockquote>
<p>这里阐述了要交替维度切割建树的原因,因为如果不交替切割,一条直线可能会直接穿过这四个部分。</p>
</blockquote>
<p>因为子树大小几乎是严格的一半,于是可以得到递推式子:</p>
<p></p><div class="math display">\[T(n) = 2T(\frac{n}{4}) + O(1)
\]</div><p></p><p>得到 <span class="math inline">\(T(n) = \sqrt n\)</span>;拓展到 <span class="math inline">\(k\)</span> 维上,类似的,是 <span class="math inline">\(T(n) = 2^{k - 1} T(\frac{n}{2^k}) + O(1)\)</span>,于是 <span class="math inline">\(T(n) = O(n^{1 - \frac{1}{k}})\)</span>(这里是将 <span class="math inline">\(k\)</span> 当做常数计算的,实际上常数要大不少)。</p>
<h3 id="插入删除">插入/删除:</h3>
<p>先说删除,比较简单,不需要真的将这个点删除,就把这个点打上懒标记,将其贡献清除即可,时间复杂度是 <span class="math inline">\(O(h) = O(\log n)\)</span> 的;如果要真删的话,也可以用下面的重构方法。</p>
<p>如果直接插入,就是递归式的,根据是否在左右子树的超立方体内判断插入到哪里,最后到达空节点。</p>
<p>但是这样可能会导致二叉树不平衡,使得查询复杂度出错;然后大家可能会想到替罪羊树的方法,定义一个平衡因子 <span class="math inline">\(\alpha\)</span>,如果子树大小超了,就子树重构,可以保证树高是 <span class="math inline">\(O(\log n)\)</span> 的。</p>
<blockquote>
<p>但是请注意,复杂度分析中,其四个孙子最多只有两个孙子被算进去,同时根据儿子子树严格减半,可以得到递推式;而替罪羊树的方法,只保证了树高是 <span class="math inline">\(O(\log n)\)</span> 的,没有保证子树节点数量,所以若那条 <span class="math inline">\(Q\)</span> 中的线恰好穿过四个孙子中两个子树最大的孙子,复杂度将会被卡满出问题,具体复杂度不太清楚,但是应该能卡?</p>
</blockquote>
<p>于是可以想到两种著名的重构算法:</p>
<ul>
<li>
<p>根号重构。</p>
</li>
<li>
<p>二进制分组。</p>
</li>
</ul>
<p>根号重构,即我每插入 <span class="math inline">\(B\)</span> 个点后再重构,存下插入的点,每次查询是 <span class="math inline">\(O(B + n^{1 - \frac{1}{k}})\)</span> 的;重构 <span class="math inline">\(\frac{n}{B}\)</span> 次,复杂度是 <span class="math inline">\(O(\frac{n^2 \log n}{B})\)</span>,均摊下来单次插入复杂度是 <span class="math inline">\(O(\frac{n \log n}{B})\)</span>,取 <span class="math inline">\(B = \sqrt{n \log n}\)</span> 最优;插入复杂度是 <span class="math inline">\(O(\sqrt{n \log n})\)</span>,查询 <span class="math inline">\(O(\sqrt{n \log n} + n^{1 - \frac{1}{k}})\)</span>;常数一般,可以使用。</p>
<p>二进制分组,即将 <span class="math inline">\(n\)</span> 二进制拆分,维护若干个二次幂大小的 K-D Tree;每次新加一个点,即新建一个大小 <span class="math inline">\(2^0\)</span> 的 K-D Tree,然后不断将相同大小的树合并(实现时把所有需要合并的点,全部拿出来合并即可,而不是真的依次合并,这样过于浪费);因为合并带 <span class="math inline">\(\log\)</span>,所以最后均摊复杂度是 <span class="math inline">\(O(n \log^2 n)\)</span> 的;查询就是在每个树上查一遍,最后累加起来,是一个等比数列累加的形式,也是 <span class="math inline">\(O(n^{1 - \frac{1}{k}})\)</span> 的;常数很小。</p>
<h3 id="例题p4148-简单题">例题:P4148 简单题</h3>
<h4 id="题意">题意:</h4>
<p>二维平面上,单点加,区间矩形查;强制在线。</p>
<h4 id="思路">思路:</h4>
<p>板子题,使用上面任意一种重构方式即可通过。</p>
<p>code</p>
<h3 id="p14312-模板k-d-tree">P14312 【模板】K-D Tree</h3>
<h4 id="题意-1">题意:</h4>
<p><span class="math inline">\(k\)</span> 维平面上,动态插入点,高维矩形内的点加,高维矩形内的点查。</p>
<h4 id="思路-1">思路:</h4>
<p>重构使用二进制分组形式。</p>
<p>对于高维矩形内的点加,使用懒标记维护即可,注意懒标记的下传等。</p>
<p>时间复杂度是 <span class="math inline">\(O(m \log^2 m + qm^{1 - \frac{1}{k}})\)</span>。</p>
<h4 id="完整代码">完整代码:</h4>
<pre><code class="language-cpp">#include&lt;bits/stdc++.h&gt;
#define lowbit(x) x &amp; (-x)
#define pi pair&lt;ll, ll&gt;
#define ls(k) k &lt;&lt; 1
#define rs(k) k &lt;&lt; 1 | 1
#define fi first
#define se second
using namespace std;
typedef __int128 __;
typedef long double lb;
typedef double db;
typedef unsigned long long ull;
typedef long long ll;
const int N = 1.5e5 + 10, M = 18, K = 3;
inline ll read(){
        ll x = 0, f = 1;
        char c = getchar();
        while(c &lt; '0' || c &gt; '9'){
                if(c == '-')
                        f = -1;
                c = getchar();
        }
        while(c &gt;= '0' &amp;&amp; c &lt;= '9'){
                x = (x &lt;&lt; 1) + (x &lt;&lt; 3) + (c ^ 48);
                c = getchar();
        }
        return x * f;
}
inline void write(ll x){
        if(x &lt; 0){
                putchar('-');
                x = -x;
        }
        if(x &gt; 9)
                write(x / 10);
        putchar(x % 10 + '0');
}
ll ans;
int n, m, op, w, rk, nowk, cnt;
int h, rt;
struct point{
        ll X;
        point(){
                memset(X, 0, sizeof(X));
        }
        inline bool operator&lt;(const point&amp;rhs)const{
                return X &lt; rhs.X;
        }
}a;
struct KD_Node{
        point a, mn, mx;
        int siz, lson, rson;
        ll data, sum, tag;
        inline bool operator&lt;(const KD_Node&amp;rhs)const{
                return a &lt; rhs.a;
        }
}Q, T;
inline void getmin(ll &amp;x, ll y){
        x = (x &lt; y) ? x : y;
}
inline void getmax(ll &amp;x, ll y){
        x = (x &gt; y) ? x : y;
}
inline void pushup(int k){
        T.siz = T.lson].siz + 1 + T.rson].siz;
        T.sum = T.data + T.lson].sum + T.rson].sum;
        for(int i = 0; i &lt; rk; ++i){
                T.mn.X = T.mx.X = T.a.X;
                if(T.lson){
                        getmin(T.mn.X, T.lson].mn.X);
                        getmax(T.mx.X, T.lson].mx.X);
                }
                if(T.rson){
                        getmin(T.mn.X, T.rson].mn.X);
                        getmax(T.mx.X, T.rson].mx.X);                       
                }
        }
}
inline void add(int k, ll v){
        if(!k)
          return ;
        T.data += v;
        T.tag += v;
        T.sum += v * T.siz;
}
inline void push_down(int k){
        if(T.tag){
                add(T.lson, T.tag);
                add(T.rson, T.tag);
                T.tag = 0;
        }
}
inline int build(int l, int r, int k){
        if(l &gt; r)
          return 0;
        nowk = k;
        int mid = (l + r) &gt;&gt; 1;
        nth_element(h + l, h + mid, h + r + 1, [](int x, int y) {return T &lt; T;});
        k = (k + 1) % rk;
        T].lson = build(l, mid - 1, k);
        T].rson = build(mid + 1, r, k);
        pushup(h);
        return h;
}
inline void reset(int &amp;k){
        if(!k)
          return ;
        h[++cnt] = k;
        push_down(k);
        reset(T.lson);
        reset(T.rson);
        k = 0;
}
inline ll query(int k){
        if(!k)
          return 0;
        bool flag = 1;
        for(int i = 0; i &lt; rk; ++i)
          flag &amp;= (Q.mn.X &lt;= T.mn.X &amp;&amp; T.mx.X &lt;= Q.mx.X);
        if(flag)
          return T.sum;
        for(int i = 0; i &lt; rk; ++i)
          if(Q.mx.X &lt; T.mn.X || T.mx.X &lt; Q.mn.X)
                return 0;
        flag = 1;
        for(int i = 0; i &lt; rk; ++i)
          flag &amp;= (Q.mn.X &lt;= T.a.X &amp;&amp; T.a.X &lt;= Q.mx.X);
        ll sum = 0;
        if(flag)
          sum = T.data;
        push_down(k);
        sum += query(T.lson);
        sum += query(T.rson);
        return sum;
}
inline void upd(int k, ll v){
        if(!k)
          return;
        bool flag = 1;
        for(int i = 0; i &lt; rk; ++i)
          flag &amp;= (Q.mn.X &lt;= T.mn.X &amp;&amp; T.mx.X &lt;= Q.mx.X);
        if(flag){
                add(k, v);
                return ;
        }
        for(int i = 0; i &lt; rk; ++i)
          if(Q.mx.X &lt; T.mn.X || T.mx.X &lt; Q.mn.X)
                return ;
        flag = 1;
        for(int i = 0; i &lt; rk; ++i)
          flag &amp;= (Q.mn.X &lt;= T.a.X &amp;&amp; T.a.X &lt;= Q.mx.X);
        if(flag)
          T.data += v;
        push_down(k);
        upd(T.lson, v);
        upd(T.rson, v);
        pushup(k);
}
int main(){
//        freopen("A.in", "r", stdin);
        rk = read(), m = read();
        while(m--){
                op = read();
                if(op == 1){
                        for(int i = 0; i &lt; rk; ++i)
                          a.X = read() ^ ans;
                        w = read() ^ ans;
                        T[++n] = {a, a, a, 0, 0, 0, w, w, 0};
                        cnt = 0;
                        h[++cnt] = n;
                        for(int i = 0; i &lt; M; ++i){
                                if(!rt){
                                        rt = build(1, cnt, 0);
                                        break;
                                }
                                else
                                  reset(rt);
                        }
                }
                else if(op == 2){
                        for(int i = 0; i &lt; rk; ++i)
                          a.X = read() ^ ans;
                        Q.mn = a;
                        for(int i = 0; i &lt; rk; ++i)
                          a.X = read() ^ ans;       
                        Q.mx = a;
                        w = read() ^ ans;
                        for(int i = 0; i &lt; M; ++i)
                          if(rt)
                                upd(rt, w);
                }
                else if(op == 3){
                        for(int i = 0; i &lt; rk; ++i)
                          a.X = read() ^ ans;
                        Q.mn = a;
                        for(int i = 0; i &lt; rk; ++i)
                          a.X = read() ^ ans;       
                        Q.mx = a;
                        ans = 0;
                        for(int i = 0; i &lt; M; ++i)
                          if(rt)
                                ans += query(rt);
                        write(ans);
                        putchar('\n');
                }
                else
                  break;
        }
        return 0;
}
</code></pre><br><br>
来源:https://www.cnblogs.com/rgw2010/p/19292290
頁: [1]
查看完整版本: K-D Tree 相关