CPSC 320: Intermediate Algorithm Design and Analysis

CPSC 320: Intermediate Algorithm Design and Analysis
Assignment #0, due Friday, August 1th , 2014 at 18:00
1. In this question, we consider the following problem:
Given an array A containing n integer values (which could be positive, negative or 0),
find the largest sum that can be obtained by adding elements of the array, given that we
are not allowed to use elements at two consecutive positions in the array.
a. A not-overly-bright Faculty member tries to solve the problem using the following greedy
algorithm:
Algorithm LargestFirst(A)
for all elements A[i] of A in decreasing order
if A[i] > 0 and A[i-1] and A[i+1] are not selected
select A[i]
endfor
Return the sum of selected elements
Give an example where algorithm LargestFirst does not return the largest sum of nonconsecutive array elements of A.
Solution : Consider the array [5, 8, 5]. The greedy algorithm will return 8, but we can clearly
obtain a sum of 10 by using the first and last elements instead.
In the remainder of this question, you will design a dynamic programming algorithm to solve the
problem. For each integer i, we compute the following two sums:
• Using(i): the maximum sum we can obtain by adding non-consecutive elements from A[0] . . . A[i],
including the element A[i] in the sum.
• NotUsing(i): the maximum sum we can obtain using non-consecutive elements from A[0] . . . A[i],
without including the element A[i] in the sum.
b. First give a recurrence relation for Using(i) as a function of NotUsing(i − 1).
Solution :
Using(i) = A[i] + NotUsing(i − 1).
c. Now give a recurrence relation for NotUsing(i) as a function of the two values Using(i − 1)
and NotUsing(i − 1).
Solution :
NotUsing(i) = max{NotUsing(i − 1), Using(i − 1)}.
d. Using these two recurrence relations, describe a dynamic programming algorithm that computes the maximum sum for an input array A. Your algorithm should return the positions of
the elements of A that are used to compute the sum.
Solution :
Algorithm LargestNonConsecutiveSum(A)
n ← length[A]
//
1
// Add a dummy entry at the end of the array so we
// can use Without(n) as starting point to construct
// the solution.
//
A[n] ← -1
//
// Base cases.
//
Using[0] ← A[0]
NotUsing[0] ← 0
for i ← 1 to n do
Using[i] ← A[i] + NotUsing[i-1]
if (NotUsing[i-1] > Using[i-1]) then
NotUsing[i] ← NotUsing[i-1]
HowForNotUsing[i] ← "not using i-1"
else
NotUsing[i] ← Using[i-1]
HowForNotUsing[i] ← "using i-1"
endif
endfor
positions ← new array
how ← "not using i-1" // won’t use A[n] = -1
while (n ≥ 0) do
if (how = "using i-1") then
add n to the positions array
how ← "not using i-1"
else // how = "not using i-1"
how ← HowForNotUsing[i-1]
endif
n ← n - 1
endwhile
return positions
e. Analyze the running time of your algorithm as a function of n.
Solution :
The algorithm described in the solution to part (d) runs in Θ(n) time.
2. This problem is much harder than the two you were asked to solve, and I can’t really think of
any practical application for it. On the other hand, it’s a fun puzzle, and it raises most of the issues
that come up when designing dynamic programming algorithms.
Consider the binary operator @ defined by the following ”addition” table. Note that @ is not
associative, and that it is not commutative either (for instance 2 @ 1 = 2, but 1 @ 2 = 3).
2
@
1
2
3
1
2
2
1
2
3
2
1
3
1
3
3
Design an efficient algorithm that takes in an expression x1 @ x2 @ x3 @ . . . @ xn , where each
xi is either 1, 2 or 3, and determines if it is possible to insert parentheses in that expression
so it evaluates to 1. For instance, your algorithm should return YES for 1 @ 2 @ 2 @ 2 @ 2, since
(1 @ 2) @ (2 @ (2 @ 2)) = 1. Hint: think of finding which @ to use as the “outermost” operation.
Solution : We use dynamic programming (surprised? probably not. . . ). We will use our 5 steps
process (as usual):
(a) As a first attempt, we might let the subproblems be all possible sub-expressions xi @ xi+1 @
. . . @ xj where i ≤ j. That is, we might use i and j as the parameters that define a subproblem.
However, one then realizes that in order to know if the expression evaluates to 1, we might
need to know if it is possible for some of the subexpressions to evaluate not only to 1, but also
to 2 or 3. To be fair, you probably would not realize this until you try writing the recurrence
relation in step 3; however I don’t want to have to redo part of the solutions, so I’m pointing
this out now.
So we will have three parameters i, j and v (where v will have value 1, 2 or 3).
(b) We will let Ei,j,v be true if it is possible to insert parentheses in the expression xi @ xi+1 @
. . . @ xj so that the expression evaluates to v.
(c) Now comes the hard part: coming up with a recurrence relation. For each value of v, we look
at every possible position k of the last @ being evaluated, and treat the parts of xi @ xi+1 @
. . . @ xj to the left and right of that @ as our subproblems. In other words, we act as if we
had inserted parentheses around the part to the left of that @, and then around the part to
the right of the @, as in
(xi @ . . . @ xk ) @ (xk+1 @ . . . @ xj )
So, let us now write the recurrence. The base cases occur when the expression contains a
W
P
single integer. We will use the same way we would use .

