Finding an Eulerian Path

In Udacity’s Introduction to Algorithms course, one of the challenge problems asks you to define a function that, given a graph that contains an Eulerian path, returns one such path. I’ve reproduced my solution below. The solutions posted on the Udacity forum where often in two camps: somewhat verbose imperative, and very concise functional ones that implemented Hierholzer’s algorithm for finding cycles in Eulerian paths. My solution is somewhat in the middle. It’s relatively concise, but it makes use of an additional data structure I found helpful as it allows to easily inspect the state during program execution: a list containing the number of unused edges per node. I think it’s quite easy to follow the flow of execution, but I’ll walk you through step by step afterwards.

Here is the program listing:

def find_eulerian_tour(graph):
            
    def freqencies():
        my_list = [x for (x, y) in graph]
        result = [0 for i in range(max(my_list) + 1)]
        for i in my_list:
            result[i] += 1
        return result
        
    def find_node(tour):
        for i in tour:
            if freq[i] != 0:
                return i
        return -1
    
    def helper(tour, next):
        find_path(tour, next)
        u = find_node(tour)
        while sum(freq) != 0:     
            sub = find_path([], u)
            tour = tour[:tour.index(u)] + sub + tour[tour.index(u) + 1:]  
            u = find_node(tour)
        return tour
                 
    def find_path(tour, next):
        for (x, y) in graph:
            if x == next:
                current = graph.pop(graph.index((x,y)))
                graph.pop(graph.index((current[1], current[0])))
                tour.append(current[0])
                freq[current[0]] -= 1
                freq[current[1]] -= 1
                return find_path(tour, current[1])
        tour.append(next)
        return tour             
             
    graph += [(y, x) for (x, y) in graph]
    freq = freqencies()   
    return helper([], graph[0][0])

The graph is given as a list of tuples. Tuple (x, y) indicates that node x has an (undirected) edge to node y. In line 37 the list with nodes is enlarged by adding tuples (y, x) to it, which reflects the fact that the edges are undirected.

In addition, a list of the number of edges per node (“freq”) is created and updated as the list with the eventual Eulerian tour is built. This means that whenever a node that is part of the Eulerian tour is found, the two affected nodes have their number of edges reduced by one.

If the graph has cycles, it is possible that the first pass of find_path() doesn’t process part of the graph. In this case, the program checks whether the sum of the list “freq” is equal to zero. If it is, then the Eulerian tour has been computed since there are no unused edges left. On the other hand, if “sum(freq)” is greater than zero, then unused edges are left, so the program proceeds finding a node with unused edges, finds another cycle, and inserts it into the resulting Eulerian tour. This step will be repeated for as long as there are unused edges in the graph.

Please note that any node can be used as a staring point. This follows from the existence of an Eulerian path, which uses all edges. Since it contains all edges, you can pick any node and from there pick any edge to start the Eulerian tour with.

On the Udacity forums I found a very helpful test case, which is a graph with three cycles. When working on the program, I used this and two derivative cases to test and guide development. Here they are:

graph1 = [(0, 1), (1, 2), (2, 0)]
# returns [0, 1, 2, 0]

graph2 = [(0, 1), (1, 2), (2, 0), (2, 3), (3, 4), (4, 2)]
# returns [0, 1, 2, 3, 4, 2, 0]

graph3 = [(0, 1), (1, 2), (2, 0), (2, 3), (3, 4), (4, 2), (4, 5), (5, 6), (6, 4)]
# returns [0, 1, 2, 3, 4, 5, 6, 4, 2, 0]

Review: CS102: Introduction to Computer Science II — Saylor Foundation

After covering basic concepts of programming and computer science in CS101, Saylor’s CS102: Introduction to Computer Science II moves on to discuss some more advanced material, using the C family of languages, i.e. C, C++ and Java.

I did appreciate the many historic sources I was exposed to, such as Dennis Ritchie’s article “The Development of the C Language”. I do think that in computer science textbooks there is often very little appreciation of the rich history of this field. This is a pity, since learning about the evolution of programming languages can only broaden your horizon.

