What is an Algorithm?

There are many ways of conceptualizing what an algorithm is, but the gist of it is that it comprises a set of rules that precisely defines a sequence of operations.

This can apply to almost anything from how to go about counting things, to solving a math problem, or even baking a cake.

What is important is that there is an order of instructions in which inputs are being handled and transformed into useful outputs.

ProgAlgoCake.jpg

To further elaborate on this, we will introduce some useful principles for designing algorithms:

Clear and Unambiguous: Each step should be clear in all aspects and must be interpretable in a consistent way.

Well-Defined Inputs: If an algorithm requires inputs, they should be well-defined.

Well-Defined Outputs: The algorithm must clearly communicate what output it yields.

Finite-ness: An algorithm must be finite, i.e. it should not loop infinitely without producing a useful output.

Feasible: The algorithm must be implementable with the available resources (e.g., time, budget, hardware constraints).

Algorithms and Programming

Now that we have defined what an algorithm is, the next step is to consider how to utilize them in a programming context.

The following process is a useful starting point for getting from a problem to a working solution:

  1. Understand the problem
  2. Think of alternative solutions to the problem.
  3. Write down the solution, diagram, flowcharts, pseudocode.
  4. Pick a programming language and code up the program.
  5. Debug the code, make sure that the code does what you want it to do.

It is important to document the program with useful comments, use expressive variable names, and apply consistent indentation throughout the whole process. Make it easy for others and yourself to follow your train of thought when having to review your work.

When trying to better understand the problem, a powerful method is to apply the principle of divide and conquer to break down complex problems into less complex ones.

As you develop a better understanding of the problem, you will naturally think of alternative solutions. You want to be careful with going all-in on solving the first solution that comes to mind since you might discover it to be unfeasible or time-consuming down the line. Hence you should be open to new ideas as they come to you.

1%20and%202%20stepv2.png

Algorithm Implementation

Many considerations go into picking a programming language. Among those is, for example, the “level” of a programming language, ranging from low to high. The list below ranks several different languages. The lowest level corresponds to direct instructions to the CPU.

Low to high level programming languages

Levels-3-2.png

The reason you have to assess what level you want to utilize is that different languages provide different levels of freedoms and responsibilities with how you structure your code because they use different compilers and interpreters (compiles in real-time). These are pieces of software that translate what we are writing into something the computer can understand.

Therefore, different languages have different weaknesses and strengths, e.g. Python performing matrix-algebra faster with arrays than with for-loops. Usually higher level languages trade lower-level control for ease of use, resulting in quicker implementation but slower execution.

After we have chosen our language, translated and implemented our algorithm, we can evaluate our program using the following three concepts:

Correctness: The program does what we want it to do.

Design: The implementation of the solution solution isn’t needlessly verbose, there are no redundancies, no copy & paste, explicit is better than implicit.

Style: The code is easy to read by a 3rd person, which might even be yourself in the future. For this purpose, instructions should be explicit rather than implicit, which includes good documentation.

Furthermore, you can also evaluate the complexity of the algorithm by time and space.

Time complexity quantifies the amount of time taken by an algorithm to run as a function of the length of the input.

Space complexity on the other hand quantifies the amount of memory or space the algorithm takes to run as a function of the length of the input.

Algorithms in Action

Before diving into the more technical aspects of designing algorithms, let's introduce a practical use case and construct a handcrafted solution to develop a better intuition for algorithms in action. Feel free to browse through this section at your own pace and return later when you've mastered the basics of algorithm implementation. The purpose of this section is to show how we might develop a solution from start to finish for a concrete problem you might encounter in a real life situation.

In the domain of natural language processing (NLP), for example, algorithms manipulate text/speech to produce useful outputs. NLP is used in many day-to-day applications like translation, chatbots, and sentiment analysis (the process of classifying the opinion of an author about a given subject, e.g. through reviews, news articles, and social media posts). In the following, we will tackle the problem of record linkage.

In 2017, The Economist highlighted that "the world’s most valuable resource is no longer oil, but data". Businesses possess heaps of data that must be cleaned and consolidated before they can create added value. Record linkage is a useful method for automating the process of linking dispersed datasets. For the purposes of this tutorial we will program a solution by hand to illustrate the process of developing an algorithm.

For this example, we will import a dataset from the Freely Extensible Biomedical Record Linkage (Febrl) package which "contains 1000 records (500 original and 500 duplicates), with exactly one duplicate per original record”. We will split the dataset in two and attempt to consolidate the dataframes.

In [298]:
from recordlinkage.datasets import load_febrl1

# Import dataset and sort the dataframe by the record id
df = load_febrl1(return_links=False).sort_values(by=['rec_id'])

df.head(10)
Out[298]:
given_name surname street_number address_1 address_2 suburb postcode state date_of_birth soc_sec_id
rec_id
rec-0-dup-0 thomas rokobaro 12 herschell circuit killanrey lawrence 2272 nsw NaN 1451137
rec-0-org flynn rokobaro 12 herschell circuit killarney lawrence 2227 nsw 19720812 1451137
rec-1-dup-0 karli alderson 144 nulsen circuit iowanna tings lpa 3139 nsw 19510826 9541034
rec-1-org karli alderson 144 nulsen circuit iowanna tingalpa 3139 nsw 19510826 9541034
rec-10-dup-0 kayla harrington NaN maltby circuit coaling coolaroo 3465 nsw 19150612 9004242
rec-10-org kayla harrington 38 maltby circuit coaling coolaroo 3465 nsw 19150612 9004242
rec-100-dup-0 luchetti tiana 112 totterdell street villa 5 toowoomba 2008 sa 19050127 2712503
rec-100-org tiana luchetti 112 totterdell street villa 5 toowoomba 2008 sa 19050127 2712503
rec-101-dup-0 amberr coulson 26 NaN meilene retirement whitfield 3986 nsw 19950320 3079240
rec-101-org amber whillas 3 NaN meilene retirement whitfield 3987 nsw 19950320 3079240

Based on the above excerpt, we can observe that sometimes names don't entirely match, the surname and given name are swapped, entries are missing, or addresses are spelled differently. It would be incredibly time consuming to manually separate duplicate entries. Therefore, after mixing the dataset, we will design an algorithm that will identify matching entries for us.

In [299]:
# Separate all duplicate and original entries by extracting every second entry, 
# starting at the 0th and 1st index respectively
dup = df[::2]
org = df[1::2]

# Split each dataset in two (each contains 250 rows)
sp = int(len(dup) / 2)
dup_1 = dup[:sp]
dup_2 = dup[sp:]
org_1 = org[:sp]
org_2 = org[sp:]

# Combine the dataframes such that each dataframe contains the duplicates/originals
# of the other dataframe, and randomize the order of the rows
df1 = org_1.append(dup_2).sample(frac = 1)
df2 = org_2.append(dup_1).sample(frac = 1)

Coding an Algorithm

Based on the following YouTube tutorial, we have programmed an algorithm that computes the Levenshtein distance between two strings. Simply put, Levenshtein distance determines the number of operations required to transform one string into another, where the possible operations are insertion, deletion, and substitution. We will use this algorithm to identify rows with minimum edit distance between each other. Depending on the issues we are facing, we may have to resort to other techniques since our function, for example, would fail to realize that the surname and given name were swapped for record 100. However, given that the other columns match, our solution should in this case still be sufficient to identify all matching rows.

In [300]:
import numpy as np

