当前位置:首页 > 技术 > 正文内容

三维空间曼哈顿距离最近点查询:树状数组与CDQ分治的深度解析

访客 技术 2026年7月16日 1

问题背景与复杂度分析

在三维空间中动态维护点集,并支持两种操作:添加新点和查询距离给定点曼哈顿距离最近的点。该问题是经典二维问题(如"天使玩偶")的三维扩展。在二维情况下,通常采用 CDQ 分治算法解决,时间复杂度为 $O(n \log^2 n)$。然而,将其直接扩展到三维时,若使用 CDQ 分治嵌套树状数组,时间复杂度将上升至 $O(n \log^3 n)$,在严格的时间限制下极易超时。

相比之下,三维树状数组(Fenwick Tree)在理论上具有更小的常数因子。尽管三维树状数组的空间复杂度较高,但通过一维数组扁平化映射以及精细的常数优化,可以在实际运行中取得更好的性能。

核心算法思路:曼哈顿距离去绝对值

曼哈顿距离的定义为 $|x_2 - x_1| + |y_2 - y_1| + |z_2 - z_1|$。绝对值的存在使得直接进行数据结构维护变得困难。为了消除绝对值,我们可以根据查询点 $(x_2, y_2, z_2)$ 与数据点 $(x_1, y_1, z_1)$ 的相对位置关系,将空间划分为 8 个象限。

以 $x_1 \le x_2, y_1 \le y_2, z_1 \le z_2$ 这一象限为例,距离公式可化简为:

$$(x_2 - x_1) + (y_2 - y_1) + (z_2 - z_1) = (x_2 + y_2 + z_2) - (x_1 + y_1 + z_1)$$

对于固定的查询点,$(x_2 + y_2 + z_2)$ 是常数。因此,问题转化为在该象限内寻找 $(x_1 + y_1 + z_1)$ 的最大值。这可以通过三维树状数组或 CDQ 分治来维护前缀最大值。对于其他 7 个象限,只需对坐标轴进行相应的翻转(如将 $x$ 替换为 $X_{max} - x$),即可转化为相同的形式进行处理。

实现方案一:三维树状数组(BIT)

为了避免多维数组带来的内存碎片和访问开销,我们使用一维数组来模拟三维树状数组。通过哈希函数将三维坐标 $(x, y, z)$ 映射为一维索引。同时,将 8 个象限的处理逻辑封装在循环中,大幅减少代码冗余。

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

const int INF = 0x3f3f3f3f;

class Fenwick3D {
private:
    int dimX, dimY, dimZ;
    vector<int> tree;

    inline int flatten(int x, int y, int z) const {
        return (x * dimY + y) * dimZ + z;
    }

public:
    Fenwick3D(int x, int y, int z) : dimX(x), dimY(y), dimZ(z) {
        tree.assign((x + 1) * (y + 1) * (z + 1), 0);
    }

    void update(int x, int y, int z, int val) {
        for (int i = x; i <= dimX; i += i & -i) {
            for (int j = y; j <= dimY; j += j & -j) {
                for (int k = z; k <= dimZ; k += k & -k) {
                    int idx = flatten(i, j, k);
                    tree[idx] = max(tree[idx], val);
                }
            }
        }
    }

    int query(int x, int y, int z) {
        int maxVal = 0;
        for (int i = x; i > 0; i -= i & -i) {
            for (int j = y; j > 0; j -= j & -j) {
                for (int k = z; k > 0; k -= k & -k) {
                    maxVal = max(maxVal, tree[flatten(i, j, k)]);
                }
            }
        }
        return maxVal == 0 ? INF : maxVal;
    }
};

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    int dimX, dimY, dimZ, q;
    if (!(cin >> dimX >> dimY >> dimZ >> q)) return 0;

    dimX++; dimY++; dimZ++;
    vector<Fenwick3D> bits;
    bits.reserve(8);
    for (int i = 0; i < 8; ++i) {
        bits.emplace_back(dimX, dimY, dimZ);
    }

    int dx[] = {1, 1, 1, 1, -1, -1, -1, -1};
    int dy[] = {1, 1, -1, -1, 1, 1, -1, -1};
    int dz[] = {1, -1, 1, -1, 1, -1, 1, -1};

    auto getCoord = [&](int x, int y, int z, int dir) {
        int nx = (dx[dir] == 1) ? x : (dimX - x);
        int ny = (dy[dir] == 1) ? y : (dimY - y);
        int nz = (dz[dir] == 1) ? z : (dimZ - z);
        return make_tuple(nx, ny, nz);
    };

    while (q--) {
        int op, x, y, z;
        cin >> op >> x >> y >> z;
        if (op == 1) {
            for (int i = 0; i < 8; ++i) {
                auto [nx, ny, nz] = getCoord(x, y, z, i);
                bits[i].update(nx, ny, nz, nx + ny + nz);
            }
        } else {
            int ans = INF;
            for (int i = 0; i < 8; ++i) {
                auto [nx, ny, nz] = getCoord(x, y, z, i);
                int maxVal = bits[i].query(nx, ny, nz);
                if (maxVal != INF) {
                    ans = min(ans, (nx + ny + nz) - maxVal);
                }
            }
            cout << ans << "\n";
        }
    }
    return 0;
}

实现方案二:CDQ 分治嵌套树状数组