The other aspect I enjoyed was the strong comparative focus. Some assignments consisted of implementing a simple program in both Java and C++, and asking to the student to pay careful attention to similarities and differences. This reminded me of CS101, which occasionally referred to both Python and Java. Overall, this strikes me as an excellent idea. Would more students be exposed to different languages early on, even if it only happened on a superficial level, you probably wouldn’t see so many insecure people online who defend Java because it’s the only language they have ever been exposed to. This isn’t necessarily a jab at Java, but at the educational system. Had they learnt Python, they would instead pollute Reddit and other places with remarks that Python is all you’ll ever need.

CS102 was mostly about concepts. You could learn about object-oriented programming and its history, tip your toes into functional programming by going through introductory examples of recursion, and familiarize yourself with the Java collections framework as well as the C++ STL on a basic level. It was all intended as a first exposure to the topics, presumably to make subsequent courses more accessible.

The teaching materials were mostly of a high quality. David Eck’s free online book, Introduction to Programming Using Java, featured prominently in the course. It is very well written. The author makes sure to explain each concept and gradually build upon it. I have seen quite a few introductory books that were full of hand-wavy explanations, or appeals to the reader to just accept something at face value for the time being. In addition, there were a few lectures taken from Stanford’s CS106B: Programming Abstractions, some from MIT’s 6.00: Introduction to Computer Science and Programming and some videos on sorting and recursion from Khan Academy. Those were sources I was familiar with already. I did not know about the accessible illustrations of sorting algorithms by Xoax.net, though. For a beginner, they may well be the most suitable resource online. I wish I had seen those videos when I first studied sorting algorithms.

I had very few gripes with the course. First, while the materials were generally very good, one (external) assessment quiz was so bad that it made me laugh. Here is a screenshot of the quick sort quiz:

Amateurish quiz

So, what are the problems with it? The presentation is poor, and so is the content. The first question is ill-defined since quick sort requires picking a pivot element first. Amusingly enough, the second question asks about this. Lastly, the definition of “divide and conquer” is far too sloppy. It should not take many resources for Saylor to drop this quiz and instead create one themselves.

Second, some sources were too specific and technical. In particular, one introduction to the STL and generic programming, in Unit 7.1, was addressing experienced programmers and was therefore inappropriate for this course. Third, even though CS102 covered a relatively wide spectrum, the final exam mostly focussed on definitions related to OOP and language particulars of Java and C++. Thus, this experience was anticlimactic. I would have expected at least some coverage of sorting algorithms, or a question or two on the analysis of algorithms. Further, a few of the questions were downright pedantic, asking for minutiae like the exact names of specific Java object methods. With a bit of programming experience, they are trivial answer, which made me think of Feynman’s statement that there is a difference between knowing the name of something and knowing something. I would have appreciated a stronger focus on conceptual understanding.

Before I enrolled in this course, I peeked ahead, and learnt that CS107: C++ Programming contains substantial programming assignments. Thus, I viewed CS102 as a course that introduces concepts in preparation for follow-up courses. Therefore, I don’t view the lack of more challenging programming exercises as negative. Overall, I think that this course is well-designed. In fact, the approach Saylor pursues by laying a strong theoretical foundation first before properly introducing students to programming may be superior to what you normally experience, i.e. either mostly ignoring theory and history and merely focussing on practical knowledge and risking that students won’t acquire a solid foundation since they don’t really know what they are doing, or trying to teach programming and CS concepts at the same time, which can be confusing for students.

Coding Bat: Python. List-2

All solutions were successfully tested on 18 April 2013.

count_evens:

def count_evens(nums):
  count = 0
  for element in nums:
    if element % 2 == 0:
      count += 1
  return count

big_diff:

def big_diff(nums):
  return max(nums) - min(nums)

centered_average:

def centered_average(nums):
  sum = 0
  for element in nums:
    sum += element
  return (sum - min(nums) - max(nums)) / (len(nums)-2) 

