computional alg

 

算法

常用算法方法

  • 凸包算法:凸包是指一组点在平面上的最小凸多边形,也就是说,它包含了所有的点,并且没有任何一个点在它的内部或边界上。计算凸包的算法有很多,比如Graham扫描法,Jarvis步进法,分治法等
  • 线段相交算法:线段相交是指给定平面上的一些线段,求出它们之间的所有交点。线段相交的算法有很多,比如扫描线法,平面扫描法,弯曲扫描法等
  • 最近点对算法:最近点对是指给定平面上的一些点,求出距离最近的两个点。最近点对的算法有很多,比如分治法,旋转卡壳法,随机化增量法等。
  • 最大空圆算法:最大空圆是指给定平面上的一些点,求出一个圆,使得它不包含任何一个点,并且它的半径最大。最大空圆的算法有很多,比如随机化增量法,Voronoi图法,Delaunay三角剖分法等。

知道两个点,求直线方程

#include <iostream>
#include <tuple>
#include <optional>
#include <format>
using namespace std;

const double epsilon = 1e-6;

struct Point{
    double x;
    double y;
};

std::optional<std::tuple<double,double,double>> line_equation(const Point& p1, const Point& p2){
  bool x_equal = abs(p1.x - p2.x) < epsilon;
  bool y_equal = abs(p1.y - p2.y) < epsilon;
  if(x_equal && y_equal){
    return std::nullopt;
  }
  double a = p2.y - p1.y;
  double b = p1.x - p2.x;
  double c = p2.x * p1.y - p1.x * p2.y;
  return std::make_tuple(a,b,c);
}

点到直线的距离

#include <cmath>

//定义一个函数,接收点的坐标和直线的系数作为参数,返回点到直线的距离
constexpr double distance(double x0, double y0, double A, double B, double C) noexcept
{
    //判断A和B是否同时为0
    if (A == 0 && B == 0)
    {
        //抛出异常
        throw std::invalid_argument("A and B cannot be both zero");
    }
    //计算分子
    double numerator = std::abs(A * x0 + B * y0 + C);
    //计算分母
    double denominator = std::hypot(A, B);
    //计算并返回距离
    return numerator / denominator;
}

点到直线的投影

#include <cmath>

struct Point
{
    double x;
    double y;
};

//定义一个函数,接收点的坐标和直线的系数作为参数,返回点到直线的投影点的坐标
constexpr Point projection(double x0, double y0, double A, double B, double C) noexcept
{
    //判断A和B是否同时为0
    if (A == 0 && B == 0)
    {
        //抛出异常
        throw std::invalid_argument("A and B cannot be both zero");
    }
    //取直线上任意一点Q,可以令x1=0, y1=-C/B
    double x1 = 0;
    double y1 = -C / B;
    //计算向量PQ和n的内积
    double dot = (x0 - x1) * A + (y0 - y1) * B;
    //计算向量n的模长
    double norm = std::hypot(A, B);
    //计算投影点R的坐标
    double x2 = x0 - dot / norm * A;
    double y2 = y0 - dot / norm * B;
    //返回投影点R
    return Point{x2, y2};
}

点关于直线的对称点

#include <cmath>

//定义一个结构体,表示一个点的坐标
struct Point
{
    double x;
    double y;
};

//定义一个函数,接收点的坐标和直线的系数作为参数,返回点关于直线的对称点的坐标
constexpr Point symmetric_point(double x0, double y0, double A, double B, double C) noexcept
{
    //判断A和B是否同时为0
    if (A == 0 && B == 0)
    {
        //抛出异常
        throw std::invalid_argument("A and B cannot be both zero");
    }
    //计算k和b
    double k = -A / B;
    double b = -C / B;
    //计算d
    double d = std::abs(k * x0 - y0 + b) / std::sqrt(k * k + 1);
    //计算d'
    double d_prime = d * std::abs(k) / std::sqrt(k * k + 1);
    //计算n的模长
    double norm = std::hypot(A, B);
    //计算R的坐标
    double x2 = x0 - 2 * d_prime * (-B) / norm;
    double y2 = y0 - 2 * d_prime * A / norm;
    //返回R
    return Point{x2, y2};
}

