Solutions to the readable material from the 12 September 2017 resit: 24 printed subparts in Questions 4–8. Source: released examination archive, member
2016/L/a10097w1.pdf. The cover establishes the 2017 date despite the archive directory name.TODO: Printed pages 2–3, containing Questions 1–3, are absent or unrenderable in the held source. Their contents and subpart labels cannot be recovered from the different June examination.
TODO: Question 1 is on the unavailable printed pages 2–3. No statement, subpart structure or solution is inferred from a different paper.
TODO: Question 2 is on the unavailable printed pages 2–3. No statement, subpart structure or solution is inferred from a different paper.
TODO: Question 3 is on the unavailable printed pages 2–3. No statement, subpart structure or solution is inferred from a different paper.
The most general polymorphic type is
unfold :: (s -> Bool) -> (s -> a) -> (s -> s) -> s -> [a]The stopping predicate consumes a state, the output function turns that state into one list element, and the transition function supplies another state of the same type. The state type and output type need not coincide. This is a list unfold with separate stopping, output and transition arguments.
iterate :: (a -> a) -> a -> [a]
iterate f x = x : iterate f (f x)Under lazy evaluation, this produces successive states as demanded. An infinite spine does not require all elements to be evaluated at once; productivity still depends on the demanded computations terminating.
The required function composition is
unfold p f g = map f . takeWhile (not . p) . iterate giterate g supplies the successive states, takeWhile (not . p) retains precisely those before the first stopping state, and map f produces their outputs. If the initial state satisfies the stopping predicate, both sides return the empty list. Otherwise both emit the same head and repeat from g x; the same argument applies to every demanded finite prefix of an infinite result.Using the specified list unfold, the definitions are
map f = unfold null (f . head) tail
iterate f = unfold (const False) id fFor
map, the current state is the remaining input list. null prevents head and tail from being evaluated on an empty list. For iterate, the stop predicate is always false and the output is the current state itself. Its transition is f, so the original seed is emitted before the first application of f.Store two consecutive Fibonacci numbers as the state:
fibs :: [Integer]
fibs = unfold (const False) fst step (0,1)
where
step (a,b) = (b,a+b)The invariant is that the state is , with . Emitting its first component and updating the pair therefore produces .
Integer avoids fixed-width overflow; the bit cost of adding increasingly large integers is separate from the number of unfold steps.Arrange that each state already begins with the next passing element, or is empty:
filter p = unfold null head next . seek
where
seek = dropWhile (not . p)
next = seek . tailThe initial
seek skips the rejected prefix. After an element is emitted, next removes it and skips the following rejected run. Thus null guards both partial list operations and the filtering a list by unfolding invariant is preserved.For a finite input of length and constant-cost predicate evaluation, each input element is tested once and each list cell is passed a bounded number of times: the total work is , as for the usual recursive list filter. This version may have different constant overhead. Recomputing
dropWhile independently in the stopping, output and transition expressions could repeat scans; keeping the already-filtered suffix as state avoids that. On an infinite rejected tail, finding another output diverges, just as the standard lazy filter does.Initialize and every other tentative distance to infinity. The Bellman-Ford algorithm repeatedly relaxes every directed edge:
for v in V: d[v] = infinity; parent[v] = none
d[s] = 0
repeat |V|-1 times:
changed = false
for each edge (u,v) of weight w:
if d[u] != infinity and d[u] + w < d[v]:
d[v] = d[u] + w
parent[v] = u
changed = true
if not changed: break
for each edge (u,v) of weight w:
if d[u] != infinity and d[u] + w < d[v]:
report a reachable negative-weight cycleAn unreachable vertex retains infinity. A reachable negative-weight cycle prevents finite shortest-walk distances for vertices reachable from that cycle, so the detection case must not be reported as an ordinary finite answer. With edge-list scans the worst-case work is , conventionally stated as , and the auxiliary storage is beyond the graph.
Every finite distance label is the weight of a discovered directed walk, so it can never underestimate a finite shortest-path distance. After complete passes, each label is at most the cost of every source-to-vertex path using at most edges. To prove that assertion, consider such a path's last edge : after the previous pass, already has a label no greater than the prefix cost, and the current pass relaxes that last edge. In-place improvements can only make the label smaller, preserving the assertion.
When no negative-weight cycle is reachable, any repeated-vertex portion of an optimal walk can be removed without increasing its cost. Some shortest path therefore has at most edges. The two bounds meet after that many passes, proving correctness. If a complete pass makes no change, all edge relaxation inequalities already hold and further passes cannot improve anything. A further strict relaxation after passes implies a reachable negative cycle by the same simple-path bound.
Bellman–Ford permits negative edge weights, and can detect a reachable negative-weight cycle. The usual settled-label Dijkstra's algorithm requires nonnegative weights for its correctness argument. Negative edges are not themselves a problem for Bellman–Ford when no relevant negative cycle exists.
Use minimum-hop shortest paths by minimizing the lexicographic order pair : total weight first, number of edges second. Extend a pair across an edge of weight by adding , and compare the full pair in both the priority queue and relaxation:
for v in V: key[v] = (infinity,infinity)
key[s] = (0,0)
Q = indexed min-priority-queue containing all vertices, keyed by key
while Q is not empty:
u = extract-min(Q)
if key[u].distance == infinity: break
for each edge (u,v) of weight w with v still in Q:
candidate = (key[u].distance + w, key[u].hops + 1)
if candidate <lex key[v]:
key[v] = candidate
decrease-key(Q,v,candidate)
for v in V: best[v] = key[v].hopsAll weights are positive, so extending a path strictly increases its first component. The usual Dijkstra's algorithm settled-label proof therefore applies to these ordered pairs: no path through a not-yet-settled vertex can later improve an extracted minimum. Among equal minimum weights, the secondary component selects the fewest edges. Merely breaking extraction ties by vertex name would not implement this objective.
With a binary heap and adjacency lists, the bound is , with extra storage. Unreachable vertices have
best = infinity. In the paper's example, the resulting pairs in order are , so the requested edge counts are .Dynamic programming defines subproblem optima and combines their stored answers through a recurrence. It is useful when optimal substructure and overlapping subproblems allow repeated work to be shared. Several alternative choices can be compared before an optimum is selected.
A greedy algorithm instead commits to a locally selected choice and does not later reconsider it. To be correct, its choice needs a justification such as an exchange argument or a safe-cut property, beyond merely having optimal substructure. Neither method is automatically applicable to every optimization problem. Prim's algorithm illustrates a justified greedy choice; the maximum-segment recurrence below illustrates dynamic programming.
For a connected, weighted, undirected graph, grow a tree from any root . For every vertex outside the tree, store the cheapest edge currently connecting it to the tree, not its source-path distance:
for v in V: key[v] = infinity; parent[v] = none
key[r] = 0
Q = indexed min-priority-queue containing all vertices
while Q is not empty:
u = extract-min(Q)
if parent[u] != none: output edge (parent[u],u)
for each edge {u,v} of weight w with v still in Q:
if w < key[v]:
key[v] = w
parent[v] = u
decrease-key(Q,v,w)The maintained key chooses the minimum-weight edge crossing the cut between the tree and remaining vertices. The minimum spanning tree cut property makes that edge safe; induction therefore preserves the existence of a minimum spanning tree containing all selected edges. This proves why Prim's algorithm is greedy and correct. With adjacency lists and a binary heap its time is . For a disconnected graph, starting another component when the minimum key is infinity produces a minimum spanning forest instead of claiming that a spanning tree exists.
Let be the best sum of a segment ending at position , allowing the empty segment with sum zero. The maximum subarray problem recurrence isAn optimal nonempty segment ending at extends the best suffix ending at , or starts fresh; a negative suffix cannot help. Keep its start index and record endpoints whenever a new overall best is found:
suffix = 0; best = 0
start = 0; bestLeft = 0; bestRight = 0
for i = 0,...,n-1:
suffix = suffix + A[i]
if suffix < 0:
suffix = 0; start = i+1
if suffix > best:
best = suffix
bestLeft = start; bestRight = i+1
return A[bestLeft:bestRight]The half-open endpoint interval is empty initially, so empty input and all-negative input correctly return an empty segment. The invariant identifies the best nonnegative suffix and the best segment in the scanned prefix, proving correctness. This is Kadane's algorithm, using time and auxiliary space, excluding the returned slice. The displayed example yields sum , from the segment .
Maintain white, gray and black colors for undiscovered, active and completed vertices. A single traversal clock records DFS discovery and finishing times:
for v in V: color[v] = white; parent[v] = none
time = 0
visit(u):
time = time+1; d[u] = time; color[u] = gray
for v in adjacency[u]:
if color[v] == white:
parent[v] = u
visit(v)
color[u] = black; time = time+1; f[u] = time
for u in V:
if color[u] == white: visit(u)The outer loop makes a depth-first search forest even when the directed graph is disconnected. Each vertex is entered once and each directed adjacency entry is inspected once. Thus an adjacency list representation gives time and auxiliary storage, including the worst-case recursive stack. Sorting neighbors to impose a particular visit order would be additional work.
Relative to the resulting DFS edge classification, a tree edge in depth-first search discovers a white vertex and becomes its parent edge. A forward edge in depth-first search goes to a proper descendant but is not that descendant's discovery edge. A back edge in depth-first search goes to an active ancestor, with a self-loop also counted as a back edge when such edges are allowed. A cross edge in depth-first search joins vertices with no ancestor-descendant relationship in the forest, such as different completed branches or different trees.
The definitions refer to directed edges; an undirected graph is normally classified only with tree and back edges, accounting for the reverse copy of a parent edge separately.
False. Use vertices , edges , , , and start depth-first search at , scanning before . The times areThere is a directed path and , but and are separate children of . Thus is not a descendant of . The path leaves through an already gray ancestor; its later branch need not be explored inside 's recursive call. This is consistent with the DFS parenthesis property.
DFS counterexamples distinguish reachability from tree ancestry
. Left: the path from u through its ancestor x reaches a later sibling v, after u has finished. Right: root order a,u,c makes three singleton DFS trees, though u has an incoming and an outgoing edge. Details.False. The same ordered traversal gives a path , butA path from can pass through an already discovered ancestor, whose remaining edges are scanned only after finishes. A reachability statement alone therefore does not impose the claimed finishing-time bound.
True for a directed cycle. Suppose there were no back edges. On examining an edge , its target would either be white, in which case its recursive call finishes before ; or black, in which case it has already finished. A gray target would be an active ancestor and hence give a back edge, contrary to the supposition. Therefore every edge would satisfy .
Applying those strict inequalities around a directed cycle would imply , a contradiction. Thus some back edge in depth-first search must exist in the search's edge classification. Such an edge is classified relative to the DFS forest; it is not one of the forest's tree edges.
False. Take edges and , and let the outer DFS loop try vertices in order . The first call finishes immediately. The next discovers , whose only outgoing edge reaches already black , so its tree contains only . Finally sees already black .
Vertex has both an incoming edge from and an outgoing edge to , yet is the sole vertex of its DFS tree. Nonzero incoming and outgoing degrees do not require either edge to become a tree edge in depth-first search.
Build a max-heap, then repeatedly swap its root with the last element of the active prefix, shrink that prefix and restore heap order by heapify down. A zero-based implementation is
for i = floor(n/2)-1,...,0:
siftDown(A,i,n)
for end = n-1,...,1:
swap A[0],A[end]
siftDown(A,0,end)siftDown(A,i,end) repeatedly exchanges entry i with its larger child while the child lies below end and has a larger key. The suffix beyond end is already sorted and never touched again. For the specified input, key stages of heapsort areThe final array is . Bottom-up construction takes time because there are progressively fewer nodes at each greater subtree height; the extractions give worst-case time and constant auxiliary storage for iterative sifting.
A deterministic comparison sort on distinct keys must distinguish possible initial orders. Its binary comparison decision tree therefore has at least leaves: a leaf fixes one output ordering and cannot be correct for two different strict orders of the original records.
A binary tree of height has at most leaves, henceThis is the comparison sorting lower bound on the worst-case number of comparisons. Distinct keys have only two possible comparison outcomes, even if an implementation's comparator also supports equality. Algorithms exploiting numeric key values, such as counting sort, fall outside this model.
The printed split uses while naming the current length . For the intended stooge sort, read that split as at every recursive call. Keeping one fixed original instead is not the standard algorithm and does not generally sort correctly.
The three recursive calls then each have length , and all other work is constant. ThusA version of the master theorem sufficient here states: if with constants , fixed-size base cases and for some , then , provided the recursive sizes shrink above the base threshold. Apply it with to obtainThe rounded size is important for correctness; it does not change this asymptotic exponent.
Prefer the alternatives for large unrestricted arrays. Merge sort and heapsort have worst-case time, while insertion sort has worst-case time and can be much faster on nearly sorted data. The intended stooge sort has worst-case , so even insertion sort has the better worst-case exponent.
The first two give the stronger general asymptotic guarantee. Choices between them can depend on storage and stable sorting requirements; these considerations do not rescue Stooge sort's much larger work count.
No: Stooge sort is not stable. Give equal keys distinct record identities and start withThe initial endpoint swap gives . The later two-element calls either see increasing keys or equal keys, so they make no swap. The two records with key 2 have reversed their original order, contradicting stable sorting. The example uses length three, where the intended current-length split is unambiguous.
Articles by others on the same topic
There are currently no matching articles.
