CF 106189D - An oscillating subsequence
We are given an integer sequence and may delete any elements while preserving the order of the remaining ones. The goal is to keep as many elements as possible so that in the resulting subsequence every internal element is a strict peak or a strict valley.
CF 106189D - An oscillating subsequence
Rating: -
Tags: -
Solve time: 39s
Verified: yes
Solution
Problem Understanding
We are given an integer sequence and may delete any elements while preserving the order of the remaining ones.
The goal is to keep as many elements as possible so that in the resulting subsequence every internal element is a strict peak or a strict valley. For every position inside the subsequence, its value must be either larger than both neighbors or smaller than both neighbors. In other words, the differences between consecutive elements must alternate in sign.
The output is not the subsequence itself. We must print the indices of the chosen elements in the original array. If no oscillating subsequence of length at least three exists, we print 0.
The array length can reach 50,000. A quadratic dynamic programming solution would require roughly 2.5 billion comparisons in the worst case, which is far beyond the available time. Anything around O(n²) is ruled out immediately. A linear or O(n log n) solution is required.
The tricky cases come from equal values and long monotone runs.
Consider:
3
1 1 2
The correct answer is 0. A careless implementation that only checks increasing versus decreasing might incorrectly treat the first two values as a valid direction change.
Consider:
5
1 2 3 4 5
Every subsequence is monotone. No oscillation of length at least three exists, so the answer is 0.
Consider:
5
1 5 3 4 2
The whole sequence already alternates:
1 < 5 > 3 < 4 > 2
The answer contains all five indices.
Consider:
6
1 4 7 5 2 6
The values 4 and 5 are not both needed. The longest oscillating subsequence keeps only the turning points:
1 < 7 > 2 < 6
A naive strategy that keeps every change of value would not produce a valid oscillation.
Approaches
The brute-force idea is dynamic programming.
For every position and every previous position, we could try to extend a subsequence while remembering whether the last step was increasing or decreasing. This correctly finds the longest oscillating subsequence, but it requires examining all pairs of indices. With n = 50,000, the worst-case work is on the order of 50,000², which is completely infeasible.
The key observation is that inside a strictly increasing run, only the last element matters. Suppose we currently have:
... < 3 < 5 < 8
If we want a future decrease, keeping 8 is always at least as good as keeping 5 or 3, because 8 gives more freedom for the next downward step.
The same reasoning applies to decreasing runs. Inside
... > 10 > 7 > 4
only the smallest endpoint is useful.
This means every monotone segment can be compressed to its endpoint. The elements that survive are exactly the turning points, peaks and valleys. A greedy algorithm can maintain this online.
We build the answer indices from left to right.
Whenever the last three considered values are monotone:
x ≤ y ≤ z
or
x ≥ y ≥ z
the middle value y can never be a peak or valley in an optimal oscillating subsequence. We remove it and keep the more extreme endpoint.
After processing the whole array, the remaining indices form a maximum-length oscillating subsequence. This is the same greedy principle used in the classical wiggle subsequence problem.
| Approach | Time Complexity | Space Complexity | Verdict |
|---|---|---|---|
| Brute Force DP | O(n²) | O(n²) or O(n) | Too slow |
| Greedy Turning Points | O(n) | O(n) | Accepted |
Algorithm Walkthrough
- Start the answer list with index
0. - Process indices from left to right.
- For the current index
i, check the last two selected indices andi. - Let their values be
x,y, andz. - If
x ≤ y ≤ z, thenylies inside a non-decreasing segment. Removeyand replace it withi. - If
x ≥ y ≥ z, thenylies inside a non-increasing segment. Removeyand replace it withi. - Otherwise, the direction changes between
yandz. The pointybecomes a peak or valley, so appendi. - After all indices are processed, the stored indices describe a longest oscillating subsequence.
- If fewer than three indices remain, print
0. Otherwise print the length and the indices.
Why it works
The invariant is that after processing any prefix of the array, the stored indices form the longest possible oscillating subsequence ending inside that prefix.
Whenever three consecutive candidate values satisfy
x ≤ y ≤ z
the middle value is never useful. Any future element that can follow y can also follow z, and z is at least as extreme. Replacing y with z cannot reduce the maximum achievable oscillating length.
The same argument holds for
x ≥ y ≥ z
where the newest value is at least as useful as the middle one.
As a result, every removed element is provably redundant. Every kept internal element corresponds to a genuine direction change, making it a peak or valley. Since only redundant points are discarded, the resulting subsequence has maximum possible length.
Python Solution
import sys
input = sys.stdin.readline
n = int(input())
a = list(map(int, input().split()))
ans = [0]
for i in range(1, n):
while len(ans) >= 2:
x = a[ans[-2]]
y = a[ans[-1]]
z = a[i]
if (x <= y <= z) or (x >= y >= z):
ans.pop()
else:
break
ans.append(i)
if len(ans) < 3:
print(0)
else:
print(len(ans))
print(*ans)
The list ans stores indices, not values, because the problem asks for positions in the original sequence.
The while loop is the heart of the greedy strategy. If the newest value continues the current monotone trend, the previous endpoint is no longer useful and is removed.
Using a while instead of a single if is important. After removing one index, a new triple is formed and may also be monotone. The process continues until the last selected point becomes a genuine turning point or only one previous point remains.
Equal values are handled naturally by the conditions
x <= y <= z
x >= y >= z
This prevents flat segments from creating fake oscillations.
Every index is inserted once and removed at most once, so the total running time remains linear.
Worked Examples
Example 1
Input:
5
1 4 7 5 2
| Current index | Value | Stored indices | Stored values |
|---|---|---|---|
| 0 | 1 | [0] | [1] |
| 1 | 4 | [0,1] | [1,4] |
| 2 | 7 | [0,2] | [1,7] |
| 3 | 5 | [0,2,3] | [1,7,5] |
| 4 | 2 | [0,2,4] | [1,7,2] |
The value 4 disappears because it lies inside an increasing run. Later, 5 disappears because it lies inside a decreasing run. The surviving elements are exactly the turning points.
Example 2
Input:
6
1 3 2 4 3 5
| Current index | Value | Stored indices | Stored values |
|---|---|---|---|
| 0 | 1 | [0] | [1] |
| 1 | 3 | [0,1] | [1,3] |
| 2 | 2 | [0,1,2] | [1,3,2] |
| 3 | 4 | [0,1,2,3] | [1,3,2,4] |
| 4 | 3 | [0,1,2,3,4] | [1,3,2,4,3] |
| 5 | 5 | [0,1,2,3,4,5] | [1,3,2,4,3,5] |
Every internal element is already a peak or valley, so nothing is removed.
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n) | Every index is pushed once and popped at most once |
| Space | O(n) | The answer indices are stored |
With n = 50,000, a linear scan is easily fast enough for the given limits.
Test Cases
# helper: run solution on input string, return output string
import sys
import io
def run(inp: str) -> str:
sys.stdin = io.StringIO(inp)
n = int(sys.stdin.readline())
a = list(map(int, sys.stdin.readline().split()))
ans = [0]
for i in range(1, n):
while len(ans) >= 2:
x = a[ans[-2]]
y = a[ans[-1]]
z = a[i]
if (x <= y <= z) or (x >= y >= z):
ans.pop()
else:
break
ans.append(i)
out = []
if len(ans) < 3:
out.append("0")
else:
out.append(str(len(ans)))
out.append(" ".join(map(str, ans)))
return "\n".join(out)
# sample-like cases
assert run("3\n1 3 2\n") == "3\n0 1 2"
assert run("3\n1 2 3\n") == "0"
# minimum size
assert run("1\n5\n") == "0"
# all equal
assert run("5\n7 7 7 7 7\n") == "0"
# already oscillating
assert run("5\n1 5 2 6 3\n") == "5\n0 1 2 3 4"
# long monotone run compressed to endpoints
assert run("6\n1 4 7 5 2 6\n") == "4\n0 2 4 5"
| Test input | Expected output | What it validates |
|---|---|---|
1 / 5 |
0 |
Minimum size |
7 7 7 7 7 |
0 |
Equal values do not create oscillations |
1 5 2 6 3 |
All indices | Already optimal sequence |
1 4 7 5 2 6 |
Turning points only | Compression of monotone segments |
Edge Cases
Consider:
3
1 1 2
Processing 2 creates the pattern:
1 ≤ 1 ≤ 2
The middle index is removed. The final index set becomes:
[0, 2]
Its length is only two, so the algorithm prints 0. Equal values never generate a false peak or valley.
Consider:
3
1 2 3
The pattern is always increasing:
1 ≤ 2 ≤ 3
The middle point is discarded and only endpoints remain. Again the final length is two, so the answer is 0.
Consider:
6
5 4 3 2 1 0
Every new value continues the same decreasing trend. The algorithm repeatedly removes the previous endpoint and keeps the newest one. The final subsequence contains only the first and last indices, which is correct because no oscillation of length at least three exists.
Consider:
5
1 3 2 4 1
Every internal element is already a strict peak or valley:
1 < 3 > 2 < 4 > 1
None of the monotonicity conditions trigger, all indices remain, and the algorithm returns the full sequence.