sum13:

def sum13(nums):
  if len(nums) == 0:
    return 0

  for i in range(0, len(nums)):
    if nums[i] == 13:
      nums[i] = 0
      if i+1 < len(nums): 
        nums[i+1] = 0
  return sum(nums)

sum67:

def sum67(nums):
  for i in range(0, len(nums)):
    if nums[i] == 6:
      nums[i] = 0
      for j in range(i+1, len(nums)):
        temp = nums[j]
        nums[j] = 0
        if temp == 7:
          i = j + 1
          break
  return sum(nums)

Line 9 is not necessary. However, by adjusting “i” you ensure that this script runs in linear time, despite the nested loop.

has22:

def has22(nums):
  for i in range(0, len(nums)-1):
    #if nums[i] == 2 and nums[i+1] == 2:
    if nums[i:i+2] == [2,2]:
      return True     
  return False

The second option is much nicer to look at, but either way is fine.

Coding Bat: Python. String-2

All solutions were successfully tested on 18 April 2013.

double_char:

def double_char(str):
  result = ''
  for char in str:
    result += char * 2
  return result

count_hi:

def count_hi(str):
  count = 0
  for i in range(len(str)-1):
    if str[i:i+2] == 'hi':
      count += 1
  return count

cat_dog:

def cat_dog(str):
  count_cat = 0
  count_dog = 0
  for i in range(len(str)-2):
    if str[i:i+3] == 'dog':
      count_dog += 1
    if str[i:i+3] == 'cat':
      count_cat += 1
  
  return count_cat == count_dog

count_code:

def count_code(str):
  count = 0
  for i in range(0, len(str)-3):
    if str[i:i+2] == 'co' and str[i+3] == 'e':
      count += 1
  return count

end_other:

def end_other(a, b):
  a = a.lower()
  b = b.lower()
  #return (b.endswith(a) or a.endswith(b))
  return a[-(len(b)):] == b or a == b[-(len(a)):] 

Either way is fine.

xyz_there:

def xyz_there(str):
  for i in range(len(str)):
    if str[i] != '.' and str[i+1:i+4] == 'xyz':
      return True
  if str[0:3] == 'xyz':
    return True
  return False

Coding Bat: Python. Logic-2

All solutions were successfully tested on 18 April 2013.

make_bricks:

def make_bricks(small, big, goal):
  return goal%5 >= 0 and goal%5 - small <= 0 and small + 5*big >= goal

lone_sum:

def lone_sum(a, b, c):
  if a == b == c:
    return 0
  if b == c:
    return a
  if a == c:
    return b
  if a == b:
    return c  
  return a + b + c

lucky_sum:

def lucky_sum(a, b, c):
  if a == 13:
    return 0
  if b == 13:
    return a
  if c == 13:
    return a + b
  return a + b + c

no_teen_sum:

def no_teen_sum(a, b, c):
  return fix_teen(a) + fix_teen(b) + fix_teen(c)

def fix_teen(n):
  #if 13 <= n <= 14 or 17 <= n <= 19:
  if n in [13, 14, 17, 18, 19]:
    return 0
  return n

I consider checking for list membership to be more elegant than multiple comparison operations.

round_sum:

def round_sum(a, b, c):
  return round10(a) + round10(b) + round10(c)

def round10(n):
  if n % 10 >= 5:
    return n + 10 - (n % 10)
  return n - (n % 10)  

close_far:

def close_far(a, b, c):
  cond1 = abs(a-b) <= 1 and abs(b-c) >=2 and abs(a-c) >= 2
  cond2 = abs(a-c) <= 1 and abs(a-b) >=2 and abs(c-b) >= 2
  return cond1 or cond2

make_chocolate:

def make_chocolate(small, big, goal):
  maxBig = goal / 5
  
  if big >= maxBig:
    if small >= (goal - maxBig * 5):
      return goal - maxBig * 5
  if big < maxBig:
    if small >= (goal - big * 5):
      return goal - big * 5
  return -1