两条直线的位置关系


#include <cmath>

//定义一个结构体,表示一个二维平面上的点
struct Point
{
    double x; // x坐标
    double y; // y坐标
};

//定义一个结构体,表示一个二维平面上的直线
struct Line
{
    double A; // 直线方程的系数A
    double B; // 直线方程的系数B
    double C; // 直线方程的系数C

    // 构造函数,通过两个点来确定一条直线
    Line (const Point& p1, const Point& p2)
    {
        A = p2.y - p1.y;
        B = p1.x - p2.x;
        C = p2.x * p1.y - p1.x * p2.y;
    }

    // 构造函数,通过直线方程的系数来确定一条直线
    Line (double A, double B, double C)
    {
        this->A = A;
        this->B = B;
        this->C = C;
    }

    // 判断两条直线是否相交,如果相交,返回true,并计算出交点
    constexpr bool intersect (const Line& l, Point& p) const noexcept
    {
        // 计算两条直线方程的行列式
        double D = std::hypot(A * l.B, B * l.A);
        // 如果行列式为零,说明两条直线平行或重合
        if (D == 0)
        {
            return false;
        }
        // 否则,计算出交点的坐标
        p.x = (B * l.C - C * l.B) / D;
        p.y = (C * l.A - A * l.C) / D;
        return true;
    }

    // 判断两条直线是否平行,如果平行,返回true,并计算出平行距离
    constexpr bool parallel (const Line& l, double& d) const noexcept
    {
        // 计算两条直线方程的行列式
        double D = std::hypot(A * l.B, B * l.A);
        // 如果行列式不为零,说明两条直线不平行
        if (D != 0)
        {
            return false;
        }
        // 否则,计算出平行距离
        d = std::abs (C - l.C) / std::hypot (A, B);
        return true;
    }

    // 判断两条直线是否重合,如果重合,返回true
    constexpr bool coincide (const Line& l) const noexcept
    {
        // 计算两条直线方程的行列式
        double D = std::hypot(A * l.B, B * l.A);
        // 如果行列式不为零,说明两条直线不重合
        if (D != 0)
        {
            return false;
        }
        // 否则,判断常数项是否相等
        return C == l.C;
    }
};

判断点是否在多边形内部

#include <cmath>
#include <vector>

//定义一个结构体,表示一个二维平面上的点
struct Point
{
    double x; // x坐标
    double y; // y坐标
};

//定义一个函数,接收一个点和一个多边形(用std::vector表示)作为参数,返回这个点是否在多边形内部
constexpr bool point_in_polygon(const Point& p, const std::vector<Point>& polygon) noexcept
{
    bool inside = false; // 初始化结果为false
    for (std::size_t i = 0, j = polygon.size() - 1; i < polygon.size(); j = i++) // 遍历多边形的每条边
    {
        // 判断p的y坐标是否在边的y坐标范围内
        if ((polygon[i].y > p.y) != (polygon[j].y > p.y))
        {
            // 计算p在边上的投影点的x坐标
            auto x = (polygon[j].x - polygon[i].x) * (p.y - polygon[i].y) / (polygon[j].y - polygon[i].y) + polygon[i].x;
            // 判断p是否在投影点的左侧
            if (p.x < x)
            {
                // 如果是,说明射线与边相交,结果取反
                inside = !inside;
            }
        }
    }
    return inside; // 返回结果
}

给定平面内n个点,构造一个多边形

#include <iostream>
#include <vector>
#include <algorithm>
#include <stack>
using namespace std;

// 定义一个二维点的结构体
struct Point {
    double x, y; // 点的横纵坐标
    Point(double x = 0, double y = 0) : x(x), y(y) {} // 构造函数
};

// 计算两个向量的叉积
double cross(const Point& a, const Point& b) {
    return a.x * b.y - a.y * b.x;
}

// 计算两个向量的夹角
double angle(const Point& a, const Point& b) {
    return atan2(cross(a, b), a.x * b.x + a.y * b.y);
}

