Programming abstractions and analysis –– recursiona1120.cs.aalto.fi/slides/lecture8.pdf · 2....

58
01110111010110 11110101010101 00101011010011 01010111010101 01001010101010 10101010101010 10101011110101 01010101011101 01010111010110 10101101010110 10101110101010 11101010101101 01110111010110 10111011010101 11110101010101 00010101010101 01011010101110 Programming abstractions and analysis –– recursion Mikko Kivelä Department of Computer Science Aalto University CS-A1120 Programming 2 15 April 2020 Lecture notes based on material created by Tommi Junttila & Petteri Kaski

Transcript of Programming abstractions and analysis –– recursiona1120.cs.aalto.fi/slides/lecture8.pdf · 2....

Page 1: Programming abstractions and analysis –– recursiona1120.cs.aalto.fi/slides/lecture8.pdf · 2. Bits and data 3. Combinational logic 4. Sequential logic 5. Programmable computer

011101110101101111010101010100101011010011010101110101010100101010101010101010101010

101010111101010101010101110101010111010110101011010101101010111010101011101010101101011101110101101011101101010111110101010101000101010101010101101010111010101010100101

Programming abstractions and analysis –– recursion

Mikko KiveläDepartment of Computer Science

Aalto University

CS-A1120 Programming 215 April 2020

Lecture notes based on material created by Tommi Junttila & Petteri Kaski

Page 2: Programming abstractions and analysis –– recursiona1120.cs.aalto.fi/slides/lecture8.pdf · 2. Bits and data 3. Combinational logic 4. Sequential logic 5. Programmable computer

Contents (rounds and modules)• 1. Warmup round

• I The mystery of the computer 2. Bits and data 3. Combinational logic 4. Sequential logic 5. Programmable computer

• II Programming abstractions and analysis 6. Collections and functions 7. Efficiency 8. Recursion 9. Algorithms and representations of information

• III Frontiers 10. Concurrency and parallelism 11. Virtualization and scalability 12. Machines that learn?

Page 3: Programming abstractions and analysis –– recursiona1120.cs.aalto.fi/slides/lecture8.pdf · 2. Bits and data 3. Combinational logic 4. Sequential logic 5. Programmable computer

RecursionDefinition: see recursion

Page 4: Programming abstractions and analysis –– recursiona1120.cs.aalto.fi/slides/lecture8.pdf · 2. Bits and data 3. Combinational logic 4. Sequential logic 5. Programmable computer

Recursion

• Recursion is a basic principle for…

• replacing iteration in functional programming

• defining structural objects such as data types or collections and traverse them

• solving computational problems

Page 5: Programming abstractions and analysis –– recursiona1120.cs.aalto.fi/slides/lecture8.pdf · 2. Bits and data 3. Combinational logic 4. Sequential logic 5. Programmable computer

Palindromes (reminder from O1)

• Palindromes are words or sentences that read the same forwards and backwards (not including spaces and punctuation)

• E.g., “testset” or “A man, a plan, a canal: Panama”

• Can be defined (and checked for) recursively:

A. String with zero or one characters is a palindrome

B. String is a palindrome if it (1) starts and ends with the same character and (2) the substring between these is a palindrome

Page 6: Programming abstractions and analysis –– recursiona1120.cs.aalto.fi/slides/lecture8.pdf · 2. Bits and data 3. Combinational logic 4. Sequential logic 5. Programmable computer

Palindromes (reminder from O1)

• Animation for a similar code in Programming 1: https://plus.cs.aalto.fi/o1/2019/w12/ch01/

