Mit6.006-problemSession02

2-1解决递归

以两种方式推断下列递归的答案:通过递归树、主定理。答案应该包含最贴切的上下边界(递归允许)。假设 T ( 1 ) ∈ θ ( 1 ) T(1)\in\theta(1) T(1)θ(1)

(a) T ( n ) = 2 T ( n 2 ) + O ( n ) T(n)=2T(\frac{n}{2})+\mathcal{O}(\sqrt{n}) T(n)=2T(2n)+O(n )

根据主定理: T ( n ) = Θ ( n ) T(n)=\Theta(n) T(n)=Θ(n)

(b) T ( n ) = 8 T ( n 4 ) + O ( n n ) T(n)=8T(\frac{n}{4})+\mathcal{O}(n\sqrt{n}) T(n)=8T(4n)+O(nn )

根据主定理: T ( n ) = Θ ( n 3 2 log ⁡ n ) T(n)=\Theta(n^\frac{3}{2}\log n) T(n)=Θ(n23logn)

(c) T ( n ) = T ( n 3 ) + T ( n 4 ) + Θ ( n ) ,假设对于所有 a < b , T ( a ) < T ( b ) T(n)=T(\frac{n}{3})+T(\frac{n}{4})+\Theta(n),假设对于所有a<b,T(a)<T(b) T(n)=T(3n)+T(4n)+Θ(n),假设对于所有a<bT(a)<T(b)

根据主定理: T ( n ) = Θ ( n ) T(n)=\Theta(n) T(n)=Θ(n)

2-2查找石头

萨诺斯是一个超级恶棍,在银河系间寻找一个古老而强大的神器,名为:通灵石。不幸地是,她不知道石头在哪个行星上。宇宙由无数颗行星组成,每个由一个唯一的正数标识。在每个行星上都有一个神谕,经过一番劝说,神谕会告诉萨诺斯,通灵石是否在行星上,它的行星标识严格高于他们自己的行星标识。面见宇宙中的每个神谕将花费永久,因此萨诺斯想快速地找到通灵石。假设通灵石在行星k上,描述一个算法(通过面试至多 O ( log ⁡ k ) \mathcal{O}(\log k) O(logk)个神谕),来帮助萨诺斯找到通灵石。

首先来看:如果我们可以找到一个x行星,x>k(x并不远大于k, x = Θ ( k ) x=\Theta(k) x=Θ(k)),那么就可行了,随着从1到x-1二分查找行星,将通过遍历至多 O ( log ⁡ x ) = O ( log ⁡ k ) \mathcal{O}(\log x)=\mathcal{O}(\log k) O(logx)=O(logk)个神谕找到k的值。那么剩下的是找到这个行星x。

为了找到x,指引萨诺斯遍历行星 2 i 2^i 2i,始于 i = 0 i=0 i=0,直到 x = 2 i x=2^i x=2i上的神谕告知萨诺斯:x>k。因为x是首个 2 i > k 2^i>k 2i>k的行星,那么x/2<k,x<2k= Θ ( k ) \Theta(k) Θ(k),符合预期。为了到达行星 x = 2 i x=2^i x=2i,萨诺斯面见 i = ⌈ log ⁡ 2 k ⌉ = Θ ( log ⁡ k ) i=\lceil \log_2k \rceil=\Theta(\log k) i=log2k=Θ(logk)个神谕,因此为了找到k,这个算法正如期望的那样,至多面见 O ( log ⁡ k ) \mathcal{O}(\log k) O(logk)个神谕。

2-3Collage Collating

Fodoby是一家为创意人士订制软件工具的公司。他们的最新软件,Ottoshop,通过允许他们在单个文档中叠加图片,来帮助用户制作拼贴。描述一个数据库来追踪给定文档中的图片,它支持以下操作:

(1)make_document():构建一个不包含图片的空文档

(2)import_image(x):将带有唯一整数ID x的图片加到文档顶部

