Alex's Anthology of Algorithms Common Code for Contests in Concise C++
Geometry / Advanced Planar Geometry

7.4.3 Minkowski Sum

7-Geometry/7.4.3_Minkowski_Sum.cpp

View on GitHub

Given two convex polygons, compute their Minkowski sum: the set of all points $a + b$ where $a$ comes from the first polygon and $b$ comes from the second. After rotating both polygons to start at their lowest-leftmost vertex, the boundary edges of the sum are obtained by merging the two cyclic edge-angle lists. Minkowski sums are useful for collision/translation problems, convex polygon distance checks, and diameter computations via $P + (-P)$.

  • minkowski_sum(a, b) returns the convex polygon representing the sum. Inputs must be convex polygons in boundary order, clockwise or counter-clockwise, with no repeated final vertex. Collinear consecutive output vertices are removed.

Implementation

#include <algorithm>
#include <cmath>
#include <utility>
#include <vector>

const double EPS = 1e-9;

#define EQ(a, b) (fabs((a) - (b)) <= EPS)

struct Point {
  double x, y;
  Point(double x = 0, double y = 0) : x(x), y(y) {}
  bool operator==(const Point &p) const { return EQ(x, p.x) && EQ(y, p.y); }
  Point operator+(const Point &p) const { return {x + p.x, y + p.y}; }
  Point operator-(const Point &p) const { return {x - p.x, y - p.y}; }
  double cross(const Point &p) const { return x * p.y - y * p.x; }
};

double polygon_area_2x(const std::vector<Point> &p) {
  double area = 0;
  for (int i = 0, j = static_cast<int>(p.size()) - 1; i < static_cast<int>(p.size()); j = i++) {
    area += p[j].cross(p[i]);
  }
  return area;
}

std::vector<Point> normalize(std::vector<Point> p) {
  while (p.size() > 1 && p.front() == p.back()) {
    p.pop_back();
  }
  if (p.size() <= 1) {
    return p;
  }
  if (polygon_area_2x(p) < 0) {
    std::reverse(p.begin(), p.end());
  }
  int start = 0;
  for (int i = 1; i < static_cast<int>(p.size()); i++) {
    if (p[i].y < p[start].y || (EQ(p[i].y, p[start].y) && p[i].x < p[start].x)) {
      start = i;
    }
  }
  std::rotate(p.begin(), p.begin() + start, p.end());
  return p;
}

std::vector<Point> remove_collinear(std::vector<Point> p) {
  std::vector<Point> res;
  for (const Point &q : p) {
    while (res.size() >= 2 && EQ((res.back() - res[res.size() - 2]).cross(q - res.back()), 0)) {
      res.pop_back();
    }
    if (res.empty() || !(q == res.back())) {
      res.push_back(q);
    }
  }
  while (res.size() >= 3 && EQ((res.back() - res[res.size() - 2]).cross(res[0] - res.back()), 0)) {
    res.pop_back();
  }
  while (res.size() >= 3 && EQ((res[0] - res.back()).cross(res[1] - res[0]), 0)) {
    res.erase(res.begin());
  }
  return res;
}

std::vector<Point> minkowski_sum(std::vector<Point> a, std::vector<Point> b) {
  a = normalize(std::move(a));
  b = normalize(std::move(b));
  if (a.empty() || b.empty()) {
    return {};
  }
  if (a.size() == 1) {
    for (Point &p : b) {
      p = p + a[0];
    }
    return b;
  }
  if (b.size() == 1) {
    for (Point &p : a) {
      p = p + b[0];
    }
    return a;
  }
  int n = static_cast<int>(a.size()), m = static_cast<int>(b.size());
  a.push_back(a[0]);
  a.push_back(a[1]);
  b.push_back(b[0]);
  b.push_back(b[1]);
  std::vector<Point> res;
  int i = 0, j = 0;
  while (i < n || j < m) {
    res.push_back(a[i] + b[j]);
    if (i == n) {
      j++;
      continue;
    }
    if (j == m) {
      i++;
      continue;
    }
    double cr = (a[i + 1] - a[i]).cross(b[j + 1] - b[j]);
    if (cr > -EPS) {
      i++;
    }
    if (cr < EPS) {
      j++;
    }
  }
  return remove_collinear(res);
}

Example Usage

#include <cassert>
using namespace std;

int main() {
  vector<Point> square{{0, 0}, {1, 0}, {1, 1}, {0, 1}};
  vector<Point> tri{{0, 0}, {2, 0}, {0, 2}};
  vector<Point> sum = minkowski_sum(square, tri);
  vector<Point> expected{{0, 0}, {3, 0}, {3, 1}, {1, 3}, {0, 3}};
  assert(sum == expected);

  vector<Point> shifted = minkowski_sum(square, {Point(2, 3)});
  vector<Point> expected_shifted{{2, 3}, {3, 3}, {3, 4}, {2, 4}};
  assert(shifted == expected_shifted);
  return 0;
}