Problem Set 2
Problem Set 2.1
Problem 2.1.1
将\(2n\)个人按照评分从大到小排序,前\(n\)个人为一组,后\(n\)个人为一组,根据贪心不难知道算法正确
def max_score_difference(scores, n):
scores.sort(reverse=True)
sum_team1 = sum(scores[:n])
sum_team2 = sum(scores[n:])
return sum_team1 - sum_team2
Problem 2.1.2
a)
考虑一个DFS,对每个元素枚举三种操作:加一,减一,不变;最后检查整个数据是否是一个等差数列即可
def can_transform_to_arithmetic(A):
n = len(A)
if n <= 1:
return True
def is_arithmetic(arr):
if len(arr) <= 1:
return True
d = arr[1] - arr[0]
for i in range(2, len(arr)):
if arr[i] - arr[i-1] != d:
return False
return True
def dfs(index, current):
if index == n:
return is_arithmetic(current)
else:
for delta in [-1, 0, 1]:
new_val = A[index] + delta
if dfs(index + 1, current + [new_val]):
return True
return False
return dfs(0, [])
b)
枚举前两个元素的操作方法,一共有\(3\times 3=9\)种,然后就可以确定公差;最后循环一遍序列检查是否可以达到等差数列即可
def can_form_arithmetic_b(a):
n = len(a)
if n == 1:
return True
candidates = set()
a0 = a[0]
a1 = a[1]
for da0 in [-1, 0, 1]:
for da1 in [-1, 0, 1]:
a0_adj = a0 + da0
a1_adj = a1 + da1
if a0_adj == a1_adj:
d = 0
else:
d = (a1_adj - a0_adj)
candidates.add(d)
for d in candidates:
for a0_adj in [a0-1, a0, a0+1]:
valid = True
for i in range(n):
target = a0_adj + d * i
if abs(target - a[i]) > 1:
valid = False
break
if valid:
return True
return False
Problem 2.1.3
考虑将所有数从小到大排序,最小的与最大的组成一对,次小的与次大的组成一对,以此类推。最终计算所有组合的和的最大值就是题目所求的答案
证明:下面的证明假设数组已经排序。如果\(a_1\)不与\(a_n\)结合,假设最终的答案中,\(a_1\)与\(a_i\)结合,\(a_n\)与\(a_j\)结合,且\(i≠n,j≠1\),那么这两对数的较大值为\(\max(a_1+a_i,a_j+a_n)\),此时我们换一下这两对数的组合方法,变成\(a_1\)与\(a_n\)结合,\(a_i\)与\(a_j\)结合,最大值就变成了\(\max(a_1+a_n,a_i+a_j)\).由于\(a_1+a_n\leq a_n+a_i,a_i+a_j\leq a_i+a_n\),所以\(\max(a_1+a_n,a_i+a_j\leq\max(a_1+a_i,a_n+a_j)\),而其他的组合不变。也就是说我们交换的方案不会比最终的方案更劣,所以一定可以构造一种最优方案,使得\(a_1\)与\(a_n\)组合。利用数学归纳法可以得到上述结论
def min_max_pair_sum(arr):
arr.sort()
n = len(arr) // 2
max_sum = 0
for i in range(n):
current_sum = arr[i] + arr[len(arr) - 1 - i]
if current_sum > max_sum:
max_sum = current_sum
return max_sum
Problem Set 2.2
Problem 2.2.1
a)
时间复杂度为\(O(n+n)+O(2n+n)+...+O((k-1)n+n)=O(2n+3n+...+nk)=O(nk^2)\)
b)
定义solve(l,r)表示将这\(k\)个有序数组中的第\(l\sim r\)个合并。分治算法如下:对于solve(l,r),计算mid=(l+r) // 2,然后递归调用solve(l,mid)和solve(mid+1,r),在递归回来之后,使用merge将\(l\sim mid\)和\(mid+1\sim r\)进行合并即可。时间复杂度为\(O(n\log n)\)
def merge(left, right):
merged = []
i = j = 0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
merged.append(left[i])
i += 1
else:
merged.append(right[j])
j += 1
merged.extend(left[i:])
merged.extend(right[j:])
return merged
def solve(arrays):
if not arrays:
return []
def solve(l, r):
if l == r:
return arrays[l]
mid = (l + r) // 2
left = solve(l, mid)
right = solve(mid+1, r)
return merge(left, right)
return solve(0, len(arrays)-1)
Problem 2.2.2
\((1)\)
先将\(A,B\)排序,然后利用merge将两者合并,最后将合并数组去重即可
def union(A, B):
A_sorted = sorted(A)
B_sorted = sorted(B)
merged = []
i = j = 0
while i < len(A_sorted) and j < len(B_sorted):
if A_sorted[i] < B_sorted[j]:
merged.append(A_sorted[i])
i += 1
else:
merged.append(B_sorted[j])
j += 1
merged.extend(A_sorted[i:])
merged.extend(B_sorted[j:])
if not merged:
return []
result = [merged[0]]
for num in merged[1:]:
if num != result[-1]:
result.append(num)
return result
\((2)\)
与上一问相比,少了排序的过程,剩下的一样
Problem 2.2.3
\((1)\)
在merge中,合并数组的时候,假设合并的两个数组为\(A,B\),那么在\(A_i>B_j\)的时候,可以知道\(A_i\)及之后的所有\(A\)都与\(B_j\)构成逆序对,计入计数变量即可。可以知道上面的算法不重不漏
def count_inversion(arr):
def merge(left, right):
i = j = 0
merged = []
count = 0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
merged.append(left[i])
i += 1
else:
merged.append(right[j])
j += 1
count += len(left) - i
merged.extend(left[i:])
merged.extend(right[j:])
return merged, count
def merge_sort(arr):
if len(arr) <= 1:
return arr, 0
mid = len(arr) // 2
left, a = merge_sort(arr[:mid])
right, b = merge_sort(arr[mid:])
merged, c = merge(left, right)
return merged, a + b + c
_, total = merge_sort(arr)
return total
\((2)\)
在merge中,先将left数组复制一遍为leftcopy,right复制一遍为rightcopy,然后将leftcopy的每个元素除以C,再将leftcopy和rightcopy进行上述的合并和计数,再将left重新与right进行合并(但这个过程中不计数,只合并)
def count_inverse_pairs(arr, C):
def merge_sort(arr):
if len(arr) <= 1:
return arr, 0
mid = len(arr) // 2
left, inv_left = merge_sort(arr[:mid])
right, inv_right = merge_sort(arr[mid:])
merged, inv_merge = merge(left, right, C)
return merged, inv_left + inv_right + inv_merge
def merge(left, right, C):
count = 0
i = j = 0
leftcopy = [x / C for x in left]
rightcopy = right[:]
while i < len(leftcopy) and j < len(rightcopy):
if leftcopy[i] > rightcopy[j]:
count += len(leftcopy) - i
j += 1
else:
i += 1
merged = []
i = j = 0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
merged.append(left[i])
i += 1
else:
merged.append(right[j])
j += 1
merged.extend(left[i:])
merged.extend(right[j:])
return merged, count
_, total = merge_sort(arr)
return total
Problem 2.2.4
\((1)\)
维护两个数组\(D,E\),最开始\(D\)和\(E\)都包含一个相同的元素,为\(A\)中的最小值减一。遍历\(A\),假设现在遍历到了\(A_i\),如果\(A_i\)只比\(D_{-1}\)和\(E_{-1}\)中的一个大,那么\(A_i\)显然只能接到这一个数之后,所以选择是唯一的,否则的话,\(A_i\)接到两者中的较大值后面。用数学归纳法和决策包容性可以证明这个算法的正确性
当\(D\)和\(E\)为空的时候,根据对称性,\(A_0\)放到哪一个当中都可以;之后假设我们已经构建的\(D\)和\(E\)可以形成可行解,那么对于\(A_i\),假设其选择唯一,那么根据归纳假设,我们就把\(A_i\)放入到那个唯一的选择当中就可以了,最终可以形成可行解,否则的话,假设\(A_i\)放到的是较小值的后面并且形成了可行解,那么我们在最终的可行解中,将\(A_i\)之后的部分和此时\(E_{-1}\)之后的部分进行交换(可以知道这个交换是合法的),仍然可以形成可行解
def split_array(A):
D = [float('-inf')]
E = [float('-inf')]
for x in A:
d_last = D[-1]
e_last = E[-1]
can_d = x > d_last
can_e = x > e_last
if can_d and can_e:
if d_last >= e_last:
D.append(x)
else:
E.append(x)
elif can_d:
D.append(x)
elif can_e:
E.append(x)
return D[1:], E[1:]
\((2)\)
先将\(A\)利用\((1)\)中的算法分成\(D\)和\(E\),然后再对\(D\)和\(E\)利用归并排序中的merge操作进行合并即可。merge函数在前面写了很多次了就不写了
Problem Set 2.3
Problem 2.3.1
\((1)\)
假设\(i\)是其父节点\(p\)的第\(j\)个子节点,实际上有\(i=(p-1)d+j+1\).不妨这个样子考虑这个公式:最开始数组都是空的,只有第一个位置有元素,这个元素产生了\(d\)个元素,这\(d\)个元素各自占了\(d\)个位置,这\(d\)个元素每个都产生了\(d\)个元素,且每个产生的元素都独自占了一个位置;那么对于前\(p-1\)个节点,每个节点都产生了\(d\)个元素,这些产生的元素都独自占了一个位置,所以在\(p\)产生儿子之前,数组之中一共有\(d(p-1)+1\)个位置被占用了,所以\(p\)的儿子的区间就是\([d(p-1)+2,d(p-1)+d+1]\),所以有上面的公式
将上面的公式进行化简有\(p=\frac{i-j-1}{d}+1\),由于\(j∈[1,d]\)并且注意到\(\frac{i-j-1}{d}\)是整数,所以\(\frac{i-j-1}{d}=\lfloor\frac{i-2}{d}\rfloor\),于是结论不难得证
另一种做法:设\(\text{id}(i,j)\)表示第\(i\)层的第\(j\)个节点,将\(\text{id}\)与原始序号做映射就好了
\((2)\)
在\((1)\)中已经证明了这个结论
Problem 2.3.2
如果任务的开始时间或结束时间存在很大的值的话,就先进行离散化(只不过这里不需要离散化,考试的时候不会考虑内存不够的情况,除非明确说明)。创建一个初始为全\(0\)的计数数组,并且遍历每个任务,对于每个任务,设开始时间为st,结束时间为ed,则将计数数组从st开始到ed结束的每一个位置加一(这个使用差分数组即可,时间复杂度为\(O(1)\))。在处理完所有数组之后,遍历计数数组,其中最长的空闲时间就是连续的\(0\)的最长长度,最长的非空闲时间就是最长的连续非\(0\)长度
import bisect
def find_max_times(tasks):
times = set()
for st, ed in tasks:
times.add(st)
times.add(ed)
times.add(0)
sorted_times = sorted(times)
n = len(sorted_times)
diff = [0] * n
for st, ed in tasks:
i = bisect.bisect_left(sorted_times, st)
j = bisect.bisect_left(sorted_times, ed)
diff[i] += 1
if j + 1 < n:
diff[j + 1] -= 1
cnt = [0] * n
cnt[0] = diff[0]
for i in range(1, n):
cnt[i] = cnt[i - 1] + diff[i]
max_idle = 0
max_busy = 0
current_idle = 0
current_busy = 0
prev_time = sorted_times[0]
for i in range(1, n):
current_time = sorted_times[i]
duration = current_time - prev_time
if cnt[i - 1] == 0:
current_idle += duration
current_busy = 0
if current_idle > max_idle:
max_idle = current_idle
else:
current_busy += duration
current_idle = 0
if current_busy > max_busy:
max_busy = current_busy
prev_time = current_time
return (max_idle, max_busy)
另一种做法:将所有任务按照开始时间递增排序,不断取出队头(取出的条件是当前机器结束的时间不小于队友的开始时间)并更新结束时间;在循环的过程中更新答案即可
另一种做法见答案PPT
Problem 2.3.3
\((1)\)
考虑化简条件得到\(A_i\leq A_{i+k}\)。我们将原数组分成\(k\)个数组:第一个数组是\(A_1,A_{1+k},A_{1+2k},...\),第二个数组是\(A_2,A_{2+k},A_{2+2k},...\),以此类推。我们对每个数组排序,每个数组的时间复杂度为\(O(\frac{n}{k}\log\frac{n}{k})\),一共有\(k\)个数组,故时间复杂度为\(O(n\log\frac{n}{k})\)
def k_sort(A, k):
n = len(A)
for i in range(k):
sub = []
for j in range(i, n, k):
sub.append(A[j])
sub.sort()
idx = 0
for j in range(i, n, k):
A[j] = sub[idx]
idx += 1
\((2)\)
由\((1)\),满足题意的数组可以分成\((1)\)中的\(k\)个数组,每个数组都是单调递增的。此时用一个堆,堆中的每个元素是一个二元组,包含元素值和这个元素所属于的数组的编号,堆的排序关键字是元素值。每次取出堆顶,并将堆顶元素所属数组的在这个元素的下一个元素放入堆中,重复上述过程即可,不难知道时间复杂度为\(O(n\log k)\)
def sort_k_sorted(A, k):
n = len(A)
subarrays = [[] for _ in range(k)]
for i in range(n):
subarrays[i % k].append(A[i])
import heapq
heap = []
pointers = [0] * k
for i in range(k):
if pointers[i] < len(subarrays[i]):
heapq.heappush(heap, (subarrays[i][0], i))
pointers[i] += 1
result = []
while heap:
val, sub_idx = heapq.heappop(heap)
result.append(val)
if pointers[sub_idx] < len(subarrays[sub_idx]):
next_val = subarrays[sub_idx][pointers[sub_idx]]
heapq.heappush(heap, (next_val, sub_idx))
pointers[sub_idx] += 1
return result
Problem 2.3.4
使用对顶堆即可。维护两个堆,A堆是大根堆,B堆是小根堆,且A堆的堆顶不超过B堆,始终保证A堆的元素个数不少于B堆的元素个数,但是不会比B堆元素个数多两个及以上。当一个元素被插入的时候,判断其应该被放入A堆还是B堆中,然后将其放入(删除的时候同理),注意这个过程中动态调整A堆和B堆的数量,如果发现A堆的元素数量少于B堆了就要将B堆的堆顶放入A堆中(直到满足两者数量的相对条件),同理如果A堆的元素数量多于B堆的元素数量两个及以上的时候就要将A堆的堆顶放入B堆中(直到满足两者数量的相对条件)。查询的时候,如果现在A堆元素和B堆元素的总数量为奇数,那么此时一定是A堆的元素比B堆的元素多一个,直接输出A堆的堆顶即可,否则就是两个堆的元素数量相等,直接输出两个堆堆顶的平均值即可
import heapq
from collections import defaultdict
class MedianFinder:
def __init__(self):
self.A = []
self.B = []
self.countA = 0
self.countB = 0
self.removed = defaultdict(int)
def add_num(self, num):
if not self.A or num <= -self.A[0]:
heapq.heappush(self.A, -num)
self.countA +=1
else:
heapq.heappush(self.B, num)
self.countB +=1
while self.countA > self.countB + 1:
val = self._pop_A()
heapq.heappush(self.B, val)
self.countA -=1
self.countB +=1
while self.countB > self.countA:
val = self._pop_B()
heapq.heappush(self.A, -val)
self.countB -=1
self.countA +=1
def remove_num(self, num):
self.removed[num] +=1
self._clean_top()
def _clean_top(self):
while self.A and (-self.A[0] in self.removed and self.removed[-self.A[0]]>0):
val = -heapq.heappop(self.A)
if self.removed[val]>1:
self.removed[val] -=1
else:
del self.removed[val]
self.countA -=1
while self.B and (self.B[0] in self.removed and self.removed[self.B[0]]>0):
val = heapq.heappop(self.B)
if self.removed[val]>1:
self.removed[val] -=1
else:
del self.removed[val]
self.countB -=1
def _pop_A(self):
self._clean_top()
val = -heapq.heappop(self.A)
self.countA -=1
return val
def _pop_B(self):
self._clean_top()
val = heapq.heappop(self.B)
self.countB -=1
return va
def find_median(self):
self._clean_top()
total = self.countA + self.countB
if total %2 ==1:
return -self.A[0]
else:
return (-self.A[0] + self.B[0])/2
上述代码用的是系统的内置堆,只能使用懒惰删除法,均摊复杂度是\(O(1)\)(可以直接手写堆支持删除操作)
Problem 2.3.5
将所有区间按照左端点为关键字从小到大排序,遍历所有区间,中途维护一个Max变量,表示之前已经遍历过了的区间的右端点的最大值。设ans表示已经找到了的最长长度,假设当时遍历到了区间\(i\),那么如如果Max不小于区间\(i\)的左端点,就尝试更新ans=max(ans,min(f[i],Max)-s[i]+1),最后输出ans即可
证明:显然我们算法中,每一次更新的ans都是重合的长度,所以我们只需要证明最优答案没有漏掉就好了。考虑最优答案一定是两个区间的重合,当我们遍历到左端点更大的这个区间的时候,我们的max一定不会小于左端点更小的区间的右端点(因为左端点更小的区间的右端点一定在max的选择集合中,具体可以见代码),而这又是最优的,所以max就是左端点更小区间的右端点,证毕
时间复杂度为\(O(n\log n)\),瓶颈是排序
def max_overlap(intervals):
intervals.sort(key=lambda x: x[0])
if len(intervals) < 2:
return 0
max_right = intervals[0][1]
ans = 0
for s, e in intervals[1:]:
if s <= max_right:
overlap = min(e, max_right) - s
if overlap > ans:
ans = overlap
max_right = max(max_right, e)
return ans+1

浙公网安备 33010602011771号