Skip to content
CCC Python Course

Online algorithms and encrypted input

Module
M7.11
Lesson
1 of 1
Reading time
6 min

In this lesson

  • Decode encrypted input using the previous answer.
  • Understand why encryption forces an online algorithm.
  • Solve problems where queries cannot be sorted or reordered.
  • Implement fast I/O for problems with many queries and answers.

Most contest problems give you all the input at once, so you can preprocess, sort queries, and answer them in any order. Some problems add a twist: the input is encrypted, and each query is decoded using the answer to the previous query. This forces an online algorithm: you must answer each query before you can read the next one.

Encryption prevents offline tricks like sorting queries by the value they ask for, or grouping them by type. You are forced to solve each query as it arrives.

Encrypted input and online processing

The input is given as encrypted values. To decrypt a value, you apply a transformation using the answer from the previous query. The typical pattern is:

s' = (s + ans) mod p

where s' is the encrypted value, s is the actual value, ans is the previous answer, and p is a modulus.

You read s', compute s = (s' - ans + p) mod p, and use s in the current query.

The first query has ans = 0 from a dummy previous answer.

examples/online_segment_tree.py
import sys

def main() -> None:    input_data = sys.stdin.read().split()    n = int(input_data[0])    q = int(input_data[1])    p = int(input_data[2])
    # Segment tree for range sum queries, indices 1..n.    tree = [0] * (4 * n + 4)
    def update(node, start, end, pos, val):        if start == end:            tree[node] = val            return        mid = (start + end) // 2        if pos <= mid:            update(2 * node, start, mid, pos, val)        else:            update(2 * node + 1, mid + 1, end, pos, val)        tree[node] = tree[2 * node] + tree[2 * node + 1]
    def query(node, start, end, l, r):        if r < start or end < l:            return 0        if l <= start and end <= r:            return tree[node]        mid = (start + end) // 2        return query(2 * node, start, mid, l, r) + query(2 * node + 1, mid + 1, end, l, r)
    answers = []    ans = 0    idx = 3    for _ in range(q):        op = int(input_data[idx])        # Each query gives two encrypted numbers. Decrypt both the same way,        # using the answer to the previous query.        a = (int(input_data[idx + 1]) - ans + p) % p        b = (int(input_data[idx + 2]) - ans + p) % p        idx += 3        if op == 1:            update(1, 1, n, a, b)        else:            ans = query(1, 1, n, a, b)            answers.append(ans)
    print("\n".join(str(x) for x in answers))

if __name__ == "__main__":    main()

Input

5 4 1000000007
1 3 10
2 1 5
1 12 30
2 11 15

Output

10
30
Segment tree with encrypted input

Each query line carries an operation type and two encrypted numbers, decrypted the same way with the running answer ans. An update line decodes to a position and a value; a range-sum query decodes to the left and right ends of the range. The program processes queries in order: it decrypts a line, answers or applies it, and only then moves to the next line, because the next line's own decryption needs the answer this one produced.

This serialisation is the only difference from a standard segment tree query. Everything else remains the same.

Why encryption matters

Encryption ensures that you cannot sort queries offline. If you could rearrange queries, you might group them by range or type and answer them more efficiently. You could use techniques like square-root decomposition with query sorting or offline batch processing. Encryption forces you to handle each query as it arrives, which eliminates these optimisations.

A problem with encrypted, high-volume queries usually needs an O(log N) or O(1) per-query solution to handle every query in time. Check the constraints for the number of queries before deciding whether a slower structure can still clear a smaller subtask.

Implementing the cipher

The cipher operates on individual values. Each input value s' is transformed into s = (s' - ans + p) mod p, where ans is the answer from the previous query. For the first query, ans = 0.

You must decode before processing the query, which means you cannot read ahead or preprocess batches of inputs. The serial dependency means the algorithm itself must be fast. If you try to batch-decode queries, you will fail because you do not know the answers yet.

The encryption prevents a common optimisation technique: processing queries offline by sorting them and answering in a different order. Many range-query problems benefit from sorting queries by left endpoint or by some other heuristic. Encryption makes this impossible.

