Author: Alei Reyes
Tester: Suchan Park
Editorialist: William Lin

Challenge

# PROBLEM:

There are N points. In one operation, you can choose a convex polygon P from those points. Let M be the number of points of P. The sum of M over all operations should be \le 5\cdot 10^5. For each i from 1 to M-1, consider the segment from P_i to P_{i+1}. Move P_i to the nearest lattice point on the segment. Whenever two points coincide, they merge together and always move as one point afterwards. Find the minimum number of operations to merge all points.

# Observations:

• The coordinates are relatively small (at most 1024) compared to N (around 2^{17}).
• This means each row / column should have an average of at least 100 points.

# Simplest Valid Solution:

Since each row should have at least one point, for some point (x1, y1), there should exist some point (x2, y2) with x2=x1+1. Since the points are in adjacent rows, there canâ€™t be any lattice points on the segment connecting them, so the two points can be merged in just one operation.

The full algorithm is to start from the bottom row and merge each point in one row with a point in the next row. After that, all points with be on the last row and we can finish by merging them from left to right (the coordinates are small so this doesnâ€™t waste too many operations).

Code
#include <bits/stdc++.h>
using namespace std;

#define ar array

int n;
vector<ar<int, 2>> v[1030];
vector<vector<int>> ans;

int main() {
ios::sync_with_stdio(0);
cin.tie(0);

//input
cin >> n;
for(int i=0, x, y; i<n; ++i) {
cin >> x >> y;
v[x].push_back({y, i});
}

int x=0;
while(1) {
if(v[x+1].empty())
break;
//merge all points on row i with a point on row i+1
for(ar<int, 2> a : v[x])
ans.push_back({a[1], v[x+1][0][1]});
++x;
}

//all points on the last row, merge them
sort(v[x].begin(), v[x].end());
for(int i=0; i+1<v[x].size(); ++i) {
//the first i points have been merged
//now merge point i+1
while(v[x][i][0]<v[x][i+1][0]) {
ans.push_back({v[x][i][1], v[x][i+1][1]});
//in one operation, point i becomes 1 closer
++v[x][i][0];
}
}

//output
cout << ans.size() << "\n";
for(vector<int> a : ans) {
cout << a.size();
for(int ai : a)
cout << " " << ai+1;
cout << "\n";
}
}


# Bigger polygons:

The previous solution is inefficient because we only merge two points for each operation. If we can somehow choose bigger polygons, we can merge more points for each operation, which will decrease the number of operations.

We can basically use the same solution as before, but we will try to add as many points in the next few rows to our polygon while making sure it is still convex.

Code
#include <bits/stdc++.h>
using namespace std;

#define ar array

int n;
vector<ar<int, 2>> v[1030];
vector<vector<ar<int, 2>>> ans;

int main() {
ios::sync_with_stdio(0);
cin.tie(0);

//input
cin >> n;
for(int i=0, x, y; i<n; ++i) {
cin >> x >> y;
v[x].push_back({y, i});
}

int x=0;
while(1) {
if(v[x+1].empty())
break;
//merge all points on row i with a point on row i+1
for(ar<int, 2> a : v[x]) {
vector<ar<int, 2>> c={a, v[x+1][0]};
int x2=x+1;
while(v[x2].size()>1&&!v[x2+1].empty()) {
//find some point on row x2+1
bool ok=0;
for(ar<int, 2> b : v[x2+1]) {
//check cross product to check convexity
if(c.back()[0]-c[c.size()-2][0]>b[0]-c.back()[0]) {
c.push_back(b);
ok=1;
break;
}
}
if(ok) {
//point at row x2 is merged to x2+1
v[x2].erase(find(v[x2].begin(), v[x2].end(), c[c.size()-2]));
} else
break;
++x2;
}
ans.push_back(c);
}
++x;
}

//all points on the last row, merge them
sort(v[x].begin(), v[x].end());
for(int i=0; i+1<v[x].size(); ++i) {
//the first i points have been merged
//now merge point i+1
while(v[x][i][0]<v[x][i+1][0]) {
ans.push_back({v[x][i], v[x][i+1]});
//in one operation, point i becomes 1 closer
++v[x][i][0];
}
}

//output
cout << ans.size() << "\n";
for(vector<ar<int, 2>> a : ans) {
cout << a.size();
for(ar<int, 2> ai : a)
cout << " " << ai[1]+1;
cout << "\n";
}
}


# Better polygon finder:

Finding next lattice point on a segment

