CF 172B - Pseudorandom Sequence Period
We are given a linear congruential generator, a classic pseudorandom sequence formula: $$ri = (a cdot r{i-1} + b) bmod m$$ The sequence starts from r0, and every next value is computed from the previous one.
CF 172B - Pseudorandom Sequence Period
Rating: 1200
Tags: *special, implementation, number theory
Solve time: 3m 33s
Verified: yes
Solution
Problem Understanding
We are given a linear congruential generator, a classic pseudorandom sequence formula:
$$r_i = (a \cdot r_{i-1} + b) \bmod m$$
The sequence starts from r0, and every next value is computed from the previous one. Since every value is taken modulo m, there are only m possible states: 0 through m - 1.
The task is not to find when the sequence first repeats. We specifically need the length of the repeating cycle that eventually appears. A sequence may have some non-repeating prefix before entering the cycle.
For example, with:
a = 2
b = 6
m = 12
r0 = 11
the sequence becomes:
11 -> 4 -> 2 -> 10 -> 2 -> 10 -> ...
The value 11 appears only once. After that, the sequence alternates forever between 2 and 10, so the period is 2.
The constraint m ≤ 100000 is the key observation. Since every generated value is between 0 and m - 1, the sequence can visit at most m distinct states before some value repeats. That immediately rules out any infinite exploration concerns. Even an O(m) or O(m log m) solution easily fits within the limits.
A naive approach that compares every pair of positions would become quadratic. With m = 100000, an O(m²) algorithm could require around 10^10 operations, which is far beyond the time limit.
The tricky part is distinguishing the preperiod from the actual cycle.
Consider this input:
0 0 10 7
The sequence is:
7 -> 0 -> 0 -> 0 -> ...
The correct answer is 1, not 2. A careless implementation that measures distance between the first and second occurrence of any value could incorrectly count part of the preperiod.
Another subtle case is when the cycle starts immediately:
1 1 5 0
The sequence becomes:
0 -> 1 -> 2 -> 3 -> 4 -> 0 -> ...
The whole sequence is cyclic from the beginning, so the answer is 5.
A third edge case happens when the sequence reaches a fixed point:
5 3 100 15
Suppose the sequence eventually becomes:
78 -> 78 -> 78 -> ...
The period is still 1, because a single value repeats forever.
Approaches
The brute-force idea is straightforward. Generate the sequence step by step and store every produced value. Whenever a value appears again, try to determine the cycle length by comparing future elements.
This works because the sequence is deterministic. Once we revisit a previous state, everything after that point repeats identically. The problem is efficiency. If we repeatedly compare large suffixes to verify candidate periods, the worst-case complexity becomes quadratic.
The important observation is that the generator behaves like a directed graph where every node has exactly one outgoing edge. From any state, there is exactly one next state. Such graphs always consist of a path leading into a cycle.
Since there are only m possible states, the first repeated value immediately identifies the start of the cycle.
Suppose value x first appears at position p, and later appears again at position q.
Because the transition function is deterministic:
next(x) is always the same
the entire suffix after position p must match the suffix after position q. That means the cycle length is simply:
q - p
So instead of storing the entire sequence and performing comparisons, we only need to remember the first position where each value appeared.
The algorithm becomes:
- Generate values one by one.
- Store the first index where each value appears.
- When a value repeats, return the difference between current index and first occurrence index.
Since each state is processed at most once before repetition, the algorithm runs in linear time.
| Approach | Time Complexity | Space Complexity | Verdict |
|---|---|---|---|
| Brute Force | O(m²) | O(m) | Too slow |
| Optimal | O(m) | O(m) | Accepted |
Algorithm Walkthrough
- Read
a,b,m, andr0. - Create a dictionary or array
first_seenthat stores the first index where each value appears. - Mark the starting value
r0as seen at position0. - Repeatedly generate the next value using:
$$r = (a \cdot r + b) \bmod m$$
5. Increase the current position counter after generating each new value.
6. Check whether the new value has appeared before.
7. If the value is new, store its first occurrence position and continue.
8. If the value already appeared at position p, the sequence has entered a cycle. The cycle length is:
$$\text{current position} - p$$ 9. Print that length and stop.
Why it works
The sequence is completely determined by its current value. If some value x appears at two different positions, the future evolution from both positions must be identical.
Suppose:
r_p = r_q
with p < q.
Then:
r_{p+1} = r_{q+1}
r_{p+2} = r_{q+2}
...
because both positions apply the same transition formula to the same value.
That means the sequence repeats every q - p steps starting from position p. Since we stop at the first repeated value, this difference is the minimum valid period.
Python Solution
import sys
input = sys.stdin.readline
a, b, m, r = map(int, input().split())
first_seen = [-1] * m
first_seen[r] = 0
pos = 0
while True:
r = (a * r + b) % m
pos += 1
if first_seen[r] != -1:
print(pos - first_seen[r])
break
first_seen[r] = pos
The array first_seen stores the first position where each remainder appears. Since every value is in the range [0, m - 1], using an array is faster and simpler than using a dictionary.
The variable pos represents the index of the current value in the sequence. We start with r0 at position 0.
Inside the loop, we first generate the next value, then increment the position. The order matters. If we incremented before generating the next state, the stored indices would shift incorrectly and produce off-by-one errors in the cycle length.
The moment we encounter a previously seen value, we subtract its first occurrence position from the current position. That difference is exactly the cycle length.
The loop is guaranteed to terminate because there are only m distinct states.
Worked Examples
Example 1
Input:
2 6 12 11
Generated sequence:
11 -> 4 -> 2 -> 10 -> 2 -> ...
| Position | Current Value | First Seen Before? | Action |
|---|---|---|---|
| 0 | 11 | No | store 11 at 0 |
| 1 | 4 | No | store 4 at 1 |
| 2 | 2 | No | store 2 at 2 |
| 3 | 10 | No | store 10 at 3 |
| 4 | 2 | Yes, at 2 | answer = 4 - 2 = 2 |
The repeated value is 2. Its first occurrence was at position 2, and it appears again at position 4, so the period is 2.
Example 2
Input:
1 3 5 2
Generated sequence:
2 -> 0 -> 3 -> 1 -> 4 -> 2 -> ...
| Position | Current Value | First Seen Before? | Action |
|---|---|---|---|
| 0 | 2 | No | store 2 at 0 |
| 1 | 0 | No | store 0 at 1 |
| 2 | 3 | No | store 3 at 2 |
| 3 | 1 | No | store 1 at 3 |
| 4 | 4 | No | store 4 at 4 |
| 5 | 2 | Yes, at 0 | answer = 5 - 0 = 5 |
This trace shows a cycle that starts immediately. Every value in modulo 5 appears exactly once before repetition.
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(m) | each state is processed at most once |
| Space | O(m) | stores first occurrence for each possible remainder |
With m ≤ 100000, the algorithm performs at most one iteration per possible state. Both the runtime and memory usage comfortably fit within the limits.
Test Cases
# helper: run solution on input string, return output string
import sys, io
def solve():
input = sys.stdin.readline
a, b, m, r = map(int, input().split())
first_seen = [-1] * m
first_seen[r] = 0
pos = 0
while True:
r = (a * r + b) % m
pos += 1
if first_seen[r] != -1:
print(pos - first_seen[r])
return
first_seen[r] = pos
def run(inp: str) -> str:
backup_stdin = sys.stdin
backup_stdout = sys.stdout
sys.stdin = io.StringIO(inp)
sys.stdout = io.StringIO()
solve()
output = sys.stdout.getvalue()
sys.stdin = backup_stdin
sys.stdout = backup_stdout
return output.strip()
# provided sample
assert run("2 6 12 11\n") == "2", "sample 1"
# minimum-size input
assert run("0 0 1 0\n") == "1", "single state"
# cycle starts immediately
assert run("1 1 5 0\n") == "5", "full modulo cycle"
# fixed point after one step
assert run("0 0 10 7\n") == "1", "constant sequence"
# off-by-one detection
assert run("2 0 7 1\n") == "3", "cycle length should be 3"
| Test input | Expected output | What it validates |
|---|---|---|
0 0 1 0 |
1 |
smallest possible state space |
1 1 5 0 |
5 |
cycle begins immediately |
0 0 10 7 |
1 |
fixed-point cycle after preperiod |
2 0 7 1 |
3 |
catches index-shift mistakes |
Edge Cases
Consider the input:
0 0 10 7
The generated sequence is:
7 -> 0 -> 0 -> 0 -> ...
Trace:
| Position | Value |
|---|---|
| 0 | 7 |
| 1 | 0 |
| 2 | 0 |
The value 0 first appears at position 1 and repeats at position 2. The algorithm returns 2 - 1 = 1, which is correct. The initial value 7 belongs to the preperiod and is not part of the cycle.
Now consider:
1 1 5 0
The sequence becomes:
0 -> 1 -> 2 -> 3 -> 4 -> 0
Trace:
| Position | Value |
|---|---|
| 0 | 0 |
| 1 | 1 |
| 2 | 2 |
| 3 | 3 |
| 4 | 4 |
| 5 | 0 |
The repeated value 0 was first seen at position 0, so the algorithm returns 5. This confirms that cycles beginning immediately are handled correctly.
Finally, consider:
5 3 100 15
Suppose the sequence reaches:
78 -> 78 -> 78 -> ...
The first repetition occurs one step later, so the algorithm computes:
current_position - first_seen[78] = 1
A self-loop is still a valid cycle, and the algorithm naturally handles it without special cases.