Search code examples
javagraphpath-finding

Calculating distance between non directly-connected nodes in matrix


I made a adjacency matrix for cities and connecting between them. And for example A-B, B-C, C-D. Now I am wonder if I can calculate distance between cities that aren't connected. Is it possible to calculate distance in matrix between non connected nodes and find path?

Cities class

import java.util.LinkedList;
import java.util.List;
import java.util.Scanner;

public class GraphCities {

    private List<String> cities;
    private int[][] matrix;
    Scanner s = new Scanner(System.in);

    public GraphCities() {
        this.cities = new LinkedList<String>();
    }

    public void addCity(String name) {
        if (!cities.contains(name)) {
            cities.add(name);
        } else {
            System.out.println("City " + name + " is already added.");
        }
    }

    public void makeGraph() {
        System.out
                .println("Distance between cities, if they aren't connected insert -1");
        matrix = new int[cities.size()][cities.size()];
        for (int i = 0; i < matrix.length; i++) {
            for (int j = i; j < matrix.length; j++) {
                if (i == j) {
                    matrix[i][j] = 0;
                } else {
                    System.out.println("Distance from "
                            + cities.get(i) + " to " + cities.get(j));
                    int distance = s.nextInt();
                    matrix[i][j] = distance;
                    matrix[j][i] = distance;

                }
            }
        }
    }

    public void show() {
        String show = "\t";
        for (int i = 0; i < cities.size(); i++) {
            show += cities.get(i) + "\t";
        }
        show += "\n";
        for (int i = 0; i < matrix.length; i++) {
            show += cities.get(i) + "\t";
            for (int j = 0; j < matrix.length; j++) {
                if (matrix[i][j] != -1) {
                    show += matrix[i][j] + "\t";
                } else {
                    show += "-\t";
                }
            }
            show += "\n";
        }
        System.out.println(show);
    }
}

Main method

public class Main {

    public static void main(String[] args) {
        GraphCities c = new GraphCities();
        c.addCity("A");
        c.addCity("B");
        c.addCity("C");
        c.addCity("D");
        c.addCity("E");
        c.makeGraph();
        System.out.println();
        c.show();
    }
}

This is my output when i run main method and i think everything is ok.

    A   B   C   D   E   
A   0   50  -   -   -   
B   50  0   30  -   -   
C   -   30  0   40  -   
D   -   -   40  0   20  
E   -   -   -   20  0   

Now I want to calculate distance from A to D, but i haven't any idea how to do it. I will appreciate any help. Thanks!


