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数组复制一遍为leftcopyright复制一遍为rightcopy,然后将leftcopy的每个元素除以C,再将leftcopyrightcopy进行上述的合并和计数,再将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
posted @ 2025-03-11 18:47  最爱丁珰  阅读(14)  评论(0)    收藏  举报