// 比较两个点的极角大小,如果相等则比较距离大小
bool cmp(const Point& p1, const Point& p2) {
    double ang = angle(p1, p2);
    if (ang == 0) { // 极角相等,比较距离
        return p1.x * p1.x + p1.y * p1.y < p2.x * p2.x + p2.y * p2.y;
    }
    return ang > 0; // 极角大于0,说明p1在p2的逆时针方向
}

// 求给定平面内n个点的凸包
vector<Point> convexHull(vector<Point>& points) {
    int n = points.size(); // 点的个数
    if (n <= 3) return points; // 如果点数小于等于3,直接返回

    // 找到最左下角的点,并将其交换到第一个位置
    int minIndex = 0;
    for (int i = 1; i < n; i++) {
        if (points[i].y < points[minIndex].y || (points[i].y == points[minIndex].y && points[i].x < points[minIndex].x)) {
            minIndex = i;
        }
    }
    swap(points[0], points[minIndex]);

    // 将其他点按照极角排序
    sort(points.begin() + 1, points.end(), cmp);

    // 用一个栈来存储凸包上的点
    stack<Point> s;
    s.push(points[0]); // 先将极点入栈
    s.push(points[1]); // 再将第二个点入栈

    // 从第三个点开始遍历
    for (int i = 2; i < n; i++) {
        // 如果栈顶的两个点和当前点不构成逆时针旋转,则出栈
        while (s.size() > 1) {
            Point top = s.top(); // 栈顶元素
            s.pop(); // 出栈
            Point nextToTop = s.top(); // 出栈后的栈顶元素
            if (cross(top - nextToTop, points[i] - top) > 0) { // 构成逆时针旋转
                s.push(top); // 将原来的栈顶元素再入栈
                break; // 跳出循环
            }
        }
        s.push(points[i]); // 将当前点入栈
    }

    // 将栈中的点放入结果向量中
    vector<Point> res;
    while (!s.empty()) {
        res.push_back(s.top());
        s.pop();
    }
    return res;
}

给定n个点,找出距离最小的点

#include <iostream>
#include <algorithm>
#include <cmath>
#include <vector>
using namespace std;

// 定义一个点结构体
struct Point {
    double x, y; // 点的坐标
    Point(double x = 0, double y = 0): x(x), y(y) {} // 构造函数
};

// 定义一个比较函数,按照x坐标升序排序
bool cmp_x(const Point& a, const Point& b) {
    return a.x < b.x;
}

// 定义一个比较函数,按照y坐标升序排序
bool cmp_y(const Point& a, const Point& b) {
    return a.y < b.y;
}

// 定义一个计算两点之间距离的函数
double dist(const Point& a, const Point& b) {
    return sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y));
}

// 定义一个求解给定点集中最小距离的函数
double solve(vector<Point>& points, int left, int right) {
    // 如果只有一个点,返回无穷大
    if (left == right) return 1e9;
    // 如果只有两个点,返回它们之间的距离
    if (left + 1 == right) return dist(points[left], points[right]);
    // 如果有三个点,返回任意两点之间最小的距离
    if (left + 2 == right) return min(dist(points[left], points[left + 1]), min(dist(points[left], points[right]), dist(points[left + 1], points[right])));
    // 否则,将点集分成两部分
    int mid = (left + right) / 2;
    // 分别求解左右两部分的最小距离
    double d1 = solve(points, left, mid);
    double d2 = solve(points, mid + 1, right);
    // 取两者中较小的值作为当前最小距离
    double d = min(d1, d2);
    // 找出所有距离中线不超过最小距离的点,并按照y坐标排序
    vector<Point> temp;
    for (int i = left; i <= right; i++) {
        if (fabs(points[i].x - points[mid].x) <= d) {
            temp.push_back(points[i]);
        }
    }
    sort(temp.begin(), temp.end(), cmp_y);
    // 遍历这些点,检查是否有更小的距离
    for (int i = 0; i < temp.size(); i++) {
        for (int j = i + 1; j < temp.size() && j <= i + 7; j++) {
            d = min(d, dist(temp[i], temp[j]));
        }
    }
    // 返回最小距离
    return d;
}

对于平面内n条水平线,m条竖直线,求交点集合

