Solutions - UCSD VLSI CAD Laboratory

CSE 101 Homework 3
March 4, 2015
CSE 101: Design and Analysis of Algorithms
Winter 2015
Homework 3
Due: February 12, 2015
1. Given an array of non-negative integers, you are initially positioned at the first index of the array
(we will call this index 0). Each element in the array represents your maximum jump length at that
position. Your goal is to reach the last index in the minimum number of jumps.
For example:
Suppose you are given array A = [2, 3, 1, 1, 4]. The minimum number of jumps to reach the last index
is 2. (Starting with index 0, move to index 1 with the first jump. Move 3 indices with the second jump
and reach index 4.)
Give a linear time algorithm to find the minimum number of jumps required to reach the last index.
-make a graph with each index being a vertex and an edge from each vertex to its jump destination
-then you have a DAG and can find shortest path
(a) Algorithm Description
Use a greedy algorithm. With each jump, jump to the index that would theoretically allow you
to reach the highest index possible with the next jump. That is, jump the the index i with the
highest i + A[i], where i is in the set of possible indices you can jump to from your current index.
(b) Pseudocode
Array of positive integers A
return minimumJumpSequence(A, 0)
define minimumJumpSequence(A, startIndex)
1 currentIndex = startIndex
2 solution = []
3 while currentIndex ! = n − 1
4
bestIndex = null
5
bestReachability = 0 6
for possibleIndex in indices reachable from currentIndex
7
currentReachability = max(possibleIndex + A[possibleIndex], len(A)-1)
8
if currentReachability > bestReachability
9
bestIndex = possibleIndex
10
currentIndex = bestIndex
11
solution.append(bestIndex)
12 return solution
(c) Proof of Correctness
Assume for contradiction that an optimal solution exists that is strictly better than our greedy
solution. This means that the two solutions must be different. Let i be the last index that both
solutions reach before diverging and becoming different.
Say the greedy solution jumps to some index jg and the optimal solution jumps to some index
jo . We know that because of the way our greedy solution is set up, no indices before jg can
1
reach farther in one jump than jg , otherwise they would have been chosen instead of jg by our
algorithm.
By the way we designed our algorithm, every index reachable by jo must also necessarily be
reachable by jg , because the greedy algorithm chose the index out of the reachable indices that
could reach the furthest index in the array. This means that on the next jump, the greedy
algorithm gets to choose from the same indices as the optimal solution, and possibly more. Since
the greedy algorithm chooses the index with the largest reach, it is never possible for the optimal
algorithm to be able to reach indices that the greedy algorithm cannot. Therefore, it is impossible
for the optimal algorithm to reach the end of the array in fewer jumps than the greedy algorithm,
as that would require at some step for the optimal algorithm to have a larger reachable range than
the greedy algorithm. This contradicts our assumption that an optimal algorithm exists that is
strictly better than the greedy algorithm. Therefore, the greedy algorithm must be optimal.
(d) Running Time analysis
You only need to do one i + A[i] calculation per index in the array. That will take O(n) time.
In addition, before each jump, you must calculate the minimum of i + A[i] values of the numbers
reachable by a jump from the current index. However, any such index can be used in at most two
max calculations.
Suppose you start at some index j, and you can jump a maximum of A[j] indices. Consider some
index k that can be jumped to from j. j + A[k] will be used in calculating the maximum that will
dictate which index to jump to next. There are two possible scenarios that can arise from this
situation:
i. The next jump jumps to an index that is k or greater. Then, k + A[k] will not be used in
another maximum calculation for the rest of the algorithm’s duration, since it is only possible
to jump forward.
ii. The next jump lands at an index less than k. Then, k+A[k] will be used in the next maximum
calculation. However, since our algorithm chose the index before k, call it l, it means that
l + A[l] is greater than k + A[k]. The same logic applies to any indices between l and k. This
means that the algorithm will never choose an index before k, because it is possible to jump
to index l + A[l] from l, and that index already exceeds k + A[k], regardless of the value of
A[l + A[l]].
Therefore, each of the n indices can be used in a maximum of two maximum calculations. Overall,
this means that the algorithm has an O(n) running time.
0
2. A feedback edge set of an undirected graph G = (V, E) is a subset of edges E ⊂ E that intersects
0
every cycle of the graph. Thus, removing the edges E will render the graph acyclic.
Give an efficient algorithm for the following problem:
Input: Undirected graph G = (V, E) with positive edge weights w
Pe .
0
Output: A feedback edge set E ⊂ E of minimum total weight
we .
e∈E 0
-negate edges and run Kruskal’s. Take rejected edges and everything left at the end.
(a) Algorithm Description
First of all, negate the weights of all of the edges in the graph. Then, run Kruskal’s algorithm.
The feedback edge set will be every edge not in the resultant minimum spanning tree.
(b) Pseudocode
Set of vertices V
Set of edges E
return getFeedbackEdgeSet(V , E)
define getFeedbackEdgeSet(V , E)
1 for edge in E
2
edge.weight = -edge.weight
2
3
4
6
mst = Kruskals(V , E)
solution = E 5 for edge in mst.edges:
solution.remove(edge) 7 return solution
(c) Proof of Correctness
By negating the edges and running Kruskal’s algorithm, we can ensure that the largest edges
will be chosen first for the minimum spanning tree (or forest if the graph is not assumed to be
connected). Kruskal’s rejects edges if adding those edges would connect two vertices that are
already in the same connected component. This means that it rejects edges that if added to the
current partial minimum spanning tree (or forest) would create a cycle. By negating the edge
weights and running Kruskal’s, we are finding the edges that form the maximum spanning tree
(or forest) in the original graph.
Call G0 the graph with the feedback edge set removed. Our feedback edge set must have the
property that if any of the edges are added back into G0 , the graph would have a cycle containing
that edge. We know this because all edges have positive weights, so that if any edges that do not
break up any cycles are added to the set, it would not have minimal weight because it would still
be a feedback edge set with that edge removed.
This means that every edge in the feedback edge set must have as endpoints two vertices that
are connected in G0 . This means that G0 must be a spanning forest, since adding back any edges
must create a cycle but G0 itself does not have any cycles.
Since the graph G has a set number of edges with set weights, in order for the feedback edge
set to have minimal total edge weights, G0 must have maximal total edge weights. By negating
the edge weights and running Kruskal’s, we are able to find the maximal spanning forest, so its
complement must be the minimum weight feedback edge set.
Do not take off points for the tree/forest distinction.
(d) Running Time analysis
You need to negate all edge weights. That takes O(E) time. Then, running Kruskal’s takes
O(E log V ) time. Finally, finding all edges not in the MST takes O(E) time. This means the
overall running time is O(E log V ).
3. (DPV textbook, Problem 5.20.) Give a linear-time algorithm that takes as input a tree and determines
whether it has a perfect matching: a set of edges that touches each node once.
(a) Algorithm Description
We will use a greedy approach to solve this problem. We know that the leaves of a tree must by
definition only have one parent and no children. Therefore, in order for this tree to have a perfect
matching, it must contain the edges that connect the leaves to the tree. We will use a greedy
approach to solve this problem. First, we will choose a leaf and add the edge connecting to it to
our solution. Then, we will remove the vertex on the other side of the edge from the graph, along
with any other edges that connect to it. We will then choose another leaf and repeat until either
all vertices are gone from the graph or there is a vertex with no remaining edges connected to it.
(b) Pseudocode
Set of vertices V
Set of edges E
return hasPerfectMatching(V , E)
define hasPerfectMatching(V , E)
1 while !V .isEmpty()
2
leaf = V .getALeaf()
3
if E.isEmpty()
4
return false 5
parent = leaf.parent
6
for edge in all edges connected to parent
7
E.remove(edge)
8
V .remove(leaf)
3
9
10
V .remove(parent)
return true
(c) Proof of Correctness
Leaves of a tree can only be connected to one edge: the edge to their parents. Therefore, any
perfect matching must contain the edges that connect to the leaves of the tree, or else the perfect
matching would not touch the leaves. Therefore, in constructing a solution, we can add those edges
to our potential solution without loss of generality because any perfect matching must contain
those edges. Therefore, by choosing this edge, we are not eliminating any possible solutions.
By definition, a perfect matching must touch each vertex exactly once. This means that we can
remove all of the vertices that connect to the edges that we have added to our solution, as well
as all of the other edges that connect to those vertices. This is because a perfect matching must
contain edges that touch each vertex exactly once. Since we already selected an edge that touches
the vertices we are removing, we cannot use any other edges that touch those vertices in our
solution. As a result, given the edges we have chosen so far, we are not eliminating any possible
solutions.
As a result, since our algorithm can never eliminate possible solutions, if a solution exists our
algorithm is guaranteed to find it.
(d) Running Time Analysis
For every vertex in the graph, we must either add at most one edge to our solution and check if
it has any edges connected to it. We will remove edges from the graph once per edge. Therefore,
our algorithm requires a constant amount of work per vertex and a constant amout of work per
edge. Therefore, our algorithm will run in O(V + E) time.
4. (DPV textbook, Problem 5.32.) A server has n customers waiting to be served. The service time
required by each customer is known in advance: it is ti minutes for customer i. So if, for example, the
i
P
customers are served in order of increasing i, then the ith customer has to wait
tj minutes.
j=1
We wish to minimize the total waiting time
T =
n
P
(time spent waiting by customer i).
i=1
Give an efficient algorithm for computing the optimal order in which to process the customers.
-greedy algorithm. Choose people who take the shortest amount of time to be served first.
(a) Algorithm Description Use a greedy algorithm. Serve the people who take the shortest amount
of time to serve first. That is, sort the customers by their t values and serve them in that order.
(b) Pseudocode
1 mergesort(customers) //sort by increasing amount of time it takes to serve them
(c) Proof of Correctness
Assume for contradiction that there exists some optimal solution that is strictly better than our
greedy solution. To be strictly better than our greedy solution, it must be different from our
greedy solution. Since our greedy solution sorts the customers by the amount of time it would
take to serve them, the optimal solution must have at least one pair of consecutive customers
where the second customer takes less time to serve than the first customer. From now on, I will
say that a pair of customers that satisfy these conditions are ”out of order”. Call these customers
ci and ci+1 and call their service times ti and ti+1 . By our assumption, we know ti > ti+1 .
We can try flipping their order around so that ci+1 is served before ci . This will not change the
waiting times of any other customers, as switching them will not change the time the schedule
starts serving ci and ci+1 or the time it finishes serving them. If we switch them, ci ’s waiting time
will increase by ti+1 , but ci+1 ’s waiting time will increase by ti , so since we assumed ti > ti+1 , that
4
causes the overall waiting time to go down. Therefore, switching a pair of out of order customers
can only decrease the total waiting time.
Eventually, by switching all out of order customers until none are left, we arrive at the sorted
solution we found with our greedy algorithm. Every swap will decrease the total amount of waiting
time. This raises a contradiction, as we assumed the optimal solution was better than the greedy
solution. Therefore, it is impossible for there to exist a solution better than the greedy solution.
The greedy solution must then be optimal.
(d) Running Time analysis
Mergesorting the customers will take O(n log n) time, where n is the total number of customers.
5. (CLRS textbook, Problem 22.1-4.) Suppose that a weighted, directed graph G = (V, E) has a negativeweight cycle. Give an efficient algorithm to list the vertices of one such cycle. Prove that your algorithm
is correct.
-only one negative weight cycle -run BF 2V − 1 times and see which vertices change between iterations
V and 2V − 1
-negative cycle means that distance values will not converge
-every iteration increases max number of edges in path by 1 -2V − 1 is enough iterations to have a full
2 loops around the cycle
-Still O(V E) because you only increase your work by a power of 2
(a) Algorithm Description
To solve this problem, we will run Bellman-Ford on the graph, but instead of stopping at V − 1
iterations, we will stop at 2V −1 iterations. We will compare the distance values at V −1 iterations
and 2V − 1 iterations. For any vertex v with a changed distance value, there must exist some path
from some vertex on the negative cycle to v. Therefore, if we start at v and follow our previous
vertex pointers backwards until we have visited a vertex twice, we will have found the vertices in
our negative cycle.
(b) Pseudocode
Set of vertices V
Set of edges E
source vertex s findNegativeCycle(V , E, s)
define modifiedBellmanFord(V , E, s)
1 run V − 1 iterations of Bellman-Ford
2 let dist1(v) be the distance after V − 1 iterations of Bellman-Ford for every v ∈ V
3 run another V iterations of Bellman-Ford
4 let dist2(v) be the distance after 2V − 1 iterations of Bellman-Ford for every v ∈ V
5 for every vertex in V
6
if dist1(vertex) ! = dist2(vertex)
8
possibleCycleVertices = [vertex]
9
nextV ertex = vertex.prev //The previous vertex as set by Bellman-Ford
10
while !possibleCycleVertices.contains(nextV ertex)
11
possibleCycleVertices.append(nextV ertex)
12
nextV ertex = nextV ertex.prev
13
return the vertices in possibleCycleVertices from the repeated vertex onward
14 return []
(c) Proof of Correctness
In the absence of negative cycles, the distance values obtained by running Bellman-Ford for V − 1
iterations on a graph will have converged to their true values, meaning that if more iterations are
run, the distances will remain the same. In the presence of a negative cycle, however, it is still
possible to achieve a shorter path by going around the negative cycle.
Recall that at the k th iteration of Bellman-Ford, we have found the shortest path of at most k
5
edges. Furthermore, a cycle in a graph with V vertices can have at most V edges, if it runs
through every single vertex in the graph before returning to a previous vertex. Therefore, if we
run an additional V iterations of Bellman-Ford, we are adding V edges to the maximum size of
the shortest path we are finding. This ensures that the shortest path to any vertex v on the
negative cycle must be to take the shortest path to v from s and then to go around the negative
cycle at least once. This ensures that by following the pointers to previous vertices starting at v
we are able to find every vertex in the negative cycle.
It is possible for vertices not in the negative cycle to change between iterations V − 1 and 2V − 1,
but the only way for that to happen is if one of their ancestors is on the negative cycle. Therefore,
even if you choose to start at a vertex not on the negative cycle, following the previous vertex
pointer will eventually lead you to the negative cycle.
(d) Running Time analysis
Recall that every iteration of Bellman-Ford takes O(E) time. We are doing 2V − 1 iterations, so
just running Bellman-Ford takes O((2V − 1)E) = O(V E) time. To recover the solution, we must
folow the previous vertex pointer backwards along the graph for a maximum of V vertices. This
adds an additional O(V ) to our running time, but that term gets dominated by O(V E) so our
overall running time is O(V E).
6. Suppose you are only interested in finding shortest paths in directed acyclic graphs. Modify the
Bellman-Ford algorithm so that it finds the shortest paths from the starting vertex s ∈ V to all other
vertices in one iteration.
-sort vertices in topological order
-expand out vertices in topological order, using values from the current iteration
-O(V log V ) for the sort, O(E) for the one iteration of BF, for O(V log V ).
(a) Algorithm Description
The first thing we have to do is to sort the vertices in topological order.
Next, iterating through every vertex in topological order, we will update the distances of each
vertex based on the distance of its parents, the same way you do updates in Bellman-Ford. At the
end of the first iteration, each vertex will have its true minimum distance from the source vertex.
(b) Pseudocode
Set of vertices V
Set of edges E
source vertex s Sort V in topological order
modifiedBellmanFord(sorted version of V , E, s)
define modifiedBellmanFord(V , E, s)
1 for vertex in V
2
dist(vertex) = ∞
3 dist(s) = 0
4 for vertex in V − s
5
dist(vertex) = minparent ∈ parents(vertex) dist(parent) + length(parent, vertex)
6
prev(vertex) = whichever parent led to the minimum distance
(c) Proof of Correctness
Because we go through the vertices in topological order, we will set the distance of a vertex’s
parents before we set the distance of the vertex itself. Furthermore, since we are given that the
graph is a DAG, then we can be sure that no negative cycles exist, so that when we update the
distance of a vertex we are considering every possible path from the source vertex to that vertex.
Any such path must come through one of the parents of that vertex, and if we are updating the
vertices in topological order, that guarantees that any vertex’s parents will be updated before the
vertex itself.
When we update a vertex, we can be sure that there are no shorter paths to any of its parents that
have not been considered yet. This logic is similar to the logic that allows us to run Dijkstra’s
6
algorithm with one pass through every vertex instead of with multiple iterations like BellmanFord. As a result, when we update the distance of each vertex to be the minimum combination
of the distance to a parent plus the length of the edge from the parent to the vertex, we can be
assured that the distance of the parents will not change, so that the minimum distance to the
current vertex we are setting will not change.
(d) Running Time analysis
Sorting the vertices in topological order will take O(E+V ). Running one iteration of Bellman-Ford
takes O(E) time. Thus, the overall running time is O(E + V ).
7