PrevNext
Somewhat Frequent
 0/15

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 = 4
edges = []
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 than y, return true.
  • If y is greater than or equal to y, return false.
  • If compare(x, y) is true, then compare(y, x) must be false. (antisymmetry)
  • If compare(x, y) is true and compare(y, z) is true, then compare(x, z) must be true. (transitivity)

Essentially, the comparator determines whether xx belongs to the left of yy 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 than y, return -1.
  • If y is greater than y, return 1.
  • If the two are equal, return 0.
  • If compare(x, y) > 0, then compare(y, x) < 0. (antisymmetry)
  • If compare(x, y) > 0 and compare(y, z) > 0, then compare(x, z) > 0. (transitivity)

Essentially, the comparator determines whether xx belongs to the left of yy 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.

Implementing Comparable<T>

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 = a
self.b = b
self.width = width
edge_num = 4
edges = [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 than y, return -1.
  • If y is greater than y, return 1.
  • If the two are equal, return 0.
  • If compare(x, y) > 0, then compare(y, x) < 0. (antisymmetry)
  • If compare(x, y) > 0 and compare(y, z) > 0, then compare(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_key
class Edge:
def __init__(self, a: int, b: int, width: int):
self.a = a
self.b = b
self.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 Edges 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 {7,3,4,1}\{7, 3, 4, 1\} would be compressed to {3,1,2,0}\{3, 1, 2, 0\}. Notice that 11 is the least value in the first list, so it becomes 00, and 77 is the greatest value, so it becomes 33, 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 00 to 10910^9, we can't use them as array indices because we'd have to create an array of size 10910^9, which would cause an MLE verdict. However, if there are only N106N \leq 10^6 such integers, we can coordinate-compress their values, which guarantees that the values will all be in the range from 00 to N1N-1, which can be used as array indices.

Focus Problem – try your best to solve this problem before continuing!

View Internal Solution

Example 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 00 to N1N-1 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 sys
input = sys.stdin.readline
n = 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.

StatusSourceProblem NameDifficultyTags
CSESEasy
Show TagsPrefix Sums, Sorting
SilverNormal
Show TagsPrefix Sums, Sorting
SilverNormal
Show TagsPrefix Sums, Sorting
SilverNormal
Show TagsSorting
SilverNormal
Show TagsSorting
GoldNormal
Show TagsPrefix Sums, Sorting
CFNormal
Show TagsSorting
CFNormal
Show TagsPrefix Sums, Sorting
CFNormal
Show TagsSorting
SilverHard
Show TagsSorting
SilverHard
Show TagsSorting
SilverVery Hard
Show TagsSorting

Quiz

What would the list {40,21,4,1000,5}\{40, 21, -4, 1000, 5\} be coordinate compressed to?

Question 1 of 2

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!

PrevNext