// 定义点的数据类型
struct Point {
    int x; // 横坐标
    int y; // 纵坐标
    Point(int x = 0, int y = 0) : x(x), y(y) {} // 构造函数
    // 重载比较运算符
    bool operator < (const Point& p) const {
        return x < p.x || (x == p.x && y < p.y);
    }
    bool operator == (const Point& p) const {
        return x == p.x && y == p.y;
    }
    bool operator != (const Point& p) const {
        return !(*this == p);
    }
    // 重载加法运算符
    Point operator + (const Point& p) const {
        return Point(x + p.x, y + p.y);
    }
};

// 定义线段的数据类型
struct Segment {
    Point a; // 端点a
    Point b; // 端点b
    Segment(Point a = Point(), Point b = Point()) : a(a), b(b) {} // 构造函数
};

// 定义端点类型的枚举类型
enum EndpointType {
    LEFT, // 左端点
    RIGHT, // 右端点
    UP, // 上端点
    DOWN // 下端点
};

// 定义横纵坐标范围的常量
const int MIN_X = -10000; // 最小横坐标
const int MAX_X = 10000; // 最大横坐标
const int MIN_Y = -10000; // 最小纵坐标
const int MAX_Y = 10000; // 最大纵坐标


// 定义一个比较函数,用于优先队列的排序规则
struct CompareEndpoint {
    bool operator () (const Endpoint& e1, const Endpoint& e2) const {
        return e1.first > e2.first || (e1.first == e2.first && e1.second > e2.second);
    }
};

// 定义一个优先队列,存储所有的端点,并按照横坐标从小到大排序
priority_queue<Endpoint, vector<Endpoint>, CompareEndpoint> pq;

// 定义一个比较函数,用于set的排序规则
struct CompareHorizontal {
    bool operator () (const int& y1, const int& y2) const {
        return y1 < y2;
    }
};

// 定义一个set,存储当前活跃的水平线,并按照纵坐标从小到大排序
set<int, CompareHorizontal> horizontal;

// 定义一个map,存储当前活跃的竖直线,并记录它们的横坐标
map<int, bool> vertical;


// 定义一个函数,实现扫描线算法的主要逻辑
vector<Point> scanLine(vector<Segment>& horizontalLines, vector<Segment>& verticalLines) {
    // 定义一个向量,存储所有的交点集合
    vector<Point> intersections;

    // 将所有的水平线和竖直线的端点放入优先队列中,并按照横坐标从小到大排序
    for (auto& l : horizontalLines) {
        pq.push({l.a, LEFT});
        pq.push({l.b, RIGHT});
    }
    for (auto& l : verticalLines) {
        pq.push({l.a, UP});
        pq.push({l.b, DOWN});
    }

    // 从左到右扫描平面上的所有线段的端点
    while (!pq.empty()) {
        Endpoint p = pq.top();
        pq.pop();

        // 根据端点的类型和所属的线段,更新活跃集合,并检查是否有新的交点产生
        switch (p.second) {
            case LEFT: // 如果p是水平线l的左端点
                horizontal.insert(p.first.y); // 将l插入到平衡二叉树中
                for (auto& v : vertical) { // 遍历哈希表中所有竖直线
                    if (v.second && v.first >= p.first.x && v.first <= p.first.x + 1) { // 如果v存在且与l有交点(假设水平线和竖直线都是单位长度)
                        intersections.push_back(Point(v.first, p.first.y)); // 将交点加入到向量中
                    }
                }
                break;
            case RIGHT: // 如果p是水平线l的右端点
                horizontal.erase(p.first.y); // 将l从平衡二叉树中删除
                break;
            case UP: // 如果p是竖直线v的上端点
                vertical[p.first.x] = true; // 将v插入到哈希表中,并标记为存在
                auto it = horizontal.lower_bound(p.first.y); // 找到平衡二叉树中第一个大于等于p纵坐标的水平线y1
                if (it != horizontal.end() && *it <= p.first.y + 1) { // 如果y1存在且与v有交点(假设水平线和竖直线都是单位长度)
                    intersections.push_back(Point(p.first.x, *it)); // 将交点加入到向量中
                }
                if (it != horizontal.begin()) { // 如果y1不是第一个水平线
                    it--; // 找到前一个水平线y2
                    if (*it >= p.first.y - 1) { // 如果y2与v有交点(假设水平线和竖直线都是单位长度)
                        intersections.push_back(Point(p.first.x, *it)); // 将交点加入到向量中
                    }
                }
                break;
            case DOWN: // 如果p是竖直线v的下端点
                vertical[p.first.x] = false; // 将v从哈希表中删除,并标记为不存在
                break;
        }
    }

    // 返回交点集合
    return intersections;
}