(3)display():以从底到顶的顺序,返回文档图片id的数组

(4)move_below(x, y):把ID为x的图片移动到ID为y的图片下面

操作(1)应该以最坏情形 O ( 1 ) \mathcal{O}(1) O(1)时间执行,操作(2)和(3)应该每个以最坏情形 O ( n ) \mathcal{O}(n) O(n)时间执行,操作(4)应该以最坏情形 O ( log ⁡ n ) \mathcal{O}(\log n) O(logn)时间执行,n是操作时文档中包含图片的数量。

这个数据库需要我们保存一个外部有序的图片序列,但也支持基于它们ID的内部查找。因此我们将实现一个数据库,结合了序列结构(一个存储图片ID的双向链表)和集合结构(存储键值对 ( x , v x ) (x,v_x) (x,vx)以x的值排序的数组,x是图片的ID, v x v_x vx是一个指向包含x的链表节点的指针)。为了实现make_document,简单地初始化一个空链表L和一个空有序数组S,每个以 O ( 1 ) \mathcal{O}(1) O(1)时间。这个操作没有输出,它是正确的。

为了实现import_image(x),以节点 v x v_x vx添加x到L的头部,用时 O ( 1 ) \mathcal{O}(1) O(1)时间,并添加 ( x , v x ) (x,v_x) (x,vx)到S,用时 O ( n ) \mathcal{O}(n) O(n)。代理这些数据结构,确保x被添加到序列L头部,S现在包含 ( x , v x ) (x,v_x) (x,vx),并在插入后保持有序。

为了实现display(),通过遍历L中的项目,构建并返回一个数组,可以用 O ( n ) \mathcal{O}(n) O(n)时间完成。

为了实现move_below(x, y),使用二分查找,找到S中的键值对 ( x , v x ) (x,v_x) (x,vx) ( y , v y ) (y,v_y) (y,vy),每个用时 O ( log ⁡ n ) \mathcal{O}(\log n) O(logn)。通过重链(relink)指针,然后我们可以从L中以 O ( 1 ) \mathcal{O}(1) O(1)时间移除节点 v x v_x vx,并在节点 v y v_y vy后插入,也用时 O ( 1 ) \mathcal{O}(1) O(1)。这是一个在双向链表中重链指针的方法:

def relink(S, vx, vy):
    if vx.prev: vx.prev.next = vx.next
    else:    S.head = vx.next
    if vx.next: vx.next.prev = vx.prev
    else:    S.tail = vx.prev
    vx.prev = vy
    vx.next = vy.next
    if vy.next: vy.next.prev = vx
    else:    S.tail = vx
    vy.next = vx

2-4Brick Blowing

Porkland是一个由猪组成的社区,它们居住在从东向西延申的街道一边,排成一排的n个房子里。对猪来说是不幸的,这个狼非常擅长吹倒猪舍,这得益于从西向东的大风。如果狼向东吹向包含b块砖的房子,房子将会倒塌,它东边低于b块砖的房子都会倒塌。对于Porkland中的每个房子,狼想知道它的破坏情况,例如:从西向东吹向它后,倒塌房子的数量。

(a)假设n=10,从西向东Porkland中每个房子的砖数是:[34,57,70,19,48,2,94,7,63,75]。计算这个实例中:Porkland每个房子的损坏。

[4, 5, 6, 3, 3, 1, 4, 1, 1, 1]

(b)Porkland房子是特殊的,要么1)东边没有邻居,要么2)它东边的邻居至少有和它一样的砖。给定一个数组,包含Porkland中每个房子的砖数,描述一个 O ( n ) \mathcal{O}(n) O(n)时间复杂度算法,来返回Porkland中每个房子的破坏,Porkland只有一个房子是非特殊的。

