Why polynomial multiplication? While it may not be the most relevant application of the Fourier transform, it's one of the easiest to understand. It can serve as an entry point to understand other more complex applications of Fourier analysis.

This article isn't meant to cover the theory regarding Fourier analysis nor demonstrate every single theorem or concept formally. I kept it simple and informal on purpose.

First, let's talk about the concept of convolutions. An easy way to understand what a convolution is is by using polynomial multiplication as an example, which is in itself a convolution. We'll specifically be using discrete convolutions, as opposed to convolutions that operate on continuous functions.

Let's say we have two polynomials:

$$P(x) = -6 + 2x + 4x^2$$

$$Q(x) = 2 - x + 8x^2$$

The multiplication \(P(x)Q(x)\) will give us:

$$P(x)Q(x) = -12 + 10x - 42x^2 + 12x^3 + 32x^4$$

Now let's say instead of polynomials, we use the sequence of coefficients, such as:

$$u = (-6, 2, 4)$$

$$v = (2, -1, 8)$$

We can then apply the convolution operator (\(*\)), which gives us the same sequence of coefficients as when we multiplied the polynomials earlier:

$$u * v = (-12, 10, -42, 12, 32)$$

You can verify this result in Python:

import numpy as np np.convolve([-6, 2, 4], [2, -1, 8]) # Returns: # array([-12, 10, -42, 12, 32])

Therefore, convolution and polynomial multiplication can (for practical purposes) be considered as the same operation.

Explaining the theory behind the Fourier transform in full detail is out of the scope of this article, but here's a quick definition (check Wikipedia for more details):

Given a sequence of \(N\) numbers, \((x_0, x_1, ... x_{N-1})\), applying the Fourier transform returns another sequence \((X_0, X_1, ..., X_{N-1})\), defined by the following formula:

$$X_k = \sum_{n=0}^{N-1} x_n e^{-i2\pi \frac{k}{N}n}$$

One thing to note is that this formula resembles a polynomial evaluated using a complex number.

For instance, let's say our sequence is \((5, 4, 2)\). If we expand the element \(X_1\) we get:

$$X_1 = 5\ e^{(-i2\pi \frac{1}{3})0} + 4\ e^{(-i2\pi \frac{1}{3})1} + 2\ e^{(-i2\pi \frac{1}{3})2}

\\

= 5 + 4\ e^{-i2\pi \frac{1}{3}} + 2\ e^{(-i2\pi \frac{1}{3})2}$$

On a close inspection, you'll notice this is the same as the polynomial \(P(x) = 5 + 4x + 2x^2\) evaluated on \(x = e^{-i2\pi \frac{1}{3}}\).

Generalizing, \(X_k = P(e^{-i2\pi \frac{k}{N}})\) that is, the \(k\)-th element of the resulting Fourier transform sequence is the polynomial evaluated on the complex number \(e^{-i2\pi \frac{k}{N}}\), which depends both on \(k\) (current element index) and \(N\) (total sequence length)^{1}.

For \(N = 10\), if we plot the values for \(k = 0, 1, .., 9\) on the complex plane, we get the following. The red point is the value for \(k = 0\). The takeaway here is that each element of the resulting sequence is equal to the polynomial being evaluated on each of the complex values that lie on the circle of radius \(1\), with the circle being divided into \(N\) equal parts (i.e., the angle between each consecutive point is the same). Points are traversed in order when building the sequence^{2}.

This can help understand the relationship between Fourier transforms, convolutions, and evaluation of polynomial functions; however, I'm not going to go further into this topic.

If you have trouble understanding this, then at least know that the Fourier transform of a sequence outputs another sequence.

Let's explore how we can use the Fourier transform to find the convolution of two sequences.

The convolution theorem states that if we have two sequences and obtain the Fourier transform of both, we can simply multiply each item with its corresponding item in the other sequence (i.e., multiplication of elements in the same position).

This is called *element-wise (or point-wise) multiplication* and can be done in \(\mathcal{O}(n)\), as opposed to the convolution operation, which can only be done in \(\mathcal{O}(n^2)\) using the vanilla naïve algorithm.

Then, we simply apply the inverse Fourier transform to the result obtained in the previous step, and this would yield the convolution of the two original sequences. This is the final result we are looking for.

Both the Fourier transform and its inverse can be computed in \(\mathcal{O}(n\ log\ n)\) by using the Fast Fourier Transform. In this article, I present both the \(\mathcal{O}(n^2)\) (which is a lot easier to understand) and the faster \(\mathcal{O}(n\ log\ n)\) version of it.

Since the point-wise multiplication has linear time complexity, and computing the Fourier transform (and its inverse) has \(\mathcal{O}(n\ log\ n)\) time complexity, then the final algorithm has \(\mathcal{O}(n\ log\ n)\) time as well.

Here, we will compute the Fourier transform using the naïve algorithm. To compute the Fourier transform, you could use the mathematical definition introduced earlier, but an easier way is to use the DFT matrix (discrete Fourier transform matrix).

The DFT matrix is a transformation matrix containing the complex numbers described above conveniently organized in appropriate positions. Applying this transformation to a sequence yields its Fourier transform. Since this matrix is always invertible, we get the inverse transformation for free by computing its inverse.

Remember, a transformation matrix (linear transformation) \(A\) is applied by doing a multiplication \(A\mathbf x\) where \(\mathbf x\) is a column vector, in this case representing the sequence to transform.

# Use Numpy for linear algebra. import numpy as np # Create DFT matrix (square matrix). # It only depends on size "n". def create_transformation_matrix(n): # Based on mathematical definition. result = np.empty((n, n), dtype='complex_') w = np.exp(-np.pi * 2j / n) for i in range(n): for j in range(n): result[i][j] = w**(i * j) return result

Now, here's the code for the actual convolution.

One thing to mention is that it's necessary to pad the sequence with zeros so they are a certain length. Otherwise, the convolution result won't be correct. Since the degree of the product of two polynomials is the sum of the degrees of each polynomial, we set that value as the final length.

def convolution(seq1, seq2): # This is the amount of coefficients the resulting polynomial will have. n = len(seq1) + len(seq2) - 1 # Make both sequences the correct size by adding zeros. seq1 = np.pad(seq1, (0, n - len(seq1)), 'constant', constant_values=0) seq2 = np.pad(seq2, (0, n - len(seq2)), 'constant', constant_values=0) # Build the linear transformation (DFT matrix). matrix = create_transformation_matrix(n) # Find the Fourier transform of both sequences, by applying # the linear transformation (done using matrix multiplication). seq1_dft = np.matmul(matrix, seq1) seq2_dft = np.matmul(matrix, seq2) # Element-wise multiplication. product = np.multiply(seq1_dft, seq2_dft) # Compute the inverse Fourier transform. This is done # by getting the inverse of the DFT matrix, and applying # it as a linear transformation. return np.matmul(np.linalg.inv(matrix), product)

