算法导论15 Problems 答案

15-1

a

1
2
3
4
5
6
7
8
9
COIN-CHANGING-SPECIAL(n)
A = [25, 10, 5, 1]
B = {}
for each m in A
k = ⌊n / m⌋
// 使用k个价值为m的硬币
B = B ∪ {m} × k
n = n - k * m
return B

在最优情况下,\(25\)分硬币可以由\(2\)\(10\)分硬币和\(1\)\(5\)分硬币表示出;类似的,\(10=5\times 2,5=1\times 5\)

不难证明这个问题满足最优子结构,如果一个拼凑\(n\)分硬币的最优解\(S\)的子集\(S'\)不是拼凑\(m\)分硬币的最优解,那么存在一个最优解\(S''\)是拼凑\(m\)枚硬币的最优解,从而构造出一个拼凑\(n\)分硬币的最优解\((S-S')\cup S''\),引出了矛盾。因此这个问题满足最优子结构。

不难发现这个问题满足贪心选择性质。在一个最优解中,最终\(10\)分硬币必定最多\(2\)个,\(5\)分硬币最多为\(1\)个。对于每个大面额硬币\(n\),它总能由多个小面额硬币表示出来。因此如果一个解没有贪心地使用大硬币来表示,那么以这个大硬币额度为子问题的解必定不是最优的。因此满足贪心选择性质。

b

最优子结构的证明过程与问题15-1-a相同。

假设一个最优解为\(S=(s_0,s_1,\dots,s_k)\)。那么\(\forall i < k,s_i< c\)总是成立的。因为如果\(\exists j< k,s_j\ge c\),那么可以考虑用\(1\)枚价值为\(c^{j+1}\)的硬币代替这\(c\)枚价值为\(c^{j}\)的硬币,从而减少了\(c-1\)枚硬币。

假设一个解\(S'\)没有贪心选择尽可能大的硬币,也就是说,一个超过价值\(c^j\)的子问题却没有用价值为\(c^j\)的硬币拼凑,那么哪怕\(\forall j< i\),每一种价值为\(c^{j}\)的硬币都使用\(c-1\)个,都只能凑出\((c-1)(c^0+c^1+c^2+\dots+c^{i-1})=c^i-1\)的价值。因此在此情况下,必定存在一个\(j< i,s_j'\ge c\)成立,和最优解成立的条件矛盾。因此这个问题下满足贪心选择性质。

故这个算法的贪心策略是正确的。

c

如果硬币币种为\([1,4,6]\),需要拼凑的值为\(8\),那么贪心算法下的解为\([1,1,6]\),但是最优解为\([4,4]\)

d

考虑使用动态规划算法来解决本题。这个问题和完全背包问题一样,每种硬币的币值视为物品的“重量”,价值视为\(1\),区别仅在于这里求的是必定装满重量\(k\)后,价值之和的最小值。

令状态\(f[i, j]\)表示前\(i\)种物品凑出的重量和为\(j\)时,最小的总价值。对于当前状态,第\(i\)种物品有两种决策:取或者是不取。如果不取,那么相当于从状态\(f[i-1,j]\)转移而来;如果取,那么相当于从\(f[i,j-w_i]\)处取一个物品\(i\)转移而来,并且从中择优转移。因此不难写出如下状态转移方程:

