首页 > 其他分享 >计算几何——平面最近点对

计算几何——平面最近点对

时间:2024-05-23 12:30:59浏览次数:17  
标签:const emm ll 几何 int 最近 ans 平面 dist

计算几何——平面最近点对

问题描述

给定平面上 \(n\)(\(n\ge2\))个点,找出一堆点,使得其间距离最短。

下午将介绍分治做法、非分治做法,以及期望线性做法。

其中运行速度(P1429 平面最近点对 加强版)大致为,非分治做法最快,期望线性做法最慢。

朴素算法

非常显然了,\(\mathcal O(n^2)\) 的遍历每一对点。

非常简略的一个代码示例,

function solve():
	min_dist = inf
	for i in point_set:
		for j in point_set:
			min_dist = min(min_dist, dist(i,j))
	return min_dist

简单剪枝

考虑一种常见的统计序列的思想:

依次加入每一个元素,统计它和其左边所有元素的贡献。

具体地,

  • 我们把所有点按照 \(x_i\) 为第一关键字、\(y_i\) 为第二关键字排序。

  • 同时,建立一个以 \(y_i\) 为第一关键字、 \(x_i\) 为第二关键字排序的 multiset

  • 对于每一个位置 \(i\),我们执行以下操作:

  1. 假设我们已经算出来的最小距离是 \(d\),

  2. 将所有满足 \(|x_i-x_j|\ge d\) 的点从集合中删除。它们不会再对答案有贡献。

  3. 对于集合内满足 \(|y_i-y_j|< d\) 的所有点,统计它们和 \((x_i,y_i)\) 的距离。

  4. 将 \((x_i,y_i)\) 插入到集合中。

这个算法的复杂度为 \(\mathcal O(n\log n)\),比分治做法常数略小,证明略。

代码:

#include <bits/stdc++.h>

using namespace std;

using ll = long long;

struct emm {
    ll x, y;
    emm() = default;
    emm(ll x, ll y): x(x), y(y) {}
    friend ll operator *(const emm &a, const emm &b) {
        return (a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y);
    }
};

struct cmp_x {
    bool operator ()(const emm &a, const emm &b) const {
        return a.x == b.x ? a.y < b.y : a.x < b.x;
    }
};

struct cmp_y {
    bool operator ()(const emm &a, const emm &b) const {
        return a.y < b.y;
    }
};

double ans = 1e10;

void upd_ans(const emm &a, const emm &b) {
    double dist = sqrt(a * b);
    if (dist < ans) ans = dist;
}

vector<emm> a;

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr), cout.tie(nullptr);
    int n; cin >> n; a.resize(n);
    for (int i = 0; i < n; ++i) cin >> a[i].x >> a[i].y;
    sort(a.begin(), a.end(), cmp_x());
    multiset<emm, cmp_y> s;
    for (int i = 0, l = 0; i < n; ++i) {
        while (l < i && a[i].x - a[l].x >= ans) s.erase(s.find(a[l++]));
        auto it = s.lower_bound(emm(a[i].x, a[i].y - ans));
        for (; it != s.end() && it->y - a[i].y < ans; ++it) upd_ans(a[i], *it);
        s.insert(a[i]);
    }
    printf("%.4lf\n", ans);
    return 0;
}

这个做法的问题在于 multiset 的大常数,但是好写。

分治算法

考虑如果我们把所有点按照 \(x_i\) 排序,分治解决。

  1. 假设我们已经算出来的最小距离是 \(d\)。

  2. 考虑如何合并,显然只有两个集合分界线处各 \(d\) 距离内的点需要考虑。

  3. 我们枚举这个小集合内的点,计算每个点向下最多 \(d\) 个单位的点的贡献。

因为当前最小距离 \(d\)、向下枚举的是 \(d\times2d\) 的矩阵,其内部的点的个数是 \(\mathcal O(1)\) 的。

因此,整体复杂度即考虑分治的复杂度,即 \(\mathcal O(n\log n)\),但是常数比非分治略大。

代码:

#include <bits/stdc++.h>

using namespace std;

using ll = long long;
using db = double;

struct emm {
    ll x, y;
    emm() = default;
    emm(ll x, ll y): x(x), y(y) {}
    friend ll operator *(const emm &a, const emm &b) {
        return (a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y);
    }
};

struct cmp_x {
    bool operator ()(const emm &a, const emm &b) const {
        return a.x == b.x ? a.y < b.y : a.x < b.x;
    }
};

