Skip to content
CCC Python Course

Python constant-factor engineering at scale

Module
M7.12
Lesson
1 of 1
Reading time
5 min

In this lesson

  • Identify performance bottlenecks in Python programs that run on PyPy.
  • Use flat 1D lists and parallel lists instead of tuples and dictionaries.
  • Encode integers into single values to reduce memory and cache misses.
  • Read input in one call, join output, and measure on maximum-size test cases.
  • Know when further optimisation returns diminishing marks and when to stop.

Your algorithm is correct. Its complexity is right. But when you submit to the judge, it times out. The issue is not the algorithm itself. It is the speed at which Python executes your code, instruction by instruction.

Python is slower than C by nature. The contest grader runs on PyPy 3.8, which compiles frequently-executed loops to native machine code and dramatically speeds up Python. However, PyPy does not accelerate all Python code equally. The choices you make about input parsing, data structures, and loop structure determine whether you meet the time limit.

Reading input once

Calling input() in a loop for hundreds of thousands of lines is extremely slow. Each call involves line parsing, buffer management, and string allocation. In a tight loop, this overhead dominates and consumes the time that should go to your actual computation.

Instead, use sys.stdin.read().split(). This single call reads all input, and you then extract tokens by position from the resulting list. You saw this pattern in M3.10.

examples/read_once.py
import sys

# Slow: reading with input() in a loopdef slow_read():    n = int(input())    total = 0    for _ in range(n):        x = int(input())        total += x    return total

# Fast: reading everything at oncedef fast_read():    data = sys.stdin.read().split()    n = int(data[0])    total = 0    for i in range(1, n + 1):        total += int(data[i])    return total

def main() -> None:    result = fast_read()    sys.stdout.write(f"{result}\n")

if __name__ == "__main__":    main()

Input

5
10
20
15
25
30

Output

100
Reading all input at once is much faster than calling input() in a loop

The first version calls input() on each line. The second reads everything at once and indexes the list. Both do the same work, but the second is faster.

Flat lists and parallel lists

Storing data in tuples or dictionaries makes sense in small programs. On a large test case, access time adds up.

A list of tuples like [(3, 5), (1, 7), (2, 4)] looks clean. But each lookup first finds the tuple, then indexes into it. If you have 100,000 points, that is 200,000 attribute accesses.

Instead, use two flat lists: one for the first value and one for the second. Index both lists with the same integer. A list lookup on an integer index is just an array access. PyPy's compiler can make it very cheap.

examples/flat_lists.py
import sys

def main() -> None:    data = sys.stdin.read().split()    n = int(data[0])
    # Read points into parallel lists    x_coords = []    y_coords = []    for i in range(1, 2 * n + 1, 2):        x_coords.append(int(data[i]))        y_coords.append(int(data[i + 1]))
    # Process: find sum of all coordinates    total = sum(x_coords) + sum(y_coords)    sys.stdout.write(f"{total}\n")

if __name__ == "__main__":    main()

Input

3
1 2
4 5
3 6

Output

21
Parallel flat lists are faster than a list of tuples

Here we have 100,000 points. The tuple version stores them compactly in memory. The flat-list version uses two separate lists. When you process a point, you index both lists. This is faster on PyPy because the compiler optimises integer indexing heavily.

Packing integers

Sometimes you have data you want to encode compactly. For example, you might have a node number (0 to 10,000) and a time value (0 to 1,000). Instead of storing them separately or as a tuple, pack both into a single integer.

Multiply the node by a large enough constant and add the time. Then when you need the node, divide by that constant and take the remainder.

examples/pack_ints.py
import sys

def main() -> None:    data = sys.stdin.read().split()    n = int(data[0])
    # Pack node (0-10000) and time (0-1000) into one integer    PACK = 10001    packed = []    for i in range(1, 2 * n + 1, 2):        node = int(data[i])        time = int(data[i + 1])        packed_value = node * PACK + time        packed.append(packed_value)
    # Sort packed values    packed.sort()
    # Output unpacked values    output = []    for p in packed:        node = p // PACK        time = p % PACK        output.append(f"{node} {time}")
    sys.stdout.write("\n".join(output) + "\n")

if __name__ == "__main__":    main()

Input

3
5 100
2 500
5 200

Output

2 500
5 100
5 200
Pack two integers into one to reduce memory and lookups

A packed integer stores two values in one. When you sort the packed values, you sort by node first, then by time. The sorting is faster because you are moving fewer bytes. Dictionary lookups are also faster when your key is a single integer instead of a tuple.

Avoiding copies

Slicing a list creates a new copy. Joining strings creates copies of the input. These copies use memory and time.

When you do not need a copy, avoid creating one. Read from the original list. Append to an output list and join it once at the end, instead of concatenating strings in a loop.

