Quick 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.

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

  1. 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.
  2. 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.
  3. 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.

Loading coding console...

Show the approach

Approach

Every time has exactly one digit after the decimal point, so converting it to a whole number of tenths of a second ("10.5" -> 105) compares times exactly and numerically. The recording then splits into 1000 slots [k/10, (k+1)/10), k = 0..999, and no labeler's vote changes inside a slot because every boundary is a multiple of 0.1.

Algorithm: keep one difference array per label. For each segment [start, end, label], add +1 at start's slot and -1 at end's slot for that label. Sweeping k from 0 to 999 with running prefix sums gives, for every slot, the number of labelers voting for each label (invariant: after processing slot k, counts[label] equals the number of labelers whose segment containing time k/10 has that label). The slot's consensus label is the one with 2 * votes > N; at most one label can satisfy this, since two such labels would need more than N votes in total. If some slot has no such label, that 0.1-second stretch has positive length and no consensus, so the answer is []. Otherwise, maximal runs of slots with the same winner become the output segments, which yields increasing order, full coverage of [0.0, 100.0) without gaps or overlaps, and no two adjacent segments with equal labels.

Spelling: a run boundary other than 0.0 and 100.0 is a point where the winner changes, so some labeler's vote changes there, so it is the start or end of an input segment. Because every time has one canonical spelling (no leading zeros, one decimal digit), writing the tenths value back as integer part, '.', last digit reproduces the input spelling exactly.

Edge cases: N = 1 (one vote is a majority); even N, where exactly N / 2 votes is not consensus; consecutive same-label segments of one labeler, which need no special handling since only counts matter; and 0.1-second stretches without consensus at the very start or the very end, which the sweep over all 1000 slots catches.

Time complexity:
O(S + T), where S is the total number of segments over all labelers and T = 1000 is the number of 0.1-second slots
Space complexity:
O(T) besides the output, with T = 1000