struct cmp_y {
    bool operator ()(const emm &a, const emm &b) const {
        return a.y < b.y;
    }
};

double ans = 1e10;

void upd_ans(const emm &a, const emm &b) {
    double dist = sqrt(a * b);
    if (dist < ans) ans = dist;
}

vector<emm> a;

void merge(int l, int r) {
    if (l == r) return;
    int m = l + r >> 1; ll mx = a[m].x;
    merge(l, m), merge(m + 1, r);
    inplace_merge(a.begin() + l, a.begin() + m + 1, a.begin() + r + 1, cmp_y());
    vector<emm> t;
    for (int i = l; i <= r; ++i) {
        if (abs(a[i].x - mx) >= ans) continue;
        for (auto j = t.rbegin(); j != t.rend(); ++j) {
            if (a[i].y - j->y >= ans) break;
            upd_ans(a[i], *j);
        }
        t.push_back(a[i]);
    }
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr), cout.tie(nullptr);
    int n; cin >> n; a.resize(n);
    for (int i = 0; i < n; ++i) cin >> a[i].x >> a[i].y;
    sort(a.begin(), a.end(), cmp_x());
    merge(0, n - 1);
    printf("%.4lf\n", ans);
    return 0;
}

使用了 std::inplace_merge 作为归并,详见 cppreference。

期望线性

注意是期望线性做法,复杂度理论期望值是 \(\mathcal O(n)\) 的。

但是实际上常数巨大,而且容易被卡,实测速度反而最慢。

  1. 同样我们考虑加入一个点的贡献,但是这里需要先随机打乱。

  2. 记前 \(i-1\) 个点的最近点对距离为 \(d\),将平面以 \(d\) 为边长划分为若干个网格。

  3. 检查第 \(i\) 个点所在网格的周围九个网格中的所有点,并更新答案。

  4. 使用哈希表存下每个网格内的点,如果答案被更新,就重构网格图,否则不重构。

因为前 \(i-1\) 个点的最近点对距离为 \(d\),从而每个网格不超过 \(4\) 个点。

注意到需检查的点的个数是 \(\mathcal O(1)\) 的,在前 \(i\) 个点中,最近点对包含 \(i\) 的概率为
\(\mathcal O(1/i)\)。

而重构网格的代价为 \(\mathcal O(i)\),从而第 \(i\) 个点的期望代价为 \(\mathcal O(1)\)。

于是对于 \(n\) 个点,该算法期望为 \(\mathcal O(n)\)。

代码:

#include <bits/stdc++.h>

using namespace std;

struct my_hash {
  static uint64_t splitmix64(uint64_t x) {
    x += 0x9e3779b97f4a7c15;
    x = (x ^ (x >> 30)) * 0xbf58476d1ce4e5b9;
    x = (x ^ (x >> 27)) * 0x94d049bb133111eb;
    return x ^ (x >> 31);
  }

  size_t operator()(uint64_t x) const {
    static const uint64_t FIXED_RANDOM =
        chrono::steady_clock::now().time_since_epoch().count();
    return splitmix64(x + FIXED_RANDOM);
  }

  size_t operator()(pair<uint64_t, uint64_t> x) const {
    static const uint64_t FIXED_RANDOM =
        chrono::steady_clock::now().time_since_epoch().count();
    return splitmix64(x.first + FIXED_RANDOM) ^
           (splitmix64(x.second + FIXED_RANDOM) >> 1);
  }
};

mt19937 rng(time(0));

using ll = long long;
using grid = pair<int, int>;

struct emm {
    ll x, y;
    emm() = default;
    emm(ll x, ll y): x(x), y(y) {}
    friend ll operator *(const emm &a, const emm &b) {
        return (a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y);
    }
};

double ans = 1e10;

void upd_ans(const emm &a, const emm &b) {
    double dist = sqrt(a * b);
    if (dist < ans) ans = dist;
}

vector<emm> a;

unordered_map<grid, vector<emm>, my_hash> ump;