examples/avoid_copies.py
import sys

def main() -> None:    data = sys.stdin.read().split()    n = int(data[0])
    # Slow: concatenating strings    # result = ""    # for i in range(1, n + 1):    #     result += data[i] + " "
    # Fast: appending to list and joining once    output = []    for i in range(1, n + 1):        output.append(data[i])
    sys.stdout.write(" ".join(output) + "\n")

if __name__ == "__main__":    main()

Input

3
hello
world
test

Output

hello world test
Joining one list at the end is faster than many string concatenations

The first version concatenates total in a loop. Each concatenation creates a new string. The second version appends to a list and joins once. On large outputs, this is much faster.

A complete example: sorting pairs by custom order

Imagine you have to sort pairs by one field when it is small, and by the other field when the first is tied. This is a common CCC task.

The slow way is to write a custom comparison function and call the sort. A faster way is to pack both values, compute a sort key, and sort by the key.

examples/sorted_pairs.py
import sys

def main() -> None:    data = sys.stdin.read().split()    n = int(data[0])
    # Pack pairs (a, b) where we sort by a then b    PACK = 1000    pairs = []    for i in range(1, 2 * n + 1, 2):        a = int(data[i])        b = int(data[i + 1])        packed = a * PACK + b        pairs.append(packed)
    pairs.sort()
    # Unpack and output    output = []    for p in pairs:        a = p // PACK        b = p % PACK        output.append(f"{a} {b}")
    sys.stdout.write("\n".join(output) + "\n")

if __name__ == "__main__":    main()

Input

4
5 2
2 8
5 1
2 3

Output

2 3
2 8
5 1
5 2
Packing and sorting pairs efficiently

The pairs are sorted by the first value, then by the second value when the first is tied. The packing approach sorts on a single integer per pair. This is faster than using a key function that unpacks them during each comparison.

When to measure and when to stop

You have finished the algorithm. You code it up. You test on sample inputs and the output is right. The judge times out on the larger subtasks.

Before you start optimising, measure where the time is spent. Run the largest test case you can fit in memory and see which part of your code takes time.

If your algorithm's complexity is bad, no constant-factor trick will save it. Fix the algorithm first.

If the algorithm is fine but you are still too slow, use a profiler or add timing code to see which loop or which operation uses most of the time. Then optimise that part. A smaller optimisation elsewhere will not help.

examples/measure_time.py
import sysimport time

def main() -> None:    t_read = time.time()    data = sys.stdin.read().split()    t_parse = time.time()
    n = int(data[0])    total = 0
    t_compute = time.time()    for i in range(1, n + 1):        total += int(data[i])    t_done = time.time()
    read_time = (t_parse - t_read) * 1000    parse_time = (t_compute - t_parse) * 1000    compute_time = (t_done - t_compute) * 1000
    sys.stdout.write(f"{total}\n")    sys.stderr.write(f"Read: {read_time:.2f}ms Parse: {parse_time:.2f}ms Compute: {compute_time:.2f}ms\n")

if __name__ == "__main__":    main()

Input

4
10
20
30
40

Output

100
Measure which part of your code uses time

This program solves a small example and prints the time each part took. When you run it on a large test case, you see what is slow.

Another fact about CCC: the grader keeps your best submission. If the fast solution would take you longer to code than you have, submit the simpler solution first. If the fast solution is risky or complex, code it safely, so a crash never replaces working code.

The house skeleton for fast I/O

You have already seen the house skeleton in M3.10. Here it is again.

Python
import sys

def main() -> None:    input_data = sys.stdin.read().split()    if not input_data:        return    # algorithm implementation    output = []    # ... build output list ...    sys.stdout.write("\n".join(output))

if __name__ == "__main__":    main()

This is the shape to use for any problem with large input. It reads once, processes, and writes once. Use it as your default for senior problems. All the examples in this lesson use this shape.

Recap

A fast Python program reads input once with sys.stdin.read().split(), stores data in flat 1D lists or packed integers, avoids slicing and copying, and keeps loops tight. PyPy compiles hot loops to machine code, but only if the code is simple and uses cheap operations like integer indexing.

Before you optimise, measure. After you optimise, measure again. Know the complexity limit for partial marks and decide when to stop.

Practice

Try these on the judge. Each link opens the problem on DMOJ.

  1. 2019 S4
    Tourism (opens on DMOJ in a new tab) DMOJ

    Find the minimum cost to process a stream of updates to a flat array.

    Why DMOJ: An older large-input problem, good for practicing fast I/O and tight loops.

  2. 2015 S5
    Greedy For Pies (opens on DMOJ in a new tab) DMOJ

    Compute the shortest meeting time for scheduled intervals.

    Why DMOJ: An older problem that builds timing-critical fast-I/O and loop skills.