Merge Call-Recording Labels from Multiple Labelers by Majority Vote
Company: Nooks
Role: Software Engineer
Category: Coding & Algorithms
Difficulty: medium
Interview Round: Technical Screen
A team labels phone-call recordings. Every recording is exactly 100 seconds long, and each of `N` labelers has independently split the same recording into segments, giving every segment one of only three labels: `human`, `machine` or `silence`. Combine the labelers' work into a single labeling of the recording by consensus: at every moment, the final label is the one that more than half of the labelers gave to that moment. The result must also cover the full 100 seconds. In the original problem, a stretch of time on which no label reaches consensus raises an error; in this console version the function returns an empty list instead.
### Function Signature
```python
def consensus_labels(labelers: list[list[list[str]]]) -> list[list[str]]:
```
### Rules
- `labelers[i]` is labeler `i`'s segmentation: a list of segments `[start, end, label]`, where `start` and `end` are times in seconds written as decimal strings (for example `"10.5"`) and `label` is `"human"`, `"machine"` or `"silence"`. A segment covers the half-open interval `[start, end)`.
- Times are compared by numeric value, not as strings.
- At a time `t`, labeler `i` votes for the label of its segment that contains `t`. A label has consensus at `t` when strictly more than `N / 2` labelers vote for it, where `N = len(labelers)`. For `N = 3` that means at least 2 votes; for `N = 4`, at least 3.
- Return a list of segments `[start, end, label]` in increasing time order that covers `[0.0, 100.0)` with no gaps or overlaps, such that at every time the covering segment's label is the consensus label at that time, and no two adjacent segments have the same label (adjacent stretches with the same consensus label form one segment).
- Write every time in the result exactly as it is spelled in the input. Each time value has only one spelling (see Constraints), so the result is unique.
- If there is any stretch of positive length on which no label has consensus, return `[]` instead. A successful result is never empty, so `[]` stands in for the error the original problem raises.
### Constraints
- `1 <= N = len(labelers) <= 100`
- `1 <= len(labelers[i]) <= 1000` for every `i`
- Every time string has an integer part with no leading zeros (a lone `"0"` is allowed), a decimal point, and exactly one digit after it, such as `"0.0"`, `"9.0"`, `"10.5"` or `"100.0"`. Every value lies in `[0.0, 100.0]`.
- Within one labeler's list, segments are in increasing time order: the first starts at `"0.0"`, the last ends at `"100.0"`, each segment starts exactly where the previous one ends, and every segment has `start < end`.
- Two consecutive segments of the same labeler may carry the same label.
- Labels are exactly `"human"`, `"machine"` or `"silence"` (lowercase).
### Examples
**Example 1**
```text
Input: labelers = [
[["0.0", "10.5", "human"], ["10.5", "20.0", "machine"], ["20.0", "100.0", "silence"]],
[["0.0", "9.0", "human"], ["9.0", "20.0", "silence"], ["20.0", "100.0", "human"]],
[["0.0", "20.0", "silence"], ["20.0", "100.0", "human"]]
]
Output: [["0.0", "9.0", "human"], ["9.0", "20.0", "silence"], ["20.0", "100.0", "human"]]
```
With `N = 3`, a label needs at least 2 votes. On `[0.0, 9.0)` the votes are human, human, silence. On `[9.0, 10.5)` they are human, silence, silence, and on `[10.5, 20.0)` machine, silence, silence, so silence holds all of `[9.0, 20.0)` as one segment. On `[20.0, 100.0)` they are silence, human, human.
**Example 2**
```text
Input: labelers = [
[["0.0", "50.0", "human"], ["50.0", "100.0", "silence"]],
[["0.0", "22.5", "human"], ["22.5", "47.5", "human"], ["47.5", "100.0", "silence"]],
[["0.0", "50.0", "human"], ["50.0", "100.0", "machine"]],
[["0.0", "8.5", "machine"], ["8.5", "50.0", "human"], ["50.0", "100.0", "silence"]]
]
Output: [["0.0", "50.0", "human"], ["50.0", "100.0", "silence"]]
```
With `N = 4`, a label needs at least 3 votes. The votes are human, human, human, machine on `[0.0, 8.5)`; all four human on `[8.5, 47.5)`; human, silence, human, human on `[47.5, 50.0)`; and silence, silence, machine, silence on `[50.0, 100.0)`.
**Example 3**
```text
Input: labelers = [
[["0.0", "40.0", "human"], ["40.0", "100.0", "machine"]],
[["0.0", "60.0", "human"], ["60.0", "100.0", "silence"]],
[["0.0", "50.0", "silence"], ["50.0", "100.0", "machine"]]
]
Output: []
```
On `[40.0, 50.0)` the votes are machine, human, silence, so no label reaches 2 votes. Every other stretch has consensus, but one stretch without it is enough to return `[]`.
Overview: Given several labelers' segmentations of a 100-second call recording into human, machine and silence stretches, build one consensus timeline in which each label needs strictly more than half of the votes. Tests sweeping over unaligned interval boundaries, merging adjacent equal labels and detecting stretches with no majority.
Read the full Nooks Software Engineer interview experience this question came from
A team labels phone-call recordings. Every recording is exactly 100 seconds long, and each of `N` labelers has independently split the same recording into segments, giving every segment one of only three labels: `"human"`, `"machine"` or `"silence"`. Combine the labelers' work into a single labeling of the recording by consensus: at every moment, the final label is the one that strictly more than half of the labelers gave to that moment. The result must cover the full 100 seconds.
Implement `consensus_labels(labelers)`:
- `labelers[i]` is labeler `i`'s segmentation: a list of segments `[start, end, label]`, where `start` and `end` are times in seconds written as decimal strings (for example `"10.5"`) and `label` is `"human"`, `"machine"` or `"silence"`. A segment covers the half-open interval `[start, end)`.
- Times are compared by numeric value, not as strings.
- At a time `t`, labeler `i` votes for the label of its segment that contains `t`. A label has consensus at `t` when strictly more than `N / 2` labelers vote for it, where `N = len(labelers)`. For `N = 3` that means at least 2 votes; for `N = 4`, at least 3.
- Return a list of segments `[start, end, label]` in increasing time order that covers `[0.0, 100.0)` with no gaps or overlaps, such that at every time the covering segment's label is the consensus label at that time, and no two adjacent segments have the same label (adjacent stretches with the same consensus label form one segment).
- Write every time in the result exactly as it is spelled in the input. Each time value has only one spelling (see Constraints), so the result is unique.
- If there is any stretch of positive length on which no label has consensus, return `[]` instead. A successful result is never empty, so `[]` stands in for the error that a stretch without consensus would otherwise raise.
All times are short decimal strings and every count is at most 100, so no value comes anywhere near 2^31 - 1.
### Example 1
```text
Input: labelers = [
[["0.0", "10.5", "human"], ["10.5", "20.0", "machine"], ["20.0", "100.0", "silence"]],
[["0.0", "9.0", "human"], ["9.0", "20.0", "silence"], ["20.0", "100.0", "human"]],
[["0.0", "20.0", "silence"], ["20.0", "100.0", "human"]]
]
Output: [["0.0", "9.0", "human"], ["9.0", "20.0", "silence"], ["20.0", "100.0", "human"]]
```
With `N = 3`, a label needs at least 2 votes. On `[0.0, 9.0)` the votes are human, human, silence. On `[9.0, 10.5)` they are human, silence, silence, and on `[10.5, 20.0)` machine, silence, silence, so silence holds all of `[9.0, 20.0)` as one segment. On `[20.0, 100.0)` they are silence, human, human.
### Example 2
```text
Input: labelers = [
[["0.0", "40.0", "human"], ["40.0", "100.0", "machine"]],
[["0.0", "60.0", "human"], ["60.0", "100.0", "silence"]],
[["0.0", "50.0", "silence"], ["50.0", "100.0", "machine"]]
]
Output: []
```
On `[40.0, 50.0)` the votes are machine, human, silence, so no label reaches 2 votes. Every other stretch has consensus, but one stretch without it is enough to return `[]`.
### Constraints
- `1 <= N = len(labelers) <= 100`
- `1 <= len(labelers[i]) <= 1000` for every `i`
- Every time string has an integer part with no leading zeros (a lone `"0"` is allowed), a decimal point, and exactly one digit after it, such as `"0.0"`, `"9.0"`, `"10.5"` or `"100.0"`. Every value lies in `[0.0, 100.0]`.
- Within one labeler's list, segments are in increasing time order: the first starts at `"0.0"`, the last ends at `"100.0"`, each segment starts exactly where the previous one ends, and every segment has `start < end`.
- Two consecutive segments of the same labeler may carry the same label.
- Labels are exactly `"human"`, `"machine"` or `"silence"` (lowercase).
Constraints
- 1 <= N = len(labelers) <= 100
- 1 <= len(labelers[i]) <= 1000 for every i
- Every time string has an integer part with no leading zeros (a lone "0" is allowed), a decimal point, and exactly one digit after it, such as "0.0", "9.0", "10.5" or "100.0". Every value lies in [0.0, 100.0].
- Within one labeler's list, segments are in increasing time order: the first starts at "0.0", the last ends at "100.0", each segment starts exactly where the previous one ends, and every segment has start < end.
- Two consecutive segments of the same labeler may carry the same label.
- Labels are exactly "human", "machine" or "silence" (lowercase).
Examples
Input: ([[['0.0', '10.5', 'human'], ['10.5', '20.0', 'machine'], ['20.0', '100.0', 'silence']], [['0.0', '9.0', 'human'], ['9.0', '20.0', 'silence'], ['20.0', '100.0', 'human']], [['0.0', '20.0', 'silence'], ['20.0', '100.0', 'human']]],)
Expected Output: [['0.0', '9.0', 'human'], ['9.0', '20.0', 'silence'], ['20.0', '100.0', 'human']]
Explanation: Source example 1: N = 3 needs 2 votes; silence wins [9.0, 10.5) and [10.5, 20.0), which merge into one segment.
Input: ([[['0.0', '50.0', 'human'], ['50.0', '100.0', 'silence']], [['0.0', '22.5', 'human'], ['22.5', '47.5', 'human'], ['47.5', '100.0', 'silence']], [['0.0', '50.0', 'human'], ['50.0', '100.0', 'machine']], [['0.0', '8.5', 'machine'], ['8.5', '50.0', 'human'], ['50.0', '100.0', 'silence']]],)
Expected Output: [['0.0', '50.0', 'human'], ['50.0', '100.0', 'silence']]
Explanation: Source example 2: N = 4 needs 3 votes; human holds [0.0, 50.0) with 3 or 4 votes and silence holds [50.0, 100.0) with exactly 3.
Hints
- Between two consecutive moments at which some labeler's segment starts or ends, every labeler's vote stays the same, so consensus only has to be decided once for each such stretch.
- Every time has exactly one digit after the decimal point, which lets you compare and order times exactly instead of comparing the strings or using floating point.
- Strictly more than half means exactly N / 2 votes is not enough when N is even, and neighbouring stretches that end up with the same consensus label must be joined into one segment.