Custom Comparators and Coordinate Compression
Authors: Darren Yao, Siyong Huang, Michael Cao, Benjamin Qi, Nathan Chen
Using a custom comparator to sort custom objects or values in a non-default order, and compressing values from a large range to a smaller one.
Custom Sorting
Resources | ||||
---|---|---|---|---|
IUSACO | partially based off this | |||
CPH | short overview |
Sorting can apply to more than just numbers. For example, many solutions to Wormhole Sort involve first sorting the list of edges by their weight.
For example, the sample case gives us the following edges:
1 2 9 1 3 7 2 3 10 2 4 3
After sorting, it should look like
2 4 3 1 3 7 1 2 9 2 3 10
C++
There's multiple ways we can do this.
The most straightforward method is to use a pair<int, pair<int, int>>
.
When comparing these, C++ looks at the first element and only uses
the second element as a tiebreaker.
#include <algorithm>#include <iostream>#include <vector>using namespace std;int main() {constexpr int edge_num = 4;vector<pair<int, pair<int, int>>> edges(edge_num);
If you don't like all the accesses to .first
and .second
,
an alternative is to use an std::vector
or maybe even an
std::array
:
#include <algorithm>#include <iostream>#include <vector>using namespace std;int main() {constexpr int edge_num = 4;// array<int, 3> is also usable and probably faster
Java
Unfortunately, Java doesn't have much builtin support for sorting collections of multiple elements; it mostly limits us to sorting raw integer arrays and the like.
To implement sorting by custom values in Java, we need to utilize comparators, which will be covered below.
Python
There's multiple ways we can do this. In Python, we can use a list of lists:
edge_num = 4edges = []for _ in range(edge_num):a, b, width = [int(i) for i in input().split()]edges.append((width, a, b))edges.sort()for e in edges:print(f"({e[1]}, {e[2]}): {e[0]}")
Another option would be a Tuple[int, Tuple[int]]
,
but that would make indexing more convoluted.
Comparators
Most sorting functions rely on moving objects with a lower value in front of objects with a higher value if sorting in ascending order, and vice versa if in descending order. This is done through comparing two objects at a time.
C++
In C++, its comparators must obey a set of behaviors.
Let's call this comparator compare
, and the objects it compares x
and y
.
- If
x
is less thany
, returntrue
. - If
y
is greater than or equal toy
, returnfalse
. - If
compare(x, y)
istrue
, thencompare(y, x)
must be false. (antisymmetry) - If
compare(x, y)
istrue
andcompare(y, z)
istrue
, thencompare(x, z)
must be true. (transitivity)
Essentially, the comparator determines whether belongs to the left of in a sorted ordering.
Warning!
A comparator must return false for two equal objects. Not doing so results in undefined behavior.
Overloading operator<
This is the easiest to implement. However, it only works for objects (not primitives) and it doesn't allow you to define multiple ways to compare the same type of class.
Let's sort those four edges again with this new method:
#include <algorithm>#include <iostream>#include <vector>using namespace std;struct Edge {int a, b;int width;
It's also possible to define operator<
outside of the class:
struct Edge {int a, b;int width;};bool operator<(const Edge &x, const Edge &y) { return x.width < y.width; }
Comparison Function
We can also pass the comparator as a
lambda
directly into std::sort
.
This has the advantage of working with primitives as well as classes.
#include <algorithm>#include <iostream>#include <vector>using namespace std;struct Edge {int a, b;int width;};
Java
In Java, its comparators must obey a set of behaviors.
Let's call this comparator compare
, and the objects it compares x
and y
.
For compare
to be valid, the following must be true:
- If
x
is less thany
, return-1
. - If
y
is greater thany
, return1
. - If the two are equal, return
0
. - If
compare(x, y) > 0
, thencompare(y, x) < 0
. (antisymmetry) - If
compare(x, y) > 0
andcompare(y, z) > 0
, thencompare(x, z) > 0
. (transitivity)
Essentially, the comparator determines whether belongs to the left of in a sorted ordering.
Java has some builtin comparables such as Integer.compare
and Arrays.compare
,
but we often find ourselves having to define our own.
Comparable<T>
Implementing To make a class T
sortable, we have to make it extend Comparable<T>
.
After that, we also have to implement the compareTo
method
which takes an instance of another class and returns a number
according to the rules described above.
When using Comparable, we can call Arrays.sort(arr)
or
Collections.sort(list)
on the array or list as usual.
import java.util.*;class Edge implements Comparable<Edge> {public int a, b;public int width;public Edge(int a, int b, int width) {this.a = a;this.b = b;this.width = width;
Comparator Fucntion
We can also pass a comparator function
directly to Arrays.sort
(or Collections.sort
, if you're sorting an ArrayList
).
import java.io.*;class Edge {public int a, b;public int width;public Edge(int a, int b, int width) {this.a = a;this.b = b;this.width = width;
Warning: Objects Only!
Unfortunately, these methods only work with objects and don't play well (or at all) with primitives. See this SO post for some workarounds on how to sort primitives by custom criteria.
Python
Key Function
Python's sorting functions take a key
argument that takes in
an object and returns a comparable datatype like an int
.
class Edge:def __init__(self, a: int, b: int, width: int):self.a = aself.b = bself.width = widthedge_num = 4edges = [Edge(*[int(i) for i in input().split()]) for _ in range(edge_num)]edges.sort(key=lambda e: e.width)for e in edges:print(f"({e.a}, {e.b}): {e.width}")
Comparator
A less idiomatic but still supported way to sort objects in Python is with comparators that return the relative order of two objects.
Let's call this comparator compare
, and the objects it compares x
and y
.
For compare
to be valid, the following must be true:
- If
x
is less thany
, return-1
. - If
y
is greater thany
, return1
. - If the two are equal, return
0
. - If
compare(x, y) > 0
, thencompare(y, x) < 0
. (antisymmetry) - If
compare(x, y) > 0
andcompare(y, z) > 0
, thencompare(x, z) > 0
. (transitivity)
Python's sorting functions don't take comparators directly.
We have to convert them to something key
can take with
functools.cmp_to_key
like so:
from functools import cmp_to_keyclass Edge:def __init__(self, a: int, b: int, width: int):self.a = aself.b = bself.width = width
See this SO post
for an explanation of how cmp_to_key
works.
Variations
Sorting in Descending Order
We can replace all occurrences of x.w < y.w
with x.w > y.w
in our C++ code.
Similarly, we can replace all occurrences of Integer.compare(x, y)
with
-Integer.compare(x, y)
in our Java code. In Python, we can pass the parameter
reverse=True
to the sort
or sorted
function.
Sorting by Multiple Criteria
Now, suppose we wanted to sort a list of Edge
s in ascending order, primarily
by width and secondarily by first vertex (a
). We can do this quite similarly
to how we handled sorting by one criterion earlier. What the comparator function
needs to do is to compare the weights if the weights are not equal, and
otherwise compare first vertices.
C++
struct Edge {int a, b;int width;bool operator<(const Edge &y) {if (width == y.width) { return width - y.width; }return a - y.a;}};
Java
class Edge implements Comparable<Edge> {public int a, b;public int width;public Edge(int a, int b, int width) {this.a = a;this.b = b;this.width = width;}
Python
In Python, tuples have a natural order based on their elements in order. We can take advantage of this to write a comparator:
edges.sort(key=lambda edge: (edge.width, edge.a))
Coordinate Compression
Coordinate compression describes the process of mapping each value in a list to its index if that list was sorted. For example, the list would be compressed to . Notice that is the least value in the first list, so it becomes , and is the greatest value, so it becomes , the largest index in the list.
When we have values from a large range, but we only care about their relative order (for example, if we have to know if one value is above another), coordinate compression is a simple way to help with implementation. For example, if we have a set of integers ranging from to , we can't use them as array indices because we'd have to create an array of size , which would cause an MLE verdict. However, if there are only such integers, we can coordinate-compress their values, which guarantees that the values will all be in the range from to , which can be used as array indices.
Focus Problem – try your best to solve this problem before continuing!
View Internal SolutionExample 1
A good example of coordinate compression in action is in the solution of USACO
Rectangular Pasture. Again, we won't delve into the full solution but instead
discuss how coordinate compression is applied. Since the solution uses
2D-prefix sums (another Silver topic), it is helpful if
all point coordinates are coordinate-compressed to the range to so
they can be used as array indices. Without coordinate compression, creating a
large enough array would result in a Memory Limit Exceeded
verdict.
Below you will find the solution to Rectangular Pasture, which uses coordinate compression at the beginning. Observe how a custom comparator is used to sort the points:
C++
#include <algorithm>#include <iostream>using namespace std;typedef pair<int, int> Point;bool ycomp(Point p, Point q) { return p.second < q.second; }const int MAX_N = 2500;int pref[MAX_N + 1][MAX_N + 1];
Java
import java.util.Arrays;import java.util.Comparator;import java.util.Scanner;public class RectangularPasture {static int[][] sums;static int getSum(int fromX, int toX, int fromY, int toY) {return sums[toX][toY] - sums[fromX - 1][toY] - sums[toX][fromY - 1] +sums[fromX - 1][fromY - 1];
Python
Warning!
Due to Python's constant factor, the below code will TLE on a couple test cases despite having the correct time complexity.
import sysinput = sys.stdin.readlinen = int(input().strip())points = [list(map(int, input().strip().split())) for _ in range(n)]points.sort(key=lambda i: i[1])for i in range(n):points[i][1] = i + 1
The solution to Rectangular Pasture directly replaces coordinates with their compressed values, and forgets the real values of the coordinates because they are unnecessary. However, there may be problems for which we need to also remember the original values of coordinates that we compress.
Example 2
Focus Problem – try your best to solve this problem before continuing!
This problem will require prefix sums and coordinate compression. However, the
implementation of coordinate compression in this solution will also require
remembering values in addition to compressing them (as opposed to just replacing
the original values, as in the last problem). If you just want to focus on the
implementation of coordinate compression and how it can switch between
compressed indices and original values, see the contracted code below. indices
is a list of values that need to be compressed. After it gets sorted and has
duplicate values removed, it is ready to use. The method getCompressedIndex
takes in an original value, and binary searches for its
position in indices
to get its corresponding compressed index. To go from a
compressed index to an original value, the code can just access that index in
indices
.
We also provide a more detailed explanation:
Detailed Explanation
C++
#include <bits/stdc++.h>using namespace std;typedef long long ll;ll difference_array[400005];// difference_array[i] = the difference of the values between special intervals// i-1 and i
Java
import java.io.*;import java.util.*;public class StaticRangeQueries {// custom class that stores a few integers (used for directly storing the// queries and updates given in the input)static class Query {int l, r, v;public Query(int l, int r, int v) {this.l = l;
Problems
Pro Tip
Many of the problems below may use other Silver concepts, such as prefix sums.
Status | Source | Problem Name | Difficulty | Tags | |
---|---|---|---|---|---|
CSES | Easy | Show TagsPrefix Sums, Sorting | |||
Silver | Normal | Show TagsPrefix Sums, Sorting | |||
Silver | Normal | Show TagsPrefix Sums, Sorting | |||
Silver | Normal | Show TagsSorting | |||
Silver | Normal | Show TagsSorting | |||
Gold | Normal | Show TagsPrefix Sums, Sorting | |||
CF | Normal | Show TagsSorting | |||
CF | Normal | Show TagsPrefix Sums, Sorting | |||
CF | Normal | Show TagsSorting | |||
Silver | Hard | Show TagsSorting | |||
Silver | Hard | Show TagsSorting | |||
Silver | Very Hard | Show TagsSorting |
Quiz
What would the list be coordinate compressed to?
Module Progress:
Join the USACO Forum!
Stuck on a problem, or don't understand a module? Join the USACO Forum and get help from other competitive programmers!