保存一个数组D,拥有于输入数组H相同的尺寸,用来存储破坏数,D的 i t h i^{th} ith个项目是一个代表破坏数的整数。在 i t h i^{th} ith房子添加破坏数,以 O ( 1 ) \mathcal{O}(1) O(1)时间在D[i]处加数。当风吹到时,因为每个房子本身会毁坏,以 O ( n ) \mathcal{O}(n) O(n)时间初始化D中每个元素为1,使用下面算法计算其他破坏数。

如果Porkland仅一个房子是非特殊的,意味着子数组A(从最西边的房子到h,单调递增),从 ( h + 1 ) t h (h+1)^{th} (h+1)th到最东边的房子也是如此。我们可以通过线性扫描用 O ( n ) \mathcal{O}(n) O(n)时间找到h。子数组B中任意房子的破坏数为1,因为到东边的房子,没有更少的砖,因此它们的值被正确地初始化了。

那么剩下的是:计算A中房子的破坏数。使用双指算法,一个索引 i 始于A的起始处,另一个索引 j 位于B的起始处。重复下面的过程,直到 i = ∣ A ∣ i=|A| i=A:如果 j < ∣ B ∣ j<|B| j<B且房子A[i]比B[j]有更多的砖,那么j++;否则把 j 加到D[i]处的破坏数上,并且i++。当 i + j = ∣ A ∣ + ∣ B ∣ = n i+j=|A|+|B|=n i+j=A+B=n时,循环终止,每次迭代 i+j 增1。因为每次迭代的工作是 O ( 1 ) \mathcal{O}(1) O(1),所以这个算法以 O ( n ) \mathcal{O}(n) O(n)时间执行。

为了证明:这个算法正确地计算了A中每个房子的破坏数,我们首先证明:上面的循环在每次迭代开始时,保证: A [ i ] > B [ k ] , k ∈ { 0 , . . . , j − 1 } A[i]>B[k],k\in\{0,...,j-1\} A[i]>B[k]k{0,...,j1}。这个属性表明:算法为A中每个房子,正确地计算破坏数:当 A [ i ] ≤ B [ j ] A[i]\le B[j] A[i]B[j]时,算法更新A[i]的破坏数,因此该声明表明:A[i]以东,比其有更少砖的房子是: H = { B [ k ] ∣ k ∈ { 0 , . . . , j − 1 } } , ∣ H ∣ = j H=\{B[k]|k\in\{0,...,j-1\}\},|H|=j H={B[k]k{0,...,j1}}H=j。因此房子A[i]处的破坏数是 D [ i ] = j + 1 D[i]=j+1 D[i]=j+1

为了证明这个声明,我们推断当 i + j = 0 i+j=0 i+j=0时,这个声明时true,因为k的可能值集合为空。现在为推断做出假设:声明在 i+j 时成立。

如果 A [ i ] > B [ j ] A[i]>B[j] A[i]>B[j],那么j++保证成立。因为 A [ i ] > B [ k ] , k ∈ { 0 , . . . , j − 1 } A[i]>B[k],k\in\{0,...,j-1\} A[i]>B[k]k{0,...,j1}

可选择地,如果 A [ i ] ≤ B [ j ] A[i]\le B[j] A[i]B[j],那么i++,也保证了推断假设,因为 A [ i + 1 ] > A [ i ] A[i+1]>A[i] A[i+1]>A[i],证明声明。

(c)给定一个数组,包含porkland中每个房子中砖的数量,描述一个 O ( n log ⁡ n ) \mathcal{O}(n\log n) O(nlogn)时间算法来返回porkland每个房子的破坏数。

我们更改归并排序来记录破坏数(每次merge前,发生在每个子数组中房子之间)。因为我们将从原始位置,移动H中砖的数量,我们将用元组 H [ i ] = ( b i , i ) H[i]=(b_i,i) H[i]=(bi,i)取代每个砖数量,用来记录哪个房子与 b i b_i bi相关。