You can verify the results of this function by comparing them against Numpy's convolve function.

The Fast Fourier Transform (FFT) computes the Fourier transform in \(\mathcal{O}(n\ log\ n)\) time complexity, as opposed to \(\mathcal{O}(n^2)\) (when using the DFT matrix).

It's out of the scope of this article to fully explain how this speedup is obtained. I'm just going to throw some ideas that may give you hints on how FFT is made possible:

- Divide and conquer (divide the polynomial into two halves).
- Recursively compute the Fourier transform of both halves in such a way you can then merge both using some formula.
- Leverage some complex number properties and symmetries to simplify calculations. Some calculations will become unnecessary and can be removed.
- The inverse transform can also be greatly simplified using similar techniques.

Note that for computing a convolution, only the Fourier transform bit is changed. The rest of the algorithm can be kept the same.

Here's an implementation in Python (not micro-optimized). Keep in mind that the sequences now need to be padded with zeros until they are the correct length (i.e., the number of coefficients after multiplication), which in turn has to be rounded up to the next power of two. This is necessary to divide the polynomial into two halves correctly.

def fft(seq, inverse=False): n = len(seq) if n == 1: return seq w = np.exp(np.pi * 2j / n) if not inverse: w **= -1 even = seq[0:][::2] odd = seq[1:][::2] a = fft(even, inverse) b = fft(odd, inverse) y = np.empty(n, dtype='complex_') for i in range(n // 2): x = w**i y[i] = a[i] + b[i] * x y[i + (n // 2)] = a[i] - b[i] * x if inverse: y /= 2 return y

And there you have it! One of the most important algorithms of all time (perhaps only second to binary search) .

- Slow convolution (easier to understand, based directly on the mathematical definition)
- Convolution using FFT

- If the two sequences to convolve only have integer values, then the resulting sequence will only have elements with a real part.
- In most computer implementations, the result will have some precision issues since trigonometric functions need to be used.

The statement of Galactic Taxes can be summarized as follows:

Given a graph in which edges have a cost specified by \(At + B\), find the time \(t\) where the shortest path has the maximum cost. Constants \(A\) and \(B\) are given for each edge.

The source and target nodes are given in the statement.

Value of \(t\) belongs to \([0, 1440]\).

On one hand, we are told that we should always compute the shortest path from *source* to *target*. In informal terms, we can say that we should always use Dijkstra's Algorithm (or any other algorithm for computing the shortest path on a graph) for any value of \(t\). On the other hand, we are also told that we should maximize the path cost in function of \(t\).

In this problem, once a path traversal starts, the graph uses the same \(t\) value for the entire path without changing along the way. This makes it a lot easier than it'd be otherwise.

For this problem, we'll use the ternary search algorithm to find the maximum value. This algorithm finds the minimum or maximum value of unimodal functions by iteratively refining the search space or, in other words, reducing the search interval. We'll use the interval \([0, 1440]\), which is the range of valid \(t\) values.

On each iteration, two values \(t_1\) and \(t_2\) are chosen by the algorithm. We need to calculate the shortest path using Dijkstra's algorithm for both values. Since every edge is defined using the formula \(At + B\), we just need to plug the value into it. Then, the interval is modified in such a way that we get closer to the solution.

Dijkstra's algorithm may be relatively computationally expensive, but the ternary search doesn't need too many iterations and converges quickly. After a few dozen iterations, we'll get a good numerical approximation of the solution.

Sounds good, but how do we know that the function we are trying to maximize is unimodal? In simple words, this would mean the function starts as a monotonically increasing function, then at some point, it becomes monotonically decreasing. This implies there's one global maximum where the function changes from increasing to decreasing^{1}.

Every possible path in the graph has a cost that can be computed as the sum of the cost of all edges present in said path. Also, remember that each edge \(i\) has a cost defined by \(A_it + B_i\).

For each independent path, the result of \(\sum{A_it + B_i}\) will always be another line since a sum of degree one polynomials always gives you another polynomial of degree one or zero.

We can represent the cost of all possible paths by drawing a bunch of lines. Each line represents the cost of one path in function of \(t\).

It's important to note that every path computed for any value of \(t\) must be the shortest one. So, going back to our visualization, we can tell we must keep the lower part of the set of lines, so to speak. This resembles a convex polygon (or an upper convex hull).

We can clearly see the function is unimodal. However, note that we haven't really proved it yet mathematically. We just visualized it, but this is better than having no proof at all . I guess it can be proved more appropriately with some additional math and/or observations.

With this unimodal function, we can just run the ternary search algorithm described above and find the maximum value.

Here's my C++ solution.

- A unimodal function can also have the opposite shape, in which it decreases first, and then increases. In this case, the function would have a global minimum.

The statement of Mountainous Landscape can be summarized as follows:

Given a polyline, for every segment \(S\), find the first (closest) segment \(R\) to the right, which you'd be able to see if you stand on \(S\). Your vision field can be modeled as the ray \(\overrightarrow{S}\).

Points in the polyline have strictly increasing \(x\) coordinates.

A picture is worth a thousand words:

Assume the polyline has \(n = 100,000\) points, so you need to come up with a fast algorithm. A complexity of \(O(n\ log\ n)\) or similar is desired for this problem.

I'm going to walk you through the solution I implemented for this problem. But first, let's analyze the problem a little bit.

I'll go through some of the observations that have to be made in order to be able to solve this problem.

This problem can be solved in \(O(n^2)\) if, for every segment, we do a linear search to the right until we find the first visible segment. We can try to turn the linear search into something a bit more efficient.

The first thing is to realize that if the ray intersects the convex hull (upper only) of a part of the polyline, then the final answer will be somewhere inside that hull. This means we can decompose the polyline into several nested convex hulls and iteratively traverse from one hull to the next (which will be inside the previous one) until we get to the final answer.

Since each convex hull (except the largest one) is fully contained by another convex hull, we can model this as a tree of convex hulls.

We also need to come up with a way to query the tree in such a way that we find the closest visible segment and not any other segment. The query needs to consider the vision field, which is modeled as a ray.

Let's say we have the following polyline. We need to transform this into a tree of upper convex hulls.

The first step is to build the upper hull for the entire polyline. Note that all convex hulls in this problem can be built in \(O(n)\) since the points are already sorted by ascending \(x\) coordinate. This node is the root of the tree.

We then build the two children by recursively applying the same process to both the left and right halves.

After one more step, it'd look like in the following picture. Continue doing this until it can't be divided anymore.

There are several ways to build this tree, but I implemented it in a similar fashion to how one would usually build a segment tree, that is, starting by building from the range \([0, n-1]\), then dividing the range into two halves, and recursively build the tree for both halves.

Note that this gives us a binary tree.

For every segment in the polyline, you must perform a query in order to find the final node, which gives us the position of the segment that can be seen from where you are currently standing.

Since each node has two children, but we need to find the closest one to the right, we can follow this rule:

- Try querying the left node.
- If querying the left node found an answer, stop.
- If not, then query the right node.

We can also do some simple pruning by checking if the node's convex hull can never intersect the ray. Since all rays point to the right, then for a convex hull to be intersected, at least part of it should be to the right of the corresponding polyline segment. Otherwise, it'd never intersect. We can skip querying a node if there's no way it intersects.

The remaining problem is that every time we query a node, we should check whether it intersects its corresponding convex hull. You could iterate all points and check if the ray intersects it, but this brings back the complexity to \(O(n)\), so we must find a better way. I got stuck for a rather long time trying to figure out how to do this efficiently since I had never implemented something like this before.

Turns out there's a nice technique using a binary search that helps here.

Let's start by considering the ray query and all the edges in the upper hull as vectors. Now we can do some vector arithmetic and use some useful properties to find whether there's a collision or not.

As you go from the first to the last edge in the upper hull, you can tell that the orientation of the polygon edge relative to the ray transitions from counterclockwise to clockwise.

At some point in between, there's an edge that's the closest to being parallel to the ray. In other words, it has the minimum angle and in some cases, could be parallel. It's possible to find this one using a binary search.

You only need that one edge to tell whether the ray intersects the upper hull. Simply do an orientation check using both the ray and that edge^{1}.

Here's my solution in C++.

- Note that this is an ad-hoc algorithm and only works for this problem. It's not a general solution for the ray v/s convex polygon intersection problem.

- If the device hasn't been granted storage permissions, show an error.
- If the file is already downloaded, then simply attempt to open it.
- If the file exists but for some reason can't be opened, show an error.
- If the user clicks "download" when it's already being downloaded, show a notice saying it's already being downloaded.
- Else, start downloading the file and show a success message.

Pretty complex. We could model this using a flowchart:

flowchart TB
A(Click 'Download') --> B{Has storage\npermission?}
B -- Yes --> C{Being\ndownloaded?}
C -- No --> F{File exists?}
F -- Yes --> M{Can open?}
M -- Yes --> X(Open file)
M -- No --> Y(Error 'Cannot open file')
F -- No --> N(Download file and\nshow success message)
C -- Yes --> D(Show 'Already being downloaded')
B -- No ----> E(Error 'No storage permission')
classDef default fill:#282A36,color:#dfdfdf,stroke:#666,stroke-width:1.5px;
linkStyle default color:#666,stroke:#999,stroke-width:1.5px;

If we implement this model, it'll end up having a rather high cyclomatic complexity, so even if we code it correctly, it will be very fragile and may break in the future if another engineer modifies the code. We'd like to have a way to test the paths and branches of the flowchart. In practice, this means writing tests where if a condition is satisfied, then the next node in the flowchart is executed, and so on.

In the algorithm described above, we can tell that there are at least four different moments where I/O occurs:

- File download.
- Check if a file exists in the device.
- Show a message on the screen.
- Check whether the file is being downloaded (assuming download tasks are stored in a database that manages their current progress and/or download status).

Suppose you want to write unit tests for this. The problem you'll run into is that testing things like storage, content displaying on the screen, and database accesses is tricky. Testing the integration with a database will usually require a fake database with pre-populated data, which may take time to set up^{1}. Verifying that certain content or message is being displayed on the screen is even harder without specialized testing tools that can access the UI.

In some situations, you may want to refactor the logic so it's easier to manage and implement, but this isn't always possible, so I'd like to explore a different solution that works for complex cases as well. Let's assume we have to implement the flowchart as is without simplifications.

The dependency inversion principle (DIP) is one of the five SOLID principles. It states that *"One should depend upon abstractions, not concretions."* A slightly different but closely related concept is dependency injection. According to Wikipedia:

In software engineering, dependency injection is a programming technique in which an object or function receives other objects or functions that it depends on. Dependency injection aims to separate the concerns of constructing objects and using them, leading to loosely coupled programs.

[...]

Dependency injection is often used to keep code in-line with the dependency inversion principle.

Let's first show what the **opposite** of dependency inversion would look like. I'll use Javascript since many people understand it, but the language itself is irrelevant, as this concept applies to programming in general. We could implement the above logic hardcoding all I/O inside the main function^{2}:

function theProcess(fileId) { if (!OS.hasPermission()) { alert("No storage permission"); return; } const taskStatus = sqlite.query( `SELECT status FROM tasks WHERE id = ${fileId}` ); if (taskStatus === DOWNLOADING) { alert("Already being downloaded"); return; } if (OS.fileExists(fileId)) { try { OS.openFile(fileId); } catch (e) { alert("Cannot open file"); } return; } fetch(`https://my-endpoint.com/downloads/${fileId}`); alert("Download started!"); }

Not only this code depends on `sqlite`

and the vanilla `alert`

(Javascript's default dialog, used in browsers), which means it wouldn't work on mobile if we were to port it, but also, it's very inconvenient to test because we can't trigger an `alert`

during testing (at least not easily without doing a lot of hacks). If the mobile version doesn't support `fetch`

and/or `sqlite`

then we'd have to change a lot of things in order to fix it.

Note that most of this is just pseudocode and that the most important thing to keep in mind is that none of the things implemented inside the function are test-friendly.

We can refactor it and use dependency injection. This means passing all the I/O logic from the caller instead of hardcoding the logic inside the function:

function theProcess( fileId, hasPermission, showMessage, isTaskBeingDownloaded, fileExists, openFile, downloadFile ) { if (!hasPermission()) { showMessage("No storage permission"); return; } if (isTaskBeingDownloaded(fileId)) { showMessage("Already being downloaded"); return; } if (fileExists(fileId)) { if (openFile(fileId)) { // File was opened } else { showMessage("Cannot open file"); } return; } downloadFile(fileId); showMessage("Download started!"); }

Note that while `theProcess`

keeps the original control flow (i.e., the branches described in the flowchart), it now receives functions as arguments for the I/O logic. They need to be coded independently, which I won't do here.

With the refactor above it may look like nothing changed other than passing things from the caller. We still have to implement the I/O somewhere. However, the magic is that now we have control over what those functions are, and this is especially useful when used along testing frameworks because now you can mock functions and achieve the following things:

- Make a function return an arbitrary hardcoded value.
- Keep track of how many times a function has been called.
- Check the order and/or sequence in which one or more functions have been called.

Let's say we want to test that an error message is shown when the user's device has no storage permission. We could simply force `hasPermission`

to return `false`

and then verify `showMessage`

was called once with the argument `No storage permission`

. This would be enough to test the case where the user has no storage permissions. Ideally, you'd also want to assert the order in which they were called and whether no more functions were called, but as far as I know, Jest doesn't support this out of the box.

test("no storage permissions", () => { const hasPermission = jest.fn(() => false); const showMessage = jest.fn(); theProcess("some-file-id", hasPermission, showMessage); // Omit some arguments expect(hasPermission).toHaveBeenCalledTimes(1); expect(showMessage).toHaveBeenCalledTimes(1); expect(showMessage).toHaveBeenCalledWith("No storage permission"); });

If we now want to test the full flow, from the start until the download begins, we can write the following test:

test("starts download (full flow)", () => { const hasPermission = jest.fn(() => true); const showMessage = jest.fn(); const isTaskBeingDownloaded = jest.fn(() => false); const fileExists = jest.fn(() => false); const openFile = jest.fn(); const downloadFile = jest.fn(); theProcess( "some-file-id", hasPermission, showMessage, isTaskBeingDownloaded, fileExists, openFile, downloadFile ); expect(hasPermission).toHaveBeenCalledTimes(1); expect(showMessage).toHaveBeenCalledTimes(1); expect(isTaskBeingDownloaded).toHaveBeenCalledTimes(1); expect(fileExists).toHaveBeenCalledTimes(1); expect(openFile).not.toHaveBeenCalled(); expect(downloadFile).toHaveBeenCalledTimes(1); expect(downloadFile).toHaveBeenCalledWith("some-file-id"); expect(showMessage).toHaveBeenCalledWith("Download started!"); });

With these two tests, we have increased the stability of the control flow logic.

Run tests on CodeSandbox.

Note that we must still test the remaining paths in our logic (or at least the most important ones). While it may take time and code to do so, I usually prefer to have a lot of tests as opposed to a few.

Some libraries/frameworks allow for more sophisticated and compact assertions. Here's a code sample using Dart's package Mockito (from the official readme):

cat.eatFood("Milk"); cat.sound(); cat.eatFood("Fish"); verifyInOrder([ cat.eatFood("Milk"), cat.sound(), cat.eatFood("Fish") ]); verifyNoMoreInteractions(cat);

This is very useful for testing that a control flow path has been executed as expected. I'm currently unaware if something like this can be done in Jest.

The main disadvantage of this approach is that you still haven't tested the actual code, but just a mocked version of it. This means that the actual code that will run on production will not be tested entirely, and in most cases, it's still necessary to have end-to-end or acceptance tests to verify it works as expected.

With this approach, you can only test the logic's skeleton and ensure the flow happens in the expected way, but you can't test the actual non-mock code.

In the case presented in this article, we had a complex flowchart that we had to implement and make sure all the conditions and branches happened the same way we expected. This would've been very hard to do if we couldn't mock the functions required by the main process because of the limitations of doing I/O testing.

- This is relatively straightforward in frameworks like Ruby on Rails.
- For simplicity, the await keyword was omitted, even though some function calls should be awaited in practice.

There's a little neat trick that allows you to compare your phrases or sentences with those written by people on the internet.

Most people know you can google things like *"cats"* or *"banana"* and get corresponding results, but did you know there's a trick that allows you to google exact phrases? That is, not just similar, but exactly the same word by word.

If the phrase you googled gets a lot of hits, it means it's widely used by other people, and it's probably correct. Conversely, if you get fewer results, it usually hints that the phrase is incorrect, at least grammatically.

Simply type a phrase and surround it with double quotes, and that's all.

As a general rule, few or no results means the phrase is probably incorrect, and you may want to tweak it. If there are many results, it usually means it's OK.

Note that English has a huge population of non-native speakers, so you may find some grammatically incorrect phrases that are also widely used.

You can also use wildcards when you're unsure what to put between two words.

The prompt *"I'm raising * cat"* returns results containing *"I'm raising a cat,"* among others.

This trick can be used for language learning as well, especially for the output aspect of it.

When you are trying to produce a sentence and are unsure what article, preposition, etc., to use, you can add a wildcard and use phrases similar to the one you are trying to produce.

Suppose you are trying to produce a phrase containing an uncommon word. In that case, you can replace that word with a wildcard and try to find a more generic phrase or sentence. Then you can just replace back the noun you first intended to use. An example of this would be if you want to say the phrase *"[...] will colonize Pluto and live there"* but are unsure if it's grammatically correct or not (in this case, it's correct), so you can just google *"will colonize * and live there"* and verify whether the phrase is correct or not. In this case, you'll get results with the wildcard but not with *"Pluto."*

You can also use this trick to learn phrases related to the grammar structure you're currently studying. This is useful when you want to find sentences that are interesting to you or relevant to the field you want to use the language in, rather than simply copying the phrases from a textbook, which may be irrelevant to you.

]]>Instead of doing it myself manually, I figured it'd be nice to have Grammarly do it automatically. I know there are other tools (especially with the recent AI boom), but this one is fine. It is also pretty stable and mature.

I went for the Premium free trial, which costs 144 USD annually. It's a lot more expensive than I thought, but it may prove worth it if I plan to create a lot of content.

After using it for a while, my initial impression is that Grammarly changes some of my intention and style of writing, but it still keeps most of it. Sometimes I end up removing entire phrases because Grammarly keeps complaining about them.

One thing that keeps musicians, writers, and other content creators from creating lots of content is deciding when the product is ready to be published. Leonardo Da Vinci once said, *"Art is never finished, only abandoned."*

The book Simple Rules suggests defining rules that tell you when to do or stop doing something in a way that's easy to recognize and follow. The counterpart of this idea is to have a lot of complex data and use all of that to make decisions.

One good way to leverage this concept is by getting Grammarly to tell me when the article is polished enough and then call it done, even though I may not be delighted with how it's written. It's better to publish imperfect content than indefinitely trying to perfect something already good enough anyway.

]]>Given a finite set of \(N\) points, a number \(B\) of polygons to build, and a center \(C\), you must find \(B\) convex polygons having \(C\) as a common vertex such that the total area of all polygons is the smallest possible. Some other important information about the problem:

- All points in the set must either be inside or be a vertex of a polygon.
- Polygons can't overlap or intersect anywhere other than the common vertex \(C\).
- No three points of the set (including the center \(C\)) lie on the same line.
- Very few points, \(N \le 101\).

For instance, let \(B = 5\) and \(C\) be the origin, then the figure below shows a possible solution for the point set given. This solution isn't optimal though, as it doesn't have minimum area^{1}.

With experience you can sometimes easily tell whether a problem can be solved using a greedy approach, or with a dynamic programming approach. I chose the latter for this problem, even though I can't really explain the reason, other than that greedy sounds too difficult to get correctly, if possible at all.

Let's imagine we have precomputed all the possible polygons we can build. This means, we know which points they cover, and the area they have. For each starting point \(i\), move counter clockwise and build all polygons that cover from \(i\) to all other points \(j\) until we can't continue enlarging the polygon anymore. At some point the polygon will become concave, so we move on to build polygons that start at \(i + 1\), until we have built all possible polygons. All of this will be explained further in the next section.

All of this can be done in \(O(n^3)\) because building a polygon from \(i\) to \(j\) requires you to traverse all points in between. This can be lowered to \(O(n^2)\) by avoiding doing this and instead using the previous polygon as a starting point to build the next one, but it's a bit harder to implement, and unnecessary for \(N \le 101\) points.

Once all polygons have been precomputed, we can use a knapsack like approach to find the optimal solution. The main idea is that while you are building the solution, if you have placed a polygon that covers all points ranging from \(i\) to \(j\), then for the next polygon you must try all polygons that begin at the \(j+1\) point. Try all of them, and keep the minimum total area at every step. Recursively keep adding the next polygon until you have built all necessary \(B\) polygons, and all \(N\) points have been covered. This will yield the global minimum area once the process is complete.

The Graham Scan algorithm is one of the most common ways to build the convex hull of a set of points. It works by doing a lexicographic sort and the sweep line technique to find both the upper and lower hulls. I recommend to understand how this algorithm works before continuing.

For this problem though, the approach is slightly different. Instead of finding the lower and upper hulls, we center all the points at the given center \(C\), polar sort them, and then use the radial sweep technique to traverse them in counter clockwise order while building each polygon.

Consider the `ConvexHull`

data structure which contains all the essential information about a polygon:

struct ConvexHull { int start_idx, last_idx, covered; double area; };

The following procedure returns a `ConvexHull`

instance by building a polygon from \(i\) to \(j\) travelling counter clockwise. This assumes the points have already been sorted:

ConvexHull create_convex_hull(const int start_idx, const int last_idx) { int covered = 0; vector<Point> vertices; for (int i = start_idx;; i = (i + 1) % N) { if (vertices.size() < 2) { vertices.push_back(points[i]); covered++; if (i == last_idx) break; continue; } while (vertices.size() >= 2) { Point a = points[i] - *(next(vertices.rbegin())); Point b = points[i] - *vertices.rbegin(); if (a.cross(b) > 0) break; vertices.pop_back(); } vertices.push_back(points[i]); covered++; if (i == last_idx) break; } double area = 0; for (int i = 0; i < (int)vertices.size() - 1; i++) area += fabs(vertices[i].cross(vertices[i + 1])) / 2L; return {start_idx, last_idx, covered, area}; }

The process of building all polygons (briefly described in the previous section) can then be done the following way:

vector<vector<ConvexHull>> convex_hulls; // .... for (int i = 0; i < N; i++) for (int j = i + 1;; j++) { j %= N; if (points[i].cross(points[j]) < 0) break; convex_hulls[i].push_back(create_convex_hull(i, j)); }

Finding the minimum area can be done using a pretty standard dynamic programming relation:

double dp(int idx, int b, int covered) { if (b == 0 && covered == 0) return 0; if (b == 0 && covered != 0) return INF; if (b != 0 && covered <= 0) return INF; if (memo[idx][b][covered] > 0) return memo[idx][b][covered]; double min_area = INF; for (ConvexHull& c : convex_hulls[idx]) { double area = c.area + dp((c.last_idx + 1) % N, b - 1, covered - c.covered); min_area = min(min_area, area); } return memo[idx][b][covered] = min_area; }

This implementation returns \(\infty\) for all invalid configurations. For example, if \(b = 0\) (meaning that we have built all necessary polygons) and at the same time \(covered \neq 0\) (meaning that some points are not covered by any polygon) then the configuration is invalid since some points were left out.

Since we are minimizing at every step, the \(\infty\) values will eventually get replaced by a real number once a valid configuration has been found. In this problem it's guaranteed that a solution exists.

The final solution can then be found using:

double min_area = INF; for (int i = 0; i < N; i++) { min_area = min(min_area, dp(i, B, N)); }

My C++ implementation of the solution described in this article can be found here.

- Or does it? Hard to tell considering I drew this by hand.

The problem can be summarized as follows:

Pig is a simple dice game for two or more players. Each turn, a player repeatedly rolls a dice until either a 1 is rolled or the player decides to “hold”:

Hold or Continue (Problem statement). ICPC Latin American Regional – 2019

If the player rolls a 1, they score nothing in their turn and it becomes the next player’s turn.

If the player rolls any other number, it is added to their turn total and the player can decide between “hold” or “continue”.

If the player chooses to “hold”, their turn total is added to their score and it becomes the next player’s turn. Otherwise the player continues rolling the dice.

The first player to score exactly 75 wins the game. If a player’s score plus their turn total exceeds 75, they score nothing in their turn and it becomes the next player’s turn.

Based on the above game rules, your objective is then to determine, given a game state, which decision (either hold or continue) yields the greatest probability of winning. A game state is specified by a tuple containing three values: our total score, opponent's total score, and the current turn score. In this problem, there are only two players, and it's assumed that both play optimally.

This problem is very different to most of the Pig programs I've found on the internet. It's also different from all papers I've read about the subject. I found that in most cases, a simpler to implement variation was used.

As you'll see, this variation of Pig involves solving a rather hard subproblem involving infinite recursive calls when implementing the mathematical model naively. We are going to solve this issue by approximating some probabilities numerically.

This problem can be modeled as you would expect, by following the game instructions, and calculating probabilities normally.

All of the following functions return a value between 0 and 1 inclusive, and they represent the probability of winning with a total score of \(i\), opponent score of \(j\), and a turn score of \(k\).

What is the possibility of winning when we hold? The turn's accumulated score is added to our total, but it also becomes the opponent's turn, so we must swap the scores, and calculate the probability of the opponent *not* winning.

$$P_{hold}(i, j, k) = 1 - P(j, i + k, 0) $$

Similarly, when we continue rolling the dice, there's a probability of \(\frac{1}{6}\) that the result will be 1 (we score nothing and it becomes the opponent's turn), and there's also a probability of \(\frac{1}{6}\) for every other number, which will be added to the current turn score.

$$P_{continue}(i, j, k) = \frac{1 - P(j, i, 0)}{6} + \sum_{d=2}^6 \frac{P(i, j, k + d)}{6}$$

Next, putting it all together, we get the actual \(P\) function which is the one we need. This function is implemented as a piecewise function containing the following conditions:

- If the opponent's score is 75, then the opponent wins, and by consequence we lose.
- If our total score plus the current turn score is greater than 75, then we score nothing and it becomes the opponent's turn.
- For every other case, return the probability of the decision that yields the greatest probability of winning (hold or continue). This is because it's assumed that both players play optimally at every point in the game.

$$

P(i, j, k) = \left\{\begin{aligned}

&0 && , && j = 75\\

&1 - P(j, i, 0) && , && i + k > 75\\

&max(P_{hold}(i, j, k), P_{continue}(i, j, k)) && , && \text{default case}

\end{aligned}

\right.$$

So far it seems to make sense, but there is one huge problem with this mathematical model. If this was to be implemented this way, there would be a cycle when calling \(P(i, j, 0)\), since it would oscillate infinitely between \(P(i, j, 0)\) and \(P(j, i, 0)\), never converging to any real number.

One way to solve this, is by approximating the probability when \(k = 0\).

Intuitively, it's possible to tell that if both players have just started a new game, and both have a score of 0, then the probability of winning would be near 0.5, since both players have almost equal probability of winning. Since one player always starts first, though, that player has a slightly greater probability of winning. According to my implementation, that value is \(P(0, 0, 0) = 0.52692\).

Similarly, if our total score is 70, and the opponent has 0, then we can assume that we have a greater probability of winning than the opponent does. Using my code, that probability is \(P(70, 0, 0) = 0.929041\).

It's also possible to intuitively tell that \(P(i, j, 4)\), \(P(i, j, 3)\), \(P(i, j, 2)\), \(P(i, j, 1)\) and \(P(i, j, 0)\) are decreasing values, and that they are close to each other. Here are some examples (again, calculated by my program):

$$

P(45, 10, 4) = 0.771326\\

P(45, 10, 3) = 0.768724\\

P(45, 10, 2) = 0.766302\\

P(45, 10, 1) = 0.763970\\

P(45, 10, 0) = 0.761767

$$

Note that the probability gradually decreases when going from \(k=4\) to \(k=0\). This shows that the value for \(k=0\) could somehow be interpolated from all the other values, assuming they were known (which they aren't.)

As a side note, remember that it's actually not possible to have a \(P(i, j, 1)\) since the player loses their turn when they roll a one, but the probability can still be calculated nevertheless.

Let's refactor \(P\) to include a way to handle the problematic case:

$$

P(i, j, k) = \left\{\begin{aligned}

&0 && , && j = 75\\

&1 - P(j, i, 0) && , && i + k > 75\\

&max(P_{hold}(i, j, k), P_{continue}(i, j, k)) && , && k > 0\\

&P_{approximate}(i, j) && , && k = 0

\end{aligned}

\right.$$

Now we must implement \(P_{approximate}\), which can be done iteratively using something like this:

#define MAX_SCORE 75 // ... double p_approximate(int i, int j) { double prob = 0.5; array<double, MAX_SCORE + 1> approx; for (int t = 0; t < 41; t++) { for (int k = MAX_SCORE; k >= 0; k--) { double hold = 0; double cont = 0; if (k > 0 && i + k <= MAX_SCORE) { hold = p_hold(i, j, k); } for (int dice = 1; dice <= 6; dice++) { if (dice == 1 || k + dice > MAX_SCORE) { cont += 1 - prob; } else { cont += approx[k + dice]; } } approx[k] = max(hold, cont / 6); } prob = approx[0]; swap(i, j); } return prob; }

This code snippet will be explained in the following section.

Full source code in C++ can be found here.

This algorithm basically simulates the entire game, but using a slight variation.

It starts by assuming that the \(prob\) variable, which actually represents the value for \(P(i, j, 0)\) is equal to \(0.5\). Then we simulate the entire game by starting from \(P(i, j, 75)\), and decreasing the turn score until it reaches \(P(i, j, 0)\). Repeat the process several times to make \(prob\) converge to the correct value. Different initial values also work, but \(0.5\) requires less iterations for the approximation to converge.

The \(approx\) array is a temporary array representing \(approx[k] = P(i, j, k)\). In the end we are only interested in \(approx[0]\), which is equal to the value we are looking for, \(P(i, j, 0)\).

Since each probability being approximated depends on probabilities which index in the \(approx\) array is higher, the loop is done backwards, so that the values are computed in dependency order:

for (int k = MAX_SCORE; k >= 0; k--) { // ...

The logic for *hold* and *continue* is implemented following the game rules, and we choose the greatest value among the two, similar to the \(P\) function modeled above:

double hold = 0; double cont = 0; if (k > 0 && i + k <= MAX_SCORE) { hold = p_hold(i, j, k); } for (int dice = 1; dice <= 6; dice++) { if (dice == 1 || k + dice > MAX_SCORE) { cont += 1 - prob; } else { cont += approx[k + dice]; } } approx[k] = max(hold, cont / 6);

After one iteration of the approximation process is done, we swap the scores before doing the next iteration. This is the equivalent of simulating the opponent's turn.

prob = approx[0]; swap(i, j);

Remember that \(prob\) represents \(P(i, j, 0)\), so after the scores are swapped, the \(prob\) variable being used in the *continue* logic now represents \(P(j, i, 0)\), which is exactly the same as the mathematical model defined earlier.

for (int dice = 1; dice <= 6; dice++) { if (dice == 1 || k + dice > MAX_SCORE) { cont += 1 - prob; } else { cont += approx[k + dice]; } }

The code snippet above is exactly the same as:

$$\frac{1 - P(j, i, 0)}{6} + \sum_{d=2}^6 \frac{P(i, j, k + d)}{6}$$

The \(P(i, j, 0)\) calculated in one iteration becomes the \(P(j, i, 0)\) used in the next one. Since \(P(i, j, 0)\) and \(P(j, i, 0)\) depend circularly on each other, it's necessary to compute both simultaneously by approximating both. This is why we swap the scores on each iteration.

In other words, the only incognita in this approximation process is \(prob\), which is equal to \(P(i, j, 0)\), and since \(P(j, i, 0)\) also depends on that value, we need to approximate both at the same time.

It's also important to note that in order for this function to return \(P(i, j, 0)\), you must make sure to execute an odd amount of iterations, otherwise it'd return its swapped counterpart \(P(j, i, 0)\).

Another very simple way of approximating \(P(i, j, 0)\) is by adding a new variable: number of rounds passed. The C++ solution is here. Let's examine the most important aspects of this alternative solution.

My implementation uses top-down dynamic programming using the following 4D array for memoization, which represents the state \((i, j, k, t)\), where \(t\) is the number of rounds passed:

double memo[76][76][76][38];

For the first round with \(t = 0\), we simply assume the probability is \(0.5\) for any given score tuple. Just like the previous method, we can use different initial values, but deviating too much from the \([0, 1]\) range will require more iterations, so \(0.5\) is a safe guess.

double p(int i, int j, int k, int t) { if (t == 0) return 0.5; if (j == 75) return 0; if (i + k > 75) return 1 - p(j, i, 0, t); if (memo[i][j][k][t] > -1) return memo[i][j][k][t]; return memo[i][j][k][t] = max(p_hold(i, j, k, t), p_continue(i, j, k, t)); }

Finally, in order to resolve the problematic cycles, we can simply refer to the values of previous rounds. These values are all known.

double p_hold(int i, int j, int k, int t) { return 1 - p(j, i + k, 0, t - 1); } // ... for (int dice = 1; dice <= 6; dice++) { if (dice == 1 || i + k + dice > 75) ret += 1 - p(j, i, 0, t - 1); // ...

In order to get the correct probabilities, you must call the \(P\) function using a relatively large number of rounds passed.

p(C, H, X, 38);

This solution may look different from the previous one, but actually it does something very similar. The approximation mechanism may be slightly different, but it follows the same principle.

As you may have already noticed, this solution is pretty inefficient. By adding an extra state, we are using approximately 40 times more memory than we should.

It's possible to optimize it by replacing the value from the previous round, with the value of the next round. This way we don't consume extra memory, and still manage to simulate several rounds.

Full C++ solution for this version is here. I'd say this is the simplest and most compact version of all. It's also the one with the fastest CPU time.

int iter = 35; while (iter--) { for (int i = 0; i < 75; i++) { for (int j = 0; j < 75; j++) { for (int k = 75 - i; k >= 0; k--) { solution[i][j][k] = max(p_hold(i, j, k), p_continue(i, j, k)); } } } }

Once all of the above is implemented, getting the correct decision is trivial, since all we have to do now is, for every given query state, compare \(P_{hold}(i, j, k)\) with \(P_{continue}(i, j, k)\) and find the one with the greatest value.

If \(P_{hold}\) is greater than \(P_{continue}\), then the player should *hold*. Otherwise, the optimal move is to *continue*.

Three different methods were explained in order to approximate the values of \(P(i, j, 0)\). They work slightly differently, but in the end achieve the same result.

Proving why the value converges is out of the scope of this article, since it seems some extra math knowledge is required, which sadly I don't have at the moment.

I'm not entirely sure why it converges so quickly, and why in some implementations it works well even if the initial value is an invalid probability like \(1.5\). I guess an in depth trace of the approximation algorithm would be necessary in order to know what's going on at every step.

List of C++ solutions used in this article:

]]>Given a finite set of points, find the square centered in \((0, 0)\) with the largest size that does not contain any of the given points. The sides of the square don’t need to be aligned with the coordinate axes (i.e. the square can be rotated arbitrarily.) Points are allowed right on the border of the square but not inside.

Check out the problem set of the contest.

This is a classic radial sweep problem, in which we fix a square of a certain size, and then rotate it while checking if there's an angle where all points are outside (or right on the border of) the square. The size is then maximized using binary search.

The algorithm is divided into two parts described below.

Let \(S\) be an arbitrary fixed square size.

As the square of size \(S\) is rotated on its center, each point will behave in one of the following ways:

- It is always inside, making it impossible to build a square of size \(S\).
- It never touches the square, in which case it can be safely ignored.
- It will enter and then exit the square (or exit and then enter.)

The points that enter and exit the square generate two events:

**Enter event:**The point has entered the square.**Exit event:**The point has exited the square.

These events are all put into a single array and then sorted by angle. Performing a radial sweep essentially means iterating over these events in order. This is the equivalent of rotating the square on its center and having points enter and exit the square at different angles. By using a simple counter variable it's possible to keep track of how many points are inside the square at any given rotation.

To summarize:

- Generate all events for all points.
- Sort the events by angle.
- Process all events in order by making the square rotate on its center.
- It's possible to build the square if there's at least one moment where there are no points detected to be inside the square.
- If there are no moments where there are no points inside, then it's impossible to build.

This process returns a boolean, which indicates whether it's possible or not to build a square of size \(S\).

The process above must be executed multiple times for many \(S\) values using binary search. This will yield the highest \(S\) such that a square of that size can be built without having any point inside.

*Note: \(S\) can be the side, diagonal, area, etc. Since it's a square, all these measures are proportional to each other, so you can use whatever you want.*

**Rotating all the points until they are in the first quadrant does not change the final answer:**Because squares are symmetric, we can just put all points in the first quadrant. Simply rotate each point \(90^{\circ}\) a few times until they are all in the first quadrant.**We can ignore a point if its distance to the origin is greater than the square diagonal:**No matter how we rotate the square, the point will never enter the square, so these points can be ignored.**If at least one point is inside the circle inscribed inside the square, then the square can never be built:**In other words, if a point's distance to the origin is less than half of the square side, then no matter how we rotate the square, that point will never exit it.**A square can only be rotated \(90^{\circ}\):**Again, because the square is symmetric, we only have to rotate it \(90^{\circ}\). No new information will be gained by rotating it further.

Moreover, if we consider all the points that were not ignored (i.e. points that lie outside the circle inscribed inside the square, and which distance to the origin does not exceed the square diagonal), then we can also make the following observations:

- After the square has been rotated by \(90^{\circ}\), if the point was initially inside, then it will exit once, and then re-enter.
- Similarly, if the point was initially outside, it will enter the square, and then exit again.
- As a conclusion, all non-ignored points have two events.

The proof is that after rotating the square \(90^{\circ}\), the final state will be the same as the initial state (due to its symmetry). This means that all points that were initially inside, will end up being inside when the \(90^{\circ}\) rotation ends, but it also means that the point had to exit the square once and then enter again. This proof also applies for points that were initially outside the square.

In some radial sweep implementations, events are specified as angles, but the most common way to implement it is by simply using points as events, which is what we are going to do here.

The way I approached the radial sweep is by having the square's initial state be in such a way that the four vertices lie on the coordinate axes, and rotate it all the way until (as pointed out above) the final position is the same as the initial one.

You can visualize this algorithm as sweeping the rightmost vertex (labelled C in the figure below), passing through all the events, and finally arriving at D.

For this reason, the easiest way to transform each point into an *enter* or *exit* event, is by executing the following:

- Let \(P\) be the point you are currently handling, \(S\) be the length of the square side, and \(I\) be the circle inscribed inside the square.
- Obtain the two points of tangency from \(P\) to the circle \(I\) (there are always two).
- From each point of tangency \(T\), obtain the vector \(\vec{TP}\), normalize it, and scale it by \(\frac{S}{2}\). Keep the vector's endpoint. This is the point that represents the angle at which the square vertex is located when \(P\) enters/exits the square.

The last bit is a bit tricky to do correctly, and for me the easiest way was to simply get all four possible points (see the code for more details), get the ones in the first quadrant, and then sort them radially so I can tell which one is the first and last one to be visited during the radial sweep. The expected result is that you should end up with two points, both in the first quadrant, and know which one goes first during the sweep.

Also, in order to know whether the first event should be *enter* or *exit* you only need to check whether the point is below or above the square side (segment from D to C in the figure). Using the line equation helps:

$$y = mx + b$$

Just to avoid confusions, `ld`

means `long double`

here.

typedef long double ld;

Sort the points by bearing by implementing a `Point`

struct with a `operator<`

comparator. Note that this comparator does not work when some points are \(180^{\circ}\) or more from each other, but in this program all points are in the first quadrant, so it works fine.

struct Point { ld x, y; // ... ld cross(const Point& p) const { return x * p.y - y * p.x; } bool operator<(const Point& p) const { return cross(p) > 0; } // ... };

The next snippet shows how the radial sweep was implemented in my solution.

Binary search is used in order to find the optimal value for the \(possible(S)\) function. Check out the full C++ solution.

bool possible(ld side) { ld half_side = side / 2L; ld half_diagonal = half_side * sqrt(2); // Store the events here as a Point/boolean pair // The Point is for specifying the event position, // and the boolean means enter (true) or exit (false) vector<pair<Point, bool>> events; // Counter for keeping track of how many points are currently inside the square int inside = 0; // Generate all events for (Point& p : points) { // Is the point always inside the square? if (p.magnitude() < half_side) return false; // Should we ignore the point because it never enters the square? if (p.magnitude() > half_diagonal) continue; // Points of tangency from p to the inscribed circle inside the square. auto [t1, t2] = points_of_tangency(half_side, p); // Vectors that reach a square vertex when added to the points of tangency. Point u = (p - t1).normalize().scale(half_side); Point v = (p - t2).normalize().scale(half_side); // Radial sweep is [0, 90] degrees, so find the ones in the first quadrant. Point event1 = (t1 + u).is_first_quad() ? (t1 + u) : (t1 + u.negate()); Point event2 = (t2 + v).is_first_quad() ? (t2 + v) : (t2 + v.negate()); // Order the events if (event2 < event1) swap(event1, event2); bool point_initially_inside = p.y < half_diagonal - p.x; // When it starts inside the square, it will exit and then enter again. // When it starts outside the square, it will enter and then exit. if (point_initially_inside) { inside++; events.push_back(make_pair(event1, false)); events.push_back(make_pair(event2, true)); } else { events.push_back(make_pair(event1, true)); events.push_back(make_pair(event2, false)); } } // If there are no events, it means that all points were ignored. // The square can be built. if (events.empty()) return true; // Execute a polar sort. Use the Point comparator defined in the struct. sort(events.begin(), events.end(), [](const pair<Point, bool>& a, const pair<Point, bool>& b) { return a.first < b.first; }); // It's possible to build the square if there's at least one rotation with no // points inside. // Radial sweep of square diagonal, from 0 to 90 degrees. for (pair<Point, bool> ev : events) { // We don't need the point here. It's only used for sorting. auto [_, enter] = ev; // Keep track of how many points are inside inside += enter ? 1 : -1; // If no points are currently inside, then it can be built. if (inside == 0) return true; } return false; }]]>

Given a finite set of non-collinear points in the first quadrant, find the convex polygon which has one vertex in the origin, and that has the largest number of vertices (by choosing some or all of the given points as vertices.)

Problem statement: Elastico - Beecrowd

One of the most useful techniques used in geometry problems in competitive programming is using the cross product of two or more vectors to determine whether a polygon or part of it is convex or not.

Let \(a\) and \(b\) be two distinct points (both distinct from the origin), and \(\vec{a}\) and \(\vec{b}\) be their position vectors. Also, let's consider the \(\times\) operation to be the cross-product, but returning only the \(z\)-component scalar^{1}.

The two points are in counterclockwise order if:

$$\vec{a}\times\vec{b} > 0$$

The points are in clockwise order if:

$$\vec{a}\times\vec{b} < 0$$

And finally, the two points are collinear (with the origin) if:

$$\vec{a}\times\vec{b} = 0$$

Note that the last case can also happen if one or both vectors are zero vectors (i.e. all components are zero). Since this problem involves the origin, it's necessary to be careful about this.

This is enough to determine whether the polygon we are building remains convex or not. When we add a new vertex, we check using the cross product if the orientation of each vertex is correct.

There are several other applications of the cross-product in competitive programming, but they are out of the scope of this article.

For the maximization (i.e. finding the polygon with the maximum amount of vertices), I used a dynamic programming approach, which will be explained next:

**Use two-dimensional states**: This means that the`dp`

recursive function will have two parameters,*current point*and*previous point*. This is because it's not possible to know if the polygon will be convex just by knowing the current point.**Transition to every other point**: By using the two parameters (*current point*and*previous point*), find all transitions to a*next point*where these three points keep the polygon convex.**Find the highest value:**At all steps of the algorithm, keep the highest value found, and finally return the global maximum value.

First, let's create a `Point`

struct to make the rest of the program easier to write:

struct Point { int x, y; Point operator-(const Point& p) const { return Point{x - p.x, y - p.y}; } int cross(const Point& p) const { return x * p.y - y * p.x; } bool operator<(const Point& p) const { return cross(p) > 0; } };

Note that `operator<`

works as a comparator when sorting an array of points.

Then let's code the dynamic programming algorithm:

int dp(int p, int prev_idx) { if (memo[p][prev_idx] != -1) return memo[p][prev_idx]; int ret = 0; const Point& curr = points[p]; const Point& prev = points[prev_idx]; for (int i = p + 1; i < (int)points.size(); i++) { const Point& next = points[i]; Point prev_curr = curr - prev; Point curr_next = next - curr; if (prev_curr.cross(curr_next) > 0) { ret = max(ret, 1 + dp(i, p)); } } return memo[p][prev_idx] = ret; }

In order for this implementation to work, it's also necessary to perform some extra steps before calling the `dp`

function.

- Sort the points (using the comparator defined above).
- Insert the origin \((0, 0)\) to the set. Note that I insert it at the beginning of the array
*after*sorting. This is because my`Point`

comparator function does not handle zero vectors correctly and because I want it to be the first element so that it's easier to know where it is (it could be anywhere, though). - Execute
`dp`

for all points (otherwise, the global maxima won't be found), always having the origin as the*previous point*parameter.

sort(points.begin(), points.end()); points.insert(points.begin(), Point{0, 0}); int ans = 0; for (int i = 1; i < (int)points.size(); i++) { ans = max(ans, 1 + dp(i, 0)); } cout << (1 + ans) << endl;

The full solution in C++ can be found here.

- This is a common but informal hack used in geometry involving 2D vectors.