\(f[i,j]= \left \{\begin{aligned} &0 & & \text{if}\quad i=0\land j=0 \\ &+\infty & & \text{if}\quad i=0\land j\neq0 \\ &f[i-1,j]& & \text{if} \quad i>0 \land j< w_i\\ &\in\{f[i-1,j],f[i,j-w_i]+1\}& & \text{if} \quad i>0 \land j\ge w_i\\ \end{aligned}\right.\)

因此最终答案为\(f[n,k]\)

算法CHANGE-PROBLEM给出了具体的实现过程。第2-3行的嵌套循环说明这个算法的时间复杂度为\(O(nk)\)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
CHANGE-PROBLEM(w, n, k)
let f[0 : k] be a new array by +∞
let pre[0 : k] be a new array
f[0] = 0
for i = 1 to n
for j = w[i] to k
if f[j - w[i]] + 1 < f[i]
pre[j] = i
f[i] = f[j - w[i]] + 1
B = {}
j = k
while k > 0
x = w[pre[k]]
B = B ∪ {x}
k = k - x
return f[W]

15-2

a

将每个任务\(a_i\)按照运行时间升序排序,然后按这个顺序执行每个任务即可。

最优子结构的证明:对于一个最优解\(S\),完成了\(i\)个任务后,子问题则为剩下的\(n-i\)个任务,如果这\(n-i\)不是按照最优的方式进行的,那么存在一个更优的解\(S'\)进行这\(n-i\)个任务。那么由于两个步骤是独立的,将\(S\)的关于前\(i\)个任务的解和\(S'\)组合就能得出一个比\(S\)更优秀的解\(S''\),这与\(S\)是最优秀的矛盾。

接下来证明贪心选择性质。

假设任务为\(\{b_1,b_2,\dots,b_n\}\),那么有\(\displaystyle{C_{b_i}=\sum_{j=1}^i p_{b_j}}\)。平均时间\(\overline{C}\)可以如下计算:

\(\begin{aligned} \overline{C} &= \dfrac{1}{n}\sum_{i=1}^n C_{b_i}\\ &=\dfrac{1}{n} \sum_{i=1}^n\sum_{j=1}^i p_{b_j}\\ &=\dfrac{1}{n}\sum_{j=1}^n(n-j+1) p_{b_j} \end{aligned}\)

令序列\(\{a\}\)\(a_j=n-j+1,\{c\}\)则是\(p\)的一个置换,可以发现\(a\)是降序的。那么为了最小化\(\overline{C}\)的值,\(\{c\}\)必须是升序的。对于一个解\(S\),如果\(\exists i< j,c_i>c_j\)成立,那么考虑构造出一个解\(S'\),它是将解\(S\)中的\(c_i\)\(c_j\)交换,那么交换前后的平均完成时间的变化为\(\dfrac{1}{n}(a_jb_i+a_ib_j)-\dfrac{1}{n}(a_jb_j+a_ib_i)=-\dfrac{(a_j-a_i)(b_j-b_i)}{n}<0\)。从而说明解\(S''\)\(S\)更优。由此证明贪心选择性质成立。

因此这个贪心策略是正确的。算法的整个过程主要在于为\(\{p\}\)排序,时间复杂度为\(O(n\lg n)\)

b

这个贪心算法是,处理器总是优先处理剩余运行时间最短的任务。也就是说,如果当前任务\(a_1\)剩下的运行时间为\(p_1\),但是随后插入一个运行时间为\(p_2\)的任务\(a_2\),且\(p_2< p_1\),那么就挂起任务\(a_1\)去运行\(a_2\)

考虑如此场景:假设为当前时刻为\(t=r_2\),任务\(a_1\)正在被执行,剩余时间为\(p_1\),此时插入一个任务\(a_2\),剩余时间为\(p_2(p_2< p_1)\)。令决策\(S'\)表示不立刻挂起\(a_1\),令\(P_t\)表示从\(t\)时刻开始,决策\(S'\)下执行任务\(a_1\)或者是\(a_2\)的时间片,也就是说,\(|P|=p_1+p_2\)。那么可以发现,无论是\(a_1\)后执行完还是\(a_2\)后执行完,并不影响\(P_t\)中的最大值,因此我们需要其中一个任务尽快执行完。那么,只有当\(P_t\)中的前面的一段连续时间片分给\(a_1\)或者是\(a_2\)时,才能够保证这两个任务之一才能执行完,从而保证这两个任务的执行总时间最小化。由于\(p_2<p_1\),因此将\(P_t\)中的前\(p_2\)个时间片分给\(a_2\)才是最优的。由此构造出决策\(S\)。由于对\(P_t\)中第一个时间片中处理不同,因此\(S\)\(S'\)是不一样的。因此我们从\(S'\)得到了一个更优秀的\(S\)。因此贪心选择性质成立。

最终,这个算法的贪心策略是正确的。使用最小优先队列维护每个任务的加入和完成时间可以以\(O(n\lg n)\)的时间复杂度计算出结果。具体过程由算法GEN-SCHEDULE给出。因为使用了最小优先队列,整个过程的时间复杂度为\(O(n\lg n)\)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 假设所有任务已经按释放时间r排好序。
GEN-SCHEDULE(r, p, n)
// 优先队列的排序关键字为time,也就是剩余运行时间,卫星数据为ID,也就是当前任务编号。
let Q be a new MIN-PRIORITY
l = 1
r = 1
// 上一次处理的时刻。
pre-time = 0
while l <= n
time = r[i] - pre-time
while not PRIORITY-EMPTY(Q) and time > 0
p = EXTRACT-MIN(Q)
w = min{time, p.time}
time = time - w
print run task p.ID w time
p.time = p.time - w
if p.time == 0
print task p.ID finish
else
INSERT(Q, p)
while r <= n
let p be a new node
p.time = p[i]
p.ID = r[i]
INSERT(Q, p)
r = r + 1
l = r
while not PRIORITY-EMPTY(Q)
p = EXTRACT-MIN(Q)
print run task p.ID p.time time units
print task p.ID finish