When I first encountered this problem on LeetCode, I did what most developers do - jumped straight to the naive solution. What followed was a journey through increasingly elegant optimizations, each revealing deeper insights about algorithmic thinking. This documents that evolution from a working-but-slow O(n³) solution to an optimal O(n) algorithm.
the problem
Given an array of integers, find the maximum value of k for which there exist two adjacent subarrays of length k each, where both subarrays are strictly increasing.
Constraints:
- Both subarrays must be strictly increasing
- The subarrays must be adjacent
- Array length can be up to 200,000 elements
Example:
nums = [2,5,7,8,9,2,3,4,3,1] output = 3 The subarray [7,8,9] is strictly increasing The subarray [2,3,4] is strictly increasing They are adjacent, and k=3 is the maximumthe naive approach
The straightforward solution: try every possible value of k, check every possible window, and verify if both halves are increasing.
class Solution { public: bool isStrictlyIncreasing(vector<int>& nums, int start, int length) { for (int i = start + 1; i < start + length; i++) { if (nums[i] <= nums[i-1]) return false; } return true; } int maxIncreasingSubarrays(vector<int>& nums) { int n = nums.size(); for (int k = n/2; k >= 1; k--) { for (int i = 0; i <= n - 2*k; i++) { if (isStrictlyIncreasing(nums, i, k) && isStrictlyIncreasing(nums, i + k, k)) { return k; } } } return 0; } };Time complexity: O(n² × k) ≈ O(n³) Space complexity: O(1)
With n = 200,000, this times out immediately.
first optimization: precomputation
The naive solution repeatedly checks if subarrays are increasing. The key insight: precompute how long each increasing sequence is.
run length array
nums = [2, 5, 7, 8, 9, 2, 3, 4, 3, 1] runLength = [1, 2, 3, 4, 5, 1, 2, 3, 1, 1]At index 4, runLength[4] = 5 means there's an increasing sequence of length 5 ending at position 4.
class Solution { public: int maxIncreasingSubarrays(vector<int>& nums) { int n = nums.size(); vector<int> runLength(n, 1); for (int i = 1; i < n; i++) { if (nums[i] > nums[i-1]) { runLength[i] = runLength[i-1] + 1; } } int maxK = 0; for (int i = 0; i < n - 1; i++) { for (int k = 1; k <= (n - i) / 2; k++) { if (runLength[i + k - 1] >= k && runLength[i + 2*k - 1] >= k) { maxK = max(maxK, k); } } } return maxK; } };Time complexity: O(n²) Space complexity: O(n)
Better, but still quadratic. With n = 200,000, we're at 40 billion operations.
the optimal solution
The breakthrough: at any point in the array, there are exactly two ways to form adjacent increasing subarrays.
scenario 1: bridge two separate runs
Previous Run Current Run [...........] [break] [...........] length=A length=B maximum k = min(A, B)scenario 2: split a single run
One Long Run [........................] length=L maximum k = L / 2the algorithm
class Solution { public: int maxIncreasingSubarrays(vector<int>& nums) { int n = nums.size(); int currentRunLength = 1; int previousRunLength = 0; int maxK = 0; for (int i = 1; i < n; i++) { if (nums[i] > nums[i - 1]) { currentRunLength++; } else { previousRunLength = currentRunLength; currentRunLength = 1; } int kFromTwoRuns = min(previousRunLength, currentRunLength); int kFromSplittingRun = currentRunLength / 2; maxK = max({maxK, kFromTwoRuns, kFromSplittingRun}); } return maxK; } };Time complexity: O(n) Space complexity: O(1)
Single pass, constant space.
the visualization
current state
maximum k
what's happening
click next step to begin
algorithm complete
complexity comparison
| approach | time | space |
|---|---|---|
| naive | O(n³) | O(1) |
| precomputation | O(n²) | O(n) |
| optimal | O(n) | O(1) |
lessons learned
start simple - The naive solution helped understand the problem deeply.
precomputation is powerful - When repeating calculations, precompute them.
state tracking enables single passes - Track just enough state to make decisions without looking back.
pattern recognition matters - Recognizing there are exactly two ways to form the answer unlocks the O(n) solution.
This problem taught me that optimization isn't just about making code faster - it's about understanding the problem structure deeply enough to see elegant solutions. The journey from O(n³) to O(n) wasn't just about removing loops; it was about recognizing fundamental patterns in the problem itself.