def isPalindrome(s: String): Boolean = { // Helper inner function doing the actual recursive task def actualSearch(t: String): Boolean = { if (t.length <= 1) true else if (t.head != t.last) false else actualSearch(t.substring(1, t.length - 1)) }

// Remove spaces and special characters val sPlain = s.filter(_.isLetterOrDigit).map(_.toLower) // Call the actual recursive search function actualSearch(sPlain) }

Page 7: Programming abstractions and analysis –– recursiona1120.cs.aalto.fi/slides/lecture8.pdf · 2. Bits and data 3. Combinational logic 4. Sequential logic 5. Programmable computer

Textual representation of a call stack• A way of visualising what happens when executing a

recursive function

isPalindrome("ufo tofu"): val sPlain = "ufotofu" // We could omit this return actualSearch("ufotofu") actualSearch("ufotofu"): // tests (t.length <= 1) and (t.head != t.last) both fail return actualSearch("fotof") actualSearch("fotof"): return actualSearch("oto"): actualSearch(“oto”):

return actualSearch("t"): actualSearch("t"):

return true // test (t.length <= 1) succeeds

• Not interesting or obvious rows such as sPlain = “ufotofu” can be left out of this illustration

Page 8: Programming abstractions and analysis –– recursiona1120.cs.aalto.fi/slides/lecture8.pdf · 2. Bits and data 3. Combinational logic 4. Sequential logic 5. Programmable computer

Expanding• Another way of visualising recursive function calls

• Computations in functions without side effects can be illustrated by “expanding” the code:

isPalisPalindrome("ufo tofu") =actualSearch("ufo tofu".filter(_.isLetterOrDigit).map(_.toLower)) =actualSearch("ufotofu".map(_.toLower)) =actualSearch("ufotofu") =actualSearch("fotof") =actualSearch("oto") =actualSearch("t") =true

• Again, not interesting or obvious rows can be left out

Page 9: Programming abstractions and analysis –– recursiona1120.cs.aalto.fi/slides/lecture8.pdf · 2. Bits and data 3. Combinational logic 4. Sequential logic 5. Programmable computer

Tail calls and recursion

Page 10: Programming abstractions and analysis –– recursiona1120.cs.aalto.fi/slides/lecture8.pdf · 2. Bits and data 3. Combinational logic 4. Sequential logic 5. Programmable computer

Recursion and the call stack • The main problem in recursion: there is only limited

amount of memory for the call stack

• For example, computing factorial recursively:

• Running this will lead to problems:

def fact(n : Int) : BigInt = { require(n >= 1, "n should be a positive integer") if(n == 1) BigInt(1) else n * fact(n-1)}

n! = {1 when n = 1n × (n − 1)! when n > 2

scala> fact(100000)java.lang.StackOverflowError

Page 11: Programming abstractions and analysis –– recursiona1120.cs.aalto.fi/slides/lecture8.pdf · 2. Bits and data 3. Combinational logic 4. Sequential logic 5. Programmable computer

Recursion and the call stack • Call stack for our recursive factorial function:

fact(4): val temp1 = fact(3) fact(3): val temp2 = fact(2) fact(2): val temp3 = fact(1) fact(1): return BigInt(1)

// temp3 = 1

return 2*temp3 // 2*1 = 2 // temp2 = 2 return 3*temp2 // 3*2 = 6// temp1 = 6

return 4*temp1 // 4*6 = 24

• For every call, we need to remember the value of n and calculate fact(n-1) before we can return the result

➔ Call stack will have the depth of the parameter n

Page 12: Programming abstractions and analysis –– recursiona1120.cs.aalto.fi/slides/lecture8.pdf · 2. Bits and data 3. Combinational logic 4. Sequential logic 5. Programmable computer

Recursion and the call stack • We can compute the result by expanding the function

(because it doesn’t have side effects)

fact(4) = (4 * fact(3)) = (4 * (3 * fact(2))) = (4 * (3 * (2 * fact(1)))) = (4 * (3 * (2 * 1))) = (4 * (3 * 2)) = (4 * 6) =24

Page 13: Programming abstractions and analysis –– recursiona1120.cs.aalto.fi/slides/lecture8.pdf · 2. Bits and data 3. Combinational logic 4. Sequential logic 5. Programmable computer

Definition

Tail calls

Function call in a method is a tail call if it is the last operation of the function

def fact(n : Int) : BigInt = { require(n >= 1, "n should be a positive integer") if(n == 1) BigInt(1) else n * fact(n-1)}

• BigInt(1) is a tail call

• fact(n-1) is not a tail call; the last operation in the function is n * fact(n-1)

• For example, in the function

Page 14: Programming abstractions and analysis –– recursiona1120.cs.aalto.fi/slides/lecture8.pdf · 2. Bits and data 3. Combinational logic 4. Sequential logic 5. Programmable computer

Definition

Tail recursion

Function is tail recursive if all of its calls to itself are tail calls

• Here we only consider direct tail recursion where the tail calls do not go through some other function (e.g., def f() = …; g() and def g() = …; f() )

• Scala can optimise tail recursive functions such that they are implemented with iteration

• This is because the compiler knows that no variables cannot be used after the tail call and the the call frame can be released

Page 15: Programming abstractions and analysis –– recursiona1120.cs.aalto.fi/slides/lecture8.pdf · 2. Bits and data 3. Combinational logic 4. Sequential logic 5. Programmable computer

Tail recursion • Lets make a tail recursive version of the factorial

function (by using tail recursive inner function):

• Note the similarity with the iterative version:

def fact(n: Int): BigInt = { require(n >= 1, "n should be a positive integer") def iterate(i: Int, result: BigInt): BigInt = { if(i == 0) result else iterate(i - 1, result * i) } iterate(n, BigInt(1)) }

def fact(n : Int) : BigInt = { require(n >= 1, "n should be a positive integer") var result = BigInt(1) for(i <- n to 1 by -1) result = result * i result }

Page 16: Programming abstractions and analysis –– recursiona1120.cs.aalto.fi/slides/lecture8.pdf · 2. Bits and data 3. Combinational logic 4. Sequential logic 5. Programmable computer

Tail recursion • Expansion of the tail recursive fact function:

• Same with call stack: the call frame of iterate is reused:

fact(4) =iterate(4, 1) =iterate(3, 4) =iterate(2, 12) =iterate(1, 24) =iterate(0, 24) = 24

fact(4): return iterate(4, 1) iterate(4, 1): return iterate(3, 4) iterate(3, 4): return iterate(2, 12) iterate(2, 12): return iterate(1, 24)

iterate(0, 24):

return 24

Page 17: Programming abstractions and analysis –– recursiona1120.cs.aalto.fi/slides/lecture8.pdf · 2. Bits and data 3. Combinational logic 4. Sequential logic 5. Programmable computer

@tailrec annotation • The @tailrec annotation in Scala declares that the

compiler must optimise tail recursion into iteration

• If the annotated function is not tail recursive, the compiler will issue an error

• Only direct tail calls are allowed

• It must not be possible to override the function: it must be an inner function or declared as final method

import scala.annotation.tailrec...@tailrec def myFunction(...) {...

}

For more information, see: http://theyougen.blogspot.com/2010/01/why-overrideable-tail-recursive-methods.html

Page 18: Programming abstractions and analysis –– recursiona1120.cs.aalto.fi/slides/lecture8.pdf · 2. Bits and data 3. Combinational logic 4. Sequential logic 5. Programmable computer

Recursively defined data structures

Page 19: Programming abstractions and analysis –– recursiona1120.cs.aalto.fi/slides/lecture8.pdf · 2. Bits and data 3. Combinational logic 4. Sequential logic 5. Programmable computer

Recursive data structures

• In addition to functions, data structure can be recursive

• They are most naturally manipulated with recursive functions

• Next we go through two examples:

1. Linked lists (similar to List in Scala)

2. Symbolic arithmetic expressions (example of more general tree data structure)

Page 20: Programming abstractions and analysis –– recursiona1120.cs.aalto.fi/slides/lecture8.pdf · 2. Bits and data 3. Combinational logic 4. Sequential logic 5. Programmable computer

Linked lists

Page 21: Programming abstractions and analysis –– recursiona1120.cs.aalto.fi/slides/lecture8.pdf · 2. Bits and data 3. Combinational logic 4. Sequential logic 5. Programmable computer

Linked lists• We consider lists containing elements of type T

• Let define such T-lists recursively:

1. Nil is the empty T-list

2. If l is a T-list and e is an element of type T, then Cons(e, l) is a T-list where e is the first element followed by the elements of the list l

Examples:

• Nil is an empty Int-list []

• Cons(1,Nil) is an Int-list [1]

• Cons(1,Cons(3,Cons(5,Nil))) is an Int-list [1,3,5]

• Cons(1,Cons(Nil,Cons(5,Nil))) is not an Int-list

Page 22: Programming abstractions and analysis –– recursiona1120.cs.aalto.fi/slides/lecture8.pdf · 2. Bits and data 3. Combinational logic 4. Sequential logic 5. Programmable computer

Linked lists• For a list

Cons(e, l)

• We say that:

• e is the head of the list

• l is the tail of the list

• Empty list does not have head or tail

Examples:

• In Int-list Cons(5, Nil) the head is 5 and tail Nil

• In String-list Cons(“first”, Cons(“second”,Nil)) the head is “first” and tail Cons(“second”,Nil)

Page 23: Programming abstractions and analysis –– recursiona1120.cs.aalto.fi/slides/lecture8.pdf · 2. Bits and data 3. Combinational logic 4. Sequential logic 5. Programmable computer

Linked lists, the first implementationabstract class LinkedList[A] {

def isEmpty: Boolean def head: A def tail: LinkedList[A]}

class Nil[A]() extends LinkedList[A] { def isEmpty = true def head = throw new java.util.NoSuchElementException("head of empty list") def tail = throw new java.util.NoSuchElementException("tail of empty list") }

class Cons[A](val head: A, val tail: LinkedList[A]) extends LinkedList[A] { def isEmpty = false}

object Nil { def apply[A]() = new Nil[A]()}

object Cons { def apply[A](head : A, tail : LinkedList[A]) = new Cons(head, tail)

}

Page 24: Programming abstractions and analysis –– recursiona1120.cs.aalto.fi/slides/lecture8.pdf · 2. Bits and data 3. Combinational logic 4. Sequential logic 5. Programmable computer

Linked lists, the first implementation

• We can now write:

val l = Cons(5, Cons(4, Cons(-7, Nil())))

• … and these objects will be created in the memory:

head

Cons

tailhead

Nil

5

Cons

tailhead

4 −7

l Cons

tail

Page 25: Programming abstractions and analysis –– recursiona1120.cs.aalto.fi/slides/lecture8.pdf · 2. Bits and data 3. Combinational logic 4. Sequential logic 5. Programmable computer

Linked lists, the first implementation• If we write:

val l = Cons(5, Cons(4, Cons(-7, Nil()))) val m = Cons(3, l)

• … these objects will be created:

Cons

tailhead

Nil

5

Cons

tailhead

Cons

tailhead

3

4 −7

l

m

Cons

tailhead

• Note that the list l is not copied

• Adding to (and removing from) the beginning of the list is efficient

Page 26: Programming abstractions and analysis –– recursiona1120.cs.aalto.fi/slides/lecture8.pdf · 2. Bits and data 3. Combinational logic 4. Sequential logic 5. Programmable computer

Linked lists, contains method• Tests if the list contains e

• Implemented recursively:A. empty list does not contain eB. if head of the list is e, then it contains eC. otherwise it only contains e if the tail contains e

@tailrec final def contains(e: A): Boolean = { if(isEmpty) false else if(head == e) true else tail.contains(e)}

• Example of the call stack:Cons(1, Cons(2, Cons(3, Nil()))).contains(3): return Cons(2, Cons(3, Nil())).contains(3)Cons(2, Cons(3, Nil())).contains(3): return Cons(3, Nil()).contains(3)Cons(3, Nil()).contains(3): return true

Page 27: Programming abstractions and analysis –– recursiona1120.cs.aalto.fi/slides/lecture8.pdf · 2. Bits and data 3. Combinational logic 4. Sequential logic 5. Programmable computer

Linked lists, length method• Calculates the length of the list

• Implemented recursively:A. empty list has length 0B. the length of list Cons(e, t) is 1 plus length of the list t

abstract class LinkedList[A] { ... def length: Int}class Nil[A]() extends LinkedList[A] { ... def length = 0}class Cons[A](val head: A, val tail: LinkedList[A]) extends LinkedList[A] {...

def length = 1 + tail.length}

Page 28: Programming abstractions and analysis –– recursiona1120.cs.aalto.fi/slides/lecture8.pdf · 2. Bits and data 3. Combinational logic 4. Sequential logic 5. Programmable computer

Linked lists, length method• This implementation is not tail recursive!

• With a long list it will result in a StackOverflowError

• Example of a call stack with a short list

Cons(1, Cons(2, Cons(3, Nil()))).length: val tmp1 = Cons(2, Cons(3, Nil())).length Cons(2, Cons(3, Nil())).length: val tmp2 = Cons(3, Nil()).length Cons(3, Nil()).length: val tmp3 = Nil().length Nil().length: return 0

// tmp3 == 0

return 1 + tmp3 // tmp2 == 1 return 1 + tmp2// tmp1 == 2

return 1 + tmp13

Page 29: Programming abstractions and analysis –– recursiona1120.cs.aalto.fi/slides/lecture8.pdf · 2. Bits and data 3. Combinational logic 4. Sequential logic 5. Programmable computer

Linked lists, length method• A tail recursive version:

def length: Int = { def inner(remaining: LinkedList[A], result:Int): Int = { if(remaining.isEmpty) result else inner(remaining.tail, result+1) } inner(this, 0)}

• Expanded function call:

Cons(1, Cons(2, Cons(3, Nil()))).length =inner(Cons(1, Cons(2, Cons(3, Nil()))), 0) =inner(Cons(2, Cons(3, Nil())), 1) =inner(Cons(3, Nil()), 2) =inner(Nil(), 3) =3

Page 30: Programming abstractions and analysis –– recursiona1120.cs.aalto.fi/slides/lecture8.pdf · 2. Bits and data 3. Combinational logic 4. Sequential logic 5. Programmable computer

Linked lists, length method• A tail recursive version:

def length: Int = { def inner(remaining: LinkedList[A], result:Int): Int = { if(remaining.isEmpty) result else inner(remaining.tail, result+1) } inner(this, 0)}

• Compare to imperative version:

def length: Int = { var result = 0 var remaining = this while(!remaining.isEmpty) {

result += 1

remaining = remaining.tail }

result

}

Page 31: Programming abstractions and analysis –– recursiona1120.cs.aalto.fi/slides/lecture8.pdf · 2. Bits and data 3. Combinational logic 4. Sequential logic 5. Programmable computer

case class

• Companion objects are defined automatically!

abstract class LinkedList[A] { def isEmpty: Boolean def head: A def tail: LinkedList[A]}case class Nil[A]() extends LinkedList[A] { def isEmpty = true def head = throw new java.util.NoSuchElementException("head of empty list") def tail = throw new java.util.NoSuchElementException("tail ofempty list")

}case class Cons[A](val head: A, val tail: LinkedList[A]) extends LinkedList[A] { def isEmpty = false}

Page 32: Programming abstractions and analysis –– recursiona1120.cs.aalto.fi/slides/lecture8.pdf · 2. Bits and data 3. Combinational logic 4. Sequential logic 5. Programmable computer

case class • The previously defined operations now implemented

using case class and the match pattern matching:

@tailrec final def contains(e: A): Boolean = this match { case Nil() => false case Cons(h, t) => if (h == e) true else t.contains(e)}

def length: Int = { @tailrec def inner(remaining: LinkedList[A], result: Int): Int = remaining match { case Nil() => result case Cons(_, t) => inner(t, result + 1) } inner(this, 0)}

Page 33: Programming abstractions and analysis –– recursiona1120.cs.aalto.fi/slides/lecture8.pdf · 2. Bits and data 3. Combinational logic 4. Sequential logic 5. Programmable computer

• The previously defined operations now implemented using case class and the match pattern matching:

abstract class LinkedList[A]{…@tailrec final def foreach(f : A => Any) : Unit = this match { case Nil() => () // The only value of type Unit is "()" case Cons(v, t) => f(v); t.foreach(f)}

foreach method

• Example of a call stack:

Cons(1, Cons(2, Cons(3, Nil()))).foreach(println _): println(1)

return Cons(2, Cons(3, Nil())).foreach(println _)Cons(2, Cons(3, Nil())).foreach(println _):println(2) return Cons(3, Nil()).foreach(println _)

Cons(3, Nil()).foreach(println _):println(3) return Nil().foreach(println _)

Nil().foreach(println _):return ()

Page 34: Programming abstractions and analysis –– recursiona1120.cs.aalto.fi/slides/lecture8.pdf · 2. Bits and data 3. Combinational logic 4. Sequential logic 5. Programmable computer

• Reverses the order of elements in the list:def reverse: LinkedList[A] = { @tailrec def inner(remaining: LinkedList[A], result: LinkedList[A]): LinkedList[A] = remaining match { case Nil() => result case Cons(h, t) => inner(t, Cons(h, result)) } inner(this, Nil())}

reverse method

• Example expansion:

Cons(1, Cons(2, Cons(3, Nil()))).reverse = inner(Cons(1, Cons(2, Cons(3, Nil()))), Nil()) = inner(Cons(2, Cons(3, Nil())), Cons(1, Nil())) = inner(Cons(3, Nil()), Cons(2, Cons(1, Nil()))) = inner(Nil(), Cons(3, Cons(2, Cons(1, Nil())))) = Cons(3, Cons(2, Cons(1, Nil())))

Page 35: Programming abstractions and analysis –– recursiona1120.cs.aalto.fi/slides/lecture8.pdf · 2. Bits and data 3. Combinational logic 4. Sequential logic 5. Programmable computer

Symbolic arithmetic expressions

Page 36: Programming abstractions and analysis –– recursiona1120.cs.aalto.fi/slides/lecture8.pdf · 2. Bits and data 3. Combinational logic 4. Sequential logic 5. Programmable computer

Target:Data structure for simple arithmetic expressions

with variables, constants, additions, multiplications, negations

Example:-((2.0*x) + y)

How to define the expressions in Scala?

Symbolic arithmetic expressions

Page 37: Programming abstractions and analysis –– recursiona1120.cs.aalto.fi/slides/lecture8.pdf · 2. Bits and data 3. Combinational logic 4. Sequential logic 5. Programmable computer

Symbolic arithmetic expressions• Let define symbolic arithmetic expressions recursively:

1. Every number is an expression (e.g., 1.2)

2. Every variable is an expression (e.g., x)

3. If e1 and e2 are expression, then the following are also expressions: (e1 + e2), (e1 - e2), (e1 * e2), and -e1

All of the following examples are expressions:

• Variables and numbers: 2.0, x, and y

• Their combinations: (2.0 * x)

• Any further combinations: ((2.0 * x) + y) and -((2.0 * x) + y)

Page 38: Programming abstractions and analysis –– recursiona1120.cs.aalto.fi/slides/lecture8.pdf · 2. Bits and data 3. Combinational logic 4. Sequential logic 5. Programmable computer

Expressions, definition abstract class Expr {

/* To enable familiar infix notation */ def +(other: Expr) = Add(this, other) def -(other: Expr) = Subtract(this, other) def *(other: Expr) = Multiply(this, other) def unary_-() = Negate(this)}

case class Var(name: String) extends Expr { override def toString = name }

case class Num(value: Double) extends Expr { override def toString = value.toString }

case class Multiply(left: Expr, right: Expr) extends Expr { override def toString = "(" + left + "*" + right + “)" }

case class Add(left: Expr, right: Expr) extends Expr { override def toString = "(" + left + " + " + right + “)" }

case class Subtract(left: Expr, right: Expr) extends Expr { override def toString = "(" + left + " - " + right + “)”} case class Negate(expr: Expr) extends Expr { override def toString = " - “ + expr}

Page 39: Programming abstractions and analysis –– recursiona1120.cs.aalto.fi/slides/lecture8.pdf · 2. Bits and data 3. Combinational logic 4. Sequential logic 5. Programmable computer

Expressions• Expression, and how the objects look like in the memory:

val e = -((Num(2.0) * Var("x")) + Var("y"))

name = "y"

VarMultiply

left

right

name = "x"

Var

value = 2.0

Num

Add

left

right

Negate

expr

Page 40: Programming abstractions and analysis –– recursiona1120.cs.aalto.fi/slides/lecture8.pdf · 2. Bits and data 3. Combinational logic 4. Sequential logic 5. Programmable computer

Expressions, evaluationclass VariableNotAssignedException(message: String) extends java.lang.RuntimeException(message)/** * Evaluate the expression in the point p, where p is a map * associating each variable name in the expression into a Double value.*/

def evaluate(p: Map[String, Double]): Double = this match { case Var(n) => p.get(n) match { case Some(v) => v case None => throw new VariableNotAssignedException(n) }

case Num(v) => v case Multiply(l, r) => l.evaluate(p) * r.evaluate(p) case Add(l, r) => l.evaluate(p) + r.evaluate(p) case Subtract(l, r) => l.evaluate(p) - r.evaluate(p) case Negate(t) => -t.evaluate(p)}

Page 41: Programming abstractions and analysis –– recursiona1120.cs.aalto.fi/slides/lecture8.pdf · 2. Bits and data 3. Combinational logic 4. Sequential logic 5. Programmable computer

Expressions, evaluation example

val e = -((Num(2.0) * Var("x")) + Var("y"))val p = Map("x" -> 2.0, "y" -> -3.5)

• By expanding we get:

e.evaluate(p) = ((Num(2.0) * Var("x")) + Var("y")).evaluate(p) =((Num(2.0) * Var("x")).evaluate(p) + Var("y").evaluate(p)) = ((Num(2.0).evaluate(p) * Var("x").evaluate(p)) + Var("y").evaluate(p)) =((2.0 * Var("x").evaluate(p)) + Var("y").evaluate(p)) =((2.0 * 2.0) + Var("y").evaluate(p)) =(4.0 + Var("y").evaluate(p)) =(4.0 + -3.5) =0.5

Page 42: Programming abstractions and analysis –– recursiona1120.cs.aalto.fi/slides/lecture8.pdf · 2. Bits and data 3. Combinational logic 4. Sequential logic 5. Programmable computer

Expressions, symbolic simplification

def simplify: Expr = { // First simplify sub-expressions val subresult = this match { case Multiply(l, r) => Multiply(l.simplify, r.simplify) case Add(l, r) => Add(l.simplify, r.simplify) case Subtract(l, r) => Subtract(l.simplify, r.simplify) case Negate(t) => Negate(t.simplify) case _ => this // Handles Var and Num } // and then the expression itself subresult match { case Multiply(Num(1.0), r) => r ... case Add(l, Num(0.0)) => l ... case _ => subresult // Could not simplify}

}

Page 43: Programming abstractions and analysis –– recursiona1120.cs.aalto.fi/slides/lecture8.pdf · 2. Bits and data 3. Combinational logic 4. Sequential logic 5. Programmable computer

Expressions, symbolic simplification

scala> val e = Var("y")* ((Num(1.0) * Var("x")) + Num(0.0))e: expressions.Multiply = (y*((1.0*x) + 0.0))

scala> e.simplifyres2: expression.Expr = (x*y)

• We can now automatically simplify an expression y(1.0x)+0.0

Page 44: Programming abstractions and analysis –– recursiona1120.cs.aalto.fi/slides/lecture8.pdf · 2. Bits and data 3. Combinational logic 4. Sequential logic 5. Programmable computer

Solving problems recursively

Page 45: Programming abstractions and analysis –– recursiona1120.cs.aalto.fi/slides/lecture8.pdf · 2. Bits and data 3. Combinational logic 4. Sequential logic 5. Programmable computer

Many problems can be solved recursively (even if they are not defined recursively)

This is usually done by backtracking search:Candidate solutions are built recursively, and backtracking is done if we notice that

the solution is not going to work

Solving problems recursively

Page 46: Programming abstractions and analysis –– recursiona1120.cs.aalto.fi/slides/lecture8.pdf · 2. Bits and data 3. Combinational logic 4. Sequential logic 5. Programmable computer

The subset sum problem

Page 47: Programming abstractions and analysis –– recursiona1120.cs.aalto.fi/slides/lecture8.pdf · 2. Bits and data 3. Combinational logic 4. Sequential logic 5. Programmable computer

Definition (subset sum problem)

The subset sum problem

Given a set of integers S = {v1, . . . , vn} and a target number t,

is there a subset R ⊆ S such that ∑v∈R

v = t .

Examples

• S = {-9, -3, 4, 7, 12} and t=10 ⇒ there is a

solution R= {-9, 7, 12}

• S = {-9, -3, 4, 7, 12} and t=6 ⇒ no solution exists

Is this problem easy to solve efficiently?

Page 48: Programming abstractions and analysis –– recursiona1120.cs.aalto.fi/slides/lecture8.pdf · 2. Bits and data 3. Combinational logic 4. Sequential logic 5. Programmable computer

How can recursion help us to solve this

problem?

The subset sum problem

Page 49: Programming abstractions and analysis –– recursiona1120.cs.aalto.fi/slides/lecture8.pdf · 2. Bits and data 3. Combinational logic 4. Sequential logic 5. Programmable computer

The subset sum problem• Example: S = {-9, -3, 4, 7, 12} and t=10

• Start by picking arbitrary element, e.g., -9

Page 50: Programming abstractions and analysis –– recursiona1120.cs.aalto.fi/slides/lecture8.pdf · 2. Bits and data 3. Combinational logic 4. Sequential logic 5. Programmable computer

The subset sum problem• Example: S = {-9, -3, 4, 7, 12} and t=10

• Start by picking arbitrary element, e.g., -9

• If there is a solution R, then -9 either (1.) belongs to or (2.) doesn’t belong to the set R:

1. Since -9 doesn’t belong to R, then S \ {-9} = {-3, 4, 7, 12} must have a subset that sums up to t

2. Since -9 belongs to R, then S \ {-9} = {-3, 4, 7, 12} must have a subset that sums up to t - (-9) = 19

Page 51: Programming abstractions and analysis –– recursiona1120.cs.aalto.fi/slides/lecture8.pdf · 2. Bits and data 3. Combinational logic 4. Sequential logic 5. Programmable computer

The subset sum problem

• We are still missing the base cases, which determine where the recursion stops

• These are:

• If S = ∅ and t = 0 then R = ∅ is a solution (or more generally if t=0 then R = ∅ is a solution)

• If S = ∅ and t ≠ 0, then there is no solution

Page 52: Programming abstractions and analysis –– recursiona1120.cs.aalto.fi/slides/lecture8.pdf · 2. Bits and data 3. Combinational logic 4. Sequential logic 5. Programmable computer

The subset sum problemDoes {-9,-3,7,12} have a subset with sum 10?

Does {-3,7,12} have a subset with sum 10?

Does {7,12} have a subset with sum 10?

Does {12} have a subset with sum 10?

Does {} have a subset with sum 10? No!

Does {} have a subset with sum 10-12=-2? No!

=> No!

Does {12} have a subset with sum 10-7 = 3?

Does {} have a subset with sum 3? No!

Does {} have a subset with sum 3-12=-9? No! => No!

=> No!

Does {7,12} have a subset with sum 10- -3 = 13?

[Many rows removed ...]

=> No!

Does {-3,7,12} have a subset with sum 10- -9 = 19?

Does {7,12} have a subset with sum 19?

Does {12} have a subset with sum 19?

[Few lines omitted ...]

=> No!Does {12} have a subset with sum 19-7=12?

Does {} have a subset with sum 12? **No**

Does {} have a subset with sum 12-12=0? **Yes**

=> Yes!, {12}!

The branch in which we included $7$ has a solution **Yes, {7,12}**

The branch in which we omitted $-3$ has a solution **Yes, {7,12}**

The branch in which we included $-9$ has a solution **Yes, {-9,7,12}**

Page 53: Programming abstractions and analysis –– recursiona1120.cs.aalto.fi/slides/lecture8.pdf · 2. Bits and data 3. Combinational logic 4. Sequential logic 5. Programmable computer

The subset sum problemDoes {-9,-3,7,12} have a subset with sum 10?

Does {-3,7,12} have a subset with sum 10?

Does {7,12} have a subset with sum 10?

Does {12} have a subset with sum 10?

Does {} have a subset with sum 10? No!

Does {} have a subset with sum 10-12=-2? No!

=> No!

Does {12} have a subset with sum 10-7 = 3?

Does {} have a subset with sum 3? No!

Does {} have a subset with sum 3-12=-9? No! => No!

=> No!

Does {7,12} have a subset with sum 10- -3 = 13?

[Many rows removed ...]

=> No!

Does {-3,7,12} have a subset with sum 10- -9 = 19?

Does {7,12} have a subset with sum 19?

Does {12} have a subset with sum 19?

[Few lines omitted ...]

=> No!Does {12} have a subset with sum 19-7=12?

Does {} have a subset with sum 12? **No**

Does {} have a subset with sum 12-12=0? **Yes**

=> Yes!, {12}!

The branch in which we included $7$ has a solution **Yes, {7,12}**

The branch in which we omitted $-3$ has a solution **Yes, {7,12}**

The branch in which we included $-9$ has a solution **Yes, {-9,7,12}**

Page 54: Programming abstractions and analysis –– recursiona1120.cs.aalto.fi/slides/lecture8.pdf · 2. Bits and data 3. Combinational logic 4. Sequential logic 5. Programmable computer

The subset sum problemdef solve(set: Set[Int], target: Int, elementSelector: (Set[Int], Int) => Int = selectElementSimple): Option[Set[Int]] = {def inner(s: Set[Int], t: Int): Option[Set[Int]] = { if (t == 0) return Some(Set[Int]()) else if (s.isEmpty) return None val e = elementSelector(s, t) val rest = s - e // Search for a solution without e val solNotIn = inner(rest, t) if (solNotIn.nonEmpty) return solNotIn // Search for a solution with e val solIn = inner(rest, t - e) if (solIn.nonEmpty) return Some(solIn.get + e) // No solution found here, backtrack return None}inner(set, target)}

• Let the computer do the heavy lifting:

Page 55: Programming abstractions and analysis –– recursiona1120.cs.aalto.fi/slides/lecture8.pdf · 2. Bits and data 3. Combinational logic 4. Sequential logic 5. Programmable computer

The subset sum problem• The tree of recursive function call

• execution proceed from top to bottom, left branch before the right branch

• The gray branch is not reached before we find the solution

inner({-9,-3,7,12}, 10)

inner({-3,7,12}, 10)

omit -9

inner({-3,7,12}, 19)

take -9

inner({7,12}, 10)

omit -3

inner({7,12}, 13)

take -3

inner({7,12}, 19)

omit -3

inner({7,12}, 22)

take -3

inner({12}, 10)

omit 7

inner({12}, 3)

take 7

inner({12}, 13)

omit 7

inner({12}, 6)

take 7

inner({}, 10)

omit 12

inner({}, -2)

take 12

inner({}, 3)

omit 12

inner({}, -9)

take 12

inner({}, 13)

omit 12

inner({}, 1)

take 12

inner({}, 6)

omit 12

inner({}, -6)

take 12

inner({12}, 19)

omit 7

inner({12}, 12)

take 7

inner({}, 19)

omit 12

inner({}, 7)

take 12

inner({}, 12)

omit 12

inner({}, 0)

take 12

Page 56: Programming abstractions and analysis –– recursiona1120.cs.aalto.fi/slides/lecture8.pdf · 2. Bits and data 3. Combinational logic 4. Sequential logic 5. Programmable computer

The subset sum problem• Adding pruning of branches to the inner function

else if (s.filter(_ > 0).sum < t || s.filter(_ < 0).sum > t) // The positive (negative) number cannot add up (down) to treturn None

inner({-9,-3,7,12}, 10)

inner({-3,7,12}, 10)

omit -9

inner({-3,7,12}, 19)

take -9

inner({7,12}, 10)

omit -3

inner({7,12}, 13)

take -3

inner({7,12}, 19)

omit -3

inner({7,12}, 22)

take -3

inner({12}, 10)

omit 7

inner({12}, 3)

take 7

inner({12}, 13)

omit 7

inner({12}, 6)

take 7

inner({}, 10)

omit 12

inner({}, -2)

take 12

inner({}, 3)

omit 12

inner({}, -9)

take 12

inner({}, 6)

omit 12

inner({}, -6)

take 12

inner({12}, 19)

omit 7

inner({12}, 12)

take 7

inner({}, 12)

omit 12

inner({}, 0)

take 12

Page 57: Programming abstractions and analysis –– recursiona1120.cs.aalto.fi/slides/lecture8.pdf · 2. Bits and data 3. Combinational logic 4. Sequential logic 5. Programmable computer

On computationally hard problems

• The recursion tree in our programs (and hence the run time) will grow exponentially with the size of the set S

• For example, can you solve: S = {316, 5356, 190, 1261, 4474, 4159, 6238, 442, 1702, 820, 3214, 2962, 1765, 5419, 6049, 4852, 4285, 5356, 4348, 1072, 5167, 2899, 1198, 6301, 2962, 2773, 4096, 5860, 3718, 6931, 1513, 4348, 1954, 5104, 4474, 3529, 4348, 3655, 5671, 1261,

6238, 3340, 1450, 1513, 1261, 1261, 5797, 4159, 3718, 5923} and t = 92105?

• Subset sum is an NP-complete problem

• The correctness of the solution is easy to check

• There is no known way of solve the problem in polynomial time

• If we could solve this problem in polynomial time, then we could solve all of them in polynomial time

• There are thousands of known NP-complete problems

• There has been a large number of attempts to find a polynomial time algorithm for them

Page 58: Programming abstractions and analysis –– recursiona1120.cs.aalto.fi/slides/lecture8.pdf · 2. Bits and data 3. Combinational logic 4. Sequential logic 5. Programmable computer

Exercises• Linked lists

–– Extending the linked list data structure defined in the lectures

• Expressions continued–– Extending the expressions data structure defined in the lectures

• Group scheduling–– Solving scheduling problems recursively

• Sudoku solver–– Solving sudoku instances recursively

• Subset sums with dynamic programming (challenge problem) –– In some cases dynamic programming is better solution method than recursion for subset sum