Traveling Salesman

Brandon Blatter

CS 312

View this report in your browser

Let's be honest. PDFs were not meant for displaying code, and code was not meant to be displayed in PDFs. Please view this report in your browser so it's easy to read.

Code

State.cs

using System;
using System.Collections;
using System.Collections.Generic;
using System.Drawing;

namespace TSP {

    // custom class that manages data associated with each node
    public class State {

        public State(int cityIndex, ArrayList path, double lb, double[,] matrix) {
            cityListIndex = cityIndex;
            partialPath = path;
            lowerBound = lb;
            reducedCostMatrix = matrix;
            weight = lb / (path.Count + 1);
        }

        public int cityListIndex;
        public int listIndex;
        public ArrayList partialPath;
        public double lowerBound;
        public double[,] reducedCostMatrix;
        public double weight;

    }
}

BinaryHeapPQ.cs

using System;
using System.Collections.Generic;
using System.Drawing;

namespace TSP {

    public class BinaryHeapPQ {

        public BinaryHeapPQ() {
            // reset list of states
            heapList = new List<State>();
            heapLocations = new List<int>();
        }

        public BinaryHeapPQ(List<State> states) {
            MakeQueue(states);
        }

        // locations of nodes on the heap (access from DeleteMin)
        private List<int> heapLocations;
        private List<State> heapList;

        public bool IsEmpty() {
            return heapList.Count == 0;
        }

        // gets node's parent from binary heap by index
        private int GetParentIndex(int nodeIndex) {
            return nodeIndex / 2;
        }

        private int GetSmallerChildIndex(int nodeIndex) {
            int childIndex1 = nodeIndex * 2;
            // if left child index is greater than size of heap, use nodeIndex
            if(childIndex1 >= heapList.Count) return nodeIndex;
            int childIndex2 = childIndex1 + 1;
            // if right child index is greater than size of heap, use left child index
            if(childIndex2 >= heapList.Count) return childIndex1;
            State child1 = heapList[childIndex1];
            State child2 = heapList[childIndex2];
            // if right child doesn't exist or has greater weight, use left child
            if(child1.weight < child2.weight) return childIndex1;
            // otherwise use right child
            else return childIndex2;
        }

        // swap nodes by index in heap list
        private void SwapNodes(int nodeIndex1, int nodeIndex2) {
            // get nodes to switch
            State node1 = heapList[nodeIndex1];
            State node2 = heapList[nodeIndex2];
            // swap nodes actual nodes in heapList
            heapList[nodeIndex2] = node1;
            heapList[nodeIndex1] = node2;
            // swap indices in heapLocations
            heapLocations[node1.listIndex] = nodeIndex2;
            heapLocations[node2.listIndex] = nodeIndex1;
        }

        private void BubbleUp(int nodeIndex) {
            int parentIndex = GetParentIndex(nodeIndex);
            // while parent value is higher
            while(heapList[parentIndex].weight > heapList[nodeIndex].weight) {
                // swap nodes
                SwapNodes(parentIndex, nodeIndex);
                // update current node and parent indices
                nodeIndex = parentIndex;
                parentIndex = GetParentIndex(parentIndex);
            }
        }

        private void BubbleDown(int nodeIndex) {
            int smallerChildIndex = GetSmallerChildIndex(nodeIndex);
            if(smallerChildIndex == 0) return;
            // while child value is lower
            while(heapList[smallerChildIndex].weight < heapList[nodeIndex].weight) {
                // swap nodes
                SwapNodes(smallerChildIndex, nodeIndex);
                // update current node and parent indices
                nodeIndex = smallerChildIndex;
                smallerChildIndex = GetSmallerChildIndex(smallerChildIndex);
                if(smallerChildIndex == 0) return;
            }
        }

        // MakeQueue: O(n log n)
        public void MakeQueue(List<State> states) {
            // reset list of states
            heapList = new List<State>();
            heapLocations = new List<int>();
            // add each state given; order doesn't matter
            for (int i = 0; i < states.Count; i++) {
                Insert(states[i]);
            }
        }

        // Insert: O(log n)
        public void Insert(State state) {
            // add state to end of list (bottom right of tree)
            heapList.Add(state);
            // add location as the last element in the heap list
            heapLocations.Add(heapList.Count - 1);
            state.listIndex = heapLocations.Count - 1;
            // bubble up
            BubbleUp(heapList.Count - 1);
        }

