Spectrum licence ledger
Keep a licensed-frequency ledger under grants, revocations and coverage checks by storing disjoint ranges in sorted order.
The regulator licenses slices of radio spectrum, hands slices back, and gets asked whether a whole band is licensed. Storing every kilohertz is not an option.
The problem
A licence covers a half-open band of frequencies [lo, hi) in kilohertz: lo
is licensed, hi is not. Bands that meet end to end are one continuous licence
— [10, 20) and [20, 30) together license everything from 10 up to 30.
Process a list of commands against a ledger that starts empty.
grant lo hi— add[lo, hi)to the licensed set.revoke lo hi— remove[lo, hi)from the licensed set.check lo hi— is every frequency in[lo, hi)licensed right now?
Return the answers to the check commands, in order.
Input. commands — a list of [op, lo, hi], where op is "grant",
"revoke" or "check", and lo < hi.
Output. A list of booleans, one per check.
Example.
[["grant", 100, 400], ["revoke", 200, 250],
["check", 150, 200], ["check", 150, 260]] -> [True, False]
After the revocation the ledger holds [100, 200) and [250, 400). The band
150–200 sits inside the first, so it is licensed. The band 150–260 crosses the
hole at 200–250, so it is not.
A second example, where the boundary decides both answers:
[["grant", 10, 20], ["grant", 20, 30],
["check", 12, 28], ["check", 12, 31]] -> [True, False]
The two grants meet at 20 and become one licence [10, 30), so 12–28 is
covered. 12–31 asks for frequency 30, which is above the licence, so it is not.
Constraints.
1 <= len(commands) <= 10^41 <= lo < hi <= 10^9- the ledger starts empty
Hints
Hint 1
The frequencies are far too many to store, but the number of distinct bands
after m commands is small. How small can each command make it grow?
Hint 2
If the ledger is a list of bands kept sorted and never overlapping, where in that list can a command's effect possibly land?
Hint 3
For a check, only one band can matter: the last one starting at or before
lo. Prove that, and the query becomes a binary search and a comparison.
Approach
Brute force
Hold the licensed frequencies in a set of integers. A command over a band of 10⁹ kilohertz touches 10⁹ entries, and 10⁴ commands make 10¹³ operations against a set that would need gigabytes.
The insight
Store the licensed set as a sorted list of disjoint bands that never touch, and every command is a binary search plus the handful of bands it actually destroys.
Normalising the ledger — sorted, disjoint, no two abutting — is what makes the
binary search meaningful. Each command then works on a contiguous run: once a
band starts beyond the command's hi, so does every band after it. It also
collapses check to one test, because if any band covers [lo, hi) it is the
last one starting at or before lo. The list stays short: a grant leaves one
band where several stood and a revoke adds at most one, so m commands leave
at most m bands.
Algorithm
- Keep
ledger, a list of[lo, hi)bands sorted bylo, disjoint and non-touching. grant: bisect for the first band starting at or afterlo, step back one if the band before it ends at or afterlo, absorb every band starting at or beforehiinto one widened band, splice it in.revoke: bisect the same way, step back only if the previous band ends strictly afterlo, then rebuild each overlapped band as the parts left ofloand right ofhi.check: bisect for the last band starting at or beforelo, and answer whether its end reacheshi.
Complexity
Time O(m log m) amortised over m commands — each does one binary search
plus the bands it removes, and every band is created once and removed once.
Space O(m) for the ledger.
Solution
"""Spectrum licence ledger — disjoint ranges kept sorted, reached by bisect."""
import bisect
def grant(ledger, lo, hi):
i = bisect.bisect_left(ledger, [lo])
# A band ending exactly at lo abuts the new one, so it must be swallowed
# too: the ledger never holds two ranges that touch.
if i > 0 and ledger[i - 1][1] >= lo:
i -= 1
j = i
while j < len(ledger) and ledger[j][0] <= hi:
lo = min(lo, ledger[j][0])
hi = max(hi, ledger[j][1])
j += 1
ledger[i:j] = [[lo, hi]]
def revoke(ledger, lo, hi):
i = bisect.bisect_left(ledger, [lo])
# Here abutting is not overlapping: a band ending exactly at lo loses
# nothing, so only a strictly greater end reaches back.
if i > 0 and ledger[i - 1][1] > lo:
i -= 1
kept = []
j = i
while j < len(ledger) and ledger[j][0] < hi:
band_lo, band_hi = ledger[j]
if band_lo < lo:
kept.append([band_lo, lo])
if band_hi > hi:
kept.append([hi, band_hi]) # a cut through the middle leaves two
j += 1
ledger[i:j] = kept
def covered(ledger, lo, hi):
# The last band starting at or before lo is the only one that can hold the
# whole query, because the bands are disjoint and never touch.
i = bisect.bisect_left(ledger, [lo + 1]) - 1
return i >= 0 and ledger[i][1] >= hi
def solve(commands):
ledger = []
answers = []
for op, lo, hi in commands:
if op == 'grant':
grant(ledger, lo, hi)
elif op == 'revoke':
revoke(ledger, lo, hi)
else:
answers.append(covered(ledger, lo, hi))
return answersThe cases that ran
TESTS = [
(([['grant', 100, 400], ['revoke', 200, 250],
['check', 150, 200], ['check', 150, 260]],), [True, False]),
(([['grant', 10, 20], ['grant', 20, 30],
['check', 12, 28], ['check', 12, 31]],), [True, False]),
(([['check', 1, 2]],), [False]),
(([['grant', 5, 9], ['revoke', 5, 9], ['check', 5, 6]],), [False]),
(([['grant', 1, 1000000000], ['check', 1, 1000000000],
['revoke', 500, 501], ['check', 1, 1000000000],
['check', 501, 1000000000]],), [True, False, True]),
(([['grant', 40, 50], ['grant', 10, 20], ['grant', 20, 40],
['check', 10, 50], ['revoke', 25, 26], ['check', 10, 50],
['check', 26, 50]],), [True, False, True]),
]Pitfalls
- Merging only bands that strictly overlap on a grant leaves
[10, 20)and[20, 30)as separate rows, so the check on 12–28 finds a band ending at 20 and answersFalseeven though every frequency is licensed. - Using that same
>=test on a revoke reaches back into a band that ends exactly atloand has nothing to lose, so the splice rewrites a band the command never touched. - Forgetting that a revoke can split one band into two silently deletes the
upper piece: revoking 500–501 from
[1, 10^9)must leave[501, 10^9). - Treating the bands as closed makes
check 200 250on the first example answerTrue, because the ends 200 and 250 look licensed while everything between them is not.
Variants
- Roadworks calendar — one grant against a ledger that is already sorted, without the revocations.
- Rehearsal room clashes — the same disjointness requirement, reached by dropping intervals instead.