Solution

  • To find the shortest path in a weighted graph (each part of the route has a different weight) you can use Dijkstra's Shortest Path Algorithm.
    The following code is a one-file mcve. It can be copy-pasted into one file (Main.java) , and executed.
    (This answer is base on the code posted before editing the question)
    Please note the comments:

    import java.util.ArrayList;
    import java.util.Collection;
    import java.util.HashMap;
    import java.util.HashSet;
    import java.util.Map;
    import java.util.Set;
    
    public class Main {
    
        public static void main(String[] args) {
            AdjList aList = new AdjList();
            CityNode a = new CityNode("A");
            CityNode b = new CityNode("B");
            CityNode c = new CityNode("C");
            CityNode d = new CityNode("D");
            CityNode e = new CityNode("E");
            aList.addCity(a);
            aList.addCity(b);
            aList.addCity(c);
            aList.addCity(d);
            aList.addCity(e);
            aList.connectCities(a, b, 50);
            aList.connectCities(b, c, 30);
            aList.connectCities(c, d, 40);
            aList.connectCities(d, e, 20);
            aList.show();
    
            FindPath findPath = new FindPath();
            System.out.println(findPath.calculateShortestPath(aList, a, e)); //prints 140 as expected
    
            //add some complexity
            CityNode f = new CityNode("F");
            aList.addCity(f);
            aList.connectCities(b, f, 10);
            aList.connectCities(f, d, 10);
            System.out.println(findPath.calculateShortestPath(aList, a, e));//prints 90 as expected 
    
        }
    }
    
    class FindPath{
    
        //map to hold distances of all node from origin. at the end this map should contain
        //the shortest distance between origin (from) to all other nodes
        Map<CityNode, Integer> distances;
    
        //using Dijkstra algorithm
        public int calculateShortestPath(AdjList aList, CityNode from, CityNode to) {
    
            //a container to hold which cities the algorithm has visited
            Set<CityNode> settledCities = new HashSet<>();
            //a container to hold which cities the algorithm has to visit
            Set<CityNode> unsettledCities = new HashSet<>();
            unsettledCities.add(from);
    
            //map to hold distances of all node from origin. at the end this map should contain
            //the shortest distance between origin (from) to all other nodes
            distances = new HashMap<>();
            //initialize map with values: 0 distance to origin, infinite distance to all others
            //infinite means no connection between nodes
            for(CityNode city :aList.getCities()){
                int distance = city.equals(from) ? 0 : Integer.MAX_VALUE;
                distances.put(city, distance);
            }
    
            while (unsettledCities.size() != 0) {
                //get the unvisited city with the lowest distance
                CityNode currentCity = getLowestDistanceCity(unsettledCities);
                //remove from unvisited, add to visited
                unsettledCities.remove(currentCity); settledCities.add(currentCity);
    
                Map<CityNode, Integer> connectedCities = currentCity.getConnectedCities();
    
                for( CityNode city : connectedCities.keySet()){
                    //check if new distance is shorted than the previously found distance
                    if(distances.get(currentCity) + connectedCities.get(city) < distances.get(city)){
                        //if so, keep the shortest distance found
                        distances.put(city, distances.get(currentCity) + connectedCities.get(city));
                        //if city has not been visited yet, add it to unsettledCities
                        if(! settledCities.contains(city)) {
                            unsettledCities.add(city);
                        }
                    }
                }
            }
    
            return distances.get(to);
        }
    
        private CityNode getLowestDistanceCity(Set <CityNode> unsettledCities) {
    
            return unsettledCities.stream()
                     .min((c1,c2)-> Integer.compare(distances.get(c1), distances.get(c2)))
                     .orElse(null);
        }
    }
    
    class CityNode {
    
        private static int counter =0;
        private final String name;
        //assign unique id to each node. safer than to rely on unique name
        private final int id = counter ++;
    
        //map to hold all connected cities and distance to each
        private final Map<CityNode, Integer> connectedCities;
    
        public CityNode(String name) {
            super();
            this.name = name;
            connectedCities = new HashMap<>();
        }
    
        public String getName() {
            return name;
        }
    
        //not null safe. distance must not be negative
        public void connect(CityNode connectTo, int distance) {
            if(connectTo.equals(this)) throw new IllegalArgumentException("Node can not connect to istself");
            connectedCities.put(connectTo, distance);
        }
    
        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder(name);
            sb.append(connectedCities.keySet().isEmpty() ? " not connected" : " conntected to: " );
            for ( CityNode city : connectedCities.keySet()) {
                sb.append(city.getName()).append("-")
                .append(connectedCities.get(city)).append("km ");
            }
            return sb.toString();
        }
    
        public int getId() {
            return id;
        }
    
        public Map<CityNode, Integer> getConnectedCities(){
            return connectedCities;
        }
    
        @Override
        public boolean equals(Object o) {
    
            if(o == null ||!(o instanceof CityNode)) return false;
    
            CityNode c = (CityNode) o;
            return c.getName().equalsIgnoreCase(name) && id == c.getId();
        }
    
        @Override
        public int hashCode() {
            int hash = 31 * 7 + id;
            return name == null ? hash : name.hashCode();
        }
    }
    
    class AdjList {
    
        //use set which prevents duplicate entries
        private final Set<CityNode> citiesList;
    
        public AdjList() {
            citiesList = new HashSet<>();
        }
    
        //adds city if is not already present. returns true if city was added
        public boolean addCity(CityNode city) {
    
            return citiesList.add(city);
        }
    
        //not null safe
        public void connectCities(CityNode city1, CityNode city2, int distance) {
            //assuming undirected graph
            city1.connect(city2, distance);
            city2.connect(city1, distance);
        }
    
        public CityNode getCityByName(String name) {
            for (CityNode city : citiesList) {
                if (city.getName().equalsIgnoreCase(name))
                    return city;
            }
            return null;
        }
    
        public void show() {
            for (CityNode city : citiesList) {
                System.out.println(city);
            }
        }
    
        //get a copy of cities list
        public Collection<CityNode> getCities(){
    
            return new ArrayList<>(citiesList);
        }
    }
    

    If you need a version of getLowestDistanceCity which does not use Stream use :

    private CityNode getLowestDistanceCity(Set <CityNode> unsettledCities) {
        CityNode lowestDistanceCity = null;
        int lowestDistance = Integer.MAX_VALUE;
        for (CityNode city: unsettledCities) {
            int nodeDistance = distances.get(city);
            if (nodeDistance < lowestDistance) {
                lowestDistance = nodeDistance;
                lowestDistanceCity = city;
            }
        }
        return lowestDistanceCity;
    }
    

    Edit: an implementation using adjacency matrix could look like this:

    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.Collection;
    import java.util.HashMap;
    import java.util.HashSet;
    import java.util.List;
    import java.util.Map;
    import java.util.Set;
    
    public class DijkstraAdjacencyMatrix {
    
        public static void main(String[] args) {
    
            Set<CityNode> cities = new HashSet<>();
            CityNode a = new CityNode("A");
            CityNode b = new CityNode("B");
            CityNode c = new CityNode("C");
            CityNode d = new CityNode("D");
            CityNode e = new CityNode("E");
            cities.addAll(List.of(a,b,c,d,e));
    
            CitiesGraph graph = new CitiesGraph(cities);
            graph.connectCities(a, b, 50);
            graph.connectCities(b, c, 30);
            graph.connectCities(c, d, 40);
            graph.connectCities(d, e, 20);
            graph.show();
    
            FindPath findPath = new FindPath();
            System.out.println(findPath.calculateShortestPath(graph, a, e)); //prints 140 as expected
    
            //to add some complexity we need to construct a new CitiesGraph. It is not reusable
            CityNode f = new CityNode("F");
            cities.add(f);
            graph = new CitiesGraph(cities);
            graph.connectCities(a, b, 50);
            graph.connectCities(b, c, 30);
            graph.connectCities(c, d, 40);
            graph.connectCities(d, e, 20);
            graph.connectCities(b, f, 10);
            graph.connectCities(f, d, 10);
            graph.show();
            System.out.println(findPath.calculateShortestPath(graph, a, e));//prints 90 as expected
        }
    }
    
    class FindPath{
    
        //map to hold distances of all node from origin. at the end this map should contain
        //the shortest distance between origin (from) to all other nodes
        Map<CityNode, Integer> distances;
    
        //using Dijkstra algorithm
        public int calculateShortestPath(CitiesGraph graph, CityNode from, CityNode to) {
    
            //a container to hold which cities the algorithm has visited
            Set<CityNode> settledCities = new HashSet<>();
            //a container to hold which cities the algorithm has to visit
            Set<CityNode> unsettledCities = new HashSet<>();
            unsettledCities.add(from);
    
            //map to hold distances of all node from origin. at the end this map should contain
            //the shortest distance between origin (from) to all other nodes
            distances = new HashMap<>();
            //initialize map with values: 0 distance to origin, infinite distance to all others
            //infinite means no connection between nodes
            for(CityNode city :graph.getCities()){
                int distance = city.equals(from) ? 0 : Integer.MAX_VALUE;
                distances.put(city, distance);
            }
    
            while (unsettledCities.size() != 0) {
                //get the unvisited city with the lowest distance
                CityNode currentCity = getLowestDistanceCity(unsettledCities);
                //remove from unvisited, add to visited
                unsettledCities.remove(currentCity); settledCities.add(currentCity);
    
                Collection<CityNode> connectedCities = graph.getCitiesConnectedTo(currentCity);
                //iterate over connected city to update distance to each
                for( CityNode city : connectedCities){
                    //check if new distance is shorted than the previously found distance
                    int distanceToCity = graph.getDistanceBetween(city, currentCity);
                    if(distanceToCity <= 0) {
                        continue;
                    }
                    if(distances.get(currentCity) + distanceToCity < distances.get(city)){
                        //if so, keep the shortest distance found
                        distances.put(city,distances.get(currentCity) + distanceToCity);
                        //if city has not been visited yet, add it to unsettledCities
                        if(! settledCities.contains(city)) {
                            unsettledCities.add(city);
                        }
                    }
                }
            }
    
            return distances.get(to);
        }
    
        private CityNode getLowestDistanceCity(Set <CityNode> unsettledCities) {
    
            return unsettledCities.stream()
                     .min((c1,c2)-> Integer.compare(distances.get(c1), distances.get(c2)))
                     .orElse(null);
        }
    }
    
    class CityNode {
    
        private static int counter =0;
        private final String name;
        //assign unique id to each node. safer than to rely on unique name
        private final int id = counter ++;
    
        public CityNode(String name) {
            this.name = name;
        }
    
        public String getName() {
            return name;
        }
    
        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder("City ");
            sb.append(name).append(" (id=").append(id).append(")");
            return sb.toString();
        }
    
        public int getId() {
            return id;
        }
    
        @Override
        public boolean equals(Object o) {
    
            if(o == null || !(o instanceof CityNode)) return false;
    
            CityNode c = (CityNode) o;
            return c.getName().equalsIgnoreCase(name) && id == c.getId();
        }
    
        @Override
        public int hashCode() {
            int hash = 31 * 7 + id;
            return name == null ? hash : name.hashCode();
        }
    }
    
    class CitiesGraph{
    
        //use set which prevents duplicate entries
        private final Set<CityNode> cities;
        private final int[][] adjacencyMatrix;
        private static final int NOT_CONNECTED = -1;
    
        public  CitiesGraph(Set<CityNode> cities) {
            this.cities = cities;
            adjacencyMatrix = new int[cities.size()][cities.size()];
            //initialize matrix
            for(int row = 0; row < adjacencyMatrix.length ; row++){
                for(int col = 0; col < adjacencyMatrix[row].length ; col++){
                    adjacencyMatrix[row][col] = row == col ? 0 : NOT_CONNECTED ;
                }
            }
        }
    
        public void connectCities(CityNode city1, CityNode city2, int distance) {
            //assuming undirected graph
            adjacencyMatrix[city1.getId()][city2.getId()] = distance;
            adjacencyMatrix[city2.getId()][city1.getId()] = distance;
        }
    
        public int getDistanceBetween(CityNode city1, CityNode city2) {
    
            return adjacencyMatrix[city1.getId()][city2.getId()];
        }
    
        public Collection<CityNode> getCitiesConnectedTo(CityNode city) {
    
            Collection<CityNode> connectedCities = new ArrayList<>();
            //iterate over row representing city's connections
            int column = 0;
            for(int distance : adjacencyMatrix[city.getId()]){
                if(distance != NOT_CONNECTED && distance > 0) {
                    connectedCities.add(getCityById(column));
                }
                column++;
            }
    
            return connectedCities;
        }
    
        public CityNode getCityById(int id) {
            for (CityNode city : cities) {
                if (city.getId() == id) return city;
            }
            return null;
        }
    
        public void show() {
            for(int[] row : adjacencyMatrix){
                System.out.println(Arrays.toString(row));
            }
        }
    
        //get a copy of cities list
        public Collection<CityNode> getCities(){
            return new ArrayList<>(cities);
        }
    }