        // DeleteMin: O(log n)
        public State DeleteMin() {
            // get top node
            State min = heapList[0];
            heapLocations[min.listIndex] = -1;
            // replace top node with bottommost rightmost
            heapList[0] = heapList[heapList.Count - 1];
            heapList.RemoveAt(heapList.Count - 1);
            if(heapList.Count > 0) {
                heapLocations[heapList[0].listIndex] = 0;
            }
            // bubble down
            BubbleDown(0);
            return min;
        }

        // DecreaseKey: O(log n)
        public void DecreaseKey(int listIndex) {
            // bubble up actual location in heap
            BubbleUp(heapLocations[listIndex]);
        }
    }
}

BranchAndBound.cs

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;

namespace TSP {

    class BranchAndBound {

        public BranchAndBound() {

        }

        // for storing the result of reducing a matrix
        public class MatrixAndBound {

            public MatrixAndBound(double[,] costMatrix, double oldLowerBound) {
                reducedCostMatrix = costMatrix;
                lowerBound = oldLowerBound;
            }

            public double[,] reducedCostMatrix;
            public double lowerBound;

        }

        public static double[,] CreateCostMatrix(City[] cities) {
            int length = cities.Length;
            double[,] costMatrix = new double[length, length];
            // in every cell
            for(int i = 0; i < length; i++) {
                for(int j = 0; j < length; j++) {
                    // store cost between cities
                    costMatrix[i, j] = cities[i].costToGetTo(cities[j]);
                }
            }
            // turn middle distance zeros to infinities
            for(int i = 0; i < length; i++) {
                costMatrix[i, i] = double.PositiveInfinity;
            }
            return costMatrix;
        }

        public static MatrixAndBound PrepareMatrix(    double[,] intialMatrix,
                                                                                                int rowIndex,
                                                                                                int columnIndex,
                                                                                                double oldLowerBound) {
            double[,] matrix = intialMatrix.Clone() as double[,];
            // add cost of this path
            double lowerBound = oldLowerBound + matrix[rowIndex, columnIndex];
            // infinity out row
            for(int i = 0; i < matrix.GetLength(0); i++) {
                matrix[rowIndex, i] = double.PositiveInfinity;
            }
            // infinity out column
            for(int i = 0; i < matrix.GetLength(1); i++) {
                matrix[i, columnIndex] = double.PositiveInfinity;
            }
            // infinity out reverse path
            matrix[columnIndex, rowIndex] = double.PositiveInfinity;
            // return prepared result
            MatrixAndBound result = new MatrixAndBound(matrix, lowerBound);
            return result;
        }

        public static MatrixAndBound ReduceMatrix(MatrixAndBound start) {
            double [,] matrix = start.reducedCostMatrix.Clone() as double[,];
            double lowerBound = start.lowerBound;
            // reduce each row
            double lowestRowValue;
            for(int i = 0; i < matrix.GetLength(0); i++) {
                // find the lowest non-infinity value
                lowestRowValue = double.PositiveInfinity;
                for(int j = 0; j < matrix.GetLength(1); j++) {
                    if(!double.IsPositiveInfinity(matrix[i, j]) && matrix[i, j] < lowestRowValue) {
                        lowestRowValue = matrix[i, j];
                    }
                }
                // if lowest non infinite value > 0, subtract this value from every non-infinite value
                if(lowestRowValue > 0 && !double.IsPositiveInfinity(lowestRowValue)) {
                    // add cost difference to lower bound
                    lowerBound += lowestRowValue;
                    for(int j = 0; j < matrix.GetLength(1); j++) {
                        //if(!double.IsPositiveInfinity(matrix[i, j]))
                        matrix[i, j] -= lowestRowValue;
                    }
                }
            }
            // reduce each column
            double lowestColumnValue;
            for(int i = 0; i < matrix.GetLength(1); i++) {
                // find the lowest non-infinity value
                lowestColumnValue = double.PositiveInfinity;
                for(int j = 0; j < matrix.GetLength(0); j++) {
                    if(!double.IsPositiveInfinity(matrix[j, i]) && matrix[j, i] < lowestColumnValue) {
                        lowestColumnValue = matrix[j, i];
                    }
                }
                // if lowest value > 0, subtract this value from every non-infinite value
                if(lowestColumnValue > 0 && !double.IsPositiveInfinity(lowestColumnValue)) {
                    // add cost difference to lower bound
                    lowerBound += lowestColumnValue;
                    for(int j = 0; j < matrix.GetLength(0); j++) {
                        if(!double.IsPositiveInfinity(matrix[j, i])) matrix[j, i] -= lowestColumnValue;
                    }
                }
            }

            return new MatrixAndBound(matrix, lowerBound);
        }