v = xi


j−1


_



((Ei,k,1 ∧ Ek+1,j,3 ) ∨ (Ei,k,3 ∧ (Ek+1,j,1 ∨ Ek+1,j,2 )))




k=i

 j−1
Ei,j,v = _
((Ei,k,1 ∧ Ek+1,j,1 ) ∨ (Ei,k,2 ∧ (Ek+1,j,1 ∨ Ek+1,j,2 )))




k=i



j−1


_


((Ei,k,1 ∧ Ek+1,j,2 ) ∨ (Ek+1,j,3 ∧ (Ei,k,2 ∨ Ei,k,3 )))

if i = j
if i < j and v = 1
if i < j and v = 2
if i < j and v = 3
k=i
So, for instance, if it is possible for xi @ . . . @ xk to evaluate to 1, and for xk+1 @ . . . @ xj to
evaluate to 3, then that means that Ei,j,1 will be true, etc.
(d) We will compute the entries of the table in the same order as in problem 1: by increasing
value of j − i. For each fixed j − i, the order in which we consider the values of k does not
matter.
3
(e) Here is finally the algorithm. As we have observed in class, all we need is collection of loops
to compute the Ei,j,v entries in the correct order, and code to compute the recurrence relation
for each entry. We will moreover keep track of the value of k we used (and why it was used),
so we can reconstruct the parenthesization of the input that gave us our “1” answer.
Algorithm CanWeGetOne(X)
n ← length[X]
for i ← 1 to n do
for v ← 1 to 3 do
E[i,i,v] ← (X[i] = v)
for len ← 2 to n-1 do
for i ← 1 to n - len do
j ← i + len - 1
//
// Compute E[i,j,1]
//
E[i,j,1] ← false
for k ← i to j-1 do
if (E[i,k,1] and E[k+1,j,3]) then
E[i,j,1] ← true
Which[i,j,1] ← k
How[i,j,1,left] ← 1
How[i,j,1,right] ← 3
break
if (E[i,k,3] and E[k+1,j,1]) then
E[i,j,1] ← true
Which[i,j,1] ← k
How[i,j,1,left] ← 3
How[i,j,1,right] ← 1
break
if (E[i,k,3] and E[k+1,j,2]) then
E[i,j,1] ← true
Which[i,j,1] ← k
How[i,j,1,left] ← 3
How[i,j,1,right] ← 2
break
//
// Compute E[i,j,2]
//
E[i,j,2] ← false
for k ← i to j-1 do
if (E[i,k,1] and E[k+1,j,1]) then
E[i,j,2] ← true
4
Which[i,j,2] ← k
How[1,j,2,left] ← 1
How[1,j,2,right] ← 1
break
if (E[i,k,2] and E[k+1,j,1]) then
E[i,j,2] ← true
Which[i,j,2] ← k
How[1,j,2,left] ← 2
How[1,j,2,right] ← 1
break
if (E[i,k,2] and E[k+1,j,2]) then
E[i,j,2] ← true
Which[i,j,2] ← k
How[1,j,2,left] ← 2
How[1,j,2,right] ← 2
break
//
// Compute E[i,j,3]
//
E[i,j,3] ← false
for k ← i to j-1 do
if (E[i,k,1] and E[k+1,j,2]) then
E[i,j,3] ← true
Which[i,j,3] ← k
How[i,j,3,left] ← 1
How[i,j,3,right] ← 2
break
if (E[i,k,2] and E[k+1,j,3]) then
E[i,j,3] ← true
Which[i,j,3] ← k
How[i,j,3,left] ← 2
How[i,j,3,right] ← 3
break
if (E[i,k,3] and E[k+1,j,3]) then
E[i,j,3] ← true
Which[i,j,3] ← k
How[i,j,3,left] ← 3
How[i,j,3,right] ← 3
break
//
// Now figure out the expression. We will use a recursive
// helper for that.
5
//
if (E[1,n,1]) then
return GetExpression(1,n,1)
return "Unable to obtain 1"
Algorithm GetExpression(i,j,v)
if (i = j) then
return X[i]
k ← Which[i,j,v]
return "(" + GetExpression(i,k,How[i,j,v,left]) + ") @ (" +
GetExpression(k+1,j,How[i,j,v,right]) + ")"
The running time of our algorithm is in Θ(n3 ).
3. Another fairly hard problem (more useful than the previous one, however). The owners of
an independently operated gas station are faced with the following situation. They have a large
underground tank in which they store gas; the tank can hold up to L litres at one time. Ordering
gas is quite expensive, so they want to order relatively rarely. For each order, they need to pay a
fixed price P for delivery in addition to the cost of the gas ordered. However, it costs c to store a
litre of gas for an extra day, so ordering too much ahead increases the storage cost.
They are planning to close for a week in the winter, and they want their tank to be empty by the
time they close. Luckily, based on years of experience, they have accurate projections for how much
gas they will need each day until this point in time. Assume that there are n days left until they
close, and that they need li litres of gas for each of the days i = 1, 2, . . . , n, for some value li ≤ L.
Assume that the tank is empty at the end of day 0. Give an algorithm to decide on which days
they should place orders, and how much to order so as to minimize their total cost.
Hint: define a recurrence for C[d, l]: the minimum cost for the first d days assuming you want to
be left with l litres in the tank at the end of day d.
Solution : Let us assume that a litre of gas costs c∗ . When we define the recurrence for C[d, l],
we need to consider all possible orders of gas for day d and their costs. If the tank had y litres of
gas left at the end of day d − 1, and the gas station receives x litres of gas at midnight, then the
tank will contain y + x − ld litres of gas at the end of day d. Hence
l = y + x − ld
which means that
y = l + ld − x
The smallest amount of gas that the owners of the gas station might need to receive on day d in
order to have l litres left by the end of the day is therefore l + ld − L litres (this corresponds to
y = L). The largest amount of gas that the owners of the gas station might need to receive would
be l + ld litres (that’s if y = 0).
6
Let P (x) be the delivery cost for x litres of gas:
P (x) =
0
P
if x = 0
.
if x > 0
We end up with the following recurrence:
C[d, l] =
min
max{0,l+ld −L}≤x≤l+ld
{P (x) + c∗ x + C[d − 1, l + ld − x] + c(l + ld − x)}
where P (x) is the delivery cost, c∗ x is the price of the gas that was delivered, C[d − 1, l + ld − x]
is the price for the previous d − 1 days, and c(l + ld − x) accounts for the gas that was left in the
tank at the end of day d − 1.
We therefore get the following algorithm; we will once again store for each C[d, l] the value of x
that gave us the optimal solution.
Algorithm FindMinGasCost(l1 , . . . , ln )
C[0, 0] ← 0
for l ← 1 to L do
C[0, l] ← +∞
for d ← 1 to n do
for l ← 0 to L do
X[d, l] ← -1
C[d, l] ← +∞
for x ← max{ 0, l + ld - L } to l + ld do
cost ← P(x) + c∗ * x + C[d - 1, l + ld - x] + c(l + l_d -x)
if cost < C[d, l] then
C[d, l] ← cost
X[d, l] ← x
endif
endfor
endfor
endfor
l ← 0
for d ← n downto 1 do
if X[d, l] > 0
print "Order X[d, l] litres of gas on day d"
endif
l ← l + ld - x
endfor
The running time of the algorithm is in O(L2 n).
7