当空间限制极为严格,无法开辟三维树状数组时,CDQ 分治是更优的选择。我们将问题降维:第一维通过 CDQ 分治按时间或 $x$ 坐标排序,第二维在分治合并时通过归并排序处理 $y$ 坐标,第三维则使用一维树状数组维护 $z$ 坐标的前缀最大值。

为了处理 8 个象限,我们在主函数中通过循环对坐标进行翻转,并多次执行 CDQ 分治。这种写法虽然常数较大,但空间复杂度仅为 $O(n)$。

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

const int INF = 0x3f3f3f3f;

struct Operation {
    int id, x, y, z, type, val;
    bool isActive;
};

int dimZ;
vector<int> bitTree;

inline void updateBIT(int idx, int val) {
    for (; idx <= dimZ; idx += idx & -idx)
        bitTree[idx] = max(bitTree[idx], val);
}

inline int queryBIT(int idx) {
    int res = -INF;
    for (; idx > 0; idx -= idx & -idx)
        res = max(res, bitTree[idx]);
    return res;
}

inline void clearBIT(int idx) {
    for (; idx <= dimZ; idx += idx & -idx)
        bitTree[idx] = 0;
}

vector<Operation> tempOps;

void cdqY(int left, int right, vector<Operation>& ops, vector<int>& answers) {
    if (left == right) return;
    int mid = (left + right) / 2;
    cdqY(left, mid, ops, answers);
    cdqY(mid + 1, right, ops, answers);

    int i = left, j = mid + 1, k = left;
    while (j <= right) {
        while (i <= mid && ops[i].y <= ops[j].y) {
            if (ops[i].type == 1 && ops[i].isActive) {
                updateBIT(ops[i].z, ops[i].val);
            }
            tempOps[k++] = ops[i++];
        }
        if (ops[j].type == 2 && !ops[j].isActive) {
            int maxVal = queryBIT(ops[j].z);
            if (maxVal != -INF) {
                answers[ops[j].id] = min(answers[ops[j].id], ops[j].val - maxVal);
            }
        }
        tempOps[k++] = ops[j++];
    }
    
    for (int p = left; p < i; ++p) {
        if (ops[p].type == 1 && ops[p].isActive) clearBIT(ops[p].z);
    }
    
    while (i <= mid) tempOps[k++] = ops[i++];
    for (int p = left; p <= right; ++p) ops[p] = tempOps[p];
}

void cdqX(int left, int right, vector<Operation>& ops, vector<int>& answers) {
    if (left == right) return;
    int mid = (left + right) / 2;
    cdqX(left, mid, ops, answers);
    cdqX(mid + 1, right, ops, answers);

    int i = left, j = mid + 1, k = left;
    while (i <= mid && j <= right) {
        if (ops[i].x < ops[j].x || (ops[i].x == ops[j].x && ops[i].type < ops[j].type)) {
            tempOps[k] = ops[i];
            tempOps[k++].isActive = true;
            i++;
        } else {
            tempOps[k] = ops[j];
            tempOps[k++].isActive = false;
            j++;
        }
    }
    while (i <= mid) {
        tempOps[k] = ops[i];
        tempOps[k++].isActive = true;
        i++;
    }
    while (j <= right) {
        tempOps[k] = ops[j];
        tempOps[k++].isActive = false;
        j++;
    }
    for (int p = left; p <= right; ++p) ops[p] = tempOps[p];
    
    cdqY(left, right, ops, answers);
}

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    int dimX, dimY, q;
    if (!(cin >> dimX >> dimY >> dimZ >> q)) return 0;

    dimX++; dimY++; dimZ++;
    vector<Operation> ops(q + 1);
    vector<int> answers(q + 1, INF);
    vector<int> originalTypes(q + 1);
    
    for (int i = 1; i <= q; ++i) {
        cin >> ops[i].type >> ops[i].x >> ops[i].y >> ops[i].z;
        ops[i].id = i;
        originalTypes[i] = ops[i].type;
    }

    bitTree.assign(dimZ + 2, 0);
    tempOps.resize(q + 1);

    int dx[] = {1, 1, 1, 1, -1, -1, -1, -1};
    int dy[] = {1, 1, -1, -1, 1, 1, -1, -1};
    int dz[] = {1, -1, 1, -1, 1, -1, 1, -1};

    for (int dir = 0; dir < 8; ++dir) {
        for (int i = 1; i <= q; ++i) {
            int nx = (dx[dir] == 1) ? ops[i].x : (dimX - ops[i].x);
            int ny = (dy[dir] == 1) ? ops[i].y : (dimY - ops[i].y);
            int nz = (dz[dir] == 1) ? ops[i].z : (dimZ - ops[i].z);
            
            ops[i].x = nx; ops[i].y = ny; ops[i].z = nz;
            ops[i].val = nx + ny + nz;
            ops[i].isActive = false;
        }
        
        fill(bitTree.begin(), bitTree.end(), 0);
        
        sort(ops.begin() + 1, ops.end(), [](const Operation& a, const Operation& b) {
            return a.id < b.id;
        });
        
        cdqX(1, q, ops, answers);
    }

    for (int i = 1; i <= q; ++i) {
        if (originalTypes[i] == 2) {
            cout << answers[i] << "\n";
        }
    }
    
    return 0;
}

在上述 CDQ 分治代码中,为了保持时间序,我们在每次坐标翻转后重新按原始 ID 排序。通过分离维度的处理,将三维问题降维至一维树状数组,从而在受限的内存环境下完成计算。

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。