        public static void PrintMatrix(double[,] matrix) {
            Console.Write("\n");
            Console.Write("\n");

            for(int i = 0; i < matrix.GetLength(0); i++) {
                for(int j = 0; j < matrix.GetLength(1); j++) {
                    Console.Write((double.IsPositiveInfinity(matrix[i, j]) ? "∞" : matrix[i, j].ToString()) + "\t");
                }
                Console.Write("\n");
            }
        }

        public static bool IsValidPath(ArrayList path, int numCities) {
            // not valid if count differs from number of cities
            if(path.Count != numCities) return false;

            // go through each edge in the route and add up the cost
            int x;
            City here;
            double cost = 0D;

            for (x = 0; x < path.Count - 1; x++) {
                here = path[x] as City;
                cost += here.costToGetTo(path[x + 1] as City);
            }
            // go from the last city to the first
            here = path[path.Count - 1] as City;
            cost += here.costToGetTo(path[0] as City);
            // not valid if cost is infinite
            if(double.IsPositiveInfinity(cost)) return false;

            // only valid if every item in the list is unique
            return path.Cast<City>().Distinct().Count() == path.Count;
        }

    }
}

ProblemAndSolver.cs

The following is the most relevant excerpt.

/// <summary>
/// performs a Branch and Bound search of the state space of partial tours
/// stops when time limit expires and uses BSSF as solution
/// </summary>
/// <returns>results array for GUI that contains three ints: cost of solution, time spent to find solution, number of solutions found during search (not counting initial BSSF estimate)</returns>
public string[] bBSolveProblem() {

    string[] results = new string[3];
    Stopwatch timer = new Stopwatch();
    BinaryHeapPQ branchAndBoundPQ = new BinaryHeapPQ();
    City[] bbCities = GetCities();
    ArrayList newPartialPath;
    MatrixAndBound newResult;
    TSPSolution possibleNewSolution;
    int count = 0;
    double currentCostOfBssf = double.PositiveInfinity;

    timer.Start();

    // create a matrix of costs
    double[,] costMatrix = CreateCostMatrix(GetCities());

    // produce a reduced costs matrix
    MatrixAndBound originalResult = ReduceMatrix(new MatrixAndBound(costMatrix, 0));

    // pick a random city to start
    Random r = new Random();
    int firstCityIndex = r.Next(bbCities.Length);

    // add to queue
    branchAndBoundPQ.Insert(new State(    firstCityIndex,
                                                                            new ArrayList(),
                                                                            originalResult.lowerBound,
                                                                            originalResult.reducedCostMatrix));

    State currentState;
    // while there is still stuff in the queue and time hasn't run out
    while(!branchAndBoundPQ.IsEmpty()) {
        // halt if over a certain time limit
        // if(timer.ElapsedMilliseconds > 30000) break;
        // store current cost of bssf instead of calculating it all the time
        currentCostOfBssf = bssf == null ? double.PositiveInfinity : costOfBssf();
        // get the current state for operations; delete the first item in the queue
        currentState = branchAndBoundPQ.DeleteMin();

        if(currentState.lowerBound > currentCostOfBssf) continue;

        // make a new partial path to add to children
        newPartialPath = new ArrayList(currentState.partialPath);
        newPartialPath.Add(bbCities[currentState.cityListIndex]);

        // make a new TSPSolution for testing
        possibleNewSolution = new TSPSolution(newPartialPath);
        // forget about it if it costs more than the best one
        double costOfNewSolution = possibleNewSolution.costOfRoute();
        if(costOfNewSolution > currentCostOfBssf) continue;

        // update the best solution so far (bssf) if possible
        if(IsValidPath(newPartialPath, bbCities.Length)) {
            bssf = possibleNewSolution;
            count++;
            // path is valid; don't process children
            continue;
        }

        // iterate over every city
        for(int i = 0; i < bbCities.Length; i++) {
            // if there is viable path to the next city
            if(            !double.IsPositiveInfinity(currentState.reducedCostMatrix[currentState.cityListIndex, i])
                    &&    i != firstCityIndex) {
                // calculate the cost matrix and make a little object
                newResult = ReduceMatrix(    PrepareMatrix(    currentState.reducedCostMatrix,
                                                                                                    currentState.cityListIndex,
                                                                                                    i,
                                                                                                    currentState.lowerBound));
                // add to the queue
                branchAndBoundPQ.Insert(new State(    i,
                                                                                        newPartialPath,
                                                                                        newResult.lowerBound,
                                                                                        newResult.reducedCostMatrix));
            }
        }
    }

    timer.Stop();

    results[COST] = costOfBssf().ToString();
    results[TIME] = timer.Elapsed.ToString();
    results[COUNT] = count.ToString();

    return results;
}

