"""
Backtracking solution to weighted interval scheduling.
I've set the requests have length >= 5 to make things a little faster and avoid
the trivial cases of a bunch of length 1 requests
"build" can handle 200 requests within a few seconds
"""
import random
from itertools import chain, combinations
from tqdm import tqdm
# return the powerset of the input
def powerset(iterable):
s = list(iterable)
return chain.from_iterable(combinations(s, r) for r in range(len(s) + 1))
# generate one random request with start/end in the set [0, 1, ..., 9]
# and value a uniform real number between 0 and 10
# repeatedly re-randomizes until meeting lasts at least 5 minutes
def random_request():
req = [sorted(random.sample(range(100), 2)), random.random() * 10]
while req[0][1] - req[0][0] < 5:
req = [sorted(random.sample(range(100), 2)), random.random() * 10]
return req
# generate n random requests
def make_requests(n):
return [random_request() for i in range(n)]
# return a boolean for whether the meetings r1 and r2 are compatible
# (True = are compatible = don't overlap)
def compatible(r1, r2):
return r2[0][1] <= r1[0][0] or r2[0][0] >= r1[0][1]
# return a boolean for whether "request" is compatible with EVERY
# request in the list "solution"
def is_compatible(request, solution):
r = request
return all(compatible(r, s) for s in solution)
# checks whether "solution" is valid, meaning there are no two overlapping
# requests. This is slow!
def valid_solution(solution):
return all(
is_compatible(solution[i], solution[i + 1 :]) for i in range(len(solution) - 1)
)
# compute the total value of all meetings in "solution"
def score(solution):
return sum(r[1] for r in solution)
# plot a set of requests
def plot_requests(requests):
for r in sorted(requests, key=lambda x: x[0][1]):
print(
" " * (r[0][0])
+ "-" * (r[0][1] - r[0][0])
+ " ("
+ str(round(r[1], 2))
+ ")"
)
# greedy solution from previous code
def greedy(requests, sort_key):
sorted_requests = sorted(requests, key=sort_key) # O(n*log(n))
solution = []
solution.append(sorted_requests.pop(0))
while len(sorted_requests) > 0: # O(n)
request = sorted_requests.pop(0)
if is_compatible(request, solution):
solution.append(request)
return solution
shortest = lambda x: x[0][1] - x[0][0]
most_value = lambda x: -x[1]
density = lambda x: -(x[1]) / (x[0][1] - x[0][0])
# brute force solution, slow!
def brute_force(requests):
all_poss = powerset(requests)
best_score = 0
best_sol = []
# loop over all subsets of meetings
for sol in tqdm(all_poss, total=2 ** len(requests)):
if not valid_solution(sol):
continue
# if solution has no overlaps, compute its score
sc = score(sol)
# if score is a new record, save it
if sc > best_score:
best_score = sc
best_sol = sol
return best_sol
# to help you write recursive functions, always plan out
# SUPER explicitly what the inputs and outputs are
# input:
# remaining_requests: list of remaining requests to choose from
# (at the start, all requests are remaining)
# output:
# the best solution using just "remaining_requests"
def backtracking(remaining_requests):
"""
find next valid requests
add it or not, return which of those leads to the largest score
if no other valid requests, just return score
"""
# make a copy so we don't mess things up when we pop!
# SUPER IMPORTANT
rr = list(remaining_requests)
if not rr:
return []
# take the first request
# we are going to try (1) to accept it, and (2) to reject it, recursively
# solve both versions, and keep which one is better
to_add = rr.pop(0)
# find requests compatible with "to_add"
remaining_valid_requests = [
req for req in rr if compatible(to_add, req)
]
# recursively compute best solution using "to_add"
version_accept = [to_add] + backtracking(remaining_valid_requests)
# recursively compute best solution not using "to_add"
version_reject = backtracking(rr)
# return the one that is the max, using the "score" function as
# the way of assigning value (this is like using "key" when sorting)
return max([version_accept, version_reject], key=score)