#define group(e, t) make_pair(e.x / (int)t, e.y / (int)t)

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr), cout.tie(nullptr);
    int n; cin >> n; a.resize(n);
    for (int i = 0; i < n; ++i) cin >> a[i].x >> a[i].y;
    shuffle(a.begin(), a.end(), rng);
    for (int i = 0; i < n; ++i) {
        double lt = ans;
        int tx, ty; tie(tx, ty) = group(a[i], lt);
        for (int kx = tx - 1; kx <= tx + 1; ++kx) {
            for (int ky = ty - 1; ky <= ty + 1; ++ky) {
                auto eq = make_pair(kx, ky);
                if (!ump.count(eq)) continue;
                for (emm j : ump[eq]) upd_ans(a[i], j);
            }
        }
        if (ans == 0) break;
        if (ans != lt) {
            ump = decltype(ump)();
            for (int j = 0; j < i; ++j) ump[group(a[j], ans)].push_back(a[j]);
        }
        ump[group(a[i], ans)].push_back(a[i]);
    }
    printf("%.4lf\n", ans);
    return 0;
}

这个算法的常数很大,主要在于哈希(代码里手写了哈希函数)。

标签:const,emm,ll,几何,int,最近,ans,平面,dist
From: https://www.cnblogs.com/RainPPR/p/18208157

相关文章

  • LCA(最近公共祖先)
    LCA就是最近公共祖先,表示为\(\operatorname{lca}(a,b)\),它的求解方法主要有两种。倍增法这是最常用的一种可以动态求LCA的算法。时间复杂度为\(O(\log{n})\)。中心思想这个算法中有两个特殊的数组:\(depth[i]\)和\(fa[i][k]\)。\(depth[i]\):\(i\)点的深度,以\(root\)......
  • d3d12龙书阅读----绘制几何体(上) 课后习题
    d3d12龙书阅读----绘制几何体(上)课后习题练习1完成相应的顶点结构体的输入-布局对象typedefstructD3D12_INPUT_ELEMENT_DESC{一个特定字符串将顶点结构体数组里面的顶点映射到顶点着色器的输入签名LPCSTRSemanticName;语义索引如果语义名相同的......
  • 最近参加面试的一个总结
    多线程的理解thread和task的区别委托和事件的区别IOC的概念以及优劣势反射的基本概念以及优劣势aop的概念以及使用场景装箱拆箱的基本概念?如何优化装箱拆箱DI依赖注入的生命周期概念以及使用场景一个系统如果不能写入cookie,然后要通过其他怎么授权?微信扫描登录是如何授权的什么时......
  • 最近几个SQL优化案例(水一波博客,当段子看)
    某国产数据库原厂高级工程师找我优化SQL,以下是他给的三个案例。......
  • LCA(最近公共祖先)应用
    LCA可以将一条树上路径拆成一或两半,所以我们可以将很多关于区间的算法拓展到树上。仓鼠找suger洛谷P3398考虑两条相交的纵向路径\([A,B]\)和\([C,D]\),如图:如果两条路径相交那么\(C\)是\(B\)的祖先,\(A\)是\(D\)的祖先,对于任意的路径\([A,X,B]\)和\([C,Y,D]\),如......
  • 最近的有感而发
    又是积压已久的情绪,最近两个事。继上次发完博客之后,zhb退役了。没错,就是这么突然,听到他退役的消息一时间不太能接受,他退役也是很多原因聚集一块导致的,这里就不细讲了,回顾两年acm生涯,大家在一起训练、打比赛以及平常的线下交流,回头看来,在实验室里交到的真正的朋友好像只有他一个,也......
  • 最近在写一个网页,想谈谈数据表的关系
    一对多影片(一)--剧集(多)影片表idurltitle1url1title12url2title23url3title3剧集表idmovie_idurl11url121url233url341url4在上面两个表中,可见一个影片可以有多个剧集,在表的设计中应该在多的一方设置一的一方......
  • WebGL:使用着色器进行几何造型
    前言本文将介绍如何使用着色器来进行几何造型,说到几何图形大家一定都不陌生,比如说三角形、圆形,接触过WebGL基础使用的小伙伴一定都知道怎么去在画布上绘制一个三角形,只要传入三个顶点坐标,并选择绘图模式,我们就能在WebGL的画布上画出一个三角形。但是除了这种形式之外,我们还可以......
  • 235. 二叉搜索树的最近公共祖先(leetcode)
    https://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-search-tree/要点是如果root是在p和q之间的值,意味着已经找到了最近公共祖先/***Definitionforabinarytreenode.*structTreeNode{*intval;*TreeNode*left;*TreeNode*rig......
  • 236. 二叉树的最近公共祖先(leetcode)
    https://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-tree/description/要点是后序遍历,这样就可以从树底开始搜索,寻找最近公共祖先classSolution{public://返回的是最近公共祖先,若返回null则说明没有TreeNode*lowestCommonAncestor(TreeNode*r......