像(b)那样,初始化一个 破坏数 数组D为1。递归排序、记录破坏数(发生在H前半部分房子之间),对H后半部分同样这么做。接下来使用(b)中的 O ( n ) \mathcal{O}(n) O(n)时间算法来计算破坏数(前半部分房子和后半部分房子之间),然后使用归并排序的归并步骤,以 O ( n ) \mathcal{O}(n) O(n)时间将两个有序“半边”结合成一个有序数组。因为(b)和归并都花费 O ( n ) \mathcal{O}(n) O(n)时间,所以这个算法的递归和归并排序一样,都花费 O ( n log ⁡ n ) \mathcal{O}(n\log n) O(nlogn)执行时间。

现在,通过推断子数组的尺寸,我们证明这个算法:正确地记录了给定子数组中,每个房子与子数组中其他房子的破坏数。当子数组尺寸为1,该子数组中,房子间的破坏数确定为1,且初始化步骤记录了它。可选择地,假设对于所有 k < n k<n k<n声明都是true。通过推断,算法正确地记录了子数组前半部分房子间的所有破坏数,以及后半部分房子间所有破坏数。剩下的就是:记录左半部分与右半部分房子之间的破坏数。

幸运地,因为前后两部分子数组是有序的,(b)中的算法准确地记录了那些破坏数。使用归并排序中的归并步骤的排序保证了这点。

归并步骤和(b)部分的算法都是双指算法,从两个子数组起始处遍历。我们在(d)中的实现利用了这点,记录归并排序中归并步骤的破坏数,而不是单独地。

(d)写一个python函数get_damages来实现你的算法。

def get_damages(H):
    D = [1 for _ in H]
    H2 = [(H[i], i) for i in range(len(H))]
    def merge_sort(A, a = 0, b = None):
        if b is None:    b = len(A)
        if 1 < b - a:
            c = (a + b + 1) // 2
            merge_sort(A, a, c)
            merge_sort(A, c, b)
            i, j, L, R = 0, 0, A[a:c], A[c:b]
            while a < b
                if (j >= len(R)) or (i < len(L) and L[i][0] <= R[j][0])
                    D[L[i][1]] += j
                    A[a] = L[i]
                    i += 1
                else:
                    A[a] = R[j]
                    j += 1
                a += 1
    merge_sort(H2)
    return D
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
MIT 6.824 课程的 Lab1 是关于 Map 的实现,这里单介绍一下实现过程。 MapReduce 是一种布式计算模型,它可以用来处理大规模数据集。MapReduce 的核心想是将数据划分为多个块,每个块都可以在不同的节点上并行处理,然后将结果合并在一起。 在 Lab1 中,我们需要实现 MapReduce 的基本功能,包括 Map 函数、Reduce 函数、分区函数、排序函数以及对作业的整体控制等。 首先,我们需要实现 Map 函数。Map 函数会读取输入文件,并将其解析成一系列键值对。对于每个键值对,Map 函数会将其传递给用户定义的 Map 函数,生成一些新的键值对。这些新的键值对会被分派到不同的 Reduce 任务中,进行进一步的处理。 接着,我们需要实现 Reduce 函数。Reduce 函数接收到所有具有相同键的键值对,并将它们合并成一个结果。Reduce 函数将结果写入输出文件。 然后,我们需要实现分区函数和排序函数。分区函数将 Map 函数生成的键值对映射到不同的 Reduce 任务中。排序函数将键值对按键进行排序,确保同一键的所有值都被传递给同一个 Reduce 任务。 最后,我们需要实现整个作业的控制逻辑。这包括读取输入文件、调用 Map 函数、分区、排序、调用 Reduce 函数以及写入输出文件。 Lab1 的实现可以使用 Go 语言、Python 或者其他编程语言。我们可以使用本地文件系统或者分布式文件系统(比如 HDFS)来存储输入和输出文件。 总体来说,Lab1 是一个比较简单的 MapReduce 实现,但它奠定了 MapReduce 的基础,为后续的 Lab 提供了良好的基础。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值