def lev(a, b):
    '''Compute Levenshtein distance for any two non-empty strings'''
    # The following three lines of code ensure that our function will be compatible with our dataset
    if type(a) == float or type(b) == float:
        return float("NaN")
    a, b = str(a), str(b)
    
    len_a = len(a) + 1
    len_b = len(b) + 1
    matrix = np.zeros([len_a, len_b])
    matrix[:, 0] = list(range(len_a))
    matrix[0] = list(range(len_b))
    for i in range(1, len_a):
        for j in range(1, len_b):
            if a[i-1] == b[j-1]:
                matrix[i, j] = matrix[i-1, j-1]
            else:
                matrix[i, j] = 1 + min(matrix[i-1, j], matrix[i, j-1], matrix[i-1, j-1])
    return matrix[i, j]
In [301]:
def lev_example(a, b):
    len_a = len(a) + 1
    len_b = len(b) + 1
    matrix = np.zeros([len_a, len_b])
    matrix[:, 0] = list(range(len_a))
    matrix[0] = list(range(len_b))
    for i in range(1, len_a):
        for j in range(1, len_b):
            if a[i-1] == b[j-1]:
                matrix[i, j] = matrix[i-1, j-1]
            else:
                matrix[i, j] = 1 + min(matrix[i-1, j], matrix[i, j-1], matrix[i-1, j-1])
    print(matrix)
    return matrix[i, j]

# An example of our algorithm with the same inputs as in the YouTube tutorial.
# We have amended our function so it also prints the final matrix.
lev_example("azced", "abcdef")
[[0. 1. 2. 3. 4. 5. 6.]
 [1. 0. 1. 2. 3. 4. 5.]
 [2. 1. 1. 2. 3. 4. 5.]
 [3. 2. 2. 1. 2. 3. 4.]
 [4. 3. 3. 2. 2. 2. 3.]
 [5. 4. 4. 3. 2. 3. 3.]]
Out[301]:
3.0
In [302]:
# Vectorize the lev function so that it may accept dataframes as input
vec_lev = np.vectorize(lev)

# Initialize two dataframes and extract the index so that we can compare our results
orgs = df1.copy().reset_index()
dups = df1.copy().reset_index()

draw_from1 = df1.copy().reset_index()
draw_from2 = df2.copy().reset_index()

# Perform the vectorized lev function on five entries to confirm that we can successfully identify matching pairs
for i in range(5):
    # For each row, compute the sum of minimum edit distances
    res = [np.nansum(row) for row in vec_lev(df1.iloc[i], df2)]
    insert1 = draw_from1.iloc[i]
    # Extract the row for which the summed minimum edit distance is minimized
    insert2 = draw_from2.iloc[res.index(np.min(res))]
    '''For illustrative purposes we will cheat a little and check whether a given entry is an orignal 
    or duplicate, so that we can clearly seperate them'''
    if "org" in df1.iloc[i].name:
        orgs.iloc[i] = insert1
        dups.iloc[i] = insert2
    else:
        orgs.iloc[i] = insert2
        dups.iloc[i] = insert1
In [303]:
orgs.head(5)
Out[303]:
rec_id given_name surname street_number address_1 address_2 suburb postcode state date_of_birth soc_sec_id
0 rec-61-org darcie fleet 13 fellows street braeburn somerton park 2284 vic 19950307 1563705
1 rec-321-org malakai mcvey 109 budden crescent billen cliffs caravn park manly vale 2259 nsw 19030707 7911018
2 rec-344-org NaN julius 52 florey drive north stirling downs coolaroo 2259 qld 19630521 1797144
3 rec-396-org isabelle witty NaN brereton street glen mia figtree 4555 wa 19980406 8008949
4 rec-358-org caleb ferris 41 grattan court hardways hoppers crossing 2060 wa 19960205 2947945
In [304]:
dups.head(5)
Out[304]:
rec_id given_name surname street_number address_1 address_2 suburb postcode state date_of_birth soc_sec_id
0 rec-61-dup-0 daarcie fleet 13 fellows street NaN somertonp ark 2284 vic 19950307 1563705
1 rec-321-dup-0 malskai mcvey 109 budden crescent billen cliffsc aravn park manly v ale 2295 nsw 19030707 7911018
2 rec-344-dup-0 NaN stephenson 52 NaN north stirilng downs coolaroo 2259 qld 19630521 1797144
3 rec-396-dup-0 isabelle witty NaN brereton street glen nia figtrwee 4555 wa 19980406 8008949
4 rec-358-dup-0 caleb ferrus 4 grattan court hardways hoppers crossing 2060 wa 19960205 2947945

Success! As you can imagine, performing automated record linkage can save us a lot of time when combining datasets. In practice, there are many useful packages like recordlinkage, nltk, fuzzywuzzy, and textdistance that can do much of the heavy lifting for us.

Sorting Algorithms

One of the simplest ways of demonstrating the performance differences of different algorithmic implementations is to look at sorting. Our goal will be to sort a lengthy list as quickly as possible. Note that we could use Python's inbuilt sort() or sorted() function, but that would be no fun! Before programming a few different sorting algorithms, let's generate a test set.

In [35]:
import random

lb = 0                       # lower bound
up = 10000                   # upper bound
list_range = range(lb, up)

# Create a random list containing all elements between lb and up
tutorial_list = random.sample(list_range, len(list_range))

Bubble Sort

Bubble sort is simple to understand but time intensive to perform. Bubble sort repeatedly swaps adjacent elements until the entire list is sorted. At worst, if the list is inversely sorted, Bubble sort will perform $\frac{n\times(n-1)}{2}$ comparisons in total, where $n$ is the length of the array to be sorted. Watch a Hungarian ("Csángó") folk dance rendition of the bubble sort algorithm.

[0] [5, 3, 4, 2, 1]

First we swap the 5 and the 3

[1] [3, 5, 4, 2, 1]

Next we swap the 5 and the 4

[2] [3, 4, 5, 2, 1]

Then the 5 and the 2

[3] [3, 4, 2, 5, 1]

And the 5 and the 1

[4] [3, 4, 2, 1, 5]

We've completed the first run through. We start at the beginning again. 3 is smaller than 4, so we so not swap the entries

[5] [3, 4, 2, 1, 5]

We swap the 4 and the 2. And so on . . .

[6] [3, 2, 4, 1, 5]

In [36]:
def bubble_sort(unsorted_list):
    for i in range(len(unsorted_list)-1, 0, -1):
        for j in range(i):
            if unsorted_list[j] > unsorted_list[j+1]:
                unsorted_list[j], unsorted_list[j+1] = unsorted_list[j+1], unsorted_list[j]
    return unsorted_list

# We can greatly optimize our bubble sort algorithm by realizing that if during any given run through
# no swaps are required, the list is already sorted and we can exit the loop. no_swaps tracks whether
# any swaps were performed during an entire cycle of the nested for loop
def optimized_bubble_sort(unsorted_list):
    for i in range(len(unsorted_list)-1, 0, -1):
        no_swaps = True
        for j in range(i):
            if unsorted_list[j] > unsorted_list[j+1]:
                unsorted_list[j], unsorted_list[j+1] = unsorted_list[j+1], unsorted_list[j]
                no_swaps = False
        if no_swaps:
            return unsorted_list

Insertion Sort

In Insertion sort, one pass through is made and each element swaps with its left neighbor as long as its neighbor is larger.

[0] [5, 3, 4, 2, 1]

First we swap the 5 and the 3

[1] [3, 5, 4, 2, 1]

Next we swap the 5 and the 4. Since 3 is smaller than 4, we carry on

