Uploaded by Tolu Adesanya

algo

advertisement
Week 14 - Recursions
COMP5180 - Algorithms, Correctness and Efficiency
In this lecture
• Reduction
• Divide and conquer
• Recursion
• Factorial
• Exponentiation
• Fibonacci numbers
• Iteration via recursion
• Tower of Hanoi
Reduction
• Solving a problem by using an already existing solution to another
problem
Example: Finding the minimum of an array
• We want to write a program that takes an array of integers as an input
and outputs the smallest number from that array
• E.g. if the input is [26, 53, 14, 87, 34]
• The output should be 14
• We already have a program that sorts an integer array
Example: Finding the minimum of an array
[4,2,3,1]
arr
sortedArr
[1,2,3,4]
Sort
Example: Finding the minimum of an array
[26,53,14,87,34]
arr
Minimum
arr
sortedArr
min
14
Sort
Example: Finding the minimum of an array
arr
[4,7,3,8,2]
Minimum
Sort
[4,7,3,8,2]
arr
sortedArr
[2,3,4,7,8]
2
min
Example: Finding the minimum of an array
arr
Minimum
arr
sortedArr
min
Sort
Example: Finding the minimum of an array
Program Minimum:
input array arr
sortedArr = Sort(arr)
min = sortedArr[0]
output min
Reduction
• Solving a problem by using an already existing solution to another
problem
• Accept the existing solution as a black box – we don’t care what’s
inside as long as it works correctly
• Correctness of the outer solution does not depend on how exactly
the inner solution works
Reduction: analysing the Minimum example
• Correctness:
• If Sort works correctly, Minimum will work correctly too
• Complexity:
• No pre-processing is required for array before it is passed to Sort, basically O(1)
• The complexity of running the Sort program
• Selecting the first element of an array is a simple operation, basically O(1)
• The complexity of Minimum depends on the complexity of Sort
• If Sort is a quicksort, then it’s O(n*log(n)); if it’s bogosort, then it’s O(n!)
Reduction: more general approach
• Correctness:
• If the inner program works correctly, outer program should work correctly too
• Complexity:
• Complexity of Pre-processing the input to the inner program
• The complexity of running the inner program
• Number of times the inner program is used
• Complexity of post-processing the output from the inner program
• Complexity of whatever else the outer program does
An example from my research
• I want to write a program that runs an experiment that evaluates a
new decision tree algorithm using a raw dataset that I have
• I want to write a program that reads a dataset, pre-processes it,
separates it into a training set and a test set, initialises experiment
parameters, creates a new decision tree model, constructs it, prunes
it, tests it, saves the results to a file
• Long, complicated, but…
Divide and conquer
dataset = readDataset()
preProcess(dataset)
trainingSet,testSet = makeTrainingAndTestSets(dataset)
parameters = initialiseExperimentParameters()
decisionTreeModel = makeNewDecisionTreeModel()
construct(decisionTreeModel, trainingSet, parameters)
prune(decisionTreeModel)
testResults = test(decisionTreeModel, testSet)
save(testResults)
Recursion
• Self-reduction
• Using a solution to the problem as part of itself
• Commonly used in programming to solve a big problem that can be
divided into multiple smaller problems which are similar to the big
problem
Example of recursion being applicable
Examples of recursive solutions
• Factorial
• Exponentiation
• Fibonacci numbers
• Iteration via recursion
• Tower of Hanoi
Factorial!
• A mathematical function applicable to integers >= 0
• Uses the ! operator
• Represents a product of all numbers from 1 to the integer to which it
is applied
• E.g. 5! = 1*2*3*4*5 = 120
Factorial
• 0! = 1 (by definition)
• 1! = 1 = 1
• 2! = 1*2 = 2
• 3! = 1*2*3 = 6
• 4! = 1*2*3*4 = 24
• 5! = 1*2*3*4*5 = 120
• 6! = 1*2*3*4*5*6 = 720
•…
Factorial as a recursive function
• We can see that in order to get from, say, 4! to 5!, we need to
multiply it by 5
• And to then get to 6! We multiply by 6, to get to 7! We multiply that
by 7, and so on
• And we can define this pattern recursively
Factorial as a recursive function
• n! = (n-1)! * n
• This definition defines the factorial of an integer number n as the
factorial of the previous integer number multiplied by n
• Is this correct?
Factorial
• 0! = 1 (by definition)
• 1! = 1 = 1
• 2! = 1*2 = 2
• 3! = 1*2*3 = 6
• 4! = 1*2*3*4 = 24
• 5! = 1*2*3*4*5 = 120
• 6! = 1*2*3*4*5*6 = 720
•…
Factorial as a recursive function
• If we calculate 3! step by step:
• 3! = 2! * 3
• 2! = 1! * 2
• 1! = 0! * 1
• 0! = -1! * 0
• ???
Termination condition
• If we want a recursive program to eventually arrive at a solution, we
need to tell it when to stop making new recursive calls
• We call this a Termination Condition
• There can be multiple termination conditions in the same recursive
program
Factorial
• 0! = 1 (by definition)
• 1! = 1 = 1
• 2! = 1*2 = 2
• 3! = 1*2*3 = 6
• 4! = 1*2*3*4 = 24
• 5! = 1*2*3*4*5 = 120
• 6! = 1*2*3*4*5*6 = 720
•…
Factorial as a recursive function
• If n = 0, then n! = 1
• Otherwise, n! = (n-1!) * n
Factorial as a recursive function
Function factorial(n)
if n = 0
return 1
else
return factorial(n-1) * n
end
Another example: Fibonacci numbers
• Fibonacci numbers are a sequence of integer numbers
• Starts with 1, 1
• Each number after that is a sum of the two previous numbers in the
sequence
Fibonacci numbers
• f(1) = 1 (by definition)
• f(2) = 1 (by definition)
• f(3) = 1 + 1 = 2
• f(4) = 1 + 2 = 3
• f(5) = 2 + 3 = 5
• f(6) = 3 + 5 = 8
• f(7) = 5 + 8 = 13
• f(8) = 8 + 13 = 21
•…
Fibonacci numbers as a recursive function
• Each number is a sum of two previous umbers
• f(n) = f(n-1) + f(n-2)
• What is the termination condition?
Fibonacci numbers
• f(1) = 1 (by definition)
• f(2) = 1 (by definition)
• f(3) = 1 + 1 = 2
• f(4) = 1 + 2 = 3
• f(5) = 2 + 3 = 5
• f(6) = 3 + 5 = 8
• f(7) = 5 + 8 = 13
• f(8) = 8 + 13 = 21
•…
Fibonacci numbers as a recursive function
• Each number is a sum of two previous umbers
• f(n) = f(n-1) + f(n-2)
• What is the termination condition?
• If n = 1 or n = 2 then f(n) = 1
Fibonacci numbers as a recursive function
Function fibonacci(n)
if n = 1 or n = 2
return 1
else
return fibonacci(n-1) + fibonacci(n-2)
end
Another example: Exponentiation
• Exponentiation is a mathematical operation, commonly referred to as
“power”
• E.g 23 is referred to as “two to the power of 3” and equals
• 23 = 2 * 2 * 2 = 8
• 516 = 5 * 5 * 5 * 5 * 5 * 5 * 5 * 5 * 5 * 5 * 5 * 5 * 5 * 5 * 5 * 5 = a lot
Exponentiation as a recursive function
• We can generalise exponentiation as a function with two parameters:
• n is the number that exponentiation is being applied to
• e is an exponent, the power to which n is being taken to
• ne
• exp(n,e)
Exponentiation as a recursive function
• We can see that ne is equal to n * ne-1
• exp(n, e) = n * exp(n, e-1)
• What is the termination condition?
• By definition, n0 is 1, and negative powers won’t be integers anymore
• So e = 0 is a good termination condition
Exponentiation as a recursive function
Function exp(n,e)
if e = 0
return 1
else
return n * exp(n, e-1)
end
But is this the best we can do?
We can do better…
• 16 * 16 = 256
• 24 * 24 = 28
• ne/2 * ne/2 = ne
• exp(n,e) = exp(n,e/2) * exp(n,e/2)
• exp(2,8) = exp(2,4) * exp(2,4)
Recursion as iteration
• Let’s consider a task of iterating through an array
• We usually use loops
for(int i = 0; i < arr.length; i++)
System.out.println(arr[i]);
• But we can also use recursion (very common in functional languages)
Recursion as iteration
• Let’s write a function that takes an array arr as a parameter and prints
out its elements
• We need to recursively follow from one array element to another
until we reach the end of the array
• At every step of the recursion we need to go to print the current
element
Divide and conquer
Function printArray
if we have reached the final element, stop
print the current element
go to the next element
end
What do we need?
• A function that will recursively call itself
• Some way for a function to keep track of the array that it is iterating
through
• Some way to know at which point in the array it currently is
• A print statement
Iterating over an array and printing elements
Function printArray(arr, n)
if n > length(arr)
stop
print(arr[n])
printArray(arr, n+1)
end
Iterating over an array and printing elements
Function printArray(arr, n)
if n > length(arr)
stop
arr = [1,2,3,4,5]
printArray(arr, 0)
print(arr[n])
Function printArray(arr)
printArray(arr, n+1)
printArray(arr, 0)
end
end
Using only one function and one parameter
Function printArray(arr)
if arr is empty
stop
print(arr[0])
remainder = removeFirstElement(arr)
printArray(remainder)
end
Using only one function and one parameter
Function printArray(arr)
if arr is empty
stop
print(arr[0])
[1,2,3,4,5]
Not empty
print 1
remainder = removeFirstElement(arr)
printArray(remainder)
end
[2,3,4,5]
printArray([2,3,4,5])
Example: erlang
printList([]) -> ok.
printList([H|T]) ->
print(H),
printList(T).
Tower of Hanoi
• A famous puzzle with many variations
• Classic variation is about a stack of disks of different sizes and three pegs
• The disks are stacked in a pyramid on the first peg, objective is to move all
disks to the third peg
• You can only move one disk at a time, from the top of one peg to the top of
another, you are not allowed to place a larger disk on top of a smaller disk
Tower of Hanoi
Tower of Hanoi
• Can be solved using a recursive algorithm, try and figure out how
• More about Tower of Hanoi in the upcoming lectures
Thank you
Week 16 – More Recursions
COMP5180 - Algorithms, Correctness and Efficiency
In the previous lecture: Reduction
arr
Minimum
arr
sortedArr
min
Sort
In the previous lecture: Divide and conquer
dataset = readDataset()
preProcess(dataset)
trainingSet,testSet = makeTrainingAndTestSets(dataset)
parameters = initialiseExperimentParameters()
decisionTreeModel = makeNewDecisionTreeModel()
construct(decisionTreeModel, trainingSet, parameters)
prune(decisionTreeModel)
testResults = test(decisionTreeModel, testSet)
save(testResults)
In the previous lecture: Recursive solutions
Function factorial(n)
if n = 0
return 1
else
return factorial(n-1) * n
end
In the previous lecture: Recursive solutions
Function factorial(n)
if n = 0
return 1
else
return factorial(n-1) * n
end
In the previous lecture: Termination condition
Function factorial(n)
if n = 0
return 1
else
return factorial(n-1) * n
end
In this lecture
• More examples
• Iteration via recursion
• Tower of Hanoi
• Recursion trees
• Solving recursions
• Improving recursive solutions
Recursion as iteration
• Let’s consider a task of iterating through an array
• We usually use loops
for(int i = 0; i < arr.length; i++)
System.out.println(arr[i]);
• But we can also use recursion (very common in functional languages)
Recursion as iteration
• Let’s write a function that takes an array arr as a parameter and prints
out its elements
• We need to recursively follow from one array element to another
until we reach the end of the array
• At every step of the recursion we need to go to print the current
element
Divide and conquer
Function printArray
if we have reached the final element, stop
print the current element
go to the next element
end
What do we need?
• A function that will recursively call itself
• Some way for a function to keep track of the array that it is iterating
through
• Some way to know at which point in the array it currently is
• A print statement
Iterating over an array and printing elements
Function printArray(arr, n)
if n > length(arr)
stop
print(arr[n])
printArray(arr, n+1)
end
Iterating over an array and printing elements
Function printArray(arr, n)
if n > length(arr)
stop
arr = [1,2,3,4,5]
printArray(arr, 0)
print(arr[n])
Function printArray(arr)
printArray(arr, n+1)
printArray(arr, 0)
end
end
Using only one function and one parameter
Function printArray(arr)
if arr is empty
stop
print(arr[0])
remainder = removeFirstElement(arr)
printArray(remainder)
end
Using only one function and one parameter
Function printArray(arr)
if arr is empty
stop
print(arr[0])
[1,2,3,4,5]
Not empty
print 1
remainder = removeFirstElement(arr)
printArray(remainder)
end
[2,3,4,5]
printArray([2,3,4,5])
Example: erlang
printList([]) -> ok.
printList([H|T]) ->
print(H),
printList(T).
Tower of Hanoi
• A famous puzzle with many variations
• Classic variation is about a stack of disks of different sizes and three pegs
• The disks are stacked in a pyramid on the first peg, objective is to move all
disks to the third peg
• You can only move one disk at a time, from the top of one peg to the top of
another, you are not allowed to place a larger disk on top of a smaller disk
Tower of Hanoi
Tower of Hanoi
• Can be solved using a recursive algorithm, let’s figure out how
Let’s play some Tower of Hanoi
• https://www.mathsisfun.com/games/towerofhanoi.html
Intuition
• We need to move the whole pyramid from peg 1 to peg 3
• Smallest disk is easy to move
• Larger disks are harder to move cause we have to remove everything
smaller out of the way first
• Largest disk is the hardest one to move
An algorithm
• Start with pyramid on peg 1
• Move everything except the bottom disk out of the way
• Move the bottom disk to peg 3
• Move everything else to peg 3
Parameters of the problem
• We need to know:
• How many disks are we moving (height of the pyramid)
• From which peg
• To which peg
Pseudocode
Function TowerOfHanoi(height, from, to)
move everything else from peg #from to a free peg
move disk #height from peg #from to peg #to
move everything else to peg #to
end
Pseudocode
Function TowerOfHanoi(
4
,
1
, 3 )
move everything else from peg
1
move disk
to peg
from peg
1
move everything else to peg
3
end
4
to a free peg
3
So what do we do with this?
Function TowerOfHanoi(height, from, to)
move everything else from peg #from to a free peg
move disk #height from peg #from to peg #to
move everything else to peg #to
end
This is easy
Function TowerOfHanoi(height, from, to)
move everything else from peg #from to a free peg
move disk #height from peg #from to peg #to
move everything else to peg #to
end
These are harder
Function TowerOfHanoi(height, from, to)
move everything else from peg #from to a free peg
move disk #height from peg #from to peg #to
move everything else to peg #to
end
What actually happens during the hard stages?
• Let’s first look at the process of moving everything out of the way
before moving the biggest disk
Let’s see it in practice first
• https://www.mathsisfun.com/games/towerofhanoi.html
Is there any difference?
So…
• This means that getting everything out of the way is the same as
moving a smaller pyramid of disks to a free peg
• Does it also work for moving everything on top of the biggest disk?
Yes it does
Recursion
• So, those red parts of the code are just smaller Tower of Hanoi
problems
• We can solve those recursively
Back to pseudocode
Function TowerOfHanoi(height, from, to)
move everything else from peg #from to a free peg
move disk #height from peg #from to peg #to
move everything else to peg #to
end
Rewriting the pseudocode
Function TowerOfHanoi(height, from, to)
free = determine free peg somehow
TowerOfHanoi(height – 1, from, free)
move disk #height from peg #from to peg #to
TowerOfHanoi(height – 1, free, to)
end
Rewriting the pseudocode
Function TowerOfHanoi(height, from, to)
free = determine free peg somehow
TowerOfHanoi(height – 1, from, free)
move disk #height from peg #from to peg #to
TowerOfHanoi(height – 1, free, to)
end
Looks good, right? Anything missing?
Function TowerOfHanoi(height, from, to)
free = determine free peg somehow
TowerOfHanoi(height – 1, from, free)
move disk #height from peg #from to peg #to
TowerOfHanoi(height – 1, free, to)
end
Termination condition
Function TowerOfHanoi(height, from, to)
free = determine free peg somehow
TowerOfHanoi(height – 1, from, free)
move disk #height from peg #from to peg #to
TowerOfHanoi(height – 1, free, to)
end
Function TowerOfHanoi(height, from, to)
if(height = 1) just move the disk
free = determine free peg somehow
TowerOfHanoi(height – 1, from, free)
move disk #height from peg #from to peg #to
TowerOfHanoi(height – 1, free, to)
end
Tower of Hanoi
• We have now looked at the recursive Tower of Hanoi
• Coding it is relatively easy – pseudocode covers most of it
• Things become more interesting when we have > 3 pegs
• We’ll look at it again soon
Recursion trees
• Let’s consider a recursive implementation of Fibonacci
• f(1) = 1, f(2) = 1
• f(n) = f(n-1) + f(n-2)
• How do we calculate f(3)?
Recursion trees: f(3)
f(3)
f(2)
f(1)
1
1
What about f(4)?
f(4)
f(2)
f(3)
f(2)
f(1)
1
1
1
What about f(5)?
f(5)
f(3)
f(4)
f(3)
f(2)
f(1)
1
1
f(2)
f(2)
f(1)
1
1
1
What about f(6)?
f(6)
f(4)
f(5)
f(3)
f(4)
f(3)
f(2)
f(1)
1
1
f(2)
f(3)
f(2)
f(2)
f(1)
f(2)
f(1)
1
1
1
1
1
1
Week 16 – Recursion trees
COMP5180 - Algorithms, Correctness and Efficiency
In the previous lectures
• Reduction
• Divide and Conquer
• Recursions
• Various examples, e.g. Factorial, Exponentiation, Tower of Hanoi
In this lecture
• General structure of recursive problems
• Divide and Conquer
• Recursion trees
• Solving recursions
So what does a recursive solution look like?
• Takes some input
• Checks termination condition
• Makes a recursive call (or several)
• Maybe does pre-processing
• Maybe does post-processing
• Maybe does something at the current iteration
• Maybe returns something
Let’s consider a general implementation:
Function myFunction(input size n)
myFunction(input size n/c)
myFunction(input size n/c)
myFunction(input size n/c)
…………………………………………………………………
end
r
General implementation
• Function with input size n
• Makes r recursive calls
• Each call uses input size n/c
Example: searching a tree
• Let’s write a method that takes a binary tree as input
• Tree has n nodes
• Searches the tree recursively for a specific element
• Returns true if it finds it, returns false if it doesn’t
• Starts with root node, recursively checks both child nodes
complete tree with N = 16 nodes (height = 4)
Example: searching a tree
Function searchTree(node, element)
if node is the element then return true
if node is a leaf then return false
leftFound = searchTree(left child, element)
rightFound = searchTree(right child, element)
return leftFound OR rightFound
end
Analysing the pseudocode
Function searchTree(node, element)
Input: Tree of n nodes
if node is the element then return true
if node is a leaf then return false
Termination
conditions
leftFound = searchTree(left child, element)
rightFound = searchTree(right child, element)
2 recursive
calls
Each of size n/2
return leftFound OR rightFound
end
Post-processing
and return statement
General implementation for treeSearch
• Function with input size n
Input: Tree of n nodes
• Makes r recursive calls
2 recursive
calls
• Each call uses input size n/c
Each of size n/2
Divide and Conquer
• A general framework for algorithm design
1. Divide
2. Delegate
3. Combine
1. Divide
• Divide the problem into smaller sub-problems
• Includes:
• Pre-processing of input
• Checking termination conditions
2. Delegate
• Delegate each sub-problem to a separate function call
• Can be a different function
• Can be a recursive call to the same function
• Includes
• Making recursive calls
• Calling helper functions
3. Combine
• Combine the sub-problem solutions into a final result
• Includes
• Post-processing
• Outputting results
Divide and Conquer framework
1. Divide
• Divide the problem into sub-problems
• Pre-process inputs
2. Delegate
• Make recursive calls
• Call helper functions
3. Combine
• Combine results of function calls
• Output results
Analysing the recursive solutions
• It is useful to estimate the computational complexity of recursive
solutions
• We have a method for doing that
Example: Factorial
n! = n * (n – 1)!
Problem
Sub-Problem
T(n) = T(n-1) + O(1)
Problem time
Sub-Problem time
Step time
Example: Exponentiation
T(e) = T(e/2) + O(1)
• Calculating ne
Problem time
• Recursive formula:
• If e is even then ne = ne/2 * ne/2
• If e is odd then ne = n * n(e-1)/2 * n(e-1)/2
Problem
Sub-Problems
Sub-Problem time
Step time
Example: Fibonacci numbers
• Calculating f(n)
• f(n) = f(n-1) + f(n-2)
Problem Sub-Problems
T(n) = T(n-1) + T(n-2) + O(1)
Problem time Sub-Problem times
Step time
Example: Tower of Hanoi
• Solving Tower of Hanoi for n disks
• TowerOfHanoi(height, from, to)
Problem
TowerOfHanoi(height-1, from, free)
move disk from peg #from to peg #to
TowerOfHanoi(height-1, free, to)
Sub-Problems
T(height) = 2*T(height-1) + O(1)
Problem time
Sub-Problem times
Step time
Examples so far:
• Factorial
T(n) = T(n-1) + O(1)
• Exponentiation
T(e) = T(e/2) + O(1)
• Fibonacci
T(n) = T(n-1) + T(n-2) + O(1)
• Tower of Hanoi
T(height) = 2*T(height-1) + O(1)
Recursion trees
• A way to visualise recursions and their complexities
• Looks like a tree graph where
• Each node is a recursive call
• Each level of height is a level of recursion
Example from previous lecture
f(6)
f(4)
f(5)
f(3)
f(4)
f(3)
f(2)
f(1)
1
1
f(2)
f(3)
f(2)
f(2)
f(1)
f(2)
f(1)
1
1
1
1
1
1
We can generalise this
Recursion tree
• An algorithm spends O(f(n)) time on non-recursive work
• Algorithm makes r recursive calls
• At level d, the number of nodes is rd
• Divides input size by c at every recursive call
• Termination condition is reached at some level L
• It is reached when input cannot be divided anymore, eg. size 1
• n/cL = 1
• Therefore, L = logcn
More in the next lecture
• Thank you
Week 17 - Mergesort
COMP5180 - Algorithms, Correctness and Efficiency
Mergesort
• A sorting algorithm
• Takes array as an input, returns the same array but sorted
• Very simple recursive algorithm:
1. Sort left half
2. Sort right half
3. Merge into a sorted array
Mergesort example
52867134
5286
52
5
7134
86
2
8
25
6
68
2568
71
7
34
1
3
17
1347
12345678
34
4
Mergesort algorithm
function mergeSort(arr)
n = length(arr)
m = n/2
leftHalf = mergeSort(arr[0-m])
rightHalf = mergeSort(arr[m-n])
sortedArr = merge(leftHalf, rightHalf)
return sortedArr
end
Mergesort reduction
arr[0-m]
arr
mergeSort
sortedArr[0-m]
arr[m-n]
mergeSort
sortedArr[m-n]
sortedArr[0-m]
sortedArr[m-n]
sortedArr
sortedArr[0-n]
merge
mergeSort
Overview
• Select a number m
• Run two mergesorts
• Merge their outputs
• Return the result
Correctness of Mergesort
• We assume mergesort is correct
• Correctness of selecting m
• m = n/2
• Correctness of merge
• How do we merge two arrays?
Merge
• A function that takes two arrays as parameters
• Assumes both arrays are sorted
• Returns a sorted array consisting of the elements of both input arrays
Merge example
merge( 2 5 6 8 , 1 3 4 7 )
a= 2568
b= 1347
c = new array of size ???
length(a) + length(b) = 4 + 4 = 8
Merge example
i= 0123
a[ i ] = 2 5 6 8
merge( 2 5 6 8 , 1 3 4 7 )
j= 0123
b[ j ] = 1 3 4 7
k= 01234567
c[ k ] = 0 0 0 0 0 0 0 0
What’s the first number we need to select?
Merge example
i= 0123
a[ i ] = 2 5 6 8
merge( 2 5 6 8 , 1 3 4 7 )
j= 0123
b[ j ] = 1 3 4 7
k= 01234567
c[ k ] = 0 0 0 0 0 0 0 0
Start with
i=0
j=0
k=0
Merge example
i= 0123
a[ i ] = 2 5 6 8
merge( 2 5 6 8 , 1 3 4 7 )
j= 0123
b[ j ] = 1 3 4 7
k= 01234567
c[ k ] = 1
00000000
a[i] = 2
b[j] = 1
1 is smaller than 2
so next number in c is 1
we have used b[j]
so j needs to increase
also we assigned c[k]
so k needs to increase
Merge example
i= 0123
a[ i ] = 2 5 6 8
merge( 2 5 6 8 , 1 3 4 7 )
j= 0123
b[ j ] = 1 3 4 7
k= 01234567
c[ k ] = 1 0
2000000
a[i] = 2
b[j] = 3
2 is smaller than 3
so next number in c is 2
we have used a[i]
so i needs to increase
also we assigned c[k]
so k needs to increase
Merge example
i= 0123
a[ i ] = 2 5 6 8
merge( 2 5 6 8 , 1 3 4 7 )
j= 0123
b[ j ] = 1 3 4 7
k= 01234567
c[ k ] = 1 2 0
300000
a[i] = 5
b[j] = 3
3 is smaller than 5
so next number in c is 3
we have used b[j]
so j needs to increase
also we assigned c[k]
so k needs to increase
Merge example
i= 0123
a[ i ] = 2 5 6 8
merge( 2 5 6 8 , 1 3 4 7 )
j= 0123
b[ j ] = 1 3 4 7
k= 01234567
c[ k ] = 1 2 3 4
00000
a[i] = 5
b[j] = 4
4 is smaller than 5
so next number in c is 4
we have used b[j]
so j needs to increase
also we assigned c[k]
so k needs to increase
Merge example
i= 0123
a[ i ] = 2 5 6 8
merge( 2 5 6 8 , 1 3 4 7 )
j= 0123
b[ j ] = 1 3 4 7
k= 01234567
c[ k ] = 1 2 3 4 5
0000
a[i] = 5
b[j] = 7
5 is smaller than 7
so next number in c is 5
we have used a[i]
so i needs to increase
also we assigned c[k]
so k needs to increase
Merge example
i= 0123
a[ i ] = 2 5 6 8
merge( 2 5 6 8 , 1 3 4 7 )
j= 0123
b[ j ] = 1 3 4 7
k= 01234567
c[ k ] = 1 2 3 4 5 6
000
a[i] = 6
b[j] = 7
6 is smaller than 7
so next number in c is 6
we have used a[i]
so i needs to increase
also we assigned c[k]
so k needs to increase
Merge example
i= 0123
a[ i ] = 2 5 6 8
merge( 2 5 6 8 , 1 3 4 7 )
j= 0123 4
b[ j ] = 1 3 4 7
k= 01234567
c[ k ] = 1 2 3 4 5 6 7
00
a[i] = 8
b[j] = 7
7 is smaller than 8
so next number in c is 7
we have used b[j]
so j needs to increase
also we assigned c[k]
so k needs to increase
Merge example
merge( 2 5 6 8 , 1 3 4 7 )
i= 0123 4
j= 0123 4
a[ i ] = 2 5 6 8 b[ j ] = 1 3 4 7
k= 01234567 8
c[ k ] = 1 2 3 4 5 6 7 8
0
a[i] = 8
j is out of bounds
so next number in c is 8
we have used a[i]
so i needs to increase
also we assigned c[k]
so k needs to increase
Merge example
merge( 2 5 6 8 , 1 3 4 7 )
i= 0123 4
j= 0123 4
a[ i ] = 2 5 6 8 b[ j ] = 1 3 4 7
k= 01234567 8
c[ k ] = 1 2 3 4 5 6 7 8
return c
So what now?
The merge is complete
function merge(a, b)
c = new array of size length(a) + length(b)
keep running until there are no more elements to add
pick the smallest next element from a and b
put that element in c
move indexes
end
function merge(a, b)
c = new array [length(a) + length(b)]
i = 0, j = 0, k = 0
while i < length(a) OR j < length(b)
if a[i] < b[j]
c[k] = a[i], i++, k++
if i > length(a)
else
???
c[k] = b[j], j++, k++
if j > length(b)
???
end
Mergesort correctness
• If merge is correct, then mergeSort is correct
Mergesort recurrence
• To mergesort an array, we need to:
T(n)
• Mergesort the left half
T(n/2)
• Mergesort the right half
T(n/2)
• Merge the results
O(n)
Mergesort recurrence
• To mergesort an array, we need to:
T(n) = 2*T(n/2) + O(n)
• Mergesort the left half
T(n/2)
• Mergesort the right half
T(n/2)
• Merge the results
O(n)
From the previous lecture
• T(n) = r * T(n/c) + f(n)
• n is size of input
• r is number of recursive calls made by a recursive function
• c is a constant by which the size of the input is divided at each level
• f(n) is time spent on non-recursive stuff
• L is the lowest level of recursive tree, L = logcn
Mergesort recurrence
• T(n) = 2*T(n/2) + O(n)
• r = 2, because mergesort makes 2 recursive calls
• c = 2, because mergesort divides input in half
• f(n) = O(n), because complexity of merging results is O(n)
• L = logcn = log2n, because after log2n divisions, the arrays will reach size 1
Solving mergesort recurrence
• T(n) = 2*T(n/2) + O(n)
• T(n) = O(n) + 2*O(n/2) + 4*O(n/4) + 8*O(n/8) + …… + 2
O(n)
O(n)
O(n)
• This is a recurrence of “equal” type
• So T(n) = L * f(n)
• L = log2n and f(n) = O(n)
• So T(n) = log2n * O(n) = O(n*log2n)
O(n)
log2n
*O(n/2
O(n)
log2n
)
Before you go
Computing society
•Meeting this Friday 14:00-17:00 in Cornwallis SW101
•https://discord.gg/f7y2MRm6Hd
Thank you
Week 17 – Solving recursions
COMP5180 - Algorithms, Correctness and Efficiency
In the previous lecture
• Generalised recursive solution
• Recurrences
• Recursion trees
• With some examples
Recurrences
• It takes T(n) time to solve a recursive problem with input size n
• We can define T(n) recursively too
• To calculate the time it would take solve the recursive problem we
need to calculate the time it would make the recursive calls plus the
time it would take to do all of the non-recursive stuff
Recurrences: definitions
• A recursive function gets called with input of size n
• A recursive function makes r recursive calls
• For each recursive call it uses input of size n/c
• It also spends f(n) time on non-recursive processing
Example from previous lecture
Function searchTree(node, element)
Input: Tree of n nodes
if node is the element then return true
if node is a leaf then return false
Termination
conditions
leftFound = searchTree(left child, element)
rightFound = searchTree(right child, element)
2 recursive
calls
Each of size n/2
return leftFound OR rightFound
end
Post-processing
and return statement
Recursion trees
In this lecture
• Solving recursions
• Examples
So, we have made these definitions:
• A recursive function gets called with input of size n
• A recursive function makes r recursive calls
• For each recursive call it uses input of size n/c
• It also spends f(n) time on non-recursive processing
• T(n) = r * T(n/c) + f(n) is a recurrence formula for a recursive function
How deep is the recursion tree?
Recursion trees
How deep is the recursion tree?
• Let’s define the lowest level of the tree as level L
Recursion trees
How deep is the recursion tree?
• Let’s define the lowest level of the tree as level L
• It will be reached when the input can not be divided anymore,
effectively the input will become 1
Recursion trees
How deep is the recursion tree?
• Let’s define the lowest level of the tree as level L
• It will be reached when the input can not be divided anymore,
effectively the input will become 1
• So, n/cL = 1
• n = cL
• Therefore: L = logcn
What can we do with those definitions?
• T(n) = r * T(n/c) + f(n)
• n is size of input
• r is number of recursive calls made by a recursive function
• c is a constant by which the size of the input is divided at each level
• f(n) is time spent on non-recursive stuff
• L is the lowest level of recursive tree, L = logcn
Examples of recurrences from previous lecture
• Factorial
T(n) = T(n-1) + O(1)
• Exponentiation
T(e) = T(e/2) + O(1)
• Fibonacci
T(n) = T(n-1) + T(n-2) + O(1)
• Tower of Hanoi
T(height) = 2*T(height-1) + O(1)
But these are all recursively defined
• Factorial
T(n) = T(n-1) + O(1)
• Exponentiation
T(e) = T(e/2) + O(1)
• Fibonacci
T(n) = T(n-1) + T(n-2) + O(1)
• Tower of Hanoi
T(height) = 2*T(height-1) + O(1)
Solving recurrences
• We can “solve” a recurrence formula and turn it into a non-recursive
estimation of time complexity of a recursive function
• Basically O-notation for recursive functions
Time complexity of a recursive function
Recursion trees
Time complexity of a recursive function
T(n) =
Time complexity of a recursive function
T(n) = f(n)
Time complexity of a recursive function
T(n) = f(n) + r * f(n/c)
Time complexity of a recursive function
T(n) = f(n) + r * f(n/c) + r2 * f(n/c2)
Time complexity of a recursive function
T(n) = f(n) + r * f(n/c) + r2 * f(n/c2) + ………
Time complexity of a recursive function
T(n) = f(n) + r * f(n/c) + r2 * f(n/c2) + ……… + rL * f(n/cL)
Recursion trees
This seems correct, what now?
T(n) = f(n) + r * f(n/c) + r2 * f(n/c2) + ……… + rL * f(n/cL)
It’s a long expression
T(n) = f(n) + r * f(n/c) + r2 * f(n/c2) + ……… + rL * f(n/cL)
Which term is the most important here?
T(n) = f(n) + r * f(n/c) + r2 * f(n/c2) + ……… + rL * f(n/cL)
Recursion trees
Three types
T(n) = f(n) + r * f(n/c) + r2 * f(n/c2) + ……… + rL * f(n/cL)
• Decreasing
• T(n) = O(f(n))
• Equal or almost equal
• T(n) = O(L * f(n))
• Increasing
• T(n) = O(rL * f(1)) = O(rL)
Example: decreasing recurrence
• T(n) = T(n/2) + O(n)
• T(n) = f(n) + f(n/2) + f(n/4) + f(n/8) + …
• T(n) ≈ f(n)
• T(n) = O(n)
Example: equal or almost equal
• T(n) = T(n-1) + O(1)
• T(n) = f(n) + f(n-1) + f(n-2) + f(n-3) …… + f(1)
• T(n) = L * f(n)
• L = n in this example
• T(n) = n * O(1) = O(n)
Example: increasing
• T(n) = 3*T(n/2) + O(1)
• T(n) = f(n) + 3*f(n/2) + 9*f(n/4) + 27*f(n/8) + 81*f(n/16) + … + 3L*f(n/2L)
• T(n) = rL
• L = logcn = log2n
• T(n) = 3 log2n
Three types
T(n) = f(n) + r * f(n/c) + r2 * f(n/c2) + ……… + rL * f(n/cL)
• Decreasing
• T(n) = O(f(n))
• Equal or almost equal
• T(n) = O(L * f(n))
• Increasing
• T(n) = O(rL * f(1)) = O(rL)
Determining time complexity of recursions
1. Determine the number of recursive calls r
2. Determine the reduction in input size at each recursive call c
3. Determine the step time f(n)
4. Determine which type of recurrence it is and solve it:
• Decreasing: T(n) = O(f(n))
• Equal: L = n; T(n) = L * f(n) = O(n * f(n))
• Increasing: L = logcn; T(n) = rL = O(r logcn )
Recursion trees
Thank you
• See you in the next lecture
Week 18 – Backtracking
COMP5180 - Algorithms, Correctness and Efficiency
In this lecture
• Backtracking
• N Queens problem
• Sum of Subset problem
Let’s consider a problem
• We have a chess board
• 8x8
• How many queens can we place
• So that they can’t attack each other?
N Queens
N Queens
• So how many queens can we place?
• What’s the theoretical limit?
N Queens algorithm
• Let’s implement an algorithm for solving this problem
• Since there can only be one queen per row, let it iterate through rows
and try and place a queen at the first available spot in that row
N Queens
N Queens
N Queens
N Queens
N Queens
N Queens
N Queens
• So we found a possible solution
• This one has 5 queens
• But maybe we can do better
• Let’s go back a step
N Queens
N Queens
N Queens
N Queens
Better
• We have managed to achieve a
better solution
• This time with 7 queens
• We achieved it by arriving at a
possible solution with 5 queens, and
taking a step back to try an
alternative move
Backtracking
• A technique for recursive solutions that allows us to recursively try
different solutions
• Most applicable in search algorithms and optimisation problems
• Can be applied to non-recursive solutions too, sometimes
N Queens with backtracking
• Let’s write a method that solves N Queens
• It takes a grid as input and returns a number
• Tries to make add a queen on this grid
• If it can, it adds it on the grid, and recursively runs itself on this grid
• If it can’t, it just returns the number of queens on the grid
Backtracking
• So at each step we need to:
• Check if there are any available moves
• If there are available moves:
• Iterate through those moves
• For each move, recursively solve the N Queens problem on the resulting grid
• Select the highest result
• If there are no available moves:
• Just return the number of queens on current grid
Recursive formula
NQueens(
NQueens(
) = countQueens(
) = max( NQueens(
)
), NQueens(
)
Function NQueens(grid)
find all free spaces on grid
count how many queens are on grid
if there are no free spaces
return the number of queens
if there are free spaces
iterate through those spaces
placing queen on grid
and running Nqueens on that grid
update n whenever a recursive call
returns a higher value
return n
end
N Queens with backtracking
Function NQueens(grid)
freeSpaces = findFreeSpaces(grid)
n = countQueens(grid)
if notEmpty(freeSpaces)
for each freeSpace in freeSpaces
nextGrid = addQueen(grid, freeSpace)
nextGridQueens = NQueens(nextGrid)
if(nextGridQueens > n)
n = nextGridQueens
return n
end
Backtracking
• So instead of directly using the result of each recursive call, we
compare it to results of other recursive calls, and then select the best
one
• After the recursive call is done, the execution backtracks to the level
above and makes decisions there
Let’s look at another classic problem
• We have a set of numbers
• E.g. {6, 13, 25, 12, 71, 33, 5}
• And we want to find if a certain sum k can be achieved by adding up
some of these numbers
• E.g. if k = 51
• A subset {6, 12, 33} would satisfy this condition
Sum of subset algorithm
• Let’s implement an algorithm for this problem
• It will take an array of integers as input representing the set, and an
integer k representing the target sum
• It will produce a binary value as output:
• true if there is a subset that adds up to k
• false if there is no such subset
Recursive formula
• We need to derive a recursive formula for the sum of subset algorithm
• Input size is the length of the input array
• We can reduce the input size at each iteration by removing one of the
numbers from the array; this will represent selecting that number for
the addition
• Sum needs to be adjusted too…
Reduction
• We can reduce the problem of solving the sum of subset problem
• To solving several smaller problems where each one represents
selecting one of the numbers
• If we have set of numbers {4, 3, 5} and we aim to get sum of 8
• After selecting number 4, we now need to now solve the problem
where set is {3, 5} and target sum is 8 – 4 = 4
Set = {4, 3, 5}
K=8
Set = {3, 5}
K=4
Set = {4, 5}
K=5
Set = {5}
K=1
Set = {3}
K = -1
Set = {5}
K=1
Set = {}
K = -4
Set = {}
K = -4
Set = {}
K = -4
Set = {4}
K=0
Set = {3, 4}
K=3
Set = {4}
K=0
Set = {3}
K = -1
Set = {}
K = -4
Recursive formula
Set = *
sumOfSubset(
) = true
K=0
Set = {*}
sumOfSubset(
)=
K≠0
n
OR
i=0
Set = {}
sumOfSubset(
) = false
K≠0
Set – set[i]
sumOfSubset(
K – set[i]
)
Pseudocode
Function sumOfSubset(set, k)
if k = 0
return true
if empty(set)
return false
sumFound = false
for i = 0:length(set)
sumFound =
sumFound OR sumOfSubset(set-set[i], K –set[i])
return sumFound
end
Backtracking
• After checking whether the sum is achievable using the set of
numbers, the sumOfSubset function passes the result back to the call
on the previous recursion layer
• That layer can then check if any of its recursive calls returned true and
return true if they did
Backtracking: general approach
• When a function makes some recursive calls, it can analyse their
results and pick the best one
• Each recursive call evaluates one decision
• And, recursively, the next possible decisions
• It can be used to find best sequences of moves, best combinations of
options, optimal paths, etc
Thank you
Week 18 – Memoisation &
Dynamic Programming
COMP5180 - Algorithms, Correctness and Efficiency
Let’s consider an example
• Fibonacci function
• f(1) = 1
• f(2) = 1
• f(n) = f(n-1) + f(n-2)
f(6)
f(6)
f(4)
f(5)
f(3)
f(4)
f(3)
f(2)
f(1)
1
1
f(2)
f(3)
f(2)
f(2)
f(1)
f(2)
f(1)
1
1
1
1
1
1
Time complexity of Fibonacci
• f(n) = f(n-1) + f(n-2)
• T(n) = T(n-1) + T(n-2) + O(1)
• T(n-1) ≈ T(n-2)
• T(n) = 2*T(n-1) + O(1)
Solving Fibonacci recurrence
• T(n) = 2*T(n-1) + O(1)
•r=2
•c=1
•L=n
• f(n) = O(1)
• T(n) = f(n) + 2*T(n-1) + 4*T(n-2) + 8*T(n-3) + ……… + 2L*T(n-L)
• Increasing recurrence
• T(n) = O(rL) = O(2n)
Recursive Fibonacci
• f(n) = f(n-1) + f(n-2)
• Time complexity O(2n)
• Can we do better?
• Any ideas?
f(6)
f(6)
f(4)
f(5)
f(3)
f(4)
f(3)
f(2)
f(1)
1
1
f(2)
f(3)
f(2)
f(2)
f(1)
f(2)
f(1)
1
1
1
1
1
1
Some ideas
• Height of the tree can be big
• Big height means lots of computations
• Maybe we can reduce height?
What if we make f(3) = 2?
f(6)
f(4)
f(5)
f(3)
f(4)
f(3)
f(2)
1
2
f(1)
1
f(2)
f(2)
1
1
2
f(2)
f(3)
f(1)
f(2)
1
1
2
f(1)
1
1
What if we make f(4) = 3?
f(6)
f(4)
f(5)
f(4)
f(3)
2
3
f(2)
1
f(3)
f(3)
2
2
3
f(2)
1
What if we make f(5) = 5?
f(6)
f(4)
f(5)
f(4)
3
5
f(3)
2
3
Extra termination conditions
• We can add new termination conditions to allow for faster
calculations
• If-statements with pre-calculated results
• Can help reduce tree depth
Introducing new termination conditions
• Calculating f(n) for small value of n is quick
• So we need to focus on improving the calculations for large values of n
• Add periodic stopping points to limit the depth of the tree
Example
• f(n) = f(n-1) + f(n-2), f(1) = 1, f(2) = 1
• f(10) = 55, f(11) = 89
• f(20) = 6765, f(21) = 10946
• f(30) = 832040, f(31) = 1346269
• And so on
Adding new termination conditions
• Advantages:
• Faster computations for large values of n
• Easy to implement
• Disadvantages:
• Limited effect
• Still O(2n)
• Have to check all termination conditions at every call
f(6)
f(6)
f(4)
f(5)
f(3)
f(4)
f(3)
f(2)
f(1)
1
1
f(2)
f(3)
f(2)
f(2)
f(1)
f(2)
f(1)
1
1
1
1
1
1
Maybe change the recursion formula?
• f(n) = f(n-1) + f(n-2)
• We can express f(n-1) as
• f(n-2) + f(n-3)
• So f(n) = f(n-2) + f(n-3) + f(n-2)
• f(n) = 2*f(n-2) + f(n-3)
Even further?
• f(n) = 2*f(n-2) + f(n-3)
• Express f(n-2) as f(n-3) + f(n-4)
• f(n) = 3*f(n-3) + 2*f(n-4)
How far can the reduction go?
• f(n) = 3*f(n-3) + 2*f(n-4)
• f(n) = 5*f(n-4) + 3*f(n-5)
• f(n) = 8*f(n-5) + 5*f(n-6)
• f(n) = 13*f(n-6) + 8*f(n-7)
• f(n) = 21*f(n-7) + 13*f(n-8)
As far as we want
f(n)
f(n-2)
f(n-1)
f(n-2)
f(n-3)
f(n-3)
f(n-4) f(n-4)
f(n-5)
f(n-3)
f(n-4)
f(n-4)
f(n-5) f(n-5)
f(n-6)
f(n)
f(n-3)
f(n-2)
f(n-4)
f(n-6)
f(n-5)
f(n-7) f(n-7)
f(n-8)
f(n-5)
f(n-7)
f(n-6)
f(n-8) f(n-7)
f(n-9)
f(n)
f(n-4)
f(n-3)
f(n-6)
f(n-7)
f(n-9) f(n-10) f(n-10) f(n-11)
f(n-7)
f(n-8)
f(n-10) f(n-11) f(n-11) f(n-12)
f(n)
f(n-5)
f(n-4)
f(n-8)
f(n-9)
f(n-12) f(n-13) f(n-13) f(n-14)
f(n-9)
f(n-10)
f(n-13) f(n-14) f(n-14) f(n-15)
Further reduction
• Advantages:
• Better recursive formula
• Affects all values of n
• L gets smaller since some levels of recursion are skipped
• Disadvantages:
• Still O(2n)
• Recursive formulas have to be added manually
The main issue
• Function f(n) is defined recursively as f(n-1) + f(n-2)
• This leads to overlaps, same results being calculated multiple times
f(6)
f(6)
f(4)
f(5)
f(3)
f(4)
f(3)
f(2)
f(1)
1
1
f(2)
f(3)
f(2)
f(2)
f(1)
f(2)
f(1)
1
1
1
1
1
1
Solution
• Keep track of previous results
• When the same results are needed, just use old ones instead of
calculating new ones
Memoisation
• Keeping track of the results of previous function calls
• When a function is called with a new input, calculate and save the
result
• When a function is called with a previously calculated input, just
return the previously calculated result
Memoisation: general approach
• Create a data structure that can store previous results
• Preferably indexed by input
• When a function is called – search the data structure for those inputs
• If found – return the result stored in the data structure
• If not found – run the function normally
f(7)
Example: fibonacci(7)
13
f(6)
8
f(4)
f(5)
5
f(3)
f(4)
3
f(3)
2
f(2)
f(2)
f(1)
1
1
1
5
3
2
n
f(n)
f(5)
1234567
1 1 2 3 5 8 13
.
function fibonacci(n)
if n = 1 OR n = 2
return 1
if previouslyCalculated[n]
return previousResult[n]
result = fibonacci(n-1) + fibonacci(n-2)
previouslyCalculated[n] = true
previousResult[n] = result
return result
end
f(7)
What is the complexity?
13
f(6)
8
f(4)
f(5)
5
f(3)
f(4)
3
f(3)
2
f(2)
f(2)
f(1)
1
1
1
5
3
2
n
f(n)
f(5)
1234567
1 1 2 3 5 8 13
.
Memoisation
• Advantages:
• No redundant calls
• O(n)
• Usually easy to implement
• Disadvantages:
• Storing results takes up memory
• Searching for old results can be time-consuming in big tasks
Recap
• If your recursive solution is inefficient, you can:
• Hard-code intermediate results
• Change the recursion formula
• Store previous results to avoid redundancy (Memoisation)
Thank you
Week 20 - Quicksort
COMP5180 - Algorithms, Correctness and Efficiency
In this lecture
• Quicksort
• Correctness of quicksort
• Efficiency of quicksort
• Variations of quicksort
Quicksort
• A sorting algorithm
• Published in 1961
• Has a simple recursive structure:
• Select an element (pivot)
• Partition the input around the pivot
• Quicksort the left part
• Quicksort the right part
Quicksort example
53
1
28
36
47
51
64
72
8
23415768
1
23
1
24
31
4
76
6
78
1243
678
43
3
4
34
3
6
8
Quicksort algorithm overview
• Select a pivot
• Partition the array around the pivot
• Recursively sort the left part
• Recursively sort the right part
Quicksort reduction
arr
arr
partition
partitionedArr, p
partitionedArr[0-p]
quickSort
sortedArr[0-p]
partitionedArr[p-n]
sortedArr
sortedArr[p-n]
quickSort
quickSort
Correctness of Quicksort
• The recursive calls are assumed to
be correct
• Quicksort correctness depends on
the correctness of the partition
Partition
• A way to divide the array into two parts
• Around a certain element called pivot
• In such a way that all elements of the left are smaller and all elements
on the right are larger
Selecting the pivot element
• If nothing is known about the array, we can just select first one by
default
• Original implementation selected last one by default
• Ideally we want to select the mean element of the array, but it’s
usually impractical to search for it
• Many varieties of quicksort exist, with different pivot selections
Partition example
i= 01234567
arr[ i ] = 5 3 8 6 7 1 4 2
Correctness of Quicksort
• If partition is correct, then the Quicksort is correct
How to implement a Quicksort
• Implement a partition function that partitions the array
function partition(arr, lo, hi)
pivot = arr[hi]
i = lo - 1
for j = lo -> hi - 1
if arr[j] <= pivot then
i = i + 1
swap arr[i] and arr[j]
i = i + 1
swap arr[i] and arr[hi]
return i
end
How to implement a Quicksort
• Implement a partition function that partitions the array
• Implement the swap function that swaps array values
function swap(arr, i, j)
exchange = arr[i]
arr[i] = arr[j]
arr[j] = exchange
end
How to implement a Quicksort
• Implement a partition function that partitions the array
• Implement the swap function that swaps array values
• Implement the quicksort function
function quicksort(arr, lo, hi)
if lo = hi
return
pivot = partition(arr, lo, hi)
quicksort(arr, lo, pivot-1)
quicksort(arr, pivot+1, hi)
end
How to implement a Quicksort
• Implement a partition function that partitions the array
• Implement the swap function that swaps array values
• Implement the quicksort function
Complexity of Quicksort
• Quicksort divides the problem of sorting an array into two smaller
sorting problems
• Sizes of those problems can vary
• Sizes of sub arrays are pivot and n – pivot
Week 20 – Complexity classes
COMP5180 - Algorithms, Correctness and Efficiency
Final lecture
In this lecture
• Circuit satisfiability
• Decision problems
• P vs NP
• How to prove P vs NP
Circuit satisfiability
• A common problem
• Analysing a circuit
• Trying to find if a particular logical formula is satisfiable
Common logic gates
Common logic gates
Circuit satisfiability example
Circuit satisfiability example
Circuit Satisfiability problem
• Given a boolean circuit:
• Is there a set of inputs that makes the circuit output True?
• Or, does the circuit always output False?
Solving circuit satisfiability problem
• Easy solution: Brute force
• Just evaluate the formula for every value combination
• For n variables, there is 2n combinations
• Evaluating the formula is usually easy, maybe around O(n)
• So you can probably solve it in O(n*2n)
Can we do better?
• Not known
• Nobody has actually formally proved that we can’t beat brute force
• Maybe, there is a clever algorithm that just hasn’t been discovered yet
P vs NP
• Running time of an efficient algorithm should be bounded by a
polynomial function of its input size
• T(n) ≤ O(nc) where n is the input size and c is some constant power
• T(n) ≤ O(1)
• T(n) ≤ O(n)
• T(n) ≤ O(n2)
• T(n) ≤ O(n3)
• T(n) ≤ O(2n)
P vs NP
O(1)
O(n)
O(n2)
O(2n)
Decision problems
• Decision problems are problems that can give a yes or no answer for a
particular question
• Search problems
• Path finding problems
• Satisfiability problems
• Etc.
P vs NP
• P is a class of all decision problems that can be solved in
polynomial time:
• Comparing two numbers
• Evaluating a set of conditions
• Binary search on an array
• Determining is a number is prime (shown in 2002)
• Etc.
P vs NP
• NP is a set of all decision problems for which if the answer is yes, the
corresponding solution (proof) can be verified in polynomial time
• Satisfiability problem
• Sum of subset problem
• Binary search on an array
• Determining if a number is prime
P vs NP
• co-NP is a set of all decision problems for which if the answer is no,
the corresponding solution (proof) can be verified in polynomial time
• Which ones are co-NP here?
• Satisfiability problem
• Sum of subset problem
• Binary search on an array
• Determining if a number is prime
P vs NP
• EXP is a set of all decision problems that can be solved in exponential
time, e.g. O(2n)
• Satisfiability problem
• Most path-finding algorithms
• Sum of subset
• Tower of Hanoi
• All P problems
P vs NP
• Some problems can have interesting properties:
• Circuit satisfiability is probably not a P problem, it has not been
proven to be solvable in polynomial time, but has not been proven
otherwise either
• At the same time, it is definitely an NP problem, as its solution can be
evaluated in polynomial time
• Also, it has not yet been proven that it is a coNP problem, it still takes
O(2n) to make sure that a circuit is unsatisfiable
P vs NP
• P is a set of decision problems solvable in polynomial time
• NP is a set of all decision problems for which a positive solution can
be evaluated in polynomial time
• Is P = NP?
• This is the single most important unanswered question in theoretical
computer science
How to prove P vs NP
• Most researchers believe that P ≠ NP
• There are a few useful definitions
How to prove P vs NP
• P ⊆ NP
• Because we can check the solution for a polynomial problem by
computing the answer again, which can be done in polynomial time
• P ⊆ co-NP
• Because we can verify that a polynomial decision problem has no
solutions in polynomial time
• Some problems are both NP and co-NP
• All polynomial classes are contained within the EXP class
How to prove P vs NP
How to prove P vs NP
Circuit Satisfiability
Some problems can be reduced to other
problems
• Let’s consider two problems: problem A and problem B
• Let’s say it is possible to reduce the solution for problem A to solving
problem B
• E.g. A is a problem of finding the minimum of an array and B is a
problem of sorting an array
Recall the example
Reductions
• What this means is that if problem A can be reduced to problem B
• In polynomial time, i.e. no exponential stuff is introduced
• Then, problem B is at least as hard as problem A
• But may be harder
• In this particular example, A is O(n) and B is O(n*log n)
Definitions
• NP-hard is a class of problems which are at least as hard as the
hardest problems in NP
• Includes problems that are in NP but not in P:
• Traveling salesman problem
• Sum of subset problem
• Includes problems that are not in NP
• Tower of Hanoi problem
• Includes undecidable problems
NP-hard
Definitions
• NP-hard is a class of problems which are at least as hard as the
hardest problems in NP
• NP-complete is a class of decision problems which contains the
hardest problems in NP
• All NP-hard problems in NP
NP-complete
Evaluating the class of a new problem
• How can we check if a problem belongs to a particular class?
Determining complexity class of a problem
•P
• Create a polynomial-time solution to the problem
• NP
• Create a polynomial-time evaluator for a positive solution
• co-NP
• Create a polynomial-time evaluator for a negative solution
What about NP-hard and NP-complete?
NP-hard
• Proof by enumeration
• Reduce every known NP-problem to the new problem
• Reduce to a known NP-hard problem
• If the problem can be reduced to an NP-hard problem than it has to be at
least NP-hard
NP-hard
NP-complete?
• Show that a problem is NP
• Show that it’s NP-hard
• If both can be proven, the problem is NP-complete
Recap
• Problems vary in complexity
• Polynomial are efficient
• Exponential are not efficient
• Solutions for NP problems can be evaluated in polynomial time
• We don’t know if P = NP
• $1,000,000 for whoever finds out
Thank you
From last week
• Motivation for why O-notation is important
• O-notation for classifying inputs
• Objections…
• Formalising
• Examples
• Dealing with earlier objections
O(f(n)): ! ∃#. ∃%! . # > 0 ∧ %! > 0 ∧ ∀% . % ≥ %! → 0 ≤ !(%) ≤ # / 0(%)}
• Manipulating and Computing with O-notation
• Alternatives to (Big) O-notation
finishing week 12 O-notation
1
Remember: bubble sort pseudocode
for i = 0 to N - 2
for j = 0 to N - 2
if (A(j) > A(j + 1)
multiplication
temp = A(j)
A(j) = A(j + 1)
A(j + 1) = temp
end-if
end-for
end-for
addition
For this for-loop,
T=1+ 1+1+1
=4
(if we assume time
'! for each line here
=1 and we assume
worst case
scenario)
• addition: if you have a
sequence of statements
"! ; "" ; where the “time”
needed to run statement "# is
$# , then the time for the
sequence is $! + $" .
• multiplication: if you have a
for-loop for(int
i=0;i<N,i++) S; and the
cost for a single loop iteration
is $ then the overall cost for
the loop is & × $.
Polynomial emerges: N × # × 4 = 4# "
finishing week 12 O-notation
(in practice, we usually ignore the constant ‘4’ , and refer to bubble sort as O !
!
- see later)
2
Manipulating O-notation
• to describe the O-notation characteristic of a growth function f we
often want the simplest growth function g, such that O(f)=O(g)
O(f(n)): ! ∃#. ∃%! . # > 0 ∧ %! > 0 ∧ ∀% . % ≥ %! → 0 ≤ !(%) ≤ # / 0(%)}
• this involves:
• algebraic manipulation, ordinary
• eliminating constant factors
• eliminating slower-growing summands
• (summands = terms summed together in the function)
finishing week 12 O-notation
3
Some ordinary algebraic laws (reminder)
• !! " #! = ! " # !
" #
• !
= !"$#
• !" " !# = !"%#
• !!%& = ! " !!
• log ' (# " )) = log ' # + log ' )
• log ' # ( = ) " log ' #
• log ' , = log ) , " log ' #
finishing week 12 O-notation
4
O(f(n)): ! ∃#. ∃%! . # > 0 ∧ %! > 0 ∧ ∀% . % ≥ %! → 0 ≤ !(%) ≤ # / 0(%)}
Egs. eliminating constant factors
NB The ( )*+ ,*+-+.*, represents multiplication
• O(3x2) = O(x2)
• because if g is bounded by & ' 3)2 it is also bounded by (3 ' &) ' )2
• 1(log ' 2) = 1(log ( 2)
• because [algebraic rule] log # / = log $ / ' log # & and log # & is a constant factor (hence
why we often just say log(/) in O-notation)
• similarly, 1 3!%& = 1 3!
• because [algebraic rule] 3%&' = 3 ' 3% , and 3 is a constant factor
• however, 1 , !%& ≠ 1 , !
• because we can use the same algebraic rule for ) %&' = ) ' ) %
factor
finishing week 12 O-notation
but x is not a constant
5
Eliminating slower growing summands
Generally, polynomials can be reduced to the term with largest degree e.g.:
2 5%( + 8% = 2(%( )
• if 0 ∈ 2(!) then 2 0 + ! = 2 !
• e.g. 8/ ∈ 2(/" ) as 0 ≤ 8/ ≤ 5 ' /" - so 2 8/ + 5/"
= 2 /"
This works because for sufficiently large n , 5%( > 8%
• E.g. n = 2 (20 > 16)
finishing week 12 O-notation
6
Where do all of these operations come from?
program analysis!
• sequences of statements: cost is the maximum sum of the costs of all the statements that
could be executed
where stA costs O(a) , stB
• E.g. cost of {stA; stB } = O(a) + O(b) = O(a +b)
• E.g. cost of { if (cond) stA; else stB; } = O(c)+max(O(a),O(b))
costs O(b) and checking
the condition costs O(c)
• loops: running stA k times is k times as expensive: cost=k×O(a)= O(k×a)
• nested loops -> polynomials
%
• divide&conquer searches cost is logarithmic; split / inputs into 7 parts of size # , take one part
%
& split into 7 parts of size #! etc. . . = log # / cost
• exhaustive trial-and-error searches have worst-case exponential cost
• exhaustively checking all possibilities for n variables – GP (a, ar, ar2, ar3, …, arn )
• Method calls: cost of its method body (+c for passing params/results)
• for recursive methods we identify a function T defined recursively to capture the recursive method calls (or
ideally, try to calculate/guess a non-recursively-defined
version that satisfies the recurrence equations
finishing week 12 O-notation
7 of T)
(Example, bubblesort loop)
for (int i=0; i<K; i++) {
for (int j=1; j<M; j++) {
if (a[j]<a[j-1]) {
int aux=a[j];
a[j]=a[j-1];
a[j-1]=aux;
}
}
}
finishing week 12 O-notation
See the recording I put up
last week, or refer back to
the week 9 example in the
maths lecture
8
Variations on big-O
• O(f) ["big O"] gives a class of growth functions for which # / 0 is an upper
bound
•
< ∃&. ∃//. & > 0 ∧ // > 0 ∧ ∀/ . / ≥ // → 0 ≤ <(/) ≤ & ' C(/)}
• (we already saw this)
• there are various other notations around, e.g.
• E(F) is the dual (& ' C is a lower bound), or < ∈ Ω(C) ⟷ C ∈ 2(<);
• I(F) has this as both upper and lower bound, i.e. Θ C = 2(C) ∩ Ω(C)
• while O(f) provides an upper bound, there is also:
e.g. Big-O vs little-o:
• o(f) ["little o"] for providing a strict upper bound:
" = 2() " )
2)
• < ∀&. ∃//. & > 0 ∧ // > 0 ∧ ∀/. / ≥ // → 0 ≤ < / < & ' C(/)}
2) " ≠ O() ")
")
2) = O()
finishing week 12 O-notation
9
Final reflections: How problems increase
N
log(N)
1
0.00
2
0.69
3
1.10
4
1.39
5
1.61
10
2.30
50
3.91
100
4.61
200
5.30
1000
6.91
NlogN
0.00
1.39
3.30
5.55
8.05
23.03
195.60
460.52
1059.66
6907.76
N2
2N
N!
1
4
9
16
25
100
2500
10000
40000
1000000
2
4
8
16
32
1024
1.1259E+15
1.26765E+30
1.60694E+60
1.0715E+301
1
2
6
24
120
3628800
3.04141E+64
9.3326E+157
#NUM!
#NUM!
• Life time of the universe about 4E+10 years (40 billion years) = approx
1E+18 seconds. At 5 Peta (5+E15) FLOPS we get 5E+33 instructions per
universe lifetime
• With a graph of 200 nodes, an algorithm taking exactly exponential time
means we need about 3E+26 universe lifetimes to solve the problem.
finishing week 12 O-notation
10
Now onto graphs…
finishing week 12 O-notation
11
Download