Branch and Bound function only returning first cycle in Python

52 views Asked by At

This is my first question here as I've usually found helpful answers by searching previous posts. I cannot find any that relate to my particular problem this time. I need to complete a branch and bound function in Python (it's for a Coursera course), and I cannot figure out how to keep the algorithm going after it finds the first cycle. I would appreciate any hints or guidance you can give. This is the most frustrated I've been with a programing assignment - and I've been taking Coursera courses for 2 years.

Most of the code has been given to us, and my portion is near the end. The graph is undirected and complete.

`# This function computes a lower bound on the length of Hamiltonian cycles starting with vertices in the list sub_cycle.
def lower_bound(g, sub_cycle):
    # The weight of the current path.
    current_weight = sum([g[sub_cycle[i]][sub_cycle[i + 1]]['weight'] for i in range(len(sub_cycle) - 1)])
    
    # For convenience we create a new graph which only contains vertices not used by g.
    unused = [v for v in g.nodes() if v not in sub_cycle]
    h = g.subgraph(unused)
    

    # Compute the weight of a minimum spanning tree.
    t = list(nx.minimum_spanning_edges(h))
    
    mst_weight = sum([h.get_edge_data(e[0], e[1])['weight'] for e in t])
    
    # If the current sub_cycle is "trivial" (i.e., it contains no vertices or all vertices), then our lower bound is
    # just the sum of the weight of a minimum spanning tree and the current weight.
    if len(sub_cycle) == 0 or len(sub_cycle) == g.number_of_nodes():
        
        return mst_weight + current_weight
    

    # If the current sub_cycle is not trivial, then we can also add the weight of two edges connecting the vertices
    # from sub_cycle and the remaining part of the graph.
    # s is the first vertex of the sub_cycle
    s = sub_cycle[0]
    # t is the last vertex of the sub_cycle
    t = sub_cycle[-1]
    # The minimum weight of an edge connecting a vertex from outside of sub_sycle to s.
    min_to_s_weight = min([g[v][s]['weight'] for v in g.nodes() if v not in sub_cycle])
    # The minimum weight of an edge connecting the vertex t to a vertex from outside of sub_cycle.
    min_from_t_weight = min([g[t][v]['weight'] for v in g.nodes() if v not in sub_cycle])

    # Any cycle which starts with sub_cycle must be of length:
    # the weight of the edges from sub_cycle +
    # the minimum weight of an edge connecting sub_cycle and the remaining vertices +
    # the minimum weight of a spanning tree on the remaining vertices +
    # the minimum weight of an edge connecting the remaining vertices to sub_cycle.
    
    return current_weight + min_from_t_weight + mst_weight + min_to_s_weight


# The branch and bound procedure takes
# 1. a graph g;
# 2. the current sub_cycle, i.e. several first vertices of cycle under consideration.
# Initially sub_cycle is empty;
# 3. currently best solution current_min, so that we don't even consider paths of greater weight.
# Initially the min weight is infinite
def branch_and_bound(g, sub_cycle=None, current_min=float("inf")):
    used_nodes = list()
    # If the current path is empty, then we can safely assume that it starts with the vertex 0.
    if sub_cycle is None:
        sub_cycle = [0]
          
    # If we already have all vertices in the cycle, then we just compute the weight of this cycle and return it.
    if len(sub_cycle) == g.number_of_nodes():
        weight = sum([g[sub_cycle[i]][sub_cycle[i + 1]]['weight'] for i in range(len(sub_cycle) - 1)])
        weight = weight + g[sub_cycle[-1]][sub_cycle[0]]['weight']
        
        return weight

    # Now we look at all nodes which aren't yet used in sub_cycle.
    unused_nodes = list()
    
    for v in g.nodes():
        if v not in sub_cycle:
            unused_nodes.append((g[sub_cycle[-1]][v]['weight'], v))
    
    # We sort them by the distance from the "current node" -- the last node in sub_cycle.
    unused_nodes = sorted(unused_nodes)
   

    for (d, v) in unused_nodes:
        assert v not in sub_cycle
        
        extended_subcycle = list(sub_cycle)
        extended_subcycle.append(v)
                      
        
        # For each unused vertex, we check if there is any chance to find a shorter cycle if we add it now.
        if lower_bound(g, extended_subcycle) < current_min:

            **# THIS IS WHERE WE ARE TO ADD OUR PORTION OF THE CODE. 
            # If there is such a chance, we add the vertex to the current cycle, and proceed  recursively.
            # If we found a short cycle, then we update the current_min value**.
            
            
           if len(extended_subcycle) == g.number_of_nodes():
            
                current_min = sum([g[extended_subcycle[i]][extended_subcycle[i + 1]]['weight'] for i in range(len(extended_subcycle) - 1)])
                current_min = current_min + g[extended_subcycle[-1]][extended_subcycle[0]]['weight']
            
            
            sub_cycle.append(v)
            return branch_and_bound(g, sub_cycle, current_min)

    # The procedure returns the shortest cycle length.
    return current_min`

The optimal cycle should be [0,4,2,1,5,3] with a weight of 695.8588106976. No matter what I try, I keep getting [0,1,2,4,3,5] with a weight of 769.1010673076235.

I believe I'm missing an ELSE: statement; and I've tried a few; but the code was never evaluated. I confirmed this by adding print('else'); but it never printed so I removed the code.

I wish I could enumerate all of the other things I've tried; but I've been revising and re-running this code for two-three days now - so I can't recall specific details.

Here is the input I'm using. They are coordinates that are passed to a function that creates a graph. I've included it below.

coordinates3 = [(199, 59), (152, 117), (68, 87), (281, 161), (11, 53), (254, 227)]

def get_graph(coordinates):
    g = nx.Graph()
    n = len(coordinates)
    for i in range(n):
        for j in range(i + 1):
            g.add_edge(i, j, weight=dist(coordinates[i][0], coordinates[i][1], coordinates[j][0], coordinates[j][1]))
    return g

Edited to add: I'm leaving this question here in case it gets answered and someone else has this same issue. I have unenrolled from the class...my first one due to frustration at an assignment. It's been 5 days, and I've worked on it more than 3 hours each day with no improvement.

0

There are 0 answers