Let the first point be (x1, y1) and the second point be (x2, y2). The vector from the first point to the second point is (vx, vy)=(x2-x1, y2-y1). If g=\gcd(vx, vy), the next lattice point on the segment will be (x1+\frac{vx}{g}, y1+\frac{vy}{g}).

It follows that if g=1 then there are no intermediate lattice points on the segment.

While there are still points left, we will take a random subset of at most 30 points (if we take too many then we will exceed the time limit). We will calculate the convex hull of the subset, but if we form a segment with an intermediate lattice point, then we will ignore the point. The goal of this is to make sure all points on the polygon of the convex hull can be merged into one point.

At some point when there are very few points left, it could be very hard to find any polygon. When that happens, merge two points at once until all remaining points have been merged.

Code
#include <bits/stdc++.h>
using namespace std;

#define ar array

int n;
vector<ar<int, 3>> v;
vector<vector<ar<int, 3>>> ans;
vector<bool> del;

//distance squared
int ds(ar<int, 3> a, ar<int, 3> b) {
return (a[0]-b[0])*(a[0]-b[0])+(a[1]-b[1])*(a[1]-b[1]);
}

//cross product
int cp(ar<int, 3> o, ar<int, 3> a, ar<int, 3> b) {
return (a[0]-o[0])*(b[1]-o[1])-(a[1]-o[1])*(b[0]-o[0]);
}

//convex hull with graham's scan
vector<ar<int, 3>> ch(vector<ar<int, 3>> u) {
sort(u.begin(), u.end());
sort(u.begin()+1, u.end(), [&](const ar<int, 3> &i, const ar<int, 3> &j) {
int cpv=cp(u[0], i, j);
if(cpv>0)
return false;
if(cpv<0)
return true;
return ds(u[0], i)<ds(u[0], j);
});
vector<ar<int, 3>> ch;
for(int i=0; i<u.size(); ++i) {
while(ch.size()>1&&cp(ch[ch.size()-2], ch.back(), u[i])>=0)
ch.pop_back();
//make sure no intermediate lattice points
if(ch.empty()||abs(__gcd(u[i][0]-ch.back()[0], u[i][1]-ch.back()[1]))==1)
ch.push_back(u[i]);
}
if(ch.size()>2)
while(ch.size()>1&&cp(ch[ch.size()-2], ch.back(), u[0])>=0)
ch.pop_back();
return ch;
}

//find the next point on a segment
ar<int, 3> next_on_segment(ar<int, 3> a, ar<int, 3> b) {
ar<int, 2> v{b[0]-a[0], b[1]-a[1]};
int g=abs(__gcd(v[0], v[1]));
return {a[0]+v[0]/g, a[1]+v[1]/g, a[2]};
}

int main() {
ios::sync_with_stdio(0);
cin.tie(0);

//input
cin >> n;
for(int i=0, x, y; i<n; ++i) {
cin >> x >> y;
v.push_back({x, y, i});
}

del=vector<bool>(n);
int lastsize=n, halt=0;
while(v.size()>1) {
random_shuffle(v.begin(), v.end());
for(int i=0; i<v.size(); i+=2000) {
//take subset
vector<ar<int, 3>> sub(v.begin()+i, v.begin()+i+min(2000, (int)v.size()-i));
//find convex hull
vector<ar<int, 3>> c=ch(sub);
if(c.size()<2)
continue;
//delete points except for last
for(int i=0; i+1<c.size(); ++i)
del[c[i][2]]=1;
ans.push_back(c);
}
//update v
vector<ar<int, 3>> v2;
for(ar<int, 3> a : v)
if(!del[a[2]])
v2.push_back(a);
v=v2;
if(lastsize>v.size()) {
lastsize=v.size();
halt=0;
} else
++halt;
if(halt>9)
break;
}

//merge two points at once
//sort to avoid accidentally merging with other points
sort(v.begin(), v.end());
for(int i=0; i+1<v.size(); ++i) {
//points 0 to i have been merged
//now merge point i+1
while(v[i][0]!=v[i+1][0]||v[i][1]!=v[i+1][1]) {
v[i]=next_on_segment(v[i], v[i+1]);
ans.push_back({v[i], v[i+1]});
}
}

//output
cout << ans.size() << "\n";
for(vector<ar<int, 3>> a : ans) {
cout << a.size();
for(ar<int, 3> ai : a)
cout << " " << ai[2]+1;
cout << "\n";
}
}


Thereâ€™s a heuristic we can add. If the subset has points spread around the grid, then the convex hull will likely only contain the points near the outer part of the grid. We can group the points into subsets by their distance from the center of the grid.