// 定义一个函数,生成n条随机的水平线
vector<Segment> generateHorizontalLines(int n) {
    vector<Segment> horizontalLines;
    srand(time(NULL)); // 设置随机数种子
    for (int i = 0; i < n; i++) {
        int x1 = rand() % (MAX_X - MIN_X + 1) + MIN_X; // 随机生成左端点的横坐标
        int x2 = x1 + 1; // 右端点的横坐标为左端点加1(假设水平线都是单位长度)
        int y = rand() % (MAX_Y - MIN_Y + 1) + MIN_Y; // 随机生成纵坐标
        horizontalLines.push_back(Segment(Point(x1, y), Point(x2, y))); // 将水平线加入到向量中
    }
    return horizontalLines;
}

// 定义一个函数,生成m条随机的竖直线
vector<Segment> generateVerticalLines(int m) {
    vector<Segment> verticalLines;
    srand(time(NULL)); // 设置随机数种子
    for (int i = 0; i < m; i++) {
        int x = rand() % (MAX_X - MIN_X + 1) + MIN_X; // 随机生成横坐标
        int y1 = rand() % (MAX_Y - MIN_Y + 1) + MIN_Y; // 随机生成下端点的纵坐标
        int y2 = y1 + 1; // 上端点的纵坐标为下端点加1(假设竖直线都是单位长度)
        verticalLines.push_back(Segment(Point(x, y1), Point(x, y2))); // 将竖直线加入到向量中
    }
    return verticalLines;
}

求凸包的直径算法

旋转卡壳方法是一种求解凸包的直径的高效算法,它的基本思想是:从凸包上任意选取两个点作为初始对踵点,然后沿着凸包的边界逆时针旋转这两个点,直到找到最远的一对点,这就是凸包的直径。旋转卡壳方法的时间复杂度是O(n),其中n是凸包上的点的个数。

C++实现旋转卡壳方法的代码如下:

#include <iostream>
#include <vector>
#include <cmath>
using namespace std;

// 定义一个二维点的结构体
struct Point {
    double x, y; // 点的坐标
    Point(double x = 0, double y = 0) : x(x), y(y) {} // 构造函数
};

//定义一个向量结构体
struct Vector {
    double x, y; //向量的坐标
    Vector(double x = 0, double y = 0): x(x), y(y) {} //构造函数
    Vector(Point a, Point b): x(b.x - a.x), y(b.y - a.y) {} //由两点构造向量
};

// 计算两个点之间的距离
double dist(const Point& a, const Point& b) {
    return sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y));
}

//计算向量的叉积
double cross(Vector a, Vector b) {
    return a.x * b.y - a.y * b.x;
}

//计算向量的模长
double length(Vector v) {
    return sqrt(v.x * v.x + v.y * v.y);
}

//旋转卡壳方法求凸包的直径
double rotating_calipers(vector<Point>& points) {
    int n = points.size(); //凸包上的点数
    if (n == 2) return dist(points[0], points[1]); //如果只有两个点,直接返回距离
    double ans = 0; //记录最大距离
    int j = 2; //第二个指针,初始指向第三个点
    for (int i = 0; i < n; i++) { //第一个指针,从第一个点开始逆时针旋转
        Point a = points[i]; //第一个顶点
        Point b = points[(i + 1) % n]; //第二个顶点,如果超过n,取余数
        while (cross(Vector(a, b), Vector(a, points[j])) < cross(Vector(a, b), Vector(a, points[(j + 1) % n]))) { //如果叉积变小,说明第二个指针需要继续旋转
            j = (j + 1) % n; //第二个指针逆时针旋转一步
        }
        ans = max(ans, max(dist(a, points[j]), dist(b, points[j]))); //更新最大距离,比较当前两对顶点的距离
    }
    return ans; //返回最大距离
}