#!/usr/bin/env python
# coding: utf-8
#
cs1001.py , Tel Aviv University, Fall 2018/19
#
# ## Exam recitation
#
# We went over various questions from previous exams
# ###### Takeaways:
# - The exam is easy, all you have to do is write down the correct answers
# - When in doubt, bet on 42
# #### Code for printing several outputs in one cell (not part of the recitation):
# In[ ]:
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"
# ## 2019AA1: Linked Lists
# A doubly linked list is similar to a linked list, the difference being that:
# - Each node has a next field (pointing forward) and a prev field (pointing backwards)
# - The list contains a head field (pointing to the first node) and a tail field (pointing to the last field)
# In[ ]:
class Node():
def __init__(self, val):
self.value = val
self.next = None
self.prev = None
class DLList():
def __init__(self, lst=None):
self.head = None
self.tail = None
self.len = 0
if lst != None:
for item in lst:
self.insert(item)
def __len__(self):
return self.len
def __repr__(self):
n = len(self)
s = "["
node = self.head
for i in range(n):
s += str(node.value)+", "
node = node.next
if len(s) > 1:
s = s[:-2] + "]"
else:
s += "]"
return s
# Implement the insert method. The input is $val$ and $first$ a boolean parameter. If $first==True$ insert at head, else insert at tail.
#
# Runtime and memory usage should be $O(1)$.
#
# The idea is that there are three cases:
# - If the list is empty the head and tail should be the new node
# - Otherwise, if $first == True$ we should place the new node at the front of the list and change existing head
# - Finally, if $first == False$ we should do the same for the tail
#
# In[ ]:
class Node():
def __init__(self, val):
self.value = val
self.next = None
self.prev = None
class DLList():
def __init__(self, lst=None):
self.head = None
self.tail = None
self.len = 0
if lst != None:
for item in lst:
self.insert(item)
def __len__(self):
return self.len
def __repr__(self):
n = len(self)
s = "["
node = self.head
for i in range(n):
s += str(node.value)+", "
node = node.next
if len(s) > 1:
s = s[:-2] + "]"
else:
s += "]"
return s
def insert(self, val, first=False):
node = Node(val)
if len(self) == 0:
self.head = self.tail = node
elif first:
tmp = self.head
self.head = node
self.head.next = tmp
tmp.prev = self.head
else:
tmp = self.tail
self.tail = node
self.tail.prev = tmp
tmp.next = self.tail
self.len += 1
# Implement the rotate method. Given $0 \leq k < n$, the $i$th node of the list will change place and become the $(i+k)~\%~n$ node.
#
# Runtime should be $O(\textrm{min}(k, n-k))$, memory usage should be $O(\log n)$ and the method should work in place.
#
# In[ ]:
class Node():
def __init__(self, val):
self.value = val
self.next = None
self.prev = None
class DLList():
def __init__(self, lst=None):
self.head = None
self.tail = None
self.len = 0
if lst != None:
for item in lst:
self.insert(item)
def __len__(self):
return self.len
def __repr__(self):
n = len(self)
s = "["
node = self.head
for i in range(n):
s += str(node.value)+", "
node = node.next
if len(s) > 1:
s = s[:-2] + "]"
else:
s += "]"
return s
def insert(self, val, first=False):
node = Node(val)
if len(self) == 0:
self.head = self.tail = node
elif first:
tmp = self.head
self.head = node
self.head.next = tmp
tmp.prev = self.head
else:
tmp = self.tail
self.tail = node
self.tail.prev = tmp
tmp.next = self.tail
self.len += 1
def rotate(self, k):
fwd = True
shift = k
if k > len(self) - k:
shift = len(self) - k
fwd = False
self.head.prev = self.tail
self.tail.next = self.head
node = self.head
for i in range(shift):
if fwd:
node = node.prev
else:
node = node.next
self.head = node
self.tail = node.prev
self.head.prev = None
self.tail.next = None
# ### Testing
# In[ ]:
L = DLList([i for i in range(10)])
print(L)
L.rotate(2)
print(L)
# ## 2018AA5: Rotating trees
#
# Given two BSTs $T,S$ we say that $T$ and $S$ are equivalent if they are the same up to a sequence of switching between left and right sons of some of the nodes in the trees.
#
# The following trees are equivalent:
#
# ![title](equiv1.png)
#
# But they are not equivalent to this tree:
#
# ![title](equiv2.png)
#
# Implement $equiv(r1, r2)$ which gets two root nodes $r1, r2$ and return $True$ iff they represent roots of equivalent trees. The function should be recursive.
#
# The idea is:
# - Two leaves are equivalent if their key and value are equivalent
# - Two trees are equivalent if:
# - Their roots agree on their keys and values
# - Their left sons $L1, L2$ and right sons $R1,R2$ are equivalent, or $L1, R2$ and $L2, R1$ are equivlanet
# In[ ]:
class Tree_node():
def __init__(self, key, val):
self.key = key
self.val = val
self.left = None
self.right = None
def __repr__(self):
return "(" + str(self.key) + ":" + str(self.val) + ")"
def __eq__(self,other):
if other==None:
return False
if self.key==other.key and self.val==other.val:
return True
else:
return False
class Binary_search_tree():
def __init__(self):
self.root = None
def __repr__(self): #no need to understand the implementation of this one
out = ""
for row in printree(self.root): #need printree.py file
out = out + row + "\n"
return out
def lookup(self, key):
''' return node with key, uses recursion '''
def lookup_rec(node, key):
if node == None:
return None
elif key == node.key:
return node
elif key < node.key:
return lookup_rec(node.left, key)
else:
return lookup_rec(node.right, key)
return lookup_rec(self.root, key)
def insert(self, key, val):
''' insert node with key,val into tree, uses recursion '''
def insert_rec(node, key, val):
if key == node.key:
node.val = val # update the val for this key
elif key < node.key:
if node.left == None:
node.left = Tree_node(key, val)
else:
insert_rec(node.left, key, val)
else: #key > node.key:
if node.right == None:
node.right = Tree_node(key, val)
else:
insert_rec(node.right, key, val)
return
if self.root == None: #empty tree
self.root = Tree_node(key, val)
else:
insert_rec(self.root, key, val)
def equiv(node1, node2):
if node1 == node2 == None:
return True
if node1 == None or node2 == None or node1 != node2:
return False
no_switch = equiv(node1.left, node2.left) and equiv(node1.right, node2.right)
switch = equiv(node1.left, node2.right) and equiv(node1.right, node2.left)
return switch or no_switch
# ### Testing
# In[ ]:
T1 = Binary_search_tree()
T1.insert(1,1)
T1.root.left = Tree_node(2,2)
T1.root.right = Tree_node(3,3)
T1.root.left.left = Tree_node(4,4)
T2 = Binary_search_tree()
T2.insert(1,1)
T2.root.left = Tree_node(3,3)
T2.root.right = Tree_node(2,2)
T2.root.right.left = Tree_node(4,4)
T3 = Binary_search_tree()
T3.insert(1,1)
T3.root.left = Tree_node(3,3)
T3.root.right = Tree_node(2,2)
T3.root.left.left = Tree_node(4,4)
equiv(T1.root,T2.root)
equiv(T1.root,T3.root)
#k = Tree_node(1,1)
#p = Tree_node(1,1)
#k == p
# Can memoization help? Not really, since subtrees can be disjoint, we will never reach a tree we've already examined.
# Next, given two roots of $T1,T2$ determine if there exists a non-empty subtree of $T1$ equivalent to a subtree of $T2$.
#
# The idea
# - If the two roots are equivalent (and not empty) we are done
# - If one of the roots is $None$, we are done
# - Otherwise:
# - There may be a subtree of $T2$ equivalent to $T1$
# - Or a subtree of $T1$ equivalent to $T2$
# In[ ]:
def subequiv(node1, node2):
if equiv(node1, node2) and node1 != None:
return True
if node1 == None or node2 == None:
return False
t1_stays = subequiv(node1, node2.left) or subequiv(node1, node2.right)
t2_stays = subequiv(node1.left, node2) or subequiv(node1.right, node2)
return t1_stays or t2_stays
# In[ ]:
subequiv(T1.root,T2.root)
T4 = Binary_search_tree()
T4.insert(100,100)
T4.root.left = T2.root
subequiv(T4.root, T2.root)
# Since they share leaves
subequiv(T2.root, T3.root)
# ## 2018BB5: Hashing
#
# For each of the following hash functions for tuples give 2 tuples of integers of length $\geq 3$ (same length for both tuples) that collide.
# Possible solutions:
#
# - Since $hash1$ is dependant on values and parities we can simply mix lists while maintaining parity of positions, e.g.: $$(3, 2, 5), (5, 2, 3)$$
# - For $hash2$ if the tuples have length $3$ then $hash2(t) = t[0] + 2t[1]+4t[2]$. We can take any $t1$ and place all the weight of $t2$ on the first entry, e.g.: $$(1,1,1), (7,0,0)$$
# - For $hash3$, each entry is mapped to its integer value when represented in binary (e.g. - $5 \to 101$). We can get the value of $4 \to 100$ by summing 10 entries of $2 \to 10$, so a possible solution is (note that this is not proper python code):
# $$(2 \text{ for i in } range(10)),(4, 0 \text{ for i in } range(9))$$
# In[ ]:
hash1 = lambda t : sum([(-1)**i * t[i] for i in range(len(t))])
hash2 = lambda t : sum([2**i * t[i] for i in range(len(t))])
hash3 = lambda t : sum(int(bin(t[i])[2:]) for i in range(len(t)))
# In[ ]:
hash1((3,2,5))
hash1((5,2,3))
# In[ ]:
hash2((1,1,1))
hash2((7,0,0))
# In[ ]:
hash3([2 for i in range(10)])
hash3([4] + [0 for i in range(9)])
# Given a list $L$ of $n$ tuples of length $2$ we say that $L$ is symmetric if $(a,b) \in L \implies (b,a) \in L$.
#
# Implement $issym$ which gets $L$ as input and returns $True$ iff $L$ is symmetric. The function should run in time $O(n)$ on average.
#
# The idea: we create a set from $L$ and then simply iterate over $L$ and check if each opposite member is in the set.
# In[ ]:
def issym(L):
n = len(L)
s = set(L)
for i in range(n):
(a,b) = L[i]
if (b,a) not in s:
return False
return True
issym([(1,2), (2,1), (3,3)])
issym([(1,2), (2,1), (3,3), (4,3)])
# ## 2017AA2: Profit
#
# Given $n$ and a list of integers $values$ of length $n$, we we will think of a landlord that has an apartment of size $n$ square meters. The landlord can divide his apartment into smaller subapartments and an apartment of size $k \leq n$ will yield a rent of $value[k-1]$ dollars.
#
# E.g., for $n = 4, values=[1,5,8,9]$ we can have:
# - One apartment of size $4$ renting at $9$ dollars
# - Two apartments of size $1$ and one of size $2$ renting at $1+1+5=7$ dollars
# - And so on...
#
# Our goal is to find a partition which maximizes the landlords profit. Our solution needs to be recursive and **use no loops**.
#
# The idea:
# - If we don't have any space, or we don't have any partitions available, we make 0 dollars
# - If we have 1 meter, we make $value[0]$
# - Otherwise, say the length of $len(value)$ is $i$:
# - If we can make an apartment of size $i$, i.e. $i \leq size$ then we profit $value[i-1]$ and we need to split $size - i$ meters
# - Otherwise, we can shorten the length of $len(value)$ to $i-1$ and we still need to split $size$
# - We compute both of the above and take the max between them
# In[ ]:
def profit(value, size):
n = len(value)
return profit_rec(value, n, size)
def profit_rec(value, i, size):
if size == 0 or i == 0:
return 0
if size == 1:
return value[0]
with_last = 0
if i <= size:
with_last = profit_rec(value, i, size - i)+value[i-1]
without_last = profit_rec(value, i - 1, size)
return max(with_last, without_last)
profit([1, 5, 8, 9], 4)
profit([2, 3, 7, 8, 9], 5)
# ## 2017AB2: Jump
#
# Given a list $lst$ of $n$ integers where $lst[0]=0$ we want to get to $lst[n-1]$ by jumping to the right (i.e. - advancing in the list).
#
# We init a counter $cnt=0$ and if we add $lst[i]$ to the counter for every index $i$ we visit.
#
# Implement $jump$ which gets a list as described above and returns the minimal total penalty.
#
# The idea: We must visit $lst[-1]$ and we should visit any index $i$ s.t. $lst[i] < 0$.
# In[ ]:
def jump(lst):
# Note the range!
return lst[-1] + sum(lst[i] for i in range(len(lst)-1) if lst[i] < 0)
jump([0,4,5,1,2,-3,-5])
jump([0,4,5,1,2,-3,2])
# Jump limited - we now get a parameter $max\_jump$ and we can now advance only $1 \leq i \leq max\_jump$ steps in each move.
#
# Implement $jump\_rec$ without splicing.
# In[ ]:
def jump_lim(lst, max_jump):
return jump_rec(lst, max_jump, 0)
def jump_rec(lst, max_jump, ind):
if ind == len(lst) - 1:
return lst[ind]
cnt = float("inf")
for j in range(1, max_jump + 1):
if ind+j < len(lst):
res = jump_rec(lst, max_jump, ind+j) + lst[ind]
if res < cnt:
cnt = res
return cnt
jump_lim([0,4,5,1,2,-3,2], 100)
jump_lim([0, 2, 2, 0, 4], 3)
# Add memoization:
# In[ ]:
def jump_lim(lst, max_jump):
d = {}
return jump_rec_mem(lst, max_jump, 0, d)
def jump_rec_mem(lst, max_jump, ind, d):
if ind == len(lst) - 1:
return lst[ind]
if ind in d:
return d[ind]
cnt = float("inf")
for j in range(1, max_jump + 1):
if ind+j < len(lst):
res = jump_rec_mem(lst, max_jump, ind+j, d) + lst[ind]
if res < cnt:
cnt = res
d[ind] = cnt
return cnt
jump_lim([0,4,5,1,2,-3,2], 100)
jump_lim([0, 2, 2, 0, 4], 3)
# ## 2017BA4b: Rotated strings
#
# We say that two strings of lengthh $n$, $s,t$ are rotated if there exists and $0 \leq i \leq n$ such that:
# $$s == t[i:n] + t[0:i]$$
#
# I.e., $introtocs$ is a rotation of $tocsintro$ and of $rotocsint$ but not of $introcsto$.
#
# Implement $is_rot$ which gets two strings and return $True$ iff they are rotated.
#
# The function should work in $O(n)$ and is allowed a small chance of error.
#
# Use $fingerprint, fingerprint\_text$ we've seen when discussing KR.
# In[ ]:
def is_rot(s, t):
n = len(s)
if n != len(t):
return False
fp = fingerprint(s)
t = t+t
fp_list = substring_fingerprints(t, n)
for i in range(n):
if fp == fp_list[i]:
return True
return False
def fingerprint(string, basis=2**16, r=2**32-3):
""" used to computes karp-rabin fingerprint of the pattern
and of the first substring in the text
employs Horner method (modulo r) """
s = 0
for ch in string:
s = (s*basis + ord(ch)) % r # Horner
return s
def substring_fingerprints(string, m, basis=2**16, r=2**32-3):
""" return a list of all fingerprints of size m windows in string """
fp_list = []
b_power = pow(basis, m-1, r)
# compute first fingerprint
fp_list.append(fingerprint(string[0:m], basis, r))
# compute f_list[s], based on f_list[s-1]
for s in range(1,len(string)-m+1): # O(n-m-1), each iteration O(1)
next_fingerprint = \
((fp_list[s-1] - ord(string[s-1])*b_power) * basis \
+ ord(string[s+m-1])) % r
fp_list.append(next_fingerprint)
return fp_list
is_rot("introtocs", "introtocs")
is_rot("introtocs", "tocsintro")
is_rot("introtocs", "introtosc")
# ## 2019AB2: Matching strings
# Given a list of strings $strlst$ and a string $word$ we say we can construct $word$ from $strlst$ if we can pick at most 1 char from each string in the list that yield $word$.
#
# For example, we can construct "Raz" from $["abc", "FOR", "buzz"]$ and also from $["abc", "FOR", "aaa", "buzz", "hello"]$ but not from $["az", "FOR", "jkl"]$
#
# Implement the recursive $construct\_rec$. The idea is that we maintain an index $j$ s.t. we need to construct $word[j:]$ from the list of strings.
# - If $j==len(word)$ we are done.
# - Otherwise, we go over the list, for each string we check if it contains $word[j]$ and if so we recurse on $j+1$ and ommit that string from the list
# - If we exit the loop without success, we return $False$
# In[ ]:
def construct(str_list, word):
if len(str_list) < len(word):
return False
return construct_rec(str_list, word, 0)
def construct_rec(str_list, word, j):
if j == len(word):
return True
for i in range(len(str_list)):
if word[j] in str_list[i]:
if construct_rec(str_list[:i]+str_list[i+1:], word, j+1):
return True
return False
construct(["abc", "FOR", "buzz"], "Raz")
construct(["az", "FOR", "jkl"], "Raz")
construct(["az", "FOR", "hello","a"], "Raz")
# Now we need to solve a variant where the order of the strings we take must agree with the order of characters in $word$.
#
# E.g., now we can no longer construct "Raz" from $["abc", "FOR", "buzz"]$ but we can from $["FOR", "abc", "jkl", "buzz"]$.
#
# The new solution should run in time $O(n)$ where $n$ is the length fo the string list.
#
# The idea: we can now be greedy. Whenever we can take a certain string we should, because we will not be able to take it from characters later on.
# In[ ]:
def construct_order(str_list, word):
i = 0
j = 0
while i < len(word) and j < len(str_list):
if word[i] in str_list[j]:
i += 1
j += 1
if i == len(word):
return True
return False
construct_order(["abc", "FOR", "buzz"], "Raz")
construct_order(["FOR", "abc", "buzz"], "Raz")