Fast I/O under encryption

With many queries (up to 10^5), reading and writing are costly. Use the house skeleton: read all input at once with sys.stdin.read().split(), store it in a list, and iterate through it while decrypting. This amortises the cost of I/O over many queries.

For output, collect all answers in a list and write them together with "\n".join() at the end, or use sys.stdout.write("\n".join(str(x) for x in answers)). Avoid printing each answer individually, as that is much slower on PyPy.

Even with fast I/O, an O(N) algorithm per query will TLE on 10^5 queries. You need O(log N) per query or better. A segment tree or another logarithmic structure is usually necessary.

Segment trees and other online data structures

A segment tree is a natural choice for online range queries. It supports point updates and range queries in O(log N) time. With encryption forcing you to answer queries in order, a segment tree is fast enough.

Other logarithmic structures like balanced BSTs or Fenwick trees can also work, depending on the query type. The key is that each operation is fast, because you have no opportunity to batch or reorder.

If the problem asks for more than just range queries (e.g., dynamic connectivity), you may need a more advanced data structure like a link-cut tree. But for typical range-query problems, a segment tree is standard.

Decoding by hand

Before implementing, manually decode the sample input to verify your understanding of the cipher. This is especially important if the cipher is unusual or if you are unfamiliar with the particular modulus and operation. A single decoding error will cause all subsequent queries to be wrong.

Write down the intermediate values step by step: the encrypted query, the previous answer, the decrypted query, and the response. This builds confidence that your understanding is correct before you commit to code.

A worked example: decoding a range query

Suppose the modulus is 1000000007, and the first encrypted query is (2, 12345). The first query has ans = 0, so the decrypted values are s1 = (2 - 0 + 1000000007) mod 1000000007 = 2 and s2 = (12345 - 0 + 1000000007) mod 1000000007 = 12345. So the query is range sum from index 2 to 12345. The answer is some value, say 67890.

The second encrypted query is (50000, 100000). Now ans = 67890, so s1 = (50000 - 67890 + 1000000007) mod 1000000007 = (49932117) mod 1000000007 = 49932117. And s2 = (100000 - 67890 + 1000000007) mod 1000000007 = (100032117) mod 1000000007 = 100032117. So the second query is range sum from 49932117 to 100032117.

Note that the second query's indices depend on the first answer. If the first answer were different, the second query would be completely different. This is why you cannot precompute or sort queries.

Common mistakes and why online matters

A frequent error is using the modulo operator incorrectly. In Python, (a - b) % p can be negative if a < b, which is wrong for decoding. Instead, use (a - b + p) % p to ensure the result is in the range [0, p). This is especially important when dealing with large primes.

Another mistake is forgetting that decryption depends on the previous answer. If you decode all queries at once without processing them in order, the decrypted values will be wrong for queries after the first. The serial dependency is fundamental to the problem.

A third pitfall is misunderstanding what "online" means. An online algorithm must answer each query before reading the next query. You cannot read all input, sort it, and then answer queries. If you try to do so, the decryption will fail. The algorithm is not "online" with respect to your code; it is forced to be online by the problem structure.

Why online algorithms are harder and their cost

Online algorithms cannot use offline optimisations like query sorting or batch processing. This makes them harder to optimise. A problem that would be solvable with square-root decomposition on sorted queries might require a segment tree on unsorted, encrypted queries.

The cost of online algorithms is that they must be very fast per operation. With up to 10^5 queries, each query must run in O(log N) or better. Slower algorithms like linear search (O(N)) or even O(N log N) cannot sustain 10^5 queries in a few seconds.

However, once you have a logarithmic structure like a segment tree, the implementation is straightforward. The encryption is just an extra decryption step before each query. The hard part is recognizing that you need a fast data structure and building it correctly.

Practice

Try this on the judge. The link opens the problem on WMOJ.

  1. 2025 S5
    To-Do List (opens on WMOJ in a new tab) WMOJ

    Tracking a changing list of tasks, given as encrypted, online queries.