Code

#include <bits/stdc++.h>
using namespace std;

#define ar array

int n;
vector<ar<int, 3>> v;
vector<vector<ar<int, 3>>> ans;
vector del;

//distance squared
int ds(ar<int, 3> a, ar<int, 3> b) {
return (a[0]-b[0])(a[0]-b[0])+(a[1]-b[1])(a[1]-b[1]);
}

//cross product
int cp(ar<int, 3> o, ar<int, 3> a, ar<int, 3> b) {
return (a[0]-o[0])(b[1]-o[1])-(a[1]-o[1])(b[0]-o[0]);
}

//convex hull with grahamâ€™s scan
vector<ar<int, 3>> ch(vector<ar<int, 3>> u) {
sort(u.begin(), u.end());
sort(u.begin()+1, u.end(), [&](const ar<int, 3> &i, const ar<int, 3> &j) {
int cpv=cp(u[0], i, j);
if(cpv>0)
return false;
if(cpv<0)
return true;
return ds(u[0], i)<ds(u[0], j);
});
vector<ar<int, 3>> ch;
for(int i=0; i<u.size(); ++i) {
while(ch.size()>1&&cp(ch[ch.size()-2], ch.back(), u[i])>=0)
ch.pop_back();
//make sure no intermediate lattice points
if(ch.empty()||abs(__gcd(u[i][0]-ch.back()[0], u[i][1]-ch.back()[1]))==1)
ch.push_back(u[i]);
}
if(ch.size()>2)
while(ch.size()>1&&cp(ch[ch.size()-2], ch.back(), u[0])>=0)
ch.pop_back();
return ch;
}

//find the next point on a segment
ar<int, 3> next_on_segment(ar<int, 3> a, ar<int, 3> b) {
ar<int, 2> v{b[0]-a[0], b[1]-a[1]};
int g=abs(__gcd(v[0], v[1]));
return {a[0]+v[0]/g, a[1]+v[1]/g, a[2]};
}

int main() {
ios::sync_with_stdio(0);
cin.tie(0);

//input
cin >> n;
for(int i=0, x, y; i<n; ++i) {
cin >> x >> y;
v.push_back({x, y, i});
}

del=vector<bool>(n);
int lastsize=n, halt=0;
while(v.size()>1) {
if(halt<1) {
sort(v.begin(), v.end(), [](const ar<int, 3> &i, const ar<int, 3> &j) {
return (i[0]-512)*(i[0]-512)+(i[1]-512)*(i[1]-512)<(j[0]-512)*(j[0]-512)+(j[1]-512)*(j[1]-512);
});
} else
random_shuffle(v.begin(), v.end());
for(int i=0; i<v.size(); i+=2000) {
//take subset
vector<ar<int, 3>> sub(v.begin()+i, v.begin()+i+min(2000, (int)v.size()-i));
//find convex hull
vector<ar<int, 3>> c=ch(sub);
if(c.size()<2)
continue;
//delete points except for last
for(int i=0; i+1<c.size(); ++i)
del[c[i][2]]=1;
ans.push_back(c);
}
//update v
vector<ar<int, 3>> v2;
for(ar<int, 3> a : v)
if(!del[a[2]])
v2.push_back(a);
v=v2;
if(lastsize>v.size()) {
lastsize=v.size();
halt=0;
} else
++halt;
if(halt>9)
break;
}

//merge two points at once
//sort to avoid accidentally merging with other points
sort(v.begin(), v.end());
for(int i=0; i+1<v.size(); ++i) {
//points 0 to i have been merged
//now merge point i+1
int j=i+1;
//try to find next row
while(j<v.size()&&v[j][0]==v[i][0])
++j;
if(j>=v.size())
j=i+1;
while(v[i][0]!=v[j][0]||v[i][1]!=v[j][1]) {
v[i]=next_on_segment(v[i], v[j]);
ans.push_back({v[i], v[j]});
}
}

//output
cout << ans.size() << "\n";
for(vector<ar<int, 3>> a : ans) {
cout << a.size();
for(ar<int, 3> ai : a)
cout << " " << ai[2]+1;
cout << "\n";
}


}

# Other solutions?

Note that in the solutions presented above, the segments from the polygons never contain lattice points which are not endpoints. This makes the code a lot easier as we donâ€™t need to take care of the case when vertices on the polygon merge with points on the segments of the polygon. However, this also limits how efficient our solution can be, so if you take the time to write the code to account for polygons with segments with intermediate lattice points, the score should improve.

5 Likes