FIT1045 53 Workshop 12 Solutions FIT1045 53 Workshop 12 Solutions
FIT1045 53 Workshop 12 Solutions FIT1045 53 Workshop 12 Solutions
Objectives
After this workshop, you should be able to:
• Implement more backtracking algorithms
• Know how to use classes to define your own object types (not examinable)
Task 1: Backtracking
In the lectures you were given the following code:
def solutions(completed, options, partial=[]):
if completed(partial):
return [partial]
else:
res = []
for o in options(partial):
augmented = partial+[o]
res += solutions(completed, options, augmented)
return res
def backtracking_task(x):
def options(partial):
...
def completed(partial):
...
return solutions(completed, options, starting_partial)
This code provides a framework for many (although not all) of the backtracking problems you will see in this
unit; however, you may not fully understand everything it does. Ensure you understand what each line of code
is doing before you implement it. Remember, if you use code from the lectures you must cite it. It is sufficient
to include a comment such as #code for < f unction > adapted from Lecture #, slide #.
def palindrome_binary ( n ):
def options ( partial ):
if len ( partial ) < n //2+ n %2:
return [0 ,1]
1
Downloaded by goodgamers99 gamers ([email protected])
lOMoARcPSD|9314401
def all_paths (M , u , v ):
def options ( partial ):
opt = []
for i in range ( len ( M )):
if M [ partial [ -1]][ i ] and i not in partial :
opt += [ i ]
return opt
def completed ( partial ):
return partial [ -1]== v
return solutions ( completed , options , [ u ])
Part C: Stairs
def stairs ( staircase ):
def options ( partial ):
opt = []
if len ( partial ) == 1:
for i in range (1 ,4):
if i < len ( staircase ) and staircase [ i ]:
opt += [ i ]
else :
prohib = partial [ -1] - partial [ -2]
for i in range (1 ,4):
if i != prohib :
stair = partial [ -1] + i
if stair < len ( staircase ) and staircase [ stair ]:
opt += [ stair ]
return opt
def complete ( partial ): return partial [ -1] == len ( staircase ) -1
return len ( solutions ( complete , options , [0]))
Part A. Motivation
The built-in types like int, float, bool, list, str principally allow us to represent all sorts of things. But relying
only on them can become pretty cumbersome, and we often would like to group related values in one object.
For instance, we could represent circles in the plane by three float values representing the x and y-coordinates
of the centre and the radius like this
>>> cx1, cy1, r1 = 2.0, 2.0, 1.0
>>> cx2, cy2, r2 = 2.0, 0.0, 0.9
>>> cx3, cy3, r3 = 0.0, 2.0, 3.0
and then define a function
circles_intersect(cx1, cy1, r1, cx2, cy2, r2) that
returns True if and only if the point (x, y) is contained in the circle defined by cx, cy, and r:
>>> circles_intersect(cx1, cy1, r1, cx2, cy2, r2)
False
>>> circles_intersect(cx1, cy1, r1, cx3, cy3, r3)
True
2
Downloaded by goodgamers99 gamers ([email protected])
lOMoARcPSD|9314401
This is quite a long parameter list that makes programs that use this function hard to read and error-prone.
For instance, while developing this worksheet, the author made the following mistake (and not for didactic
reasons):
circles_intersect(cx1, cy1, r1, cx2, cx2, r2)
Things could also easily get worse. Imagine for instance we need a function that determines whether two
triangles intersect. This would result in a function with twelve parameters: three vertices per triangle!
In such situations we would much prefer to have objects that group the related information. To start, we
could use tuples to represent points as a single object. With this we could write
>>> c1, r1 = (2.0, 2.0), 1.0
>>> c2, r2 = (2.0, 0.0), 0.9
>>> c3, r3 = (0.0, 2.0), 3.0
and use a four-parameter function definition instead:
>>> circles_intersect2(c1, r1, c2, r2)
False
>>> circles_intersect2(c1, r1, c3, r3)
True
>>> circle_contains2(c1, r1, (1.5, 2.0))
True
>>> circle_contains2(c1, r1, (1.0, 1.0))
False
However, the centre c and the radius r are also related and simply adding r to the tuple would result in an
unintuitive convention that is hard to memorise and read. For instance, it will be difficult to decide which of
the following Boolean expressions correctly captures the concepts that two circles intersect:
euclidean_distance(c1[0], c1[1], c2[0], c2[1]) <= c1[2] + c2[2]
euclidean_distance(c1[1], c1[2], c2[1], c2[2]) <= c1[0] + c2[0]
This class definition creates a function-like object (more precisely a type object that can be called like a
function) with the name Circ that can be used to create objects of a brand-new Circ type (this abbreviation
is not a nice name, we just use it for this first attempt of defining a class to not clash with our final Circle
class defined further below):
>>> Circ
<class ’__main__.Circ’>
>>> c = Circ()
Great! We have just created not only our first custom type, but also one object with that type. Unfortunately,
using the empty class definition above results in very boring objects with no attributes or methods. It turns
out, we could add attributes to the object by simply assigning them:
3
Downloaded by goodgamers99 gamers ([email protected])
lOMoARcPSD|9314401
>>> circumference(c)
6.283185307179586
What is more interesting is what happens if we add this function as attribute to the Circ type object:
>>> Circ.circumference = circumference
It turns out, the circumference function is now available as method of our object c (which as we recall is of
type Circle).
>>> c.circumference()
6.283185307179586
Note how the function, which actually takes one input argument circle turned into a zero argument method.
What happens when we call circumference as a method on some circle object c is that the object c itself is used
as the function argument.
What happens if the function that we use as a method takes more than one argument? Let’s find this out by
defining another potentially useful function—one that we can use to more conveniently initialise Circ objects.
>>> def init_circle(circle, centre, radius):
... circle.centre = centre
... circle.radius = radius
>>> Circ.init = init_circle
>>> c2 = Circ()
>>> c2.init((2.0, 1.0), 0.9)
>>> c2.centre
(2.0, 1.0)
>>> c2.radius
0.9
Ok. So method calls work by using the object as the first argument to the underlying function that defines
the method.
4
Downloaded by goodgamers99 gamers ([email protected])
lOMoARcPSD|9314401
and be done with creating a circle object. So somehow we have to modify the way in which Circ can be called
(we need it to take two parameters instead of none) and what it does (it should initialise the created object
instead of just spitting out a naked one that we have to finish ourselves). Enter one of the most characteristic
features of the Python language: the ’magic method’.
Magic methods (or ’special methods’ as they are more modestly called in the Python documentation) are
methods with specific names that begin with and end with two underscores . The most important special
method is the one called init . This method is precisely what we are looking for: it determines the definition
of the Circ function/callable object.
Luckily, we have already defined a function to initialise circles. So we can directly move ahead and use it to
demonstrate the effect of defining an init method:
def __repr__(self):
return ’Circle({}, {})’.format(self.centre, self.radius)
def circumference(self):
return 2*self.radius*pi
This binds the functions defined inside the class definition directly to the Circle type. You can see two things:
Firstly, the special methods are defined in this way exactly as regular methods. Secondly, when defining
functions (to be methods) directly inside a class definition, we like to call the first parameter self. This is a
pure convention. We could have called it circle instead, but that would confuse other Python programmers
when they read our class definition.
With this we can directly write as desired:
>>> c1 = Circle((2.0, 2.0), 1.0)
>>> c2 = Circle((2.0, 0.0), 0.9)
>>> c3 = Circle((0.0, 2.0), 3.0)
and defining a suitable function circles intersect3(circle1, circle2) we get the long awaited much less
error prone calls:
5
Downloaded by goodgamers99 gamers ([email protected])
lOMoARcPSD|9314401
In fact, we can now do better than that and directly define the intersects function as method of the Circle
class. You will do that in the following task part of this activity.
>>> c1.diameter()
2.0
>>> c2.diameter()
1.8
>>> c3.diameter()
6.0
Next, let’s add a method to compute the area. I’m sure, I don’t have to remind you of the formula to
compute the area of a circle, but I do it anyway: the area of a circle is the square of its radius times π. Again,
the only challenge is to define is as a method:
>>> c1.area()
3.141592653589793
>>> c2.area()
2.5446900494077327
>>> c3.area()
28.274333882308138
Now it’s time for creating methods that take a parameter. Create a method contains point(point) that
returns True if and only if the circle contains the given input point (represented by a two-element tuple):
Perhaps we have squeezed out enough of the circle example and should move on. Now it is time to write
your first class from scratch. Our will be to come up with a better representation of points: vector objects.
6
Downloaded by goodgamers99 gamers ([email protected])
lOMoARcPSD|9314401
Let’s explore this design possibility. We will change our view on points slighlty by identifying them with the
2d-vectors pointing to them from the origin of the coordinate system.
Write a class Vector for vector objects that have two float attributes x and y. As with the Circle class, make
sure objects of type Vector can be initialised conveniently by a suitable init method and have a readable
string representation in the shell (by defining repr ):
Now we can add methods that are specific to vectors to our objects by adding them to the class definition.
Let’s start with a method for computing the (Euclidean) norm of a vector, i.e., the square-root of the sum of
its squared components:
>>> a.norm()
5.0
>>> b.norm()
1.4142135623730951
An extremely useful operation defined on two vectors is the dot product. In the next section, we will make
plenty of use of this. For now, let’s define a method ’dot’ for our vector objects that takes as input a second
vector and that computes the dot-product, i.e., the sum of the products of the two vector’s x-components and
y-components:
>>> a.dot(b)
1.0
>>> a.dot(Vector(2.0, 1.0))
10.0
The dot product and the norm operation together can be used to compute the cosine of the angle between two
vectors:
>>> c = Vector(0.0, 3.0)
>>> from math import acos # the arc cosine function
>>> from math import pi
>>> acos(b.dot(c) / (b.norm()*c.norm()))*180/pi
45.00000000000001
(This should be 45 degrees if it weren’t for rounding errors on my machine.)
We could now go ahead and define other binary operations involving vectors either as method or as ’free’
functions. For instance, for the finding the sum of two vectors we could define and use the following function:
7
Downloaded by goodgamers99 gamers ([email protected])
lOMoARcPSD|9314401
The way to make this operator overloading work is again by defining certain special ’magic’ methods. In
this case, we need to define the methods add (self, other) and sub (self, other). By adding the
methods mul (self, scalar) and truediv (self, scalar), we can also use the standard operators *
and / to multiply or divide vectors by scalar values:
>>> a*0.5
Vector(1.5, 2.0)
>>> a/2
Vector(1.5, 2.0)
One little problems remains here: we would also like to evaluate expressions where the scalar is the left-
operand of a product:
>>> 0.5*a
Vector(1.5, 2.0)
However, this will not work directly because Python will by default use the mul -method of the left operand
(which is a standard Python numeric object that does not provide a multiplication method with objects of our
custom Vector type). To allow to resolve such expressions, we have to specify a method for multiplying ’from
the right’, which can be done by defining rmul (self, scalar).
Equipped with all these operations and the knowledge of how to compute the cosine between two vectors, we
can now close this section by writing a method for finding the orthogonal projection of one vector onto another
one. The orthogonal projection of a vector v onto another vector w is the point on the line described by w that
is closest to v. Since this point p lies on the line described by w, it is given as a multiple (scaled version) of w.
With a bit of math (which skip here) we can convince ourselves that the factor with which we have to scale w
is the cosine of the angle between v and w times the ratio of the norm of v to the norm of w.
Recalling the formula for the cosine from above (dot product divided by the two norms), it will be easy
for you to write a method projection(self, other) that computes the projection of the vector self onto the
vector other. Here are some input/output examples:
>>> a.projection(Vector(1.0, 0.0))
Vector(3.0, 0.0)
>>> a.projection(Vector(2.0, 0.0))
Vector(3.0, 0.0)
>>> a.projection(Vector(0.0, 1.0))
Vector(0.0, 4.0)
>>> a.projection(b)
Vector(-0.4999999999999999, 0.4999999999999999)
(Again, these rounding errors always pester our test cases)
8
Downloaded by goodgamers99 gamers ([email protected])
lOMoARcPSD|9314401
Putting these together, write now a method area(self) that computes the area of the triangle self. Once
you have the base length and height of the triangle this can be done easily, because the area of a triangle is
equal to 0.5 times the base length times the hight:
>>> t1.area()
1.0
>>> t2.area()
0.5
Part I: Conclusion
This concludes our introduction to classes. If you followed through all the way to the end, you should now
have a powerful new programming tool at your disposal (and perhaps have refreshed your geometry knowledge
a bit). From here, there are a lot of possibilities of what to try next. Some ideas:
• If you are keen on more geometry, you could add a method to the triangle class that decides whether a
given point is inside the triangle as well as methods that finds the in-circle and the out-circle of a triangle.
• More to the point of classes, you could revisit the programs you have written in this unit and see which
of them would benefit from using custom types. One hot candidate are the grid graphs that we have seen
in various workshops. Wasn’t it very inconvenient that we had to specify the number of columns of the
grid graph when printing it to the shell? Well, if you define a class for grid graphs you can simply store
the number of columns as an attribute and turn the print function into a method!
9
Downloaded by goodgamers99 gamers ([email protected])