[2] [3, 4, 5, 2, 1]

We swap the 5 and the 2. So far no difference

[3] [3, 4, 2, 5, 1]

Now, since 4 is larger than 2, we swap them

[4] [3, 2, 4, 5, 1]

Again, 3 is larger than 2, so we swap them

[4] [2, 3, 4, 5, 1]

Lastly, we perform the same operation on the 1 until it has moved to the first position

In [37]:
def insertion_sort(unsorted_list):
    for i in range(1, len(unsorted_list)):
        while i > 0 and unsorted_list[i-1] > unsorted_list[i]:
            unsorted_list[i-1], unsorted_list[i] = unsorted_list[i], unsorted_list[i-1]
            i -= 1
    return unsorted_list

Shellsort

Shellsort is similar to Insertion sort. However, with ech run through, elements are compared that are a certain distance apart from each other. This gap decreases with each run through until neighbors are compared. By then, we should have less or even no swaps that need to be performed. The rationale behind this approach is that we can more quickly move elements to their correct position. If, for example, the smallest element is the right most element in the unsorted list, we would have to swap it many times with each of its neighbors until it moves to the left most position. If we begin by comparing elements that are further apart, we can reduce the number of swaps we have to perform.

In [38]:
def shell_sort(unsorted_list, gap_function = lambda n, i: (n // 2**i)):
    '''Sort unsorted_list using Shellsort for any valid gap_function.
    gap_function defines how gaps are computed as a function of unsorted_list length (n) and/or iteration (i).
    By default, gap_function outputs n/2, n/4, ..., 1'''
    n = len(unsorted_list)
    i = 1
    while True:
        gap = gap_function(n, i)
        for j in range(gap, n):
            while j > 0 and unsorted_list[j-gap] > unsorted_list[j]:
                unsorted_list[j-gap], unsorted_list[j] = unsorted_list[j], unsorted_list[j-gap]
                j -= 1
        i += 1
        if gap == 1:
            return unsorted_list

Below you can see four examples of our shell sort algorithm. Each uses a different function to compute the gaps. The last example functions just like our insertion sorting algorithm.

In [313]:
# Shell (1959): n/2, n/4, ..., 1
%timeit shell_sort(tutorial_list)
16.9 ms ± 28.5 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
In [310]:
# Frank & Lazarus (1960): 2*(n/4)+1, ..., 3, 1
%timeit shell_sort(tutorial_list, lambda n, i: 2*(n // 2**(i+1))+1)
17.1 ms ± 97.2 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
In [311]:
# 10, 9, ..., 2, 1
%timeit shell_sort(tutorial_list, lambda n, i: 11-i)
14.1 ms ± 6.39 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
In [312]:
# Insertion sort
%timeit shell_sort(tutorial_list, lambda n, i: 1)
1.44 ms ± 53 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

Performance measurement of sorting algorithms

Now that we have defined three alternative algorithms for our problem, it is time to consider the performance of our code. To measure performance we can use the so-called "time" package. We will measure how much time passes while executing the algorithm. To do so, there are two useful functions in the package: time.time() and time.clock().

What time.time() does is measure wallclock time between two points in the code, i.e. how many actual seconds pass from start to finish. This method however is not entirely optimal. The reason for this is that the computer might be working on other tasks during the same time one is trying to measure the performance of the code. This means that additional memory in the CPU might be occupied and therefore slowing down the process. To solve this problem one can use time.clock() instead.

time.clock() has not been updated for a while though and is not the best way of determining dedicated CPU time. The new, updated versions of time.clock()are time.perf_counter() and time.process_time().

In our examples below, we will use the updated CPU timer with fractional seconds called time.process_time().

In [40]:
import time

# measuring bubblesort performance
start_bubblesort = time.process_time()
bubble_sort(tutorial_list)
end_bubblesort = time.process_time()

# measuring optimized bubblesort performance
start_optimized_bubblesort = time.process_time()
optimized_bubble_sort(tutorial_list)
end_optimized_bubblesort = time.process_time()

# measuring isort performance
start_insertionsort = time.process_time()
insertion_sort(tutorial_list)
end_insertionsort = time.process_time()

# measuring shellsort performance
start_shellsort = time.process_time()
shell_sort(tutorial_list)
end_shellsort = time.process_time()

print('Bubblesort sorted the tutorial list in {} seconds'.format(end_bubblesort - start_bubblesort))
print('Optimized Bubblesort sorted the tutorial list in {} seconds'.format(end_optimized_bubblesort - start_optimized_bubblesort))
print('Insertion sort sorted the tutorial list in {} seconds'.format(end_insertionsort - start_insertionsort))
print('Shellsort sorted the tutorial list in {} seconds'.format(end_shellsort - start_shellsort))
Bubblesort sorted the tutorial list in 6.274165000000011 seconds
Optimized Bubblesort sorted the tutorial list in 0.001314000000007809 seconds
Insertion sort sorted the tutorial list in 0.001458000000013726 seconds
Shellsort sorted the tutorial list in 0.017922999999996136 seconds

Scientifically Testing Algorithm Performance

Usually, when testing the performance of an algorithm there is a scientific method that can be used. The method for testing the performance of different algorithms includes the following parts:

  1. Observe some feature of the universe.
  2. Hypothesize a model that is consistent with observation.
  3. Predict events using the hypothesis.
  4. Verify the predictions by making further observations.
  5. Validate by repeating until the hypothesis and observations agree.

There are generally different factors that influence the performance of an algorithm. It can be your computer or especially the performance of your CPU, but also the algoritm itself, the compiler, the input data, caching, garbage collection or just-in-time compilation. There are multiple experiments possible to test the efficiency of an algorithm. But one of the most frequently used methods is the doubling hypothesis.

Doubling Hypothesis

One possibility to measure the performance of an algorithm is the doubling hypothesis. The doubling hypothesis takes different sizes of input into account and measures the performance in regards to rising input size. More clearly, it doubles the input and measures the effect on execution time.

For example a for loop takes twice as long to iterate over a list of elements when the size of the list is doubled. Therefore, with a doubling of the relevant elements, the time for execution doubles as well. This means that the doubling factor is 2 as the execution time doubles when the amount of elements included in the list doubles.

To experiment with the doubling hypothesis, you can set up an experiment in the following way:

  • Start with a moderate input size of N
  • Measure and save the execution time with time.time()
  • Double the input size N
  • Repeat
  • Enter the results in a table and plot them on a graph

05_doubling-hypothesis2.PNG

The results can also be calculated mathematically. N is the input size, TN are the measurements that we have made for the algorithm and the log() values can be computed with the calculator.

05_doubling-hypothesis.PNG

With the measured results, you can compute the slope of the execution time and this will enable you to compute the time for any given input by replacing N with the new input.

05_doubling-hypothesis3.PNG

Order of Growth

As described above, the doubling factor of the for loop is 2. However, as shown in the graph below, different algorithms have different doubling factors. Best shown in the graph below.

05_order-of-growth.png

Results

As we can see from the results above, the shell short is more efficent than the other two less complex sorting algorithms. The question is though, will the shell short algorithm always be the better choice when one needs to sort a list? Well that depends...

When considering a programming solution for a problem one thing to keep in mind is the pay-off between the time invested in optimizing the code and the actual time saved when one is later using the code. For example, let us assume that we have the knowledge to write the simpler bubble-sort algorithm, but lack the knowledge to write something more efficient. If there is a problem that consists of a shorter list that only needs to be sorted once, it might be wiser to just use the simpler algorithm, even though it is less efficient. Because the payoff for investing time in optimizing the code and learning more efficient ways to do a sorting algorithm might be very low. On the other hand, if we are facing a more complex problem where our sorting algorithm will be run a considerable amount of times, it might be worth investing time in optimizing the code to save a lot of time later when the program is running.

So, when deciding how to implement an algorithm solution to a problem one should always keep in mind the purpose of the implementation and then make a decision about the complexity of the code.

Optimizing algorithm implementation performance

When implementing code, performance efficiency is crucial. Therefore, we will examine some tips to optimize code. It will mostly be based on practical examples.

Optimizing with @jit

We showed different ways to implement an algorithm. Now, we want to know whether it is possible to speed up the algorithm we implemented? There exist a few tools to do just that. One of them is the Python module Numba, a Just-In-Time compiler. One useful property of this library, is @jit, a decorator that can be placed before a function. There is no need to modify your code: just add @jit and it will (in most cases) speed up the implementation. Python is not the most efficient language to compute loops and inner loops. C would be much faster for example. This is why Cython was invented. It injects C into python code to make the implementation faster. @jit transforms your code into more efficient code. How does it work? It uses the Low Level Virtual Machine(LLVM), a compiler infrastructure to make code more efficient. There also exists alternatives,such as PyPy which uses JIT (Just-In-Time compiler) technology as well. However, we will only include an example with Numba.

Here is a simple example showing the usefulness of @jit with matrix multiplication.

In [1]:
import random
import numpy as np
import time
from numba import jit

random.seed(10)

# Create two random matrices
A = np.random.randint(100, size=(100,100))
B = np.random.randint(100, size=(100,100))

# Multiply the two matrices with loops
def matMult(A,B):
    #Create an empty matrix 
    C = np.zeros((100,100))
    for i in range(A.shape[0]):
        for j in range(B.shape[1]):
            for k in range(B.shape[0]):
                C[i][j] += A[i][k] * B[k][j]
    return C
        

# Implementation with @jit

# Just add the decorator
@jit
def matMultjit(A,B):
    C = np.zeros((100,100))
    for i in range(A.shape[0]):
        for j in range(B.shape[1]):
            for k in range(B.shape[0]):
                C[i][j] += A[i][k] * B[k][j]
    return C


# Create a function to measure time  
def time_processing(result, a, b):
    start_function = time.process_time()
    result(a,b)
    end_function = time.process_time()
    print('The function {} took {} seconds to execute'.format(result.__name__,end_function-start_function))                

    
# Measure the time difference between the two implementations
time_processing(matMult,A,B)
time_processing(matMultjit,A,B)
The function matMult took 1.1507639999999997 seconds to execute
The function matMultjit took 0.4103220000000003 seconds to execute

We can observe a clear difference between the original implementation and the boosted implementation with @jit, with around one second of difference. Of course, multiplying a matrix in python using loops is not optimal but the example was made to illustrate the properties of @jit. Sometimes loops are unavoidable in implementation and there are many situations where the @jit function can show its talent. However, Numba is not a magic one-size-fits-all solution. Indeed, the original code must be compatible with it. For example, Numba does not appreciate mixing types. If you deal with both integers and strings in the same list, it is most likely that @jit will not work. To prevent this, using (nopython = TRUE) will make sure that you get an error if this issue occurs, so that is possible to adjust the original code. @njit instead of @jit does exactly the same. Furthermore, Numba does not like python lists. That's why is it better to use Numpy arrays instead.

Vectorization

We know that loops are not particularly efficient to do long array computations in python. A better alternative is vectorization, which means using Numpy's linear algebra functions instead of loops. It helps to minimize the running time. A simple example to illustrate this is the dot produt of two vectors:

In [2]:
# Create two vectors
import numpy as np

random.seed(10)

v1 = np.random.randint(100000, size=(100000))
v2 = np.random.randint(100000, size=(100000))


# Compute the dot product using loops
def dot_prod_loop(v1,v2):
    v3 = 0
    for i in range(v1.shape[0]):
        v3 += v1[i]*v2[i]
    return v3


# Compute the dot product using vectorization
def dot_prod_vect(v1,v2):
    return np.dot(v1,v2)

# Make sure that the two function lead to the same output
assert(dot_prod_loop(v1,v2) == dot_prod_vect(v1,v2))

    
# Compare the two functions
time_processing(dot_prod_loop,v1,v2)
time_processing(dot_prod_vect,v1,v2)
The function dot_prod_loop took 0.04595500000000019 seconds to execute
The function dot_prod_vect took 0.0005069999999998132 seconds to execute

As expected, the time taken by the loop function is considerably higher than the numpy np.dot function. Numpy has many useful functions to avoid loops and make your code more performant.

Pre-allocation

Pre-allocation is another trick to make your code both more efficient and readable. You can pre-allocate the sizes of matrices, arrays and lists. It will prevent that the collections will grow dynamically during the process. Indeed, a matrix that grows at each step of a for loop will affect negatively time performance and memory use. Pre-allocation, on the contrary, will ensure that it has the memory space it requires from the beginning, thus improving efficiency. Let's look at another example to compare performance with and without pre-allocation:

In [3]:
n = 100000

# Without pre-allocation

# We just create an empty list, without defining its size
def no_preallocation(n):
    l = []
    for i in range(n):
        result = n*i
        l.append(result)
    
    
# With pre-allocation

# We fill a created list with n elements
def preallocation(n):
    l = [None] * n
    for i in range(n):
        l[i] = n*i
        
# Create a function to mesure time  
def time_processing2(result,n):
    start_function = time.process_time()
    result(n)
    end_function = time.process_time()
    print('The function {} took {} seconds to execute'.format(result.__name__,end_function-start_function))
    
#compare the two functions
time_processing2(no_preallocation,n)
time_processing2(preallocation,n)
The function no_preallocation took 0.01812300000000011 seconds to execute
The function preallocation took 0.011106999999999978 seconds to execute

As expected, the performance of the function with a pre-allocation is better.

Generators

Generators consists of so-called genetor functions that return generator objects. A generator function acts as a normal function, but instead of a return statement, it ends with a yield statement. The function only outputs one element at a time, while a standard function will return everything at the same time. The generator returns an object that is called a 'lazy operator', which means that we can iterate over it like a list. The difference between a lazy operator and a list is that the lazy operator does not store the values. The big advantage of generators is that they minimize the amount of memory they occupy. It can also improve computing time since it only prints what you need. To show this, let's see a practical example:

In [4]:
# Define a standard function
def compute_normal(n):
    number = 0
    numList = []
    while number < n:
        t = number**n
        numList.append(t)
        number +=1
        
    
    
# Define the generator
def compute_gen(n):
    number = 0
    while number < n:
        yield (number**n)
        number +=1

# Compare the running time and the memory usage

import memory_profiler
import time

def time_memory(result,n):
    start = time.process_time()
    memory1 = memory_profiler.memory_usage()
    output = result(n)
    end = time.process_time()
    memory2 = memory_profiler.memory_usage()
    print('The function {} took {} seconds and {} MB to execute'.format(result.__name__,end-start,memory2[0]-memory1[0]))

n = 10000 
compute_normal(n)
time_memory(compute_normal,n)
time_memory(compute_gen,n)        
The function compute_normal took 13.649114999999998 seconds and 8.13671875 MB to execute
The function compute_gen took 0.0011379999999974189 seconds and 0.0 MB to execute

The time and the memory needed for the generator are significantly lower than for the normal function. That is because we only call one element at a time. It is thus very useful if this is what we need. However, if we want to obtain the entire list of elements, it will likely take slightly longer than the normal function in terms of computing time, but the memory usage is much improved.

In [5]:
# Take the generator object
t = compute_gen(n)

# Define a function to iterate over the generator object
def printing(t):
    for i in range(10000):
        next(t)
        
# Measure performance
time_memory(printing,t)
The function printing took 13.503031 seconds and 0.0 MB to execute

Further ideas for optimization

Profiling

The idea of profiling is to find where the program is most time-consuming. The goal is to determine the time taken by each function. It is useful to find where the programm needs to be optimized. Indeed, it would be laborious to improve the code without knowing where the inefficiencies are. To do so, python offers several possibilities. A simple way is to use the module timeit, which measure the time taken by a certain chunk of code. Another option is to use cProfile, a C module, which will show the number of calls, the time taken by a function and several other useful metrics.

Parallelization

Parallization is used to execute mutiple parts of a program simultaneously. It can save substiantial processing time and thus improve code efficiency. In addition, it is particularly useful if your program consumes heavy CPU (central prcessing unit) computation power. Modern computers have multiple CPU cores which can execute instructions at the same time. Parallelization uses this facutly to run different processes or threads on several cores simultaneously. Processes represent computing tasks. Some part of the machine's memory space is specifically allocated to each of them so that the processes are independent from each other. Furthermore, every process consists of one or more threads (or subprocesses), which share access to data structure and memory with the parent process. In Python, you can either use the module multiprocessing or threading to run parallel tasks. It is also possible to use parallelization with Numba. Just add (parallel = TRUE) next to @jit or @njit and import prange. The efficiency of this method in Numba will depend on the specific problem you are dealing with.

Implementation issues

Stack Overflow and Recursion limit errors

Stack Overflow is an implementation issue where the program tries to use more memory than the call stack has available. The call stack is the data structure which allows the program to store informations about the different routines (or functions) that are called. A stack overflow issue can happen on functions that are recursive, which means that the function calls itself. It can also happen when you have a function A that call a function B that iteself call a function C that calls function A. Luckily, it is difficult to provoke a stack overflow in Python, unlike in C or java for example, since it has mechanisms to prevent it. It will instead return an error stating that the maximum recursion depth has been exceeded. This can happen in the case where a function calls itself infinitely.

Here is an example of an infinite recursion:

In [6]:
def hi():
    return hi()
print(hi())
---------------------------------------------------------------------------
RecursionError                            Traceback (most recent call last)
<ipython-input-6-23d000980372> in <module>
      1 def hi():
      2     return hi()
----> 3 print(hi())

<ipython-input-6-23d000980372> in hi()
      1 def hi():
----> 2     return hi()
      3 print(hi())

... last 1 frames repeated, from the frame below ...

<ipython-input-6-23d000980372> in hi()
      1 def hi():
----> 2     return hi()
      3 print(hi())

RecursionError: maximum recursion depth exceeded

This returns "maximum recursion depth exceeeded", which is a guard against stack overflow. To avoid infinite recursion, it is important to check whether the recursion has an end.

Recursion Limits

Very long recursions can also cause errors. Let's take a function that computes the factorial of a given number. The factorial is defined as $n!=n*(n-1)*(n-2)...*1$. At each step the function calls itself. When computing the factorial of a sufficiently large number, it will exceed the maximum number of recursions that Python is capable of.

In [7]:
def fact(n):
    if n <=1:
        return 1
    return n* fact(n-1)
print(fact(5000))        
---------------------------------------------------------------------------
RecursionError                            Traceback (most recent call last)
<ipython-input-7-9d493fa147a9> in <module>
      3         return 1
      4     return n* fact(n-1)
----> 5 print(fact(5000))

<ipython-input-7-9d493fa147a9> in fact(n)
      2     if n <=1:
      3         return 1
----> 4     return n* fact(n-1)
      5 print(fact(5000))

... last 1 frames repeated, from the frame below ...

<ipython-input-7-9d493fa147a9> in fact(n)
      2     if n <=1:
      3         return 1
----> 4     return n* fact(n-1)
      5 print(fact(5000))

RecursionError: maximum recursion depth exceeded in comparison

The program returns the error "maximum recursion depth exceeded in comparison". To solve this issue, we can either write the algorithm in an iterative way or increase the recursion limit.

In [8]:
# Iterative version the factorial algorithm

def fact_it(n):
    if n <= 1:
        return 1
    else:
        fact = 1
        while(n > 1):
            fact *= n
            n -= 1
        return fact
    
# Print the output
print(fact_it(5000))
422857792660554352220106420023358440539078667462664674884978240218135805270810820069089904787170638753708474665730068544587848606668381273633721089377278763127939036305846216064390447898698223987192970889621161265296832177550039924219683703146907264472878789790404754884162215226671928410969236910449565971736352948400223840381120644820230857671104502306174894755428309761781724040805324809927809328784055486199364548291211876258248802189173977900050213212598043639244626460770511358846595108675470585833924655225589035474435988347383178988034633008458631510209091509935653820010933047965742556741930917055172805200236075085991197635228755907902043369743123506916831211924495971556267407521462198986233088625998302859864857578749445963115286970886710046268423648178989905454690861391613218344174148807186234448114831209490361196546872767755617886828720269104814092456410341835975604276458161513178575901661071782544156980883359372729995603371371200471049437656291142488605335299499642300699972204918120100819059439140675053265004775533850899097945101551091486907004407119572336026243368132330218709287699196806656569752790422258267841561083376425781032629202687211070274681394351128601502326190649959171897364176378436491219709109840944514895358959103804176941956657834822071749105512752639148381172052604826965162642710094919393332661030104360530459117014557209584714353721948246686793467375904872268133410207860903657108806376616249749507413107077401682180585945526445171409277469230062697511346044174567946735828782261629584248675157379172942724178783105429858245117575511884506574424827574660800238588378492396247368761507015767725898321128632295537044902516387925127590841791744640466913531047347984464996154595542013996317357476301740036796192919942190762895445656261767041799538161133387312823511534152581309087915883638351664797225912944270653557142511737323807232632958121797916679692329687096923901003255574789055099807487061047230646195984955239657612208673866514171699307557691897902675157342075864796345338446835085965490727326321910504064289713096224505162064669468098869917122127404504020684923266241760132910227866687270305284709452526825496617772499645206699836925910690894082637401043498371591126455822280606361394115344316771769934353664284928294436414769615881993661388255577487709937004594753907845149034434521174560594039916268444697661821387470705325559577933196460996662145377564935474169708562389214773222865507182490430016186142192760452307670621142961767274704123616107220009743758647492753665149532164780849075146330071016691313420662882562618283865836983632108760710427516073348347788414796732427080410860761841281888307115098982135338406610652147087046874760995427473673509451553599769040367353385551052571682650317682405743993414862392331981432579182193321898940450865013610998098383993110996355981328001049731588596312131853801205046787642910669365600437305633431984879048998524701293300789344532868156679762880495532846386020133480265279836946393384995675049993707814746561543438930431384237878981847802886009971088695632988347711863122382785963653115132377931373647397429369411499028751972227999545182615488298951151926682112451355318472209990435355949887299922035062039816011086376236539782172380237846650673624510635034423187315338308212043804710999419227821039747552717416043890169723961305549371844836119803565896062025009093664399360172007383613354405094329072476518909502507724675841989412224659392163116352038147362479528539732089309533421910635702805576629720156556510767780805933453631121829561792887673002802450932122778852968418208261778476955644980385691275787372678040959158711733971103165232678060798127609246173504120182666874262805385275843979167609007743380748420751185119102921960339376280986753665085212869255321536787932521882574101866137054328973735862725370178558806639851350386944039604928258820180419178073649693885802597758398892014389747165465973510852605706234402069637065660129535734043582961473427275805630839510667375349259659518575646939723218275780003250593895303820539697558870511543073920827422440516299708739599768461206246629098112368012579891284802505094028916959765079395437191311379314427405135599630375642214527294341797246187597964074239147838993541565834716156858499036773056611353833367087548900413091981676330749041510337597307246885839246941715548295730750618505881581959528992660225626903439573313450666972952115230668696227920947779974336574472673471408928071411283888082693377378077293104110767513639476200610858040596019639058015761002337463869352228385801434957178125581445862930042479404065736859862007914604590255413929950088044710384758990326548097338166940500085452723713571394902463820308668541802838317527668064278489561005755859991718966786449154063570014497194249878920859731254275567514575206399118150736397483102490793841725653421894276769116598153430084637087769510295415136551734675054015239706042571746001089968440498845985477977905031632568489156557231006499726498721480800181770357701502983008879487243887718884416833034708723239505377642232944095773219137582371673924704216723002256883135779230394688900662466182532658490724406767024939579697217467485562998183149665611743997680482094166257463879660305171274925119226367615337524381656217330771650129520988754856467131862602387619964334867961514408328902061082833180891221325853682856469916007952105166960451695430614212305743006877217407155473217957577017595967640563812729153867513698712395570542350999228605975469962186195531354132139126436676900465429996811680550737866770665988027062972502001882845886145344368771455361304414465613369092862748276981946836480550952968681758714859972973082332924094777085275279923304892719633314751563311192746150389219290616780607901383451137066300684376267199885515143681266137319912103235469786756421210624899005553564022924345831264231038363416781719908354140411717740185950606674198348143345444247191436828225654380047860390575922417071802670646875454211626958746795398540784464654140381751149965273621123540880166990280149033225139460832668170930713868826549977374286127789417784752681328371818759103642140881783220739808059714203285309721443041845459183002833408705783138284973283761286182927136745161897366207237396132790944984014154408304074405393067540767126182547597130843470311389815695365971788564022750674237400323621850094765267521941901241387478279883426470873616812485384444012772521050072279315853096279121131160167772077952572613800240684421885453537121341902236379684012385255288607189967725694227433323948595075570839061877450159652184414998155476107548008054192318436948191732631430603548399790783307267636729090980772827355854348032260067472537097785464567761181807367424367391769863758072145859791485033700592994963793369100283444558089838054017635403737133019311293080958287612107380374800660269767842888358265737486556785868822015143046249655995760379768685318192365806469199584071845493606922169776137542662239658644989770921478134709127917460871630220821981434654245065731262683089579031012893360788644107230184805400373136014216229159146992019884148290014414312800903102107833305090238435726779416177246873411503598700003151092815700331081727415624680432977205070450456683898626301702989301145364477416856732512330376477881749036052572605520684370616116539755132541369303867783267208227323664249206432363089268768826650939691861683271739757479552993242406186992420363781929485368098035256331092448215269276219116259145886393677034653480367887126133367116968226450914997055448521259751870084720025674658752403932061045903070039438252019383102480929019684602472171298321628237994627125366359971898374425099120673688383738299653892030662843074547559074235345274029211606091346327684749522046010409575607348155101677203187580089224494752922031093841661588823584993931745149914395557357641584185479831702428523965451087525425464777294595230360946416541997797947136806344915998772409176443137371178542210740572121166868692153240490080384205921192622875440898261478908123698956367080804687628524499897440855677969456909042340530355943524640751677873953113928698614347227572144946891896093294375476741234907792754338349412323060078767610089949156126934038921148370217193387617823370358925817112869563450001367619897145400986643461922197676975930010555225198913002123021780831934330880446592954552165911855939202579781122952065357362914478404946474565003115498072056580360667380889572746464375428055819322299305089287806874537401327100274428317925355003451536693172112088227603942809788645727306979971285649576934354004030728440581746648376658498039958964243370183454151720285337810904113124462432903353964296651109482836884580127588701293156099225044518125460113274986014470437757313881001319276124676116614833528935557503106018449788994378274613854651708241613167681463911870000812845144341406739985430072772303758111613511094355614896323929750846383152930263582535361784837558519666949972251935515953807207838615142130284450051795239760968433198292598921623223582396390262548856855875458198371559008447860086745945709118128793228222051767509371866110013193625845223493949829511199280837860523506412769337548130609594264463425077601147334209139128541628183172262143783062962408149391997187528106367348876678481602342743230027158192404187686545826519361990687336892886715133840245486110982482004482721799496658712257174429044916781194824165631560303473833317665121218052780795958220298330611945164019413315550379662980215357680731124530585915969709973988055743550083279071844959752353594644354789680372126344509423070253995102864458237454677761013556916212309752286152053213998745673034127676503369636682306665552051562491132528926155863868503100849180920507680658265915276163719928694258350604859732273949286080260640627521341007801815105623787926212039424781833439433877206395801115809084190794320195178235740190546595990289617711776195270354051193727229722248442080440098750369411277686593022133010625031862085145076421052980508837197986052557750303949606158442838846866137510968441567309838079394349570013029265177957120625555851951313574029897589283475525334409858911400694449308432874005015554332587793895080241128538758725945136400838324944471346436826148195406004114845870234072926697740631325878634790667698266181501256117692275715291249164821702372884416357600996851100939411444677628186007072278522314941048564396255796808221289935799262208553889221164765220850367706476491496133789353761537391569177822237744837614120253342622508007300513473422771427333106345971803240244226950458090539326689103619381998838844036231795282435495362489670734155948067688515321073064476077859628627852283657244564306449096277517172656954238392941958409527253281659572534531428389629894005886539486824117113929627356938973482935854650278689437014798383826002058208853517073216288725214522205265969614962147884840129004507737252424605074339660818182960296019196314124998538422017695110361380561701016357743542531148669369994130940908368220071936435111978592782493491477052187226546109199597269439152400467901173602521030051886080337084840114810246351288263986170081804888380750203521448348740849154718714478857809574515499505005070789428842888410027877777455981132319940624176532148686316581736774410084063436959989519288310869124517866342559353458242589411390516469440377562665821577845936829909679754548350510473633770839151033854639602753486401635204633884342346714935641429160856846724874244782055113759168236472297793612971080302530934478115527737540458968990354808058309381267323593563098546564376209385371052808344607189076003388781618019853273759498566916704703448438363503416368325266403224174519476678140428319327482851882140344319384445475456765253419659194332132585432270070759038565239668227171300009189122050845185261514627937717597528852978637931711212529529443323757910072909001703558763798612480281463093944391916950129336315045285163539312868586427437296109446101235604877439863299611899755965996608749049271167685268675335991297583209089553296409523640116060078495005377892783750147344122123777907727134146647404489837589487675423294546899354220341669961366698976529978580795899055864050388507083137333076839766882463680992355219727241831735127646189112380485883115569477888101759708977682149644340317924443085170303692214137621194388641989508360339306459037361842937028710758321966607546113761076362543928614316242890754021082233620012309384737312220374269033838579928678572939434168287053763374091938184632261131740934278117918891642447513543478446040549455379834556163353815868441692054518698919434175386663900335756765603264363767906721626620330878425545157208117246381251512669846685887209013144861632560461019513371814585249988176629925142145014710206193190373671380347663431029705222414785030188275106347446241258707937339085095757724316735066885094208761536164440443755860160625837091300574162065273670941888667964570550744724714137001968165215954380698515999483361357521322106131884771926641942395351412233546746461491743013475866037338176532604557402925472279360288926189385899695656876783017186873988763887625972743976062813263446647679413679726184933395074665820441677989806604203937116666336696282569349097348391155869004856032512219241534268522369316036765491047702733521540143168338872968405443296967684036073182435336224865433823598123544167514608340781166661858781733980624199254577853462678039039937557802759942957205281043775666979396838109341118959475766220191217535093638985465283078692370662512323684390235587636228324657161183714078807661162179517887972801841572019639084400269037450381192797170314489871815031319992111563908303017288012610642062005359240278277393918026391717720136125984776933980647063763022608885359937595079088789081791802195768033381968605120487107610874898411568740159953020639098138993261095538868264084012160831040525974539251576403732889086736948366404734622708560040891610782221943405179794550155347682966855320097501905581419914591124181501062255627411231571377358697194374130822027383843815940638571387913337592362330440453487233047240668784133333047898995255221468847973813568083995644533005222551320155267768895412770329278670827490041172076663112783638152343547681663121189086864991380236281775275946061211813342054791801619220346912760381900528012343973598270461499814511324618195658528232044658270082064934680251556511272822083811563192256509945201222666603226059396247019707668580396286975551115189973049085051758765306785758000660424066894170620303846785860257370634352599586885088679654004465187790208942935153217316750113738031466034642429489076322228133763299919641336502028627289268087560036613770607463575515079087982099722660130472907825746908175451952405573791313113170617323191598673971588373108168916968657704150695512947652386134815766967580364762005289060222744531744305498402863048850869557761528650326080941160688570698894762046478500884303973107412774191961697450517110329082815201273888663422631492147090220016940636504812047036016738602290671629816411198202268607961324739550057567564568204754619040423011062371367395995678940884705976859514505017241517746017351430990972615509378334720000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000

Another way to solve this issue is to increase the recursion limit of your algorithm. But be careful with this solution, because the limit cannot be increased infinity and may result in a stack overflow. This is why rewriting the algorithm in an iterative way is often better.

In [9]:
import sys

# Increase the recursive limit
sys.setrecursionlimit(100000)

# Print the output
print(fact(5000))
422857792660554352220106420023358440539078667462664674884978240218135805270810820069089904787170638753708474665730068544587848606668381273633721089377278763127939036305846216064390447898698223987192970889621161265296832177550039924219683703146907264472878789790404754884162215226671928410969236910449565971736352948400223840381120644820230857671104502306174894755428309761781724040805324809927809328784055486199364548291211876258248802189173977900050213212598043639244626460770511358846595108675470585833924655225589035474435988347383178988034633008458631510209091509935653820010933047965742556741930917055172805200236075085991197635228755907902043369743123506916831211924495971556267407521462198986233088625998302859864857578749445963115286970886710046268423648178989905454690861391613218344174148807186234448114831209490361196546872767755617886828720269104814092456410341835975604276458161513178575901661071782544156980883359372729995603371371200471049437656291142488605335299499642300699972204918120100819059439140675053265004775533850899097945101551091486907004407119572336026243368132330218709287699196806656569752790422258267841561083376425781032629202687211070274681394351128601502326190649959171897364176378436491219709109840944514895358959103804176941956657834822071749105512752639148381172052604826965162642710094919393332661030104360530459117014557209584714353721948246686793467375904872268133410207860903657108806376616249749507413107077401682180585945526445171409277469230062697511346044174567946735828782261629584248675157379172942724178783105429858245117575511884506574424827574660800238588378492396247368761507015767725898321128632295537044902516387925127590841791744640466913531047347984464996154595542013996317357476301740036796192919942190762895445656261767041799538161133387312823511534152581309087915883638351664797225912944270653557142511737323807232632958121797916679692329687096923901003255574789055099807487061047230646195984955239657612208673866514171699307557691897902675157342075864796345338446835085965490727326321910504064289713096224505162064669468098869917122127404504020684923266241760132910227866687270305284709452526825496617772499645206699836925910690894082637401043498371591126455822280606361394115344316771769934353664284928294436414769615881993661388255577487709937004594753907845149034434521174560594039916268444697661821387470705325559577933196460996662145377564935474169708562389214773222865507182490430016186142192760452307670621142961767274704123616107220009743758647492753665149532164780849075146330071016691313420662882562618283865836983632108760710427516073348347788414796732427080410860761841281888307115098982135338406610652147087046874760995427473673509451553599769040367353385551052571682650317682405743993414862392331981432579182193321898940450865013610998098383993110996355981328001049731588596312131853801205046787642910669365600437305633431984879048998524701293300789344532868156679762880495532846386020133480265279836946393384995675049993707814746561543438930431384237878981847802886009971088695632988347711863122382785963653115132377931373647397429369411499028751972227999545182615488298951151926682112451355318472209990435355949887299922035062039816011086376236539782172380237846650673624510635034423187315338308212043804710999419227821039747552717416043890169723961305549371844836119803565896062025009093664399360172007383613354405094329072476518909502507724675841989412224659392163116352038147362479528539732089309533421910635702805576629720156556510767780805933453631121829561792887673002802450932122778852968418208261778476955644980385691275787372678040959158711733971103165232678060798127609246173504120182666874262805385275843979167609007743380748420751185119102921960339376280986753665085212869255321536787932521882574101866137054328973735862725370178558806639851350386944039604928258820180419178073649693885802597758398892014389747165465973510852605706234402069637065660129535734043582961473427275805630839510667375349259659518575646939723218275780003250593895303820539697558870511543073920827422440516299708739599768461206246629098112368012579891284802505094028916959765079395437191311379314427405135599630375642214527294341797246187597964074239147838993541565834716156858499036773056611353833367087548900413091981676330749041510337597307246885839246941715548295730750618505881581959528992660225626903439573313450666972952115230668696227920947779974336574472673471408928071411283888082693377378077293104110767513639476200610858040596019639058015761002337463869352228385801434957178125581445862930042479404065736859862007914604590255413929950088044710384758990326548097338166940500085452723713571394902463820308668541802838317527668064278489561005755859991718966786449154063570014497194249878920859731254275567514575206399118150736397483102490793841725653421894276769116598153430084637087769510295415136551734675054015239706042571746001089968440498845985477977905031632568489156557231006499726498721480800181770357701502983008879487243887718884416833034708723239505377642232944095773219137582371673924704216723002256883135779230394688900662466182532658490724406767024939579697217467485562998183149665611743997680482094166257463879660305171274925119226367615337524381656217330771650129520988754856467131862602387619964334867961514408328902061082833180891221325853682856469916007952105166960451695430614212305743006877217407155473217957577017595967640563812729153867513698712395570542350999228605975469962186195531354132139126436676900465429996811680550737866770665988027062972502001882845886145344368771455361304414465613369092862748276981946836480550952968681758714859972973082332924094777085275279923304892719633314751563311192746150389219290616780607901383451137066300684376267199885515143681266137319912103235469786756421210624899005553564022924345831264231038363416781719908354140411717740185950606674198348143345444247191436828225654380047860390575922417071802670646875454211626958746795398540784464654140381751149965273621123540880166990280149033225139460832668170930713868826549977374286127789417784752681328371818759103642140881783220739808059714203285309721443041845459183002833408705783138284973283761286182927136745161897366207237396132790944984014154408304074405393067540767126182547597130843470311389815695365971788564022750674237400323621850094765267521941901241387478279883426470873616812485384444012772521050072279315853096279121131160167772077952572613800240684421885453537121341902236379684012385255288607189967725694227433323948595075570839061877450159652184414998155476107548008054192318436948191732631430603548399790783307267636729090980772827355854348032260067472537097785464567761181807367424367391769863758072145859791485033700592994963793369100283444558089838054017635403737133019311293080958287612107380374800660269767842888358265737486556785868822015143046249655995760379768685318192365806469199584071845493606922169776137542662239658644989770921478134709127917460871630220821981434654245065731262683089579031012893360788644107230184805400373136014216229159146992019884148290014414312800903102107833305090238435726779416177246873411503598700003151092815700331081727415624680432977205070450456683898626301702989301145364477416856732512330376477881749036052572605520684370616116539755132541369303867783267208227323664249206432363089268768826650939691861683271739757479552993242406186992420363781929485368098035256331092448215269276219116259145886393677034653480367887126133367116968226450914997055448521259751870084720025674658752403932061045903070039438252019383102480929019684602472171298321628237994627125366359971898374425099120673688383738299653892030662843074547559074235345274029211606091346327684749522046010409575607348155101677203187580089224494752922031093841661588823584993931745149914395557357641584185479831702428523965451087525425464777294595230360946416541997797947136806344915998772409176443137371178542210740572121166868692153240490080384205921192622875440898261478908123698956367080804687628524499897440855677969456909042340530355943524640751677873953113928698614347227572144946891896093294375476741234907792754338349412323060078767610089949156126934038921148370217193387617823370358925817112869563450001367619897145400986643461922197676975930010555225198913002123021780831934330880446592954552165911855939202579781122952065357362914478404946474565003115498072056580360667380889572746464375428055819322299305089287806874537401327100274428317925355003451536693172112088227603942809788645727306979971285649576934354004030728440581746648376658498039958964243370183454151720285337810904113124462432903353964296651109482836884580127588701293156099225044518125460113274986014470437757313881001319276124676116614833528935557503106018449788994378274613854651708241613167681463911870000812845144341406739985430072772303758111613511094355614896323929750846383152930263582535361784837558519666949972251935515953807207838615142130284450051795239760968433198292598921623223582396390262548856855875458198371559008447860086745945709118128793228222051767509371866110013193625845223493949829511199280837860523506412769337548130609594264463425077601147334209139128541628183172262143783062962408149391997187528106367348876678481602342743230027158192404187686545826519361990687336892886715133840245486110982482004482721799496658712257174429044916781194824165631560303473833317665121218052780795958220298330611945164019413315550379662980215357680731124530585915969709973988055743550083279071844959752353594644354789680372126344509423070253995102864458237454677761013556916212309752286152053213998745673034127676503369636682306665552051562491132528926155863868503100849180920507680658265915276163719928694258350604859732273949286080260640627521341007801815105623787926212039424781833439433877206395801115809084190794320195178235740190546595990289617711776195270354051193727229722248442080440098750369411277686593022133010625031862085145076421052980508837197986052557750303949606158442838846866137510968441567309838079394349570013029265177957120625555851951313574029897589283475525334409858911400694449308432874005015554332587793895080241128538758725945136400838324944471346436826148195406004114845870234072926697740631325878634790667698266181501256117692275715291249164821702372884416357600996851100939411444677628186007072278522314941048564396255796808221289935799262208553889221164765220850367706476491496133789353761537391569177822237744837614120253342622508007300513473422771427333106345971803240244226950458090539326689103619381998838844036231795282435495362489670734155948067688515321073064476077859628627852283657244564306449096277517172656954238392941958409527253281659572534531428389629894005886539486824117113929627356938973482935854650278689437014798383826002058208853517073216288725214522205265969614962147884840129004507737252424605074339660818182960296019196314124998538422017695110361380561701016357743542531148669369994130940908368220071936435111978592782493491477052187226546109199597269439152400467901173602521030051886080337084840114810246351288263986170081804888380750203521448348740849154718714478857809574515499505005070789428842888410027877777455981132319940624176532148686316581736774410084063436959989519288310869124517866342559353458242589411390516469440377562665821577845936829909679754548350510473633770839151033854639602753486401635204633884342346714935641429160856846724874244782055113759168236472297793612971080302530934478115527737540458968990354808058309381267323593563098546564376209385371052808344607189076003388781618019853273759498566916704703448438363503416368325266403224174519476678140428319327482851882140344319384445475456765253419659194332132585432270070759038565239668227171300009189122050845185261514627937717597528852978637931711212529529443323757910072909001703558763798612480281463093944391916950129336315045285163539312868586427437296109446101235604877439863299611899755965996608749049271167685268675335991297583209089553296409523640116060078495005377892783750147344122123777907727134146647404489837589487675423294546899354220341669961366698976529978580795899055864050388507083137333076839766882463680992355219727241831735127646189112380485883115569477888101759708977682149644340317924443085170303692214137621194388641989508360339306459037361842937028710758321966607546113761076362543928614316242890754021082233620012309384737312220374269033838579928678572939434168287053763374091938184632261131740934278117918891642447513543478446040549455379834556163353815868441692054518698919434175386663900335756765603264363767906721626620330878425545157208117246381251512669846685887209013144861632560461019513371814585249988176629925142145014710206193190373671380347663431029705222414785030188275106347446241258707937339085095757724316735066885094208761536164440443755860160625837091300574162065273670941888667964570550744724714137001968165215954380698515999483361357521322106131884771926641942395351412233546746461491743013475866037338176532604557402925472279360288926189385899695656876783017186873988763887625972743976062813263446647679413679726184933395074665820441677989806604203937116666336696282569349097348391155869004856032512219241534268522369316036765491047702733521540143168338872968405443296967684036073182435336224865433823598123544167514608340781166661858781733980624199254577853462678039039937557802759942957205281043775666979396838109341118959475766220191217535093638985465283078692370662512323684390235587636228324657161183714078807661162179517887972801841572019639084400269037450381192797170314489871815031319992111563908303017288012610642062005359240278277393918026391717720136125984776933980647063763022608885359937595079088789081791802195768033381968605120487107610874898411568740159953020639098138993261095538868264084012160831040525974539251576403732889086736948366404734622708560040891610782221943405179794550155347682966855320097501905581419914591124181501062255627411231571377358697194374130822027383843815940638571387913337592362330440453487233047240668784133333047898995255221468847973813568083995644533005222551320155267768895412770329278670827490041172076663112783638152343547681663121189086864991380236281775275946061211813342054791801619220346912760381900528012343973598270461499814511324618195658528232044658270082064934680251556511272822083811563192256509945201222666603226059396247019707668580396286975551115189973049085051758765306785758000660424066894170620303846785860257370634352599586885088679654004465187790208942935153217316750113738031466034642429489076322228133763299919641336502028627289268087560036613770607463575515079087982099722660130472907825746908175451952405573791313113170617323191598673971588373108168916968657704150695512947652386134815766967580364762005289060222744531744305498402863048850869557761528650326080941160688570698894762046478500884303973107412774191961697450517110329082815201273888663422631492147090220016940636504812047036016738602290671629816411198202268607961324739550057567564568204754619040423011062371367395995678940884705976859514505017241517746017351430990972615509378334720000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000