Complexity

Time Complexity

The overall Big O for this algorithm is O(n! n2) and consists of two main parts: the time taken to add each state object to the queue and the matrix operations performed on each state object.

Overall, as many as O(n!) objects may need to be added to the queue. This represents the total number of possible paths that may be taken, each which could have a corresponding

Each object on the queue needs to go under a O(n2) operation so it can pass over a row and a column in the matrix. This is required for changing a row and a column's values to infinity as well as subtracting the smallest value from each cell in a row and a column.

My binary heap has a Big O of O(log n) but because this n is different from the number of cities, it is not considered in the final Big O calculation.

Space Complexity

Since every state that is created needs to be stored along with its matrix, the space complexity has the same big O as the time complexity: O(n! n2).

State Data Structure

I created a very simple state data structure (State.cs) to hold information about each state on the queue. The state's data members are as follows:

  • int cityListIndex: the index of the "from" city as found in the array of cities
  • int listIndex: the index of the state in the binary heap queue
  • ArrayList partialPath: the cities in the path this state is a part of
  • double lowerBound: the lower bound of the final cost found by following this path
  • double[,] reducedCostMatrix: the reduced cost matrix for this state
  • double weight: the weight in the priority queue (smaller has higher priority), which is calculated using the lower bound and the number of cities in the partial path

I create and add a state to the queue each time a path has a possible solution with a better lower bound than the best solution so far. States are removed from the priority queue in order of weight and processed individually.

Priority Queue Data Structure

The priority queue data structure is a simple binary heap. I repurposed the code I wrote for the network routing lab here. All operations are O(log n).

Whenever a node is added to or removed from the binary heap, the heap rebalances itself so the nodes stay sorted. Nodes are added to the end and then they "bubble up" to the top (swapping with parents if the new node has a lower weight). Nodes that are removed are replaced with the last node (the node with the highest weight) which then "bubbles down" to its correct position.

Approach for Initial BSSF

Before I have any solution to a TSP problem I continue to explore and process states in order of their weight in the queue. Any potential states are added to the queue, regardless of their upper bound. Once an initial solution is found, I check its cost against the lower bound of states being pulled off the queue. If the states coming from the queue end up having a lower bound higher than the initial solution, I don't process them.

After that, I simply update the initial solution with a new solution every time a full path is found with a smaller cost.

Outcomes

All times are measured in seconds.

Raw

Binary Heap

Num Cities Seed Running Time Cost of best tour Max num of stored states Num BSSF updates Total num states created Total num states pruned
14 1 .410 3697* 88 18320 7 18320
15 20 3.329 2430* 1197 171654 13 171654
16 902 .563 3223* 1059 19852 9 19852
17 1 29.6 4442* 347 971174 16 971174
18 1 45.5 4247* 388 1334681 8 1334681
19 1 60.0 4286 576 1658280 16 1658191
20 1 60.0 4286 680 1550111 3 1549846
21 1 60.0 4329 1231 1442483 6 1442346

Discussion

Generally problems tend to get much much harder to solve with every city added. While good pruning algorithms may be able to keep the maximum number of stored states low, the number of total states calculated is almost unavoidably high.

As explained earlier, the time complexity and space complexity grow very quickly. Pruning algorithms are effective at reducing the space complexity, but the time complexity is unavoidable as every state in the queue must be calculated at some point.

results matching ""

    No results matching ""