HT2026 Design & Analysis of Algorithms notes


Remaining TODOs: 34

Relevant reading:

T. H. Cormen, C. E. Leiserson, R. L. Rivest and C. Stein. Introduction to Algorithms


1. Program cost and asymptotic notation

Definition 1.1:An algorithm is a finite set of well-defined instructions to accomplish a specific task.
Definition 1.2:An efficient algorithm runs in polynomial time.

Definition 1.3: Insertion sort compares each (๐‘–+1)the element and compares it with the previously sorted ๐‘– elements, inserting it in the correct place.

In CRLS-style pseudocode (as used in Introduction to Algorithms):

Input: An 1-indexed array A of integers
Output: Array A is sorted in non-decreasing order
InsertionSort(A):
  for j = 2 to A.length
    key = A[j + 1]
    // insert A[j + 1] into the sorted sequence A[1..j]
    i = j
    while i > 0 and A[i] > key
      A[i + 1] = A[i]
      i = i - 1
    A[i + 1] = key

Definition 1.4: The running time of a CLRS program is defined as:

  • Line ๐‘– takes constant time ๐‘๐‘–
  • When a loop exits normally, the test is executed one more time than the loop body

Remark: The running time of insertion sort as given is ๐‘‡(๐‘›)=๐‘1๐‘›+๐‘2(๐‘›โˆ’1)+๐‘3(๐‘›โˆ’1)+๐‘4(๐‘›โˆ’1)+๐‘5โˆ‘๐‘—=1๐‘›โˆ’1๐‘ก๐‘—+๐‘6โˆ‘๐‘—=1๐‘›โˆ’1(๐‘ก๐‘—โˆ’1)+๐‘7โˆ‘๐‘—=1๐‘›โˆ’1(๐‘ก๐‘—โˆ’1)+๐‘8(๐‘›โˆ’1) where ๐‘ก๐‘— is the number of times the test of the while loop is executed for a given value of ๐‘—.

Then in the worst case, ๐‘ก๐‘—=๐‘—+1, so ๐‘‡(๐‘›)=๐‘Ž๐‘›2+๐‘๐‘›+๐‘ for some ๐‘Ž,๐‘,๐‘. Hence ๐‘‡(๐‘›) is quadratic in ๐‘›.

In the best case, ๐‘ก๐‘—=1 so ๐‘‡(๐‘›) is linear.

Definition 1.5: Let ๐‘“,๐‘”:โ„•โ†’โ„+. Then

๐‘‚(๐‘”(๐‘›))โ‰”{๐‘“:โ„•โ†’โ„+|โˆƒ๐‘›0โˆˆโ„•+.โˆƒ๐‘โˆˆโ„+.โˆ€๐‘›.๐‘›โ‰ฅ๐‘›0โŸน๐‘“(๐‘›)โ‰ค๐‘โ‹…๐‘”(๐‘›)}.

If ๐‘“โˆˆ๐‘‚(๐‘”(๐‘›)) then ๐‘” is an asymptotic upper bound for ๐‘“.

Proposition 1.6: The algorithm is correct.

Proof:

By a loop-invariant argument:

  • Initialisation - Prove the invariant ๐ผ holds prior to first iteration
  • Maintenance - Prove that if ๐ผ holds just before an iteration, then it holds just before the next iteration
  • Termination: Prove that, when the loop terminates, the invariant ๐ผ along with the reason the loop terminates imply the correctness of the program

This is similar to mathematical induction, but rather than proving for all numbers, we expect to exit the loop.

Invariant: At the start of the ๐‘—th iteration, A[1..j] is sorted.

When ๐‘—=1, ๐ด[1..๐‘—] is a singleton so is trivially sorted.

The outer loop terminates when j = A.length. So the loop invariant at termination says that A[1..A.length] = A is sorted.

To prove maintenance, we need to prove that, at the end of the while loop, the sequence A[1], ..., A[i], key, A[i+2], ..., A[j+1] are sorted.

The invariant of the while loop is: TODO

Remark: Some nice properties of insertion sort:

  • It is stable (preserves relative order of equal keys)
  • In-place
  • Online (can sort the list as it is recieved)

Lemma 1.7: Let ๐‘“,๐‘”,โ„Ž:โ„•โŸถโ„+. Then:

  • โˆ€๐‘>0 . ๐‘“โˆˆ๐‘‚(๐‘”)โŸน๐‘๐‘“โˆˆ๐‘‚(๐‘”)
  • โˆ€๐‘>0 . ๐‘“โˆˆ๐‘‚(๐‘”)โŸน๐‘“โˆˆ๐‘‚(๐‘๐‘”)
  • ๐‘“1โˆˆ๐‘‚(๐‘”1)โˆง๐‘“2โˆˆ๐‘‚(๐‘”2)โŸน๐‘“1+๐‘“2โˆˆ๐‘‚(๐‘”1+๐‘”2)
  • ๐‘“1โˆˆ๐‘‚(๐‘”1)โˆง๐‘“2โˆˆ๐‘‚(๐‘”2)โŸน๐‘“1+๐‘“2โˆˆ๐‘‚(max(๐‘”1,๐‘”2))
  • ๐‘“1โˆˆ๐‘‚(๐‘”1)โˆง๐‘“2โˆˆ๐‘‚(๐‘”2)โŸน๐‘“1โ‹…๐‘“2โˆˆ๐‘‚(๐‘”1โ‹…๐‘”2)
  • ๐‘“โˆˆ๐‘‚(๐‘”)โˆง๐‘”โˆˆ๐‘‚(โ„Ž)โŸน๐‘“โˆˆ๐‘‚(โ„Ž)
  • โˆ€๐‘™>0 . โˆ€๐‘โˆˆ๐’ซ๏ธ€๐‘™ . ๐‘(๐‘›)โˆˆ๐‘‚(๐‘›๐‘™) where ๐’ซ๏ธ€๐‘™ is the set of ๐‘™-degree polynomials
  • โˆ€๐‘>0 . lg(๐‘›๐‘)โˆˆ๐‘‚(lg(๐‘›))
  • โˆ€๐‘,๐‘‘>0 . lg๐‘(๐‘›)โˆˆ๐‘‚(๐‘›๐‘‘)
  • โˆ€๐‘>0,๐‘‘>1 . ๐‘›๐‘โˆˆ๐‘‚(๐‘‘๐‘›)
  • โˆ€0โ‰ค๐‘โ‰ค๐‘‘ . ๐‘๐‘›โˆˆ๐‘‚(๐‘‘๐‘›)
Definition 1.8: If ๐‘“(๐‘›)โˆˆฮฉ(๐‘”(๐‘›)), we say that ๐‘” is an asymptotic lower bound for ๐‘“.
Corollary 1.9: ๐‘“(๐‘›)โˆˆ๐‘‚(๐‘”(๐‘›))โŸบ๐‘”(๐‘›)โˆˆฮฉ(๐‘“(๐‘›)).

Definition 1.10: If ๐‘“(๐‘›)โˆˆฮ˜(๐‘”(๐‘›)), ๐‘” is an asymptotic tight bound for ๐‘“.

๐‘“(๐‘›)โˆˆ๐‘‚(๐‘”(๐‘›))โˆง๐‘“(๐‘›)โˆˆฮฉ(๐‘”(๐‘›))โŸบ๐‘“(๐‘›)โˆˆฮ˜(๐‘”(๐‘›)).
Remark: Big ๐‘‚/ฮฉ/ฮ˜ are not closed under function composition.

2. Divide and conquer algorithms

Definition 2.1: A divide and conquer algorithm divides the problem into subproblems, solves each subproblem separately and combines the results.

Remark: In general, a divide and conquer algorithm working on an input of size ๐‘› can be defined as:

  • If ๐‘› is small, ๐‘›โ‰ค๐“๏ธ€ for some constant ๐“๏ธ€, use constant-time brute force solution
  • Otherwise, divide the problem into ๐‘Ž subproblems, each 1๐‘ the size of the original
  • Let the time to divide a size-๐‘› problem be ๐ท(๐‘›)
  • Let the time to combine solutions be ๐ถ(๐‘›).

Let ๐ท(๐‘›) be the time taken to split a size-๐‘› problem up, and ๐ถ(๐‘›) be the time taken to combine solutions of subproblems.

Then the running time, ๐‘‡(๐‘›), of a divide-and-conquer algorithm can in general be expressed by the following recurrence:

๐‘‡(๐‘›)={๐‘if๐‘›โ‰ค๐“๏ธ€๐‘Ž๐‘‡(๐‘›๐‘)+๐ท(๐‘›)+๐ถ(๐‘›)if๐‘›>๐“๏ธ€.

Theorem 2.2 (Master Theorem):

Suppose

๐‘‡(๐‘›)โ‰ค๐‘Ž๐‘‡(โŒˆ๐‘›๐‘โŒ‰)+๐‘‚(๐‘›๐‘‘).

Then

๐‘‡(๐‘›)={๐‘‚(๐‘›๐‘‘)if๐‘‘>log๐‘๐‘Ž๐‘‚(๐‘›๐‘‘log๐‘๐‘›)if๐‘‘=log๐‘๐‘Ž๐‘‚(๐‘›log๐‘๐‘Ž)if๐‘‘<log๐‘๐‘Ž.

Proof:

TODO

Example (merge sort):

  1. Split array into 2 (๐‘‚(1))
  2. Carry out merge sort of on the subarrays (or for singletons, do nothing)
  3. Merge the elements from the sorted sublists in order, which is easy as we just pick the smallest first element from each

Pseudocode:

MergeSort(A, p, r):
  if r > p + 1
    q = โŒŠ(p + r) / 2โŒ‹
    MergeSort(A, p, q)
    MergeSort(A, q + 1, r)
    Merge(A, p, q, r)

Where Merge is defined as:

// Merge A[p..q] and A[p+1..r] so that A[p..r] is sorted
Merge(A, p, q, r):
  ๐‘›1 = q - p + 1
  ๐‘›2 = r - q
  let L[1..๐‘›1 + 1] and R[1..๐‘›2 + 1] be new arrays
  for i = 1 to ๐‘›1:
  L[i] = A[p + i - 1]
  for j = 1 to ๐‘›2:
    R[j] = A[q + j]
  // Set the last values of the arrays to be larger than
  // anything else we could encounter, so we don't need
  // to check if we have exhausted all the elements of
  // either array if they aren't balanced 
  L[๐‘›1 + 1] = โˆž
  L[๐‘›2 + 1] = โˆž
  i = 1
  j = 1
  for k = p to r:
    if L[i] โ‰ค R[j]:
      A[k] = L[i]
      i = i + 1
    else:
      A[k] = R[j]
      j = j + 1

Remark: Merging should be left-biased (i.e. in case of tie, pick the left one) so that merge sort is a stable sort.

Merge sort is not in-place (requires ฮ˜(๐‘›) extra space) and is not online.

Merge is clearly linear.

Then for MergeSort, in the form given above we have ๐ท(๐‘›)=ฮ˜(1), ๐‘Ž=๐‘=2 and ๐ถ(๐‘›)=ฮ˜(๐‘›).

Then

๐‘‡(๐‘›)={๐‘if๐‘›=12๐‘‡(๐‘›2)+ฮ˜(๐‘›)if๐‘›>1

for some constant ๐‘.

There are a few approaches we could use to solve for ๐‘‡(๐‘›):

  • Guess a solution, and use induction to find constants and prove that it works
  • Draw out a recursion tree and sum each level, either to obtain an exact answer or to gain a heuristic to guess and check
  • Or use the Master Theorem.

Using the Master Theorem, we get that ๐‘‡(๐‘›)=๐‘›log๐‘›.

Example (Integer multiplication): Suppose we want to multiply two ๐‘›-bit integers, ๐‘ฅ and ๐‘ฆ.

A simple divide-and-conquer approach would be to split each number into a top and bottom half, ๐‘ฅ๐‘™,๐‘ฅ๐‘Ÿ,๐‘ฆ๐‘™,๐‘ฆ๐‘Ÿ, then

๐‘ฅ๐‘ฆ=(2๐‘›/2๐‘ฅ๐‘™+๐‘ฅ๐‘Ÿ)(2๐‘›/2๐‘ฆ๐‘™+๐‘ฆ๐‘Ÿ)=2๐‘›๐‘ฅ๐‘™๐‘ฆ๐‘™+2๐‘›/2(๐‘ฅ๐‘™๐‘ฆ๐‘Ÿ+๐‘ฆ๐‘™๐‘ฅ๐‘Ÿ)+๐‘ฅ๐‘Ÿ๐‘ฆ๐‘Ÿ.

Therefore we can compute four multiplications and combine them with three additions (which are linear time in ๐‘›).

The time complexity of this is ๐‘‡(๐‘›)=4๐‘‡(๐‘›2)+๐‘‚(๐‘›), which by the Master Theorem is ๐‘‚(๐‘›2).

However, we can do better.

Definition 2.3: The Karatsuba algorithm, or Karatsuba-Ofman algorithm, is a subquadratic algorithm for integer multiplication. It utilises the fact that

๐‘ฅ๐‘™๐‘ฆ๐‘Ÿ+๐‘ฆ๐‘™๐‘ฅ๐‘Ÿ=(๐‘ฅ๐‘™+๐‘ฆ๐‘™)(๐‘ฅ๐‘Ÿ+๐‘ฆ๐‘Ÿ)โˆ’๐‘ฅ๐‘™๐‘ฅ๐‘Ÿโˆ’๐‘ฆ๐‘™๐‘ฆ๐‘Ÿ,

so we only need three multiplications. This needs more additions, but that is still linear time.

The recurrence is now ๐‘‡(๐‘›)=3๐‘‡(๐‘›2)+๐‘‚(๐‘›)=๐‘‚(๐‘›log32)โ‰ˆ๐‘‚(๐‘›1.59).

Example (matrix multiplication):

A Naรฏve matrix multiplication algorithm, based on the definition, operating on ๐‘›ร—๐‘› matrices has time complexity ๐‘‚(๐‘›3), as it needs to compute ๐‘› multiplications, ๐‘›2 times.

A naรฏve divide-and-conquer approach might split each matrix into four ๐‘›2ร—๐‘›2 matrices, and compute ๐‚=๐€๐ as

(๐‚11๐‚12๐‚21๐‚22)=(๐€11๐€12๐€21๐€22)(๐11๐12๐21๐22).

This requires computing 8 subproblems (๐€11๐11,๐€12๐๐Ÿ๐Ÿ etc), and the addition of the submatrices is quadratic, so the time complexity is

๐‘‡(๐‘›)=8๐‘‡(๐‘›2)+๐‘‚(๐‘›2)=๐‘‚(๐‘›3)by the Master Theorem.

This is no better than the previous approach, but it is possible to improve upon this.

Definition 2.4: Strassenโ€™s algorithm utilises a divide-and-conquer approach with only 7 subproblems.

(๐‚11๐‚12๐‚21๐‚22)=(๐4+๐5โˆ’๐2+๐6๐1+๐2๐3+๐4๐1+๐5โˆ’๐3โˆ’๐7),

where

๐1=๐€11(๐12โˆ’๐22)๐2=(๐€11+๐€12)๐22๐3=(๐€21+๐€22)๐11๐4=๐€22(๐21โˆ’๐11)๐5=(๐€11+๐€22)(๐11+๐22)๐6=(๐€12โˆ’๐€22)(๐21+๐22)๐7=(๐€11โˆ’๐€21)(๐11+๐12).

The recurrence is now ๐‘‡(๐‘›)=7๐‘‡(๐‘›2)+๐‘‚(๐‘›2), which by the Master Theorem is ๐‘‚(๐‘›lg7)โ‰ˆ๐‘‚(๐‘›2.81).

Remark: Suppose a recursion splits into two subproblem with size ๐‘›, and suppose that the division is ๐‘‚(1) and the combining is ๐‘‚(log๐‘›). Then we have ๐‘‡(๐‘›)=2๐‘‡(๐‘›12)+log๐‘›, which isnโ€™t directly expressible with the Master Theorem. We can substitute ๐‘˜=log๐‘› to get ๐‘‡(2๐‘˜)=2๐‘‡(2๐‘˜2)+๐‘˜; if we let ๐‘†(๐‘˜)=๐‘‡(2๐‘˜) then ๐‘†(๐‘˜)=2๐‘†(๐‘˜2)+๐‘˜. We can then use the Master Theorem, with ๐‘Ž=2,๐‘=2,๐‘‘=1, giving that ๐‘†(๐‘˜)=๐‘‚(๐‘˜log๐‘˜). Hence ๐‘‡(๐‘›)=๐‘‚(log๐‘›loglog๐‘›)

Definition 2.5: Binary search searches for the presence or position of an element in a sorted array:

// Search for the presence of z in A[p..r]
BinSearch(A, p, r, z):
  if p >= r:
    return "no"
  q = โŒŠ(p + r) / 2โŒ‹
  if z == A[q]:
    return "yes"
  else if z < A[q]:
    return BinSearch(A, p, q, z)
  else:
    return BinSearch(A, q + 1, r, x)

This has running time ๐‘‡(๐‘›)โ‰ค๐‘‡(โŒˆ๐‘›2โŒ‰)+๐‘‚(1); by the master theorem ๐‘‡(๐‘›)=๐‘‚(log๐‘›)

Definition 2.6: The ๐‘–th-order statistic of a set of ๐‘› (distinct) elements is the ๐‘–th smallest element - i.e. the element that has exactly (๐‘–โˆ’1) smaller elements.
Remark: The median is the โŒŠ๐‘›+12โŒ‹-order statistic.

Definition 2.7: The Select algorithm finds the ๐‘–th-order statistic in linear time. It does this by partitioning the elements of the list around an estimate for the median, based on the medians of sublists of length 5, and from there operating on one of the smaller subgroups.

Select(A, i):
  medians = []
  for j = 1 to A.length by 5:
    sort A[i..i+5)
    medians[โŒˆj / 5โŒ‰] = A[i+2] // "baby median"
  x = Select(medians, โŒŠ(medians.length + 1) / 2โŒ‹) // median of medians
  partition A into lt, gt, based on size compared to x
  k = lt.length
  if i = k + 1:
    return x
  else if i <= k:
    return Select(lt, i)
  else if i > k + 1:
    return Select(gt, i - k - 1)

How good is the median of medians as an estimate of the true median? The number of elements <๐‘ฅ is at least 3๐‘›10โˆ’6.

Proof:TODO

So then the size of each subarray is no more that โŒŠ7๐‘›/10+6โŒ‹.

Then it can be proven that the running time is linear, by guessing that ๐‘‡(๐‘›)=๐‘๐‘› for some constant ๐‘, and using induction.

Proof:TODO

Theorem 2.8: The running time of every comparison-based sorting algorithm is ฮฉ(๐‘›log๐‘›), i.e. at best it is ๐‘‚(๐‘›log๐‘›).

Proof:

In a decision tree of a comparison-based sorting algorithm on an input of length ๐‘›, there are ๐‘›! leaves. Every binary tree of depth ๐‘‘ has at most 2๐‘‘ leaves; so to get to ๐‘›! we need a depth of at least log๐‘›!=ฮฉ(๐‘›log๐‘›).

Definition 2.9: If we know our elements come from a certain discrete interval, we can use counting sort: just count how often each element appears, then insert the relevant number of each elements. This is ๐‘‚(๐‘›+๐‘˜) (=๐‘‚(๐‘›) if ๐‘˜=๐‘‚(๐‘›)) but is not in-place; however we can design it to be stable. The height of a tree is the longest simple path from the root to a leaf; a binary heap with ๐‘› nodes has height โŒŠlog๐‘›โŒ‹.

Input: An array A of n elements with elements in the interval [0..๐‘˜]
Output: An array B consisting of a sorted permutation of A

CountingSort(A, k):
  create array C of size k + 1
  for i = 0 to k:
    C[i] = 0
  for j = 1 to A.length:
    C[A[j]] += 1
  for i = 1 to k:
    C[i] = C[i] + C[i - 1]
  // C[i] now contains the number of elements <= i
  for j = n downto 1:
    B[C[A[j]]] = A[j]
    C[A[j]] -= 1

Definition 2.10: The Fast Fourier Transform (FFT) can be used to multiply two polynomials of degree (๐‘›โˆ’1) in ๐‘‚(๐‘›log๐‘›) time.

  1. Split each polynomial into 2 polynomials of even degree:

    ๐‘Ž0+๐‘Ž1๐‘ฅ+๐‘Ž2๐‘ฅ2+โ€ฆ=(๐‘Ž0+๐‘Ž2๐‘ฅ2+โ€ฆ)+๐‘ฅ(๐‘Ž1+๐‘Ž3๐‘ฅ2+โ€ฆ).
    This is nice because each polynomial is an even function. We now have that ๐ด(๐‘ฅ)=๐ดeven(๐‘ฅ2)+๐‘ฅ๐ดeven(๐‘ฅ2)
  2. Represent each polynomial by a list of values at points, which can be computed in ๐‘‚(๐‘›) by Hornerโ€™s rule,

    ๐ด(๐‘ฅ0)=๐‘Ž0+๐‘ฅ0(๐‘Ž1+๐‘ฅ0(๐‘Ž2+โ€ฆ))

โ€ฆ TODO

Essentially, we split into two polynomials and recurse. Once we are in point-value form, we can multiply each point, all in ๐‘‚(๐‘›), and then convert back to coefficient form (interpolation).

3. Data structures: heaps and queues

Remark: Unsorted arrays have ๐‘‚(1) insertion and ๐‘‚(๐‘›) finding the maximum.
Remark: Sorted arrays have ๐‘‚(๐‘›) insertion (find position by binary search but then shifting is linear) but ๐‘‚(1) finding the maximum.
Corollary 3.1: It is not possible to have a data structure with ๐‘‚(1) insertion and ๐‘‚(1) maximum extraction, as this would give a linear-time comparison-based sorting algorithm which we have seen isnโ€™t possible.

Definition 3.2: A heap is a data structure that is a tree that is completely filled on all levels except the lowest, which is filled from the left to right.

Heaps can be represented with arrays, with each level stored next to each other, read left-to-right and top-to-bottom. This is unambiguous because the tree is complete, so we know how many children each element has.

For a binary tree, if we have the root at A[0], the left child of A[i] is at A[2i + 1] and the right child is at A[2i + 2]. If arrays are 1-indexed, we instead get A[i]โ€˜s children at A[2i] and A[2i + 1].

Definition 3.3: A max-heap is a heap that satisfies the max-heap property.
Definition 3.4: The max-heap property states that, for every node ๐‘– excluding the root, the key of the parent of ๐‘– is greater than or equal to the key of ๐‘–.
Remark: The maximum element of a max-heap is at the root.
Remark: Min-heaps can be defined similarly.
Remark: Children of a node in a max-heap are not necessarily sorted.
Definition 3.5: The height of a node in a heap is defined to be the number of edges in the longest simple path from the node to a leaf. The height of a heap is the height of its root; this means that a singleton heap has a height of 0.

Definition 3.6: The MaxHeapify procedure turns a heap into a max heap, where the two subtrees of the root are already max-heaps. This works by โ€˜bubblingโ€™ the root down into a permissible location.

Input: A 1-indexed array A and index i such that the subtrees of the node A[i] are max-heaps.
Output: A max heap with root at A[i].

MaxHeapify(A, i):
  n = A.size
  l = 2i
  r = 2i + 1
  if l <= n and A[l] > A[i]:
    largest = l
  else:
    largest = i
  if r <= n and A[r] > A[largest]:
    largest = r
  if largest != i:
    swap A[i] and A[largest]
    MaxHeapify(A, largest)

This is ๐‘‚(log๐‘›) because the procedure runs at most once for each depth level in the tree; we can also see this by the master theorem: the two subtrees have a maximum size of 2๐‘›3, occurring on the left if the bottom row is exactly half full, so

๐‘‡(๐‘›)โ‰ค๐‘‡(2๐‘›3)+๐‘‚(1)โŸน๐‘‡(๐‘›)=๐‘‚(๐‘›0log3/2๐‘›)=๐‘‚(log๐‘›).

Definition 3.7: The MakeMaxHeap procedure forms a max heap from an unordered array.

We carry out MaxHeapify on every node, starting from the bottom, but skipping leaves, as they are already singleton max-heaps; allows us to skips about half of the nodes.

MakeMaxHeap(A):
  for i = ceil((n + 1) / 2) - 1 downto 1
    MaxHeapify(A, i)

This is definitely ๐‘‚(๐‘›log๐‘›), but in fact a tighter bound is ๐‘‚(๐‘›).

Proof: MaxHeapify is linear in the height of the node it operates on; therefore given a node of height โ„Ž, MaxHeapify takes time โ‰ค๐‘โ„Ž for some ๐‘โˆˆโ„+. Moreover, in any binary tree there are at most 2log๐‘›โˆ’โ„Ž nodes at height โ„Ž.

So, the running time of MakeMaxHeap is

๐‘‡(๐‘›)โ‰คโˆ‘โ„Ž=1log๐‘›2log๐‘›โˆ’โ„Ž๐‘โ„Žโ‰ค๐‘โˆ‘โ„Ž=0โˆž2log๐‘›โˆ’โ„Žโ„Ž=๐‘โˆ‘โ„Ž=0โˆž๐‘›2โ„Žโ„Ž=๐‘๐‘›โˆ‘โ„Ž=0โˆžโ„Ž2โ„Ž.

Let ๐‘†โ‰”โˆ‘โ„Ž=0โˆžโ„Ž2โ„Ž, and consider 2๐‘†=โˆ‘โ„Ž=0โˆžโ„Ž+12โ„Ž. Then 2๐‘†โˆ’๐‘†=โˆ‘โ„Ž=0โˆž12โ„Ž=2.

Therefore ๐‘‡(๐‘›)โ‰ค2๐‘๐‘›, so ๐‘‡(๐‘›)=๐‘‚(๐‘›).

Remark: To extract the maximum element from a max-heap, we extract the root, place the last vertex into the root, and carry out MakeMaxHeap. This is ๐‘‚(๐‘›).
Remark: To insert an element, append to the end of the backing array, and carry out MaxHeapify recursively on the parents of the new vertex. This is ๐‘‚(log๐‘›).

Definition 3.8: Heap sort:

  1. Build a max-heap
  2. Starting from the root, swap the maximum with the final element in the backing array, and ignore the final node from now on (it remains in the backing array, but we decrement the heap size), then carry out MaxHeapify on the root (this is fine because the children are still max-heaps)
  3. Repeat until the heap size is 1

This is ๐‘‚(๐‘›log๐‘›) worst case like merge sort, but is in-place like insertion sort.

Definition 3.9: A priority queue maintains a set of elements, each with an associated key. Max-priority queues give priority to elements with larger keys; min-priority keys are defined similarly.

Max-priority queues have the following operations:

  • Insert(S, x, k) inserts element x with key k into S
  • Maximum(S) returns the element of S with the largest key
  • ExtractMax(S) removes and returns the element of S with the largest key
  • IncreaseKey(S, x, k) increases the value of xโ€™s key to k, which is assumed to be at least as large as the existing key

If we implement the max-/min-priority queue as a max-/min-heap, both Insert and ExtractMax are ๐‘‚(log๐‘›), and are as described above. Maximum is ๐‘‚(1) and simply returns the root of the heap.

Definition 3.10: IncreaseKey works by modifying the key, and then bubbles the node up until it satisfies the max-heap property. Note that it does not just call MaxHeapify on the parents of the node, as this would unnecessarily visit sibling nodes as well.

Here A is 1-indexed as before.

IncreaseKey(A, i, key):
  require(key >= A[i])
  A[i] = key
  while i > 1 and A[floor(i / 2)] < A[i]:
    swap A[i] and A[floor(i / 2)]
    i = floor(i / 2)

This is also ๐‘‚(log๐‘›) as the while loop cannot run more times than the height of the heap.

4. Dynamic programming

Definition 4.1: Dynamic programming is an optimisation in which we define a sequence of subproblems such that:

  • The subproblems are ordered from smalles to largest
  • The largest is the one we want to solve
  • The optimal solution of a subproblem can be constructed from the optimal solutions of smaller subproblems (this property is optimal substructure)

We then solve from smallest to largest and store the solutions.


Example (Change-making): Suppose we have lots of coins of different denominations, ๐‘ฅ1,โ€ฆ,๐‘ฅ๐‘›, and want to give ๐‘ข units of change.

Then ๐ถ(๐‘ข)โ‰”min#๏ธŽcoins summing up to๐‘ข.

More formally,

๐ถ(๐‘ข)=min1โ‰ค๐‘–โ‰ค๐‘›{๐ถ(๐‘ขโˆ’๐‘ฅ๐‘–)|๐‘ฅ๐‘–โ‰ค๐‘ข}+1;๐ถ(0)=0.

The running time is ๐‘‚(๐‘ข๐‘›). This is polynomial in ๐‘› and ๐‘ฃ (i.e. in the number of inputs), but is exponential in the size of the inputs.


Example (Knapsack problem): A burglar has a knapsack with capacity ๐‘Šโˆˆโ„•. There are ๐‘› itmes to pick from, of size/weight ๐‘ค1,โ€ฆ,๐‘ค๐‘›โˆˆโ„• and value ๐‘ฃ1,โ€ฆ,๐‘ฃ๐‘›โˆˆโ„•. We want to find the most valuable configuration of items to steal, assuming that there is either 1 of each item, or an unlimited quantity of each.

Assuming first an unlimited supply of each item:

๐พ(๐‘ข)=maxvalue of items with weightโ‰ค๐‘ข๐พ(๐‘ข)โ‰”max1โ‰ค๐‘–โ‰ค๐‘›{๐พ(๐‘ขโˆ’๐‘ค๐‘–)+๐‘ฃ๐‘–|๐‘ค๐‘–โ‰ค๐‘ข}๐พ(0)=0.

Total running time is ๐‘‚(๐‘ข๐‘›).

Where there is only one of each item:

๐พ(๐‘ข,๐‘†โІโ„•)=maxvalue for weight๐‘ขand available items๐‘†;๐พ(๐‘ข,๐‘†โІโ„•)โ‰”max๐‘–โˆˆ๐‘†{๐พ(๐‘ขโˆ’๐‘ค๐‘–,๐‘†\{๐‘–})+๐‘ฃ๐‘–|๐‘ค๐‘–โ‰ค๐‘ข};๐พ(0,๐‘†)=๐พ(๐‘ข,โˆ…)=0.

The running time is ๐‘‚(๐‘ข๐‘›โ‹…2๐‘›).

But we can modify ๐พ to make it not exponential, by rather than passing a set of indices, passing the largest index that is considered - since if we remove item ๐‘—, we should have an optimal solution for ๐‘—โˆ’1.

๐พ(๐‘ข,๐‘—)=max{๐พ(๐‘ขโˆ’๐‘ค๐‘—,๐‘—โˆ’1)+๐‘ฃ๐‘—, ๐พ(๐‘ข,๐‘—โˆ’1)}

This formulation is stating that for each index ๐‘—, we either pick that item or discard it.

Now the running time is ๐‘‚(๐‘ข๐‘›).


Example (Longest increasing subsequences): Task: find the longest increasing subsequences of a sequence; the elements in a subsequence donโ€™t need to all be adjacent to each other in the original sequence. TODO

TODO edit distance

Example (Longest simple path): This is an example of a problem that does not have optimal substructure: the longest simple paths from ๐ด to ๐ต and from ๐ต to ๐ถ might share a vertex, so cannot be combined. We would therefore need to store not only the length of the longest path of a subproblem, but also the path itself; so dynamic programming cannot be used for this.

However, if the graph is acyclic, the problem does have optimal substructure.

Example (Travelling salesperson problem): Given a complete undirected graphs with weights ๐‘‘๐‘–๐‘— associated with each edge (๐‘–,๐‘—), find the Hamiltonian cycle with minimal total distance (sum of edge weights).

This is NP-complete, so we donโ€™t know if there is a polynomial time solution, but we can verify a solution in polynomial time.

Brute force is very bad, but dynamic programming gives a better (but not polynomial) solution.

Subproblems: for every subset ๐‘†โІ{1,โ€ฆ,๐‘›} containing 1, and for every ๐‘—โˆˆ๐‘†,๐‘—โ‰ 1, find the shortest path that starts from 1, ends in ๐‘—, and passes only once through all the other nodes in ๐‘†. Define ๐ถ[๐‘†,๐‘—] to be the length of that path.

Then

๐ถ[๐‘†,๐‘—]=min{๐ถ[๐‘†\{๐‘—},๐‘–]+๐‘‘๐‘–๐‘—:๐‘–โˆˆ๐‘†\{1,๐‘—}};๐ถ[{1},{1}]=0.

TODO

Example (A better algorithm for longest increasing subsequence): TODO

5. Graph decomposition

Remark: A directed acyclic graph can be abbreviated to dag.

Definition 5.1: An adjacency matrix of a graph ๐บโ‰”(๐‘‰,๐ธ) is a |๐‘‰|ร—|๐‘‰| matrix ๐€ where

๐‘Ž๐‘–,๐‘—={1if(๐‘‰๐‘–,๐‘‰๐‘—)โˆˆ๐ธ0otherwise.

This has size ๐‘‚(|๐‘‰|2), but has constant lookup for determining if an edge is present.

For undirected graphs, the adjacency matrix is symmetric.

Remark: Given an adjacency matrix ๐ด, (๐ด๐‘›)๐‘–๐‘— is the number of walk of length ๐‘› from ๐‘– to ๐‘—.

If ๐ด represents an undirected graph, (๐ด2)๐‘–,๐‘– is the degree of ๐‘–.

Remark: An adjacency list for a graph ๐บโ‰”(๐‘‰,๐ธ) is a list of |๐‘‰| linked lists, one per vertex: the list for vertex ๐‘ข holds the indices of vertices to which ๐‘ข has an outgoing edge.

This has size ๐‘‚(|๐‘‰|+|๐ธ|) but does not not allow for checking for the presence of an edge in constant time.

For undirected graphs, if ๐‘ข is in ๐‘ฃโ€™s adjacency list, then ๐‘ฃ is in ๐‘ขโ€™s.

Definition 5.2: Depth-first search (DFS) is a linear-time algorithm that tells us what parts of a graph are reachable from a given vertex. It works for both digraphs and undirected graphs.

As soon as a new vertex is discovered, explore from it. As DFS progresses, we assign each vertex a colour:

  • not discovered yet (e.g. white)
  • discovered, but not fully explored yet (e.g. grey)
  • finished (e.g. black)

Input: A graph ๐บโ‰”(๐‘‰,๐ธ)
Output: for each vertex ๐‘ฃโˆˆ๐‘‰, a backpointer ๐œ‹(๐‘ฃ) (the predecessor of ๐‘ฃ) and two timestamps, the discovery time ๐‘‘[๐‘ฃ] and finishing time ๐‘“[๐‘ฃ].

DFS(V, E):
  for u in V:
    colour[u] = white
    pi[u] = null
  time = 0
  for u in V:
    if colours[u] = white:
      DFSVisit(u)

DFSVisit(u):
  time += 1
  d[u] = time
  colour[u] = grey
  for v in neighbours(v):
    if colour[v] = white:
      pi[v] = u
      DFSVisit(v)
  time = time + 1
  f[u] = time
  colour[u] = black

Note that โˆ€๐‘ฃโˆˆ๐‘‰ . 1โ‰ค๐‘‘[๐‘ฃ]<๐‘“[๐‘ฃ]โ‰ค2|๐‘‰|.

Note that ๐œ‹,๐‘‘,๐‘“ are dependent upon the order in which the vertices in the graph are visited, and on the order of the vertices in the adjacency/neighbour lists.

This has running time ๐‘‚(|๐‘‰|+|๐ธ|) because each vertex is visited once, and DFSVisit(v) takes ฮ˜(๐‘‘(๐‘ฃ)) where ๐‘‘(๐‘ฃ) is the degree of ๐‘ฃ; โˆ‘๐‘ฃโˆˆ๐‘‰๐‘‘(๐‘ฃ)=2|๐ธ|.

Definition 5.3: Let ๐ธ๐œ‹ be the edges visited by DFS,

๐ธ๐œ‹={(๐œ‹[๐‘ฃ],๐‘ฃ)|๐‘ฃโˆˆ๐‘‰,๐œ‹[๐‘ฃ]โ‰ null}.

Then let ๐บ๐œ‹=(๐‘‰,๐ธ๐œ‹) be the DFS forest, consisting of DFS trees (note that this is a slight abuse of notation because trees and forests are usually undirected).

Definition 5.4: A vertex ๐‘ข is a descendant of ๐‘ฃ exactly if it is a descendant of ๐‘ฃ in the DFS forest, i.e. there is a path from ๐‘ฃ to ๐‘ข in ๐บ๐œ‹.

Theorem 5.5 (Parenthesis theorem): For all ๐‘ข,๐‘ฃโˆˆ๐‘‰, exactly one of the following holds:

  1. ๐‘‘[๐‘ข]<๐‘“[๐‘ข]<๐‘‘[๐‘ฃ]<๐‘“[๐‘ฃ] or ๐‘‘[๐‘ฃ]<๐‘“[๐‘ฃ]<๐‘‘[๐‘ข]<๐‘“[๐‘ข] and neither of ๐‘ข and ๐‘ฃ are descendants of each other in the DFS forest
  2. ๐‘‘[๐‘ข]<๐‘‘[๐‘ฃ]<๐‘“[๐‘ฃ]<๐‘“[๐‘ข] and ๐‘ฃ is a descendant of ๐‘ข in a DFS tree
  3. ๐‘‘[๐‘ฃ]<๐‘‘[๐‘ข]<๐‘“[๐‘ข]<๐‘“[๐‘ฃ] and ๐‘ข is a descendant of ๐‘ฃ in a DFS tree

Using the shortand โ€œ(๐‘ฅโ€ for ๐‘‘[๐‘ฅ] and โ€œ๐‘ฅ)โ€ for ๐‘“[๐‘ฅ], (๐‘ข ๐‘ข) (๐‘ฃ ๐‘ฃ) and (๐‘ข (๐‘ฃ ๐‘ฃ) ๐‘ข) are possible (and the symmetric cases), but (๐‘ข (๐‘ฃ ๐‘ข) ๐‘ฃ) is not possible.

Corollary 5.6: Vertex ๐‘ฃ is a descendant of vertex ๐‘ข iff ๐‘‘[๐‘ข]<๐‘‘[๐‘ฃ]<๐‘“[๐‘ฃ]<๐‘“[๐‘ข].

Proof:Immediate from the parenthesis theorem.

Theorem 5.7 (White edge theorem): A vertex ๐‘ฃ is a descendant of ๐‘ข iff, at time ๐‘‘[๐‘ข], there exists a path from ๐‘ข to ๐‘ฃ consisting of entirely white vertices.

Proof:TODO

Definition 5.8: We can classify edges of the searched graph:

  • Tree edges (๐‘ข,๐‘ฃ) are edges of the DFS forest; ๐‘ฃ is white when (๐‘ข,๐‘ฃ) is explored;
  • Back edges (๐‘ข,๐‘ฃ) lead from a node to an ancestor in the DFS tree. ๐‘ฃ is grey when (๐‘ข,๐‘ฃ) is explored;
  • Forward edges lead from a node ๐‘ข to a non-child descendant in the DFS tree, i.e. they lead to a descendant but are not edges in the DFS forest. ๐‘ฃ is black when (๐‘ข,๐‘ฃ) is explored;
  • Cross edges do not lead to an ancestor or descendant; this could be between nodes in the same tree or in a different tree. ๐‘ฃ is black when (๐‘ข,๐‘ฃ) is explored.

Remark: We can link the parentheses of ๐‘ข,๐‘ฃ to the classification of the edge (๐‘ข,๐‘ฃ):

  • If ๐‘‘[๐‘ข]<๐‘‘[๐‘ฃ]<๐‘“[๐‘ฃ]<๐‘“[๐‘ข], then (๐‘ข,๐‘ฃ) is either a tree edge or a forward edge.
  • If ๐‘‘[๐‘ฃ]<๐‘‘[๐‘ข]<๐‘“[๐‘ข]<๐‘“[๐‘ฃ], then (๐‘ข,๐‘ฃ) is a back edge.
  • If ๐‘‘[๐‘ฃ]<๐‘“[๐‘ฃ]<๐‘‘[๐‘ข]<๐‘“[๐‘ข], then (๐‘ข,๐‘ฃ) is a cross edge.
  • ๐‘‘[๐‘ข]<๐‘“[๐‘ข]<๐‘‘[๐‘ฃ]<๐‘“[๐‘ฃ] cannot happen because if ๐‘ฃ is not discovered when ๐‘ข is discovered, and (๐‘ข,๐‘ฃ)โˆˆ๐ธ, then ๐‘ฃ would be explored before ๐‘ข is finished.

Theorem 5.9: A directed graph ๐บ has a cycle iff ๐บ has a back edge.

Proof:

โŸธ: If (๐‘ข,๐‘ฃ) is a back edge, then there is a cycle consisting of the back edge and the path in the DFS tree from ๐‘ฃ to ๐‘ข.

โŸน: Suppose โŸจ๐‘ฃ0,โ€ฆ,๐‘ฃ๐‘˜โŸฉ is a cycle, and suppose w.l.o.g. that ๐‘ฃ0 is discovered first in DFS. Then (๐‘ฃ๐‘˜,๐‘ฃ0) is by definition a back edge.

Remark: DAGs can be used to represent dependency problems; if ๐ด depends on ๐ต, draw an edge from ๐ต to ๐ด.

Definition 5.10: A topological sort of a DAG ๐บโ‰”(๐‘‰,๐ธ) is a total ordering of vertices, <โІ๐‘‰ร—๐‘‰, such that if (๐‘ข,๐‘ฃ)โˆˆ๐ธ then ๐‘ข<๐‘ฃ, and otherwise ๐‘ขโ‰ฎ๐‘ฃ.

TopologicalSort(V, E):
  f[v | v in V] = DFS(V, E)
  return V sorted according to decreasing f[v]

This has running time ๐‘‚(|๐‘‰|+|๐ธ|).

Remark: A topological sort gives us an order to complete tasks in the case where a DAG encodes dependencies between tasks.

Proposition 5.11 (correctness of topological sort): Given a DAG ๐บโ‰”(๐‘‰,๐ธ), if (๐‘ข,๐‘ฃ)โˆˆ๐ธ then ๐‘“[๐‘ข]>๐‘“[๐‘ฃ].

Proof: When (๐‘ข,๐‘ฃ) is explored, ๐‘ข is grey (because it has been discovered but not finished). Then by case distinction on the colour of ๐‘ฃ:

  • If ๐‘ฃ is white, then ๐‘ฃ is a descendant of ๐‘ข, and by the parenthesis theorem, ๐‘‘[๐‘ข]<๐‘‘[๐‘ฃ]<๐‘“[๐‘ฃ]<๐‘“[๐‘ข].
  • If ๐‘ฃ is black, then ๐‘ฃ is finished, but ๐‘ข is not yet, so ๐‘“[๐‘ฃ]<๐‘“[๐‘ข].
  • ๐‘ฃ cannot be grey, otherwise ๐‘ข would be a descendant of ๐‘ฃ, so (๐‘ข,๐‘ฃ) would be a backedge. By Theoremย 5.9, there is a cycle in ๐บ, which is a contradiction because ๐บ is a DAG.

Remark: When DFS is applied to an undirected graph, the DFS trees correspond to the connected components of the graph. These can be identified by the discovery and finishing times.
Definition 5.12: In a digraph, two vertices ๐‘ข,๐‘ฃ are strongly connected if there is a path from ๐‘ข to ๐‘ฃ and from ๐‘ฃ to ๐‘ข.
Definition 5.13: A strongly connected component (SCC) of a digraph ๐บโ‰”(๐‘‰,๐ธ) is a maximal set of vertices ๐ถโІ๐‘‰ such that for all ๐‘ข,๐‘ฃโˆˆ๐ถ, ๐‘ข and ๐‘ฃ are strongly connected.

Lemma 5.14: Given distinct strongly connected components ๐ถ,๐ถโ€ฒ of a digraph ๐บ, ๐‘ข,๐‘ฃโˆˆ๐ถ,๐‘ขโ€ฒ,๐‘ฃโ€ฒโˆˆ๐ถโ€ฒ, if there is a path from ๐‘ข to ๐‘ขโ€ฒ in ๐บ, then there is not a path from ๐‘ฃโ€ฒ to ๐‘ฃ in ๐บ.

Proof: Suppose there is a path from ๐‘ข to ๐‘ขโ€ฒ. Then for every ๐‘ฅโˆˆ๐ถ,๐‘ฆโˆˆ๐ถโ€ฒ, there exists a path from ๐‘ฅ to ๐‘ฆ via the path connecting ๐‘ข and ๐‘ขโ€ฒ. If there were a path from ๐‘ฃโ€ฒ to ๐‘ฃ, there would be a path from ๐‘ฆ to ๐‘ฅ via that path, so ๐‘ฅ and ๐‘ฆ would be strongly connected and ๐ถ,๐ถโ€ฒ would be part of the same SCC; contradiction. Therefore there is no path from ๐‘ฃโ€ฒ to ๐‘ฃ.

Definition 5.15: The SCC graph of a digraph ๐บโ‰”(๐‘‰,๐ธ) is the graph ๐บSCCโ‰”(๐‘‰SCC,๐ธSCC), such that

  • ๐‘‰SCC has one vertex ๐‘ฃ๐ถ for every SCC ๐ถ in ๐บ
  • (๐‘ฃ๐ถ,๐‘ฃ๐ถโ€ฒ)โˆˆ๐ธSCC iff there exist ๐‘ขโˆˆ๐ถ,๐‘ขโ€ฒโˆˆ๐ถโ€ฒ such that there is a path from ๐‘ข to ๐‘ขโ€ฒ.
Remark: From Lemmaย 5.14, it follows that any SCC graph is a DAG.

Definition 5.16: We extend discovery and finishing times for sets of vertices ๐‘ˆโІ๐‘‰:

  • ๐‘‘[๐‘ˆ]โ‰”min{๐‘‘[๐‘ข]|๐‘ขโˆˆ๐‘ˆ}, the earliest discovery time amongst ๐‘ˆ;
  • ๐‘“[๐‘ˆ]โ‰”max{๐‘“[๐‘ข]|๐‘ขโˆˆ๐‘ˆ}, the latest finishing time amongst ๐‘ˆ.
Definition 5.17: We say that there is an edge between SCCs ๐ถ,๐ถโ€ฒ if โˆƒ๐‘ขโˆˆ๐ถ,๐‘ขโ€ฒโˆˆ๐ถโ€ฒ s.t. (๐‘ข,๐‘ขโ€ฒ)โˆˆ๐ธ.

Lemma 5.18: Let ๐ถ,๐ถโ€ฒ be distinct SCCs in ๐บโ‰”(๐‘‰,๐ธ). If there is an edge from ๐ถ to ๐ถโ€ฒ, then ๐‘“[๐ถ]>๐‘“[๐ถโ€ฒ].

Proof: This is equivalent to Propositionย 5.11, because if there is an edge (๐‘ขโˆˆ๐ถ,๐‘ฃโˆˆ๐ถโ€ฒ) from ๐ถ to ๐ถโ€ฒ, then ๐‘“[๐‘ข]>๐‘“[๐‘ฃ]; we can choose ๐‘ข and ๐‘ฃ to have maximal finish times within their respective SCCs, so by our extended definition of finish time for sets of vertices, ๐‘“[๐ถ]>๐‘“[๐ถโ€ฒ].

Lemma 5.19: If ๐ถโ‰ ๐ถโ€ฒ and ๐‘“[๐ถ]<๐‘“[๐ถโ€ฒ], then there cannot be an edge from ๐ถ to ๐ถโ€ฒ.

Proof: Equivalent to Lemmaย 5.18 by contraposition.

Definition 5.20: For a digraph ๐บโ‰”(๐‘‰,๐ธ), the transpose of ๐บ is ๐บโŠค=(๐‘‰,๐ธโŠค) where

๐ธโŠค={(๐‘ข,๐‘ฃ)|(๐‘ฃ,๐‘ข)โˆˆ๐ธ}.
Remark: We can create ๐บโŠค in ฮ˜(|๐‘‰|+|๐ธ|) time using adjacency lists.
Theorem 5.21: ๐บ and ๐บโŠค have the same SCCs.

Remark: For distinct SCCs ๐ถ,๐ถโ€ฒ in ๐บ, if ๐‘“[๐ถ]>๐‘“[๐ถโ€ฒ], then

  • in ๐บ there cannot be an edge from ๐ถโ€ฒ to ๐ถ
  • in ๐บโŠค there cannot be an edge from ๐ถ to ๐ถโ€ฒ.

This means that running DFS on ๐บโŠค starting from the SCC with the largest finishing time, we will not find edges to any other SCC.

Definition 5.22: The SCC algorithm, or Kosorajuโ€™s algorithm, identifies all SCCs in a digraph ๐บ:

  • run DFS on ๐บ
  • run DFS on ๐บโŠค, exploring in order of decreasing finishing time of the first DFS
  • the trees in the DFS forest of the second DFS correspond to the SCCs of ๐บ

6. Shortest paths in graphs

Definition 6.1: Consider a digraph ๐บโ‰”(๐‘‰,๐ธ) with weight function ๐‘ค:๐ธโ†’โ„.

The weight or length of a path ๐‘โ‰”โŸจ๐‘ฃ0,โ€ฆ,๐‘ฃ๐‘˜โŸฉ is

๐‘ค(๐‘)โ‰”โˆ‘๐‘–=0๐‘˜โˆ’1๐‘ค(๐‘ฃ๐‘–,๐‘ฃ๐‘–+1).

Definition 6.2: The shortest-path weight between two vertices ๐‘ข,๐‘ฃ is

๐›ฟ(๐‘ข,๐‘ฃ)โ‰”{min{๐‘ค(๐‘)|๐‘is a path from๐‘ขto๐‘ฃ}ifโˆƒa path between๐‘ขand๐‘โˆžotherwise.
Definition 6.3: A shortest path between two vertices ๐‘ข,๐‘ฃ is a path ๐‘ such that ๐‘ค(๐‘)=๐›ฟ(๐‘ข,๐‘ฃ).

Definition 6.4: A FIFO queue (first in, first out) is a data structure with the following operations:

  • enqueue(๐‘„, ๐‘ฅ) - inserts x at the end of a queue
  • dequeue(๐‘„) - removes and returns the item at the head of the queue
  • isempty(๐‘„) - equivalent to ๐‘„โ‰ โˆ…

All of these operations can be implemented in ๐‘‚(1) if using a linked list with a pointer to the start and end.

Definition 6.5: Considering first the simple case where all weights are equal to 1. Let ๐›ฟ(๐‘ข,๐‘ฃ) be the minimum number of edges on a path from ๐‘ข to ๐‘ฃ, if such a path exists, otherwise ๐›ฟ(๐‘ข,๐‘ฃ)=โˆž.

Given a source vertex ๐‘ , breadth-first search (BFS) finds all vertices ๐‘ฃ๐‘–s reachable from ๐‘ , the shortest path lengths ๐›ฟ(๐‘ ,๐‘ฃ๐‘–), and the shortest paths from ๐‘  to the ๐‘ฃ๐‘–s.

Idea:

  • send out waves of increasing length from the source ๐‘ 
  • when a vertex ๐‘ฃ is reached, put it in the (FIFO) queue ๐‘„
  • when all of ๐‘ฃs neighbours have been reached, remove ๐‘ฃ from ๐‘„

The output is a distance d[v] and predecessor ๐œ‹[v] for every v โˆˆ V.

BFS(V, E, s):
  for v โˆˆ V \ { s }:
    d[v] = โˆž
    ๐œ‹[v] = null
  d[s] = 0
  Q = โˆ…
  enqueue(Q, s)
  while Q != โˆ…:
    u = dequeue(Q)
    for v โˆˆ adj(u):
      if d[v] = โˆž:
        d[v] = d[u] + 1
        ๐œ‹[v] = u
        enqueue(Q, v)
Remark: BFS takes time ๐‘‚(|๐‘‰|+|๐ธ|), because every vertex will be put in the queue (and extracted) exactly once, and the inner adj(u) loop will run exactly once for every edge across the whole running on the algorithm.

Definition 6.6: The BFS tree consists of vertices reachable from ๐‘  and edges (๐‘ข,๐‘ฃ) where ๐‘ข=๐œ‹[๐‘ฃ].

More formally, the BFS tree is the graph

๐บ๐œ‹โ‰”({๐‘ฃโˆˆ๐‘‰|๐œ‹[๐‘ฃ]โ‰ null}โˆช{๐‘ },{(๐‘ข,๐‘ฃ)โˆˆ๐ธ|๐‘ข=๐œ‹[๐‘ฃ]})
Lemma 6.7: TODO there exists a path of length ๐‘‘[๐‘ฃ]
Corollary 6.8:TODO lower bound on ๐‘‘

Lemma 6.9: If ๐‘ข is enqueued before ๐‘ฃ, then ๐‘‘[๐‘ข]โ‰ค๐‘‘[๐‘ฃ]. Furthermore, if ๐‘„=โŸจ๐‘ฃ1,โ€ฆ,๐‘ฃ๐‘ŸโŸฉ is the queue at any given step of BFS, then

๐‘‘[๐‘ฃ1]โ‰ค๐‘‘[๐‘ฃ2]โ‰คโ€ฆโ‰ค๐‘‘[๐‘ฃ๐‘Ÿ]โ‰ค๐‘‘[๐‘ฃ1]+1
Proof: TODO by induction
Lemma 6.10: TODO upper bound on ๐‘‘

Theorem 6.11: ๐‘‘[๐‘ฃ]=๐›ฟ(๐‘ ,๐‘ฃ).

Proof: TODO just follows from previous lemmas; also consider ๐‘‘[๐‘ฃ]=โˆž.
Theorem 6.12: If 0<๐‘‘[๐‘ฃ]<โˆž, then ๐œ‹[๐‘ฃ] is the predecessor of ๐‘ฃ TODO
Remark: BFS uses a queue but DFS uses a stack (possibly implicitly).

Definition 6.13: Dijkstraโ€™s algorithm is essentially BFS for weighted graphs. It solves the โ€œsingle-source shortest pathโ€ problem for non-negative weights.

It uses a min-priority queue ๐‘„ rather than a FIFO queue, with keys given by the shortest-path weight estimates ๐‘‘[๐‘ฃ].

At termination:

  • ๐‘‘[๐‘ฃ] is the distance from ๐‘  to ๐‘ฃ
  • ๐œ‹[๐‘ฃ] is the predecessor of ๐‘ฃ on a shortest path from ๐‘  to ๐‘ฃ, if such a path exists
Dijkstra(V, E, w, s):
  for each v โˆˆ V:
    d[v] = โˆž
    ๐œ‹[v] = null
  d[s] = 0
  Q = MakeMinQueue(V) with d[v] as keys
  while Q != โˆ…:
    u = ExtractMin(Q)
    for each v โˆˆ adj(u):
      if d[u] + w(u, v) < d[v]:
        d[v] = d[u] + w(u, v)
        ๐œ‹[v] = u
        DecreaseKey(Q, v, d[v])

Invariants for the while loop:

๐ผ1โ‰”โˆ€๐‘ฃโˆˆ๐‘‰ . ๐‘‘[๐‘ฃ]โ‰ฅ๐›ฟ(๐‘ ,๐‘ฃ);๐ผ2โ‰”โˆ€๐‘ฃโˆˆ๐‘†โ‰”๐‘‰\๐‘„ . ๐‘‘[๐‘ฃ]=๐›ฟ(๐‘ ,๐‘ฃ).

TODO initialisation

Then suppose that ๐ผ1 holds before an iteration of the while loop. For any vertex ๐‘ฃโˆˆ๐‘‰, either ๐‘‘[๐‘ฃ] did not change, or it did change. If ๐‘‘[๐‘ฃ] did not change, then ๐‘‘[๐‘ฃ]โ‰ฅ๐›ฟ(๐‘ ,๐‘ฃ) by ๐ผ1. Otherwise, let ๐‘ข be the vertex selected by ExtractMin in this iteration of the loop. Then we have that

๐‘‘[๐‘ฃ]=๐‘‘[๐‘ข]+๐‘ค(๐‘ข,๐‘ฃ)โ‰ฅ๐›ฟ(๐‘ ,๐‘ข)+๐‘ค(๐‘ข,๐‘ฃ)by๐ผ1โ‰ฅ๐›ฟ(๐‘ ,๐‘ฃ)because๐›ฟ(๐‘ ,๐‘ข)+TODO

Now suppose that ๐ผ2 holds before an iteration. Then for any ๐‘ฃโˆˆ๐‘†โ€ฒ, either:

  • ๐‘ฃโˆˆ๐‘†โ€ฒ, then ๐‘‘[๐‘ฃ]=๐›ฟ(๐‘ ,๐‘ฃ) by ๐ผ2
  • ๐‘ฃโˆ‰๐‘† and there is no path from ๐‘  to ๐‘ฃ, then ๐‘‘[๐‘ฃ]โ‰ฅ๐›ฟ(๐‘ ,๐‘ฃ)=โˆžโŸน๐‘‘[๐‘ฃ]=โˆž
  • ๐‘ฃโˆ‰๐‘† and there is a path from ๐‘  to ๐‘ฃ. Then there is also a shortest path ๐‘ โ† ๐‘๐‘ฃ TODO

TODO finish maintenance of ๐ผ2

At termination, ๐‘†=๐‘‰, so โˆ€๐‘ฃโˆˆ๐‘‰ . ๐‘‘[๐‘ฃ]=๐›ฟ(๐‘ ,๐‘ฃ).

TODO remark about optimal substructure

Lemma 6.14 (Convergence property): TODO

Theorem 6.15: For ๐‘ฃโ‰ ๐‘ ,๐‘‘[๐‘ฃ]<โˆž, ๐œ‹[๐‘ฃ] is the predecessor of ๐‘ฃ on a shortest path from ๐‘  to ๐‘ฃ.

Proof: TODO
Remark: Total running time of Dijkstraโ€™s algorithm is ๐‘‚((|๐‘‰|+|๐ธ|)log|๐‘‰|), because ExtractMin (๐‘‚(log|๐‘‰|) is executed |๐‘‰| times, and DecreaseKey (๐‘‚(log|๐‘‰|)) is executed |๐ธ| times.

Definition 6.16: The Bellman-Ford algorithm takes a graph with weight function ๐‘ค:๐ธโ†’โ„ (possibly negative), and returns false if there exists a negative-weight cycle reachable from ๐‘ , otherwise true, along with ๐‘‘[๐‘ฃ],๐œ‹[๐‘ฃ] for each ๐‘ฃโˆˆ๐‘‰.

BellmanFord(V, E, w, s):
  for each v โˆˆ V:
    d[v] = โˆž
    ๐œ‹[v] = null
  d[s] = 0
  for i = 1 to |v| - 1: // run enoguh times to correctly compute distamce
    for each (u, v) โˆˆ E:
      if d[u] + w(u, v) < d[v]: // Loop body the same as Dijkstra!
        d[v] = d[u] + w(u, v)
        ๐œ‹[v] = u
  for (u, v) โˆˆ E:
    if d[u] + w(u, v) < d[v]: // check for negative-weight cycles
      return false
  return true

This is ๐‘‚(|๐‘‰||๐ธ|).

TODO invariants

TODO full topological sort algorithm for DAGs to find shortest path

Definition 6.17: The Floyd-Warshall algorithm solves the all-pairs shortest paths problem for graphs with non-negative weights; that is, it gives the shortest path between every pair of vertices.

Suppose we have vertex set ๐‘‰={1,โ€ฆ,๐‘›}.

Let ๐‘‘[๐‘–,๐‘—;๐‘˜] be the length of the shortest path from ๐‘– to ๐‘—, all of whose intermediate nodes are in the interval [1,๐‘˜].

We initialise

๐‘‘[๐‘–,๐‘—;0]={๐‘ค(๐‘–,๐‘—)if(๐‘–,๐‘—)โˆˆ๐ธโˆžotherwise.

Since there are no negative cycles, a shortest path from ๐‘– to ๐‘— using vertices in [1,๐‘˜] goes through ๐‘˜ at most once, so we use the recurrence

๐‘‘[๐‘–,๐‘—;๐‘˜+1]=min{๐‘‘[๐‘–,๐‘—;๐‘˜],๐‘‘[๐‘–,๐‘˜+1;๐‘˜]+๐‘‘[๐‘˜+1,๐‘—,๐‘˜]}.

This covers all possible routes because we consider all ๐‘˜, and choose to either pass through ๐‘˜, or not pass through ๐‘˜.

The algorithm is just dynamic programming as usual (TODO write it out anyway).

The running time is ๐‘‚(|๐‘‰|3).

Remark: Some other options for all-pairs shortest paths:

  • Run Bellman-Ford for every vertex as a source: ๐‘‚(|๐‘‰|2|๐ธ|), which is ๐‘‚(|๐‘‰|3) for sparse graphs and ๐‘‚(|๐‘‰|4) for dense graphs.

  • Run Dijkstra for every vertex as a source (if all weights not-negative): ๐‘‚(|๐‘‰|log|๐‘‰|(|๐‘‰|+|๐ธ|)), which is ๐‘‚(|๐‘‰|2log|๐‘‰|) for sparse graphs and ๐‘‚(|๐‘‰|3log|๐‘‰|) for dense graphs.

7. Greedy algorithms

Definition 7.1: In a greedy algorithm, at each step we greedily make the choice that offers the greatest immediate benefit (the greedy choice). This choice is not reconsidered at subsequent steps.
Remark: Dijkstraโ€™s algorithm is an example of a greedy algorithm.
Remark: The greedy approach doesnโ€™t always work, but when it does itโ€™s nice because itโ€™s simple and doesnโ€™t require keeping track of subproblem solutions.
Definition 7.2: Given a connected undirected graph ๐บ=(๐‘‰,๐ธ) with weights ๐‘ค:๐ธโ†’โ„โ‰ฅ0, the minimum spanning tree (MST) is the connected acyclic subgraph that has minimum weight and connects all the vertices of ๐บ. For positive weights, a minimum spanning tree always exists (because we can just look at all possible spanning trees and take the minimum).

We will identify a tree within ๐บ by its edge set ๐‘‡โІ๐ธ.

Lemma 7.3:

An undirected graph is a tree iff every pair of vertices is connected by a unique (simple) path.

Proof: A connected undirected graph has a cycle iff there exist two vertices connected by distinct paths.

Lemma 7.4: If a graph ๐บโ‰”(๐‘‰,๐ธ) is a tree, than |๐ธ|=|๐‘‰|โˆ’1.

Proof:

๐บconnectedโŸน|๐ธ|โ‰ฅ|๐‘‰|โˆ’1
๐บacyclicโŸน|๐ธ|โ‰ค|๐‘‰|โˆ’1.

TODO more formally

Definition 7.5: A spanning tree of a graph ๐บโ‰”(๐‘‰,๐ธ) is a subgraph with edge set ๐‘‡โІ๐ธ such that ๐‘‡ is a tree, and ๐‘‡ reaches all vertices of ๐บ.

Remark: A spanning tree is:

  • a minimal connected subgraph (removing an edge disconnects it)
  • a maximal acyclic subgraph (adding an edge creates a cycle).

Therefore any spanning tree has exactly |๐‘‰|โˆ’1 edges.

Lemma 7.6: Every connected graph has a spanning tree.

Proof: TODO
Definition 7.7: A minimal spanning tree (MST) is a spanning tree of minimal weight, where the weight ๐‘ค(๐‘‡) of a tree ๐‘‡ is the sum of the weights of all the edges of ๐‘‡.
Remark: A graph can have multiple MSTs, but they must all have the same number of edges (because they are spanning trees).

The general algorithm for building an MST:

Definition 7.8: Let ๐ดโІ๐ธ be a subset of an MST ๐‘‡. We say that an edge (๐‘ข,๐‘ฃ) is safe to add to ๐ด iff ๐ดโˆช{(๐‘ข,๐‘ฃ)} is a subset of some MST.

Definition 7.9: A cut is a partition of the vertex set into ๐‘† and ๐‘‰\๐‘†.

An edge (๐‘ข,๐‘ฃ)โˆˆ๐ธ crosses a cut if one endpoint is in ๐‘† and the other is in ๐‘‰\๐‘†.

A cut respects ๐ดโІ๐ธ if no edge in ๐ด crosses the cut.

An edge crossing a cut is light if its weight is minimal over all edges that cross that cut.

Lemma 7.10 (Cut Lemma): Let ๐ด be a subset of some MST. If (๐‘†,๐‘‰\๐‘†) is a cut that respects ๐ด, and (๐‘ข,๐‘ฃ) is a light edge crossing the cut, then (๐‘ข,๐‘ฃ) is safe for ๐ด.

Proof: Let ๐‘‡ be an MST that includes ๐ด. Since ๐‘‡ is a tree, it contains a unique path ๐‘ƒ between ๐‘ข and ๐‘ฃ.

Path ๐‘ƒ must cross the cut (๐‘†,๐‘‰\๐‘†) at least once, because ๐‘ข and ๐‘ฃ are on different sides of the cut, so at some point an edge must cross the cut. Let (๐‘ฅ,๐‘ฆ) be an edge of ๐‘ƒ that crosses the cut. Adding (๐‘ข,๐‘ฃ) and deleting (๐‘ฅ,๐‘ฆ) creates a tree ๐‘‡โ€ฒ.

๐‘‡โ€ฒ is a tree because we create a cycle and then remove one edge from it, keeping the graph connected. ๐‘‡โ€ฒ is minimal because the weight of ๐‘‡โ€ฒ is ๐‘ค(๐‘‡โ€ฒ)=๐‘ค(๐‘‡)โˆ’๐‘ค(๐‘ฅ,๐‘ฆ)+๐‘ค(๐‘ข,๐‘ฃ)โ‰ค๐‘ค(๐‘‡), because we chose (๐‘ข,๐‘ฃ) to be light (i.e. having minimal weight of all the edges that cross the cut). ๐‘‡โ€ฒ contains ๐ด because ๐ด was included in ๐‘‡, and ๐ด did not contain (๐‘ฅ,๐‘ฆ), because the cut respected ๐ด.

Definition 7.11: A disjoint-set data structure keeps track of a family ๐’ฎ๏ธ€ of dynamic disjoint sets ๐‘†1,โ€ฆ,๐‘†๐‘˜. Each set is identified by some arbitrary representative.

It has three operations:

  • MakeSet(x) adds {๐‘ฅ} to ๐’ฎ๏ธ€
  • FindSet(u) returns the representative of the set containing ๐‘ข
  • Union(x, y) removes ๐‘†๐‘ฅ,๐‘†๐‘ฆ from ๐’ฎ๏ธ€ and adds ๐‘†๐‘ฅโˆช๐‘†๐‘ฆ to ๐’ฎ๏ธ€.

A good representation is as a disjoint-set forest, when elements are stored in an array with pointers to a parent leading to a root of each set. Keeping this balanced gives amortised complexity ๐‘‚(๐‘š๐›ผ(๐‘›)) where ๐‘š is the number of operations and ๐‘› is the number of MakeSet operations, and ๐›ผ(๐‘›) is the inverse Ackermann function, which is extremely slowly-growing and is at most 4 for any feasible inputs.

Definition 7.12: Kruskalโ€™s algorithm:

Start from ๐ด=โˆ…. At each step, pick the edge with the smallest weight and add it to ๐ด if it does not create a cycle.

To avoid cycles, we need to keep track of the connected components, for instance by using a disjoint-set data structure.

Kruskal(V, E, w):
  A = โˆ…
  for v โˆˆ V:
    MakeSet(v)
  sort E into increading order by weight w
  for each edge (u, v) from sorted edge list:
    if FindSet(u) != FindSet(v):
      A = A โˆช {(u,v)}
      Union(u, v)
  return A

There are |๐‘‰| MakeSet operations, and ฮ˜(|๐‘‰|+|๐ธ|)=ฮ˜(|๐ธ|) total disjoint-set operations. So the disjoint-set operations take ๐‘‚(|๐ธ|๐›ผ(|๐‘‰|)) (for a good disjoint-set implementation). So the sorting of the edges dominates. Therefore Kruskalโ€™s algorithm has time complexity ๐‘‚(|๐ธ|log|๐ธ|).

Definition 7.13: Primโ€™s algorithm picks a vertex ๐‘Ÿโˆˆ๐‘‰ and grows the tree from that vertex.

We set ๐‘†={๐‘Ÿ} and ๐ด=โˆ…. Then at every step, find a light edge (๐‘ข,๐‘ฃ) connecting ๐‘ขโˆˆ๐‘† to ๐‘ฃโˆˆ๐‘‰\๐‘†. Update ๐‘† to ๐‘†โˆช{๐‘ฃ} and ๐ด to ๐ดโˆช{(๐‘ข,๐‘ฃ)}.

We use a min-heap/priority queue ๐‘„ such that ๐‘„=๐‘‰\๐‘†, the key of ๐‘ฃ is the minimum weight of any edge (๐‘ข,๐‘ฃ) where ๐‘ขโˆˆ๐‘†, and if no such edge exists set the key to be โˆž.

To find a light edge crossing the cut (๐‘†,๐‘‰\๐‘†), take the minimum from the queue. If v = ExtractMin(Q), then there exists a light edge (๐‘ข,๐‘ฃ) for some ๐‘ขโˆˆ๐‘†. The vertex ๐‘ข can be retrieved by a backpointer: when the key of ๐‘ฃ is to to ๐‘ค(๐‘ข,๐‘ฃ), we define ๐œ‹[๐‘ฃ]=๐‘ข.

Prim(V, E, w, r):
  for u โˆˆ V:
    key[u] = โˆž
    ๐œ‹[u] = null
    Insert(Q, u)
  DecreaseKey(Q, r, 0)
  while Q != โˆ…:
    u = ExtractMin(Q)
    for each v in Adj[u]:
      if v in Q and w(u,v) < key[v]:
        ๐œ‹[v] = u
        DecreaseKey(Q, v, w(u,v))

Then ๐œ‹ gives us the edges.

This has running time ๐‘‚(|๐ธ|log|๐‘‰|) because DecreaseKey (๐‘‚(log|๐‘‰|)) is executed exactly once for every edge.

Remark: Because log|๐ธ|=๐‘‚(log|๐‘‰|) for a connected graph, Kruskal and Prim have the same asymptotic running time.
Remark: Prim can be improved to ๐‘‚(|๐ธ|+|๐‘‰|log|๐‘‰|) using a better min-priority queue implementation.

Definition 7.14: The activity selection problem is, given a set of activiies (๐‘ ๐‘–,๐‘“๐‘–), ๐‘–=1,โ€ฆ,๐‘›, to select a maximum-size subset of activities that do not overlap. (Where ๐‘ ๐‘– is the start time and ๐‘“๐‘– is the finish time).

We can reduce this to a graph where edges between nodes indicate a scheduling conflict, and try to find a maximal independent set of vertices.

We could try to find this by greedily selecting vertices of minimal degree. However, this wouldnโ€™t always work:

TODO ( one node at bottom, connected to 4 in next row, connected to four in row above which form a clique )

It turns out that this is an NP-hard problem (i.e. we can verify a solution in polynomial time, but finding a solution is difficult).

However, this doesnโ€™t mean that activity selection is hard, because not all graphs are an activity selection problem. For example, a graph with an induced cycle > 3 cannot be an activity selection problem. TODO image

Lemma 7.15: There exists an optimal solution to an activity selection problem that contains an activity with minimum finish time.

Proof: Let ๐‘Ž be an activity with minimum time. Consider an optimal solution and assume it does not contain any activity with the same finish time as ๐‘Ž. By removing the activity of the optimal solution with minimum finish time and adding ๐‘Ž, we create a valid solution that has the same number of activities.

Corollary 7.16: The greedy algorithm whereby we take the activity with minimal finish time at each step always gives an optimal solution to the activity selection problem.

ActivitySelection(s, f):
  sort the activities in order of increasing f
  A = { 1 }
  k = 1
  for j = 2 to n:
   if s[j] >= f[k]:
     A = A โˆช { j }
     k = j
  return A
Remark: The runtime is dominated by the sorting, so is ๐‘‚(๐‘›log๐‘›).

8. Stable matching

Definition 8.1: A matching is an undirected graph where all connected components are pairs of vertices.

Equivalently, a matching ๐‘€ is a set of ordered pairs (โ„Ž,๐‘ ) with โ„Žโˆˆ๐ป and ๐‘ โˆˆ๐‘†, such that TODO

Goal: given a set of preferences among hospitals and med students, design a self-reinforcing admissions process.

Definition 8.2: Hospital โ„Ž and student ๐‘  form an unstable pair if:

  • โ„Ž prefers ๐‘  to one of its admitted students
  • and ๐‘  prefers โ„Ž to its assigned hospital.

Definition 8.3: A stable assignment is an assignment with no unstable pairs.

This is the desirable condition. Note that individual self-interest prevents any hospital-student side deal (if there are any unstable pairs though, the hospital and student could both complain).

Definition 8.4: A perfect matching is TODO
Definition 8.5: A stable matching is a perfect matching with no unstable pairs.

TODO Gale-Shapely deferred acceptance algorithm

Remark: This is ๐‘‚(๐‘›2).

Lemma 8.6: The Gale-Shapley algorithm finds a perfect matching.

Proof: Suppose for the sake of contradiction that some hospital โ„Ž is unmatched at termination. Then some student, say ๐‘ , is unmatched at termination. This means that ๐‘  was never proposed to, but โ„Ž must have proposed to every student, because it is unmatched. Contradiction, so the matching upon termination must be perfect.

Lemma 8.7: The matching ๐‘€ produced by the Gale-Shapley algorithm is stable.

Proof: Consider a pair (โ„Ž,๐‘ ) that is not in ๐‘€.

Either:

  • โ„Ž proposed to ๐‘ , in which case โ„Ž prefers its student in ๐‘€ to ๐‘ 
  • โ„Ž proposed to ๐‘ : therefore ๐‘  rejected โ„Ž at some point, so ๐‘  ended up with a more preferred hospital.

TODO: why does this mean itโ€™s stable?

Definition 8.8: A student ๐‘  is a valid partner for hospital โ„Ž if there exists any stable matching in which โ„Ž and ๐‘  are matched.

Remark: The Gale-Shapley algorithm gives the hospital-optimal assignment, i.e. each hospital received the best valid partner. As a corollary, we get that the hospital-optimal assignment is stable.

This is also the student-pessimal assignment (each student gets the worst valid partner). As a corollary, we get that the student-pessimal assignment is stable.

Remark: A student can get a better outcome by lying about their preferences.