##### Question
Given a list of dependency versions (e.g. [103.003.02, 103.003.03, 203.003.02]) and a black-box API isSupported(v), design an algorithm to find the earliest (lowest) version that supports a target feature.
2) Versions follow {major}.{minor}.{patch}. Support is not monotonic: a higher version may drop support, but it is guaranteed that some later version will support again. The API is rate-limited, so total calls must be sub-linear to the number of versions. Devise a strategy—e.g., group by latest patch per major, binary-search majors, then minors, then patches—to minimize API usage while reliably returning the earliest supporting version.
Quick Answer: This question evaluates a candidate's ability to design efficient search algorithms and reason about algorithmic complexity under constraints such as non-monotonic predicate behavior and a rate-limited black-box API.
Find the **earliest dependency version** that supports a target feature, using as few API probes as possible.
## What to implement
```
solution(versions, supported_versions)
```
- **`versions`** — a list of distinct version strings, each in the form `"major.minor.patch"` (numeric components, possibly zero-padded, e.g. `"104.000.01"`). The list is already sorted in ascending semantic-version order.
- **`supported_versions`** — a list that simulates a black-box API `isSupported(v)`: a version supports the feature **if and only if** it appears in `supported_versions`. This list is always a subset of `versions`.
Return the **earliest** (smallest in the sorted order) version string that supports the feature, or **`None`** if no version supports it.
## Why this isn't a plain search
Support is **not monotonic** across the full version list: an early version may support the feature, a later version (in a new major or minor) may not, and an even later one may support again. So you cannot simply scan or binary-search the whole list directly.
## Structure guarantees (these make a sub-linear API strategy possible)
The data obeys a three-level hierarchical pattern:
1. **Major level.** If any version supports the feature, there is a **first supporting major**, and every major after it also contains at least one supporting version.
2. **Minor level.** Inside that first supporting major, there is a **first supporting minor**, and every later minor within that major also contains at least one supporting version.
3. **Patch level.** Inside that first supporting major/minor pair, support is **monotonic by patch** among the listed versions — once support starts, it continues.
Additionally, **if a major or a minor contains any supporting version, then its latest listed version is guaranteed to be supporting.** This lets you probe a single representative version per major and per minor.
The earliest supporting version is therefore the first supporting patch inside the first supporting minor inside the first supporting major.
## Constraints
- `0 <= len(versions) <= 200000`
- `versions` is sorted ascending, all versions are distinct, and `supported_versions` is a subset of `versions`.
- The support pattern always obeys the hierarchical guarantees above: if support exists at all, a first supporting major exists; within it a first supporting minor exists; within it a first supporting patch exists; and the latest listed version of any supporting major or minor is also supporting.
## Examples
- `versions = ["103.003.01", "103.003.02", "103.004.01", "104.000.01", "104.001.01", "104.001.02", "105.000.01"]`, `supported_versions = ["103.004.01", "104.001.02", "105.000.01"]` → `"103.004.01"`
- `versions = ["101.000.01", "101.001.01", "102.000.01", "102.000.02", "102.001.01", "103.000.01"]`, `supported_versions = ["102.000.02", "102.001.01", "103.000.01"]` → `"102.000.02"`
- `versions = ["200.000.01"]`, `supported_versions = ["200.000.01"]` → `"200.000.01"`
- `versions = ["300.000.01", "300.000.02"]`, `supported_versions = []` → `None`
- `versions = []`, `supported_versions = []` → `None`
Constraints
- 0 <= len(versions) <= 200000
- versions is sorted in ascending semantic-version order, all versions are distinct, and supported_versions is a subset of versions
- The support pattern obeys the hierarchical guarantees described above: a first supporting major exists if support exists at all; within it, a first supporting minor exists; within it, a first supporting patch exists; and the latest listed version of any supporting major or minor is also supporting
Examples
Input: (["103.003.01", "103.003.02", "103.004.01", "104.000.01", "104.001.01", "104.001.02", "105.000.01"], ["103.004.01", "104.001.02", "105.000.01"])
Expected Output: "103.004.01"
Explanation: Major 103 is the first major whose latest version supports the feature. Inside it, minor 004 is the first supporting minor, and its only listed patch is the answer.
Input: (["101.000.01", "101.001.01", "102.000.01", "102.000.02", "102.001.01", "103.000.01"], ["102.000.02", "102.001.01", "103.000.01"])
Expected Output: "102.000.02"
Explanation: No version in major 101 supports the feature. In major 102, the first supporting minor is 000, and within that minor the first supporting patch is 02.
Input: (["200.000.01"], ["200.000.01"])
Expected Output: "200.000.01"
Explanation: The only version is supported, so it is also the earliest supporting version.
Input: (["300.000.01", "300.000.02"], [])
Expected Output: None
Explanation: No version supports the feature, so the correct return value is None.
Input: ([], [])
Expected Output: None
Explanation: An empty version list contains no supporting version.
Solution
def solution(versions, supported_versions):
if not versions:
return None
supported = set(supported_versions)
major_order = []
major_map = {}
for v in versions:
major_str, minor_str, _ = v.split('.')
major = int(major_str)
minor = int(minor_str)
if major not in major_map:
major_order.append(major)
major_map[major] = {'latest': v, 'minor_order': [], 'minors': {}}
major_info = major_map[major]
major_info['latest'] = v
if minor not in major_info['minors']:
major_info['minor_order'].append(minor)
major_info['minors'][minor] = {'latest': v, 'versions': []}
minor_info = major_info['minors'][minor]
minor_info['latest'] = v
minor_info['versions'].append(v)
def first_true(items, predicate):
lo, hi = 0, len(items) - 1
answer = None
while lo <= hi:
mid = (lo + hi) // 2
item = items[mid]
if predicate(item):
answer = item
hi = mid - 1
else:
lo = mid + 1
return answer
first_major = first_true(major_order, lambda major: major_map[major]['latest'] in supported)
if first_major is None:
return None
major_info = major_map[first_major]
first_minor = first_true(major_info['minor_order'], lambda minor: major_info['minors'][minor]['latest'] in supported)
if first_minor is None:
return None
patch_versions = major_info['minors'][first_minor]['versions']
lo, hi = 0, len(patch_versions) - 1
answer = None
while lo <= hi:
mid = (lo + hi) // 2
version = patch_versions[mid]
if version in supported:
answer = version
hi = mid - 1
else:
lo = mid + 1
return answerExplanation
The data hides a feature behind three nested layers, and within each layer the supporting versions form a *suffix* (once support starts, it never stops). So the solution does three binary searches, one per layer.
**Step 1 — bucket the versions.** A single linear pass splits every `"major.minor.patch"` string and groups versions into a nested map:
- `major_order` / `major_map[major]` — ordered list of distinct majors, each tracking its `latest` listed version, its `minor_order`, and its minors.
- each minor tracks its own `latest` version and the ordered `versions` list for its patches.
Because input is already sorted, `latest` ends up holding the highest version seen in each bucket.
**Step 2 — find the first supporting major.** `first_true` is a classic "leftmost true" binary search. The key guarantee: *if a major contains any supporting version, its latest listed version is supporting*, and supporting majors form a suffix. So testing `major_map[major]['latest'] in supported` is monotonic across `major_order`, and the binary search finds the earliest qualifying major.
**Step 3 — find the first supporting minor** inside that major using the same `latest in supported` predicate over `minor_order`.
**Step 4 — find the first supporting patch.** Within the chosen major/minor, support is monotonic by patch, so a plain leftmost binary search over `patch_versions` returns the earliest supported version.
If any layer has no supporting bucket, the search returns `None`. The empty-input and all-`None` cases short-circuit correctly. Correctness rests entirely on the problem's suffix/monotonicity guarantees, which make each `in supported` probe a valid binary-search predicate.
Time complexity: O(n). Space complexity: O(n).
Hints
- You do not need to probe every version. The latest version inside a major tells you whether that major contains any supporting version.
- After finding the first supporting major, repeat the same idea for minors, then binary search patches inside the chosen minor.