Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions Algorithms/slidingWindow/longestUnique.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from collections import OrderedDict
from pydantic import BaseModel, Field
from typing import List

class SlidingWindowParam(BaseModel):
s: str = Field(..., description="Input string", min_length=1)


def lengthOfLongestSubstringTwoDistinct(s: SlidingWindowParam) -> int:
seen = OrderedDict()
start = 0
longest = 0
for end in range(len(s)):
if s[end] in seen:
seen.move_to_end(s[end])
seen[s[end]] = end
if len(seen) > 2:
_, idx = seen.popitem(last=False)
start = idx+1
longest = max(longest, end-start+1)
return longest

def maximumUniqueSubarray(nums: List[int]) -> int:
last_seen = dict()
start, curr_sum, max_sum = 0, 0, 0
for end in range(len(nums)):
if nums[end] in last_seen:
last_idx = last_seen[nums[end]]
if last_idx >= start:
curr_sum -= sum(nums[start:(last_idx+1)])
start = last_idx + 1
curr_sum += nums[end]
max_sum = max(max_sum, curr_sum)
last_seen[nums[end]] = end
return max_sum
164 changes: 164 additions & 0 deletions tests/test_longest_substring.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import pytest
from pydantic import ValidationError
from Algorithms.slidingWindow.longestUnique import lengthOfLongestSubstringTwoDistinct, maximumUniqueSubarray, SlidingWindowParam

# -------------------------
# Basic correctness tests
# -------------------------

@pytest.mark.parametrize(
"input_str, expected",
[
("eceba", 3),
("ccaabbb", 5),
("ababffzzeee", 5),
("aa", 2),
("a", 1),
("ab", 2),
("abc", 2),
],
)
def test_basic_cases(input_str, expected):
param = SlidingWindowParam(s=input_str)
assert lengthOfLongestSubstringTwoDistinct(param.s) == expected

@pytest.mark.parametrize(
"nums, expected",
[
([4, 2, 4, 5, 6], 17), # example 1
([5, 2, 1, 2, 5, 2, 1, 2, 5], 8), # example 2
([1], 1),
([1, 2], 3),
([1, 1], 1),
([1, 2, 3], 6),
([3, 3, 3], 3),
],
)
def test_basic_cases(nums, expected):
assert maximumUniqueSubarray(nums) == expected

# -------------------------
# Regression / tricky cases
# -------------------------

def test_alternating_characters():
param = SlidingWindowParam(s="abababab")
assert lengthOfLongestSubstringTwoDistinct(param.s) == 8


def test_three_distinct_repeated():
param = SlidingWindowParam(s="abcabcabc")
assert lengthOfLongestSubstringTwoDistinct(param.s) == 2


def test_eviction_order_matters():
# Breaks implementations that don't move_to_end
param = SlidingWindowParam(s="abaccc")
# Longest valid substring: "accc"
assert lengthOfLongestSubstringTwoDistinct(param.s) == 4


def test_multiple_evictions():
param = SlidingWindowParam(s="aabbccddeeff")
assert lengthOfLongestSubstringTwoDistinct(param.s) == 4

def test_duplicate_at_start():
# Forces window to jump start
nums = [2, 1, 2, 3, 4]
# Best: [1,2,3,4]
assert maximumUniqueSubarray(nums) == 10


def test_duplicate_at_end():
nums = [1, 2, 3, 4, 1]
# Best: [1,2,3,4]
assert maximumUniqueSubarray(nums) == 10


def test_multiple_duplicates():
nums = [1, 2, 1, 3, 2, 3, 4]
# Best: [1,3,2,3,4] is invalid
# Best valid: [1,3,2] or [2,3,4]
assert maximumUniqueSubarray(nums) == 9


def test_eviction_jump_needed():
# Breaks incorrect start updates
nums = [1, 2, 3, 2, 5]
# Best: [3,2,5]
assert maximumUniqueSubarray(nums) == 10

# -------------------------
# Edge cases
# -------------------------

def test_all_same_character():
param = SlidingWindowParam(s="aaaaaaa")
assert lengthOfLongestSubstringTwoDistinct(param.s) == 7


def test_two_characters_only():
param = SlidingWindowParam(s="abababababab")
assert lengthOfLongestSubstringTwoDistinct(param.s) == 12

def test_all_unique():
nums = [1, 2, 3, 4, 5, 6]
assert maximumUniqueSubarray(nums) == sum(nums)


def test_all_same():
nums = [7, 7, 7, 7]
assert maximumUniqueSubarray(nums) == 7


def test_large_values():
nums = [10_000, 9_999, 10_000]
assert maximumUniqueSubarray(nums) == 19_999

def test_strictly_increasing_then_repeat():
nums = list(range(1, 100)) + [50]
assert maximumUniqueSubarray(nums) == sum(range(1, 100))


def test_alternating_pattern():
nums = [1, 2] * 50
assert maximumUniqueSubarray(nums) == 3 # [1,2]

# -------------------------
# Pydantic validation tests
# -------------------------

def test_empty_string_rejected():
with pytest.raises(ValidationError):
SlidingWindowParam(s="")


def test_non_string_input():
with pytest.raises(ValidationError):
SlidingWindowParam(s=123)


# -------------------------
# Invariant-style tests
# -------------------------

@pytest.mark.parametrize("s", [
"abc",
"aabcbcdbca",
"xyzzzyx",
"pwwkew",
])
def test_result_never_exceeds_string_length(s):
param = SlidingWindowParam(s=s)
result = lengthOfLongestSubstringTwoDistinct(param.s)
assert 0 < result <= len(s)

@pytest.mark.parametrize("nums", [
[1, 2, 3],
[1, 2, 1, 2, 3],
[5, 5, 5, 1, 2],
[100, 200, 300, 100, 400],
])
def test_result_never_exceeds_sum(nums):
result = maximumUniqueSubarray(nums)
assert 0 < result <= sum(nums)