Как группировать узлы, имеющие общие ребра из заданной строки пар?

#java #algorithm

#java #алгоритм

Вопрос:

У меня есть массив строк, который представляет ребра графа.

Например:

 ["6 11", "9 5", "11 9", "15 9", "13 15", "12 14", "15 16", "1 16"]
 

Теперь я хочу создать график для этого, чтобы я мог видеть, сколько узлов подключено и сколько не подключено. Я не уверен, какому подходу следовать для этого.

Я использовал карту в своей программе, но я не могу правильно сгруппировать узлы в своем коде:

 public static void process(int n, List<String> input) {
    Map<Integer, Set<Integer>> map = new HashMap<>();
    for (String in : input) {
        String[] arr = in.split(" ");
        int a1 = Integer.parseInt(arr[0]);
        int a2 = Integer.parseInt(arr[1]);
        int first = Math.min(a1, a2);
        int second = Math.max(a1, a2);

        if (map.size() == 0) {
            Set<Integer> s = new HashSet<>();
            s.add(first);
            s.add(second);
            map.put(first, s);
            continue;
        }

        boolean found = false;
        for (int k : map.keySet()) {
            Set<Integer> set = map.get(k);

            if (set.contains(first)) {
                map.get(k).add(second);
                found = true;
                break;
            }
            if (set.contains(second)) {
                map.get(k).add(first);
                found = true;
                break;
            }
        }
        if (!found) {
            Set<Integer> s = new HashSet<>();
            s.add(first);
            s.add(second);
            map.put(first, s);
        }
    }
    System.out.println(map);
}
 

Теперь с помощью этой программы я получаю карту со значениями ниже:

 {5=[16, 1, 5, 9, 11, 13, 15], 6=[6, 11], 12=[12, 14]}
 

Проблема здесь в том, что у меня уже есть 11 для map key = 5 . Таким образом, узел 6 должен быть добавлен к набору для самого key = 5 . Итак, карта должна выглядеть следующим образом:

 {5=[16, 1, 5, 9, 11, 13, 15, 6], 12=[12, 14]}
 

Как это сделать, какому подходу мне нужно следовать здесь? Здесь я использовал карту только для удобства, потому что на более позднем этапе я хотел подсчитать размер каждого ключевого средства: 5 имеет 8 узлов, 12 имеет 2 узла и т.д. Узлы, которые не имеют ребер, являются 2, 3, 4, 7, 8, 10.

Здесь ключи 5 и 12 не важны для меня, они могут быть любыми. Кроме того, порядок значений также не важен для меня, они могут быть в любом порядке.

Редактировать:

Во входных: ["6 11", "9 5", "11 9", "15 9", "13 15", "12 14", "15 16", "1 16"]

скажем "6 11" , оба 6 and 11 are nodes и "6 11" represents an edge between nodes . Теперь я просто хочу сгруппировать все подключенные узлы, такие как "6 11 9 15 13 16 1 5" и другая группа "12 14" .

Обновить:

Я попытался реализовать решение Javascript, предоставленное grodzi на Java, но оно выдает неверный результат, я допустил ошибку при преобразовании, которую я не могу понять:

 public static int process(int n, List<String> input) {
    Map<String, Integer> strToGroupId = new HashMap<>();
    Map<Integer, Set<Integer>> groupIdToGroup = new HashMap<>();
    for (String in : input) {
        String[] arr = in.split(" ");
        int a1 = Integer.parseInt(arr[0]);
        int a2 = Integer.parseInt(arr[1]);
        link(arr[1], arr[1], strToGroupId, groupIdToGroup);
    }
    System.out.println(groupIdToGroup);//{16=[16], 5=[5], 9=[9], 11=[11], 14=[14], 15=[15]
    return 0;
}

static void link(String strA, String strB, Map<String, Integer> strToGroupId, Map<Integer, Set<Integer>> groupIdToGroup) {
    if (!strToGroupId.containsKey(strA)) {
        int val = Integer.parseInt(strA);
        strToGroupId.put(strA, val);
        Set<Integer> set = new HashSet<>();
        set.add(val);
        groupIdToGroup.put(val, set);
      }
    if (!strToGroupId.containsKey(strB)) {
        int val = Integer.parseInt(strB);
        strToGroupId.put(strB, val);
        Set<Integer> set = new HashSet<>();
        set.add(val);
        groupIdToGroup.put(val, set);
      }

      int gA = strToGroupId.get(strA);
      int gB = strToGroupId.get(strB);

      if (gA == gB) return;
      // need to merge
      Set<Integer> eaters = groupIdToGroup.get(gA);
      Set<Integer> eatens = groupIdToGroup.get(gB);
      for (int n : eatens) {
        eaters.add(n);
        strToGroupId.put(n "", gA); // the eateN now links to gA instead of gB
      }
      groupIdToGroup.remove(gB); // gB does not exist anymore
}
 

Обновление с использованием подхода DFS

Теперь я пытаюсь следовать подходу, предложенному Абхинавом Матуром. Вот мой Java-код, он генерирует неправильные результаты:

 public static void process(int n, List<String> input) {
    List<Set<Integer>> graph = new ArrayList<>();
    List<Set<Integer>> output = new ArrayList<>();
    for (int i = 0; i < n; i  ) {
        graph.add(new HashSet<>());
    }
    for (int i = 0; i < n; i  ) {
        output.add(new HashSet<>());
    }

    for (String in : input) {
        String[] arr = in.split(" ");
        int a1 = Integer.parseInt(arr[0]);
        int a2 = Integer.parseInt(arr[1]);
        graph.get(a1-1).add(a2);
        graph.get(a2-1).add(a1);
    }
    boolean[] visited = new boolean[n];

    for (int i = 0; i < n; i  ) {
        dfs(1, 1, visited, graph, output);
    }
    System.out.println(output); 
}

private static void dfs(int node, int source, boolean[] visited, List<Set<Integer>> graph,
        List<Set<Integer>> output) {
    if (!visited[node-1]) {
        visited[node-1] = true;
        output.get(source-1).add(node);
        for (int neighbour : graph.get(node-1)) {
            dfs(neighbour, source, visited, graph, output);
        }
    }

}
 

Для ввода "6 11", "9 5", "11 9", "15 9", "13 15", "12 14", "15 16", "1 16" эта программа возвращает:

 [[16, 1, 5, 6, 9, 11, 13, 15], [], [], [], [], [], [], [], [], [], [], [], [], [], [], []]
 

Комментарии:

1. Почему бы не присвоить каждому узлу ключ на карте

2. @mitchel Paulin, можете ли вы уточнить, как это может решить эту проблему

3. @MitchelPaulin Это дает каждому узлу ключ. Я не уверен, почему вы это сказали.

4. Могу я просто спросить вас, по каким критериям вы указываете keys в своей карте, это первое число или любое число из графика?

5. Я добавил ответ ниже, чтобы для поиска узлов я использовал список смежности, а затем искал каждый узел, отслеживая посещенные

Ответ №1:

Я собираюсь предложить другое решение. Первое наблюдение, которое нам нужно сделать, это то, что мы на самом деле не хотим получать Map<Integer, Set<Integer>> . Ключ, например, 5, ничем не отличается от любого другого узла. Причина, по которой это ключ, заключается только в том, что он предшествует в списке. Поэтому я собираюсь построить:

 Set<Set<Integer>> map
 

Где для каждых 2 наборов в этом наборе они имеют пустое пересечение.

Алгоритм

  1. Для каждого ребра:
    1. Извлеките 2 вершины.
    2. создайте набор с этими 2 узлами.
    3. Перебрать все существующие наборы:
      1. Объедините все множества, которые имеют одну из двух вершин, в новый набор.
      2. Удалите все наборы из предыдущего шага.
      3. Добавьте агрегированный набор.

Реализация

 public static void process(List<String> input) {
    Set<Set<Integer>> map = new HashSet<>();
    for (String in : input) {
        String[] arr = in.split(" ");
        int a1 = Integer.parseInt(arr[0]);
        int a2 = Integer.parseInt(arr[1]);

        Set<Integer> aggregatedSet = new HashSet<>();
        Set<Set<Integer>> setsToRemove = new HashSet<>();
        aggregatedSet.add(a1);
        aggregatedSet.add(a2);
        for (Iterator<Set<Integer>> it = map.iterator(); it.hasNext(); ) {
            Set<Integer> currentNodes = it.next();
            if (currentNodes.contains(a1) || currentNodes.contains(a2)) {
                aggregatedSet.addAll(currentNodes);
                setsToRemove.add(currentNodes);
            }
        }

        map.removeAll(setsToRemove);
        map.add(aggregatedSet);
    }
    System.out.println(map);
}
 

Следующее:

 List<String> edges = Arrays.asList("6 11", "9 5", "11 9", "15 9", "13 15", "12 14", "15 16", "1 16");
process(edges);
 

Будет выводить:

 [[12, 14], [16, 1, 5, 6, 9, 11, 13, 15]]
 

И следующее:

 List<String> edges = Arrays.asList("40 22","60 6","22 39","43 40","22 55","48 57","42 41","22 57","6 42","33 74","70 46","4 11","6 28","22 79","61 34","77 40","4 8","72 26","62 50","72 51","1 79","34 29","77 41","2 48","43 2","62 45","43 17","19 33","76 4","35 54");
process(edges);
 

Будет выводить:

 [[1, 2, 6, 39, 40, 41, 42, 43, 77, 79, 48, 17, 22, 55, 57, 28, 60], [4, 8, 11, 76], [70, 46], [51, 72, 26], [35, 54], [34, 29, 61], [50, 45, 62], [33, 19, 74]]
 

Ответ №2:

Я довольно смущен вашим кодом. Разумным графическим представлением является отображение от узла к набору смежных узлов, т. Е. Связанных направленным ребром. Если граф неориентированный, то будут зеркальные отображения: из A -> {..B..} и B -> {..A ..} для каждого ребра A, B.

Для реализации этого вам нужно что-то вроде:

 class GraphForFun {
 static void addDirectedEdge(Map<Integer, Set<Integer>> graph, int a, int b) {
    Set<Integer> adjacent = graph.get(a);
    if (adjacent == null) {
      adjacent = new HashSet<>();
      graph.put(a, adjacent);
    }
    adjacent.add(b);
  }

  static Map<Integer, Set<Integer>> getGraph(List<String> edges) {
    Map<Integer, Set<Integer>> graph = new HashMap<>();
    for (String edge : edges) {
      String vertexNumbers[] = edge.split("\s ");
      int a = Integer.parseInt(vertexNumbers[0]);
      int b = Integer.parseInt(vertexNumbers[1]);
      addDirectedEdge(graph, a, b);
      addDirectedEdge(graph, b, a); // Remove if graph is directed.
    }
    return graph;
  }

  public static void main(String[] args) {
    String[] edges = {"6 11", "9 5", "11 9", "15 9", "13 15", "12 14", "15 16", "1 16"};
    Map<Integer, Set<Integer>> graph = getGraph(Arrays.asList(edges));
    System.out.println(graph);
  }
}
 

Это выводит

 {16=[1, 15], 1=[16], 5=[9], 6=[11], 9=[5, 11, 15], 11=[6, 9], 12=[14], 13=[15], 14=[12], 15=[16, 9, 13]}
 

Комментарии:

1. Но в моей задаче я хочу, чтобы группа значений выглядела так : {5=[16, 1, 5, 9, 11, 13, 15, 6], 12=[12, 14]} . Здесь ключи 5 и 12 могут быть любыми, для меня это не важно. Кроме того, порядок значений для меня не важен.

2. Кроме того, я не уверен, правильно ли говорить, что это проблема, связанная с графом

3. Что ж, @learner, тогда вам не следует озаглавлять свой пост «построить график». Вы должны объяснить проблему, которую вы на самом деле хотите решить. Большая часть информатики и программирования делает это правильно.

4. Теперь я обновил заголовок, чтобы избежать путаницы.

Ответ №3:

Из того, что я понял, вы в основном отслеживаете острова в своем графике. Поскольку я не владею Java, я напишу простой алгоритм для решения вашей цели.

  1. Создайте a graph[] , где каждый ключ сопоставляется со списком смежности. Для каждого i j из ваших входных данных добавьте j to graph[i] и i to graph[j] .
  2. Создайте карту output , которую мы сейчас заполним.
  3. Запустите DFS с первого узла (или любого узла, который вы предпочитаете). Функция DFS будет выглядеть так
 dfs (node, source):
    if not visited node:
        visited[node] = true
        append node to output[source]
        for neighbour in graph[node]:
            dfs(neighbour, source)
 
  1. Проверьте, не посещен ли какой-либо узел. Если вы найдете такое node , вам просто нужно позвонить dfs (node, node) .

Это даст вам желаемую карту островов, которая вам нужна.

Комментарии:

1. Я попытался реализовать это на Java, я не уверен, где я допустил ошибку, я не получаю ожидаемого результата.

2. В чем проблема с выводом?

3. Я не получаю ожидаемый результат

4. Хорошо, но что неверно? Если вы можете, приведите пример, чтобы я мог его исправить

5. для ввода ["6 11", "9 5", "11 9", "15 9", "13 15", "12 14", "15 16", "1 16"] должен быть вывод {5=[16, 1, 5, 9, 11, 13, 15, 6], 12=[12, 14]} , но программа, которую я создал на основе вашего предложения, печатает [[16, 1, 5, 6, 9, 11, 13, 15], [], [], [], [], [], [], [], [], [], [], [], [], [], [], []] , я все объяснил в своем сообщении и его обновленном разделе.

Ответ №4:

Что вы хотите, так это найти связанные компоненты (точнее, по-видимому, только их размер)

Вы можете искать лучшие алгоритмы.

После обучения давайте заново откроем некоторое колесо:

  • Если между двумя узлами есть ребро, свяжите их
  • При связывании двух узлов один является пожирателем, а другой — съеденным: свяжите каждый узел съеденных ссылок с пожирателем

например

 a1    b1
|     |
a2    b2
|     |
A     B
 
  • a1,...a2 связаны ли узлы с узлом A
  • b1,...b2 связаны ли узлы с узлом B

при слиянии A и B , (например) скажем A , является пожирателем, B съеденный затем ссылается b1, b2, B на A

 a1    
|     
a2 - b1 - b2 - b // notice B lowercase since belongs to "group" A
|     
A
 

Ниже приведена реализация js (java заняла бы у меня слишком много времени). Надеюсь, это достаточно близко к псевдокоду?

 /*
  strToGroupId     groupIdToGroup 
  _____________________________________
  strA ------> gA -----> { strA }
  strB --v
  strC -- ---> gB -----> { strB, strC }

*/
strToGroupId = new Map // str -> int
groupIdToGroup = new Map // int -> Set<str>
const link = ([strA, strB]) => {
  // assign a group if new node
  if (!strToGroupId.has(strA)) {
    strToGroupId.set(strA, strA),
    groupIdToGroup.set(strA, new Set([strA]))
  }
  if (!strToGroupId.has(strB)) {
    strToGroupId.set(strB, strB),
    groupIdToGroup.set(strB, new Set([strB]))
  }

  const gA = strToGroupId.get(strA)
  const gB = strToGroupId.get(strB)

  if (gA === gB) return
  // need to merge
  const eaters = groupIdToGroup.get(gA)
  const eatens = groupIdToGroup.get(gB)
  for (const n of eatens) {
    eaters.add(n)
    strToGroupId.set(n, gA) // the eateN now links to gA instead of gB
  }
  groupIdToGroup.delete(gB) // gB does not exist anymore
}
const data = ["6 11", "9 5", "11 9", "15 9", "13 15", "12 14", "15 16", "1 16"]
data.map(x => x.split(' ')).forEach(link)
console.log([...groupIdToGroup.values()].map(aSet => [...aSet])) 

Комментарии:

1. Не могли бы вы проверить мой обновленный раздел, я попытался реализовать этот JS-код на Java, но допустил какую-то ошибку, из-за которой я не могу ее найти. Можете ли вы увидеть и подсказать, где я допускаю ошибку в своем коде.

2. @learner Я думаю, что это часть обучения отладке вашего кода. Вероятно, вам следует (в качестве упражнения) действительно попытаться ввести много println и проверять на каждом шаге, соответствует ли нежелательный вывод вашим ожиданиям. Однако мой намек заключается в том, что, поскольку мы пропустили узлы на выходе (например, узел 6), тогда 6 было съедено, но добавление к едокам не было принято во внимание. Возможно, в java получение набора в качестве значения с карты получает копию (я не знаю). В любом случае я бы позаботился о том after , чтобы добавленные элементы были добавлены, чтобы получить groupIdToGroup.get(gA) их

3. @learner В вашем коде, кстати, вы вызываете link с. arr[1], arr[1] Должно быть arr[0], arr[1] иначе strA===strB все время, и вы никогда не объединитесь

4. Понял, я пропустил передачу arr[0], также не могли бы вы помочь мне понять, какова временная сложность вашего ответа.

5. Я бы сказал, что O (n ^ 2) наихудший случай (заданный ввод ‘2 1’, ‘3 1’, ‘4 1’, ‘5 1’… мы скопируем i eatens на шаге i (и сумма i для i = от 1 до n дает ~ n ^ 2). Если бы мы должны были проверить, кто должен быть едоком, а кто съеденным, тогда мне любопытно, какой ответ я бы подсказал O (nlog (n)), но не уверен

Ответ №5:

Интересная проблема. Я решил создать список смежности (карта идентификаторов узлов и их непосредственных дочерних элементов), а затем выполнить поиск, чтобы найти общие пути. Карта visitedAndGroupId (уродливое имя!) Используется как для отслеживания посещенных узлов, так и для того, к какой группе они принадлежат. Ключ — это исходный узел, а значение — идентификатор группы. Наконец, мы перебираем посещенную карту и создаем группы и их содержимое, чтобы подготовить выходные данные.

 private static HashMap<Integer,Set<Integer>> adjacencyList;
    private static Map<Integer, Integer> visitedAndGroupId = new HashMap<>();
    private static int groupId = 0;

    public static void main(String[] args) {
        List<String> pairs = Arrays.asList("6 11", "9 5", "11 9", "15 9", "13 15", "12 14", "15 16", "1 16");
        adjacencyList = buildAdjacencyList(pairs);

        // find the groups
        // visit each node then its children then their children etc,
        // populate map of visited ids along with group id.
        for (Map.Entry<Integer,Set<Integer>> entry: adjacencyList.entrySet()){
            if(!visitedAndGroupId.containsKey(entry.getKey())) {
                search(entry.getKey(), entry.getValue());
                groupId  ;
            }
        }

        // build groups container
        final List<Group> groups = new ArrayList<>(groupId-1);
        for (int i = 0; i < groupId; i  ) {
            groups.add(new Group(i));
        }

        // populate groups
        // each key is our original id and the associated value is the group it belongs to
        visitedAndGroupId.forEach((id,groupId)->groups.get(groupId).addItem(id));

        // print output
        System.out.print("{ ");
        groups.forEach(System.out::print);
        System.out.print("} ");
    }

    private static void search(int parent, Set<Integer> children) {
        if(!visitedAndGroupId.containsKey(parent)){
            visitedAndGroupId.put(parent, groupId);
            for (int c:children){
                search(c, adjacencyList.get(c));
            }
        }
    }
 

Построение списка смежности:

 private static HashMap<Integer,Set<Integer>> buildAdjacencyList(List<String> vertexPairs) {

        // build adjacency list
        HashMap<Integer,Set<Integer>> adjacencyList = new HashMap<>();
        int group = -1;
        for(String s:vertexPairs){
            String[] pair = s.split(" ");
            int v1 = Integer.parseInt(pair[0]);
            int v2 = Integer.parseInt(pair[1]);

            if(!adjacencyList.containsKey(v1) amp;amp; !adjacencyList.containsKey(v2)){
                // new pair
                adjacencyList.put(v1, new HashSet<>(Arrays.asList(v2)));
                adjacencyList.put(v2, new HashSet<>(Arrays.asList(v1)));
                continue;
            } else if (!adjacencyList.containsKey(v1) amp;amp; adjacencyList.containsKey(v2)){
                // one vertex is new, one vertex exists
                addAndUpdate(v1,v2,adjacencyList);
                continue;
            } else if (adjacencyList.containsKey(v1) amp;amp; !adjacencyList.containsKey(v2)){
                // one vertex is new, one vertex exists
                addAndUpdate(v2,v1,adjacencyList);
                continue;
            } else {
                // vertices already in hashset but this is a new edge between the two
                adjacencyList.get(v1).add(v2);
                adjacencyList.get(v2).add(v1);
            }
        }

        return adjacencyList;
    }

    private static void addAndUpdate(int newV, int existingV, HashMap<Integer, Set<Integer>> adjacencyList) {
        Set<Integer> t = adjacencyList.get(existingV);
        Set<Integer> newSet = new HashSet<>();
        newSet.add(existingV);
        adjacencyList.put(newV,newSet);
        t.add(newV);
    }
 

Вот класс Group: (геттеры / сеттеры не используются для краткости)

 private static class Group {
        public int GroupId;
        public int Count=0;
        public List<Integer> Contents;

        public Group(int groupId) {
            GroupId = groupId;
            Contents = new ArrayList<>();
        }

        public void addItem(int n){
            Count  ;
            Contents.add(n);
        }

        @Override
        public String toString() {
            return  " "   Count  
                    "="   Contents ", ";
        }
    }
 

Вывод:
для ввода («6 11», «9 5», «11 9», «15 9», «13 15», «12 14», «15 16», «1 16») является

 {  8=[16, 1, 5, 6, 9, 11, 13, 15],  2=[12, 14], } 
 

и
для ввода («40 22″,»60 6″,»22 39″,»43 40″,»22 55″,»48 57″,»42 41″,»22 57″,»6 42″,»33 74″,»70 46″,»4 11″,»6 28″,»22 79″,»61 34″,»77 40″,»4 8″,»72 26″,»62 50″,»72 51″,»1 79″,»34 29″,»77 41″,»2 48″,»43 2″,»62 45″,»43 17″,»19 33″,»76 4″,»35 54») является

 {  17=[1, 2, 6, 77, 79, 17, 22, 28, 39, 40, 41, 42, 43, 48, 55, 57, 60],  4=[4, 8, 11, 76],  2=[70, 46],  3=[72, 26, 51],  3=[74, 19, 33],  3=[29, 34, 61],  2=[35, 54],  3=[45, 50, 62], } 
 

Ответ №6:

Хорошо, пара наблюдений.

  • Порядок данных представляется важным. Если вы переместитесь [6,11] сразу после [11,9] того, как он будет работать так, как вы ожидаете. На самом деле, я изменил ваш входной массив на a List , чтобы я мог его перетасовать. Каждый раз ваш результат существенно отличается от предыдущей итерации. Это может дать подсказку о том, в чем заключается проблема.
  • Я рекомендую вам щедро разбрызгивать некоторые операторы печати в ключевых местах, чтобы выполнять state проверки как path, чтобы понять, что он делает.
  • Это не имеет никакого отношения к вашей проблеме, но вот пример вашего кода. У вас уже есть set, поэтому нет необходимости получать его снова, чтобы добавить значение. Просто используйте set то, что у вас уже есть.
 Set<Integer> set = map.get(k);
 
if (set.contains(first)) {
        map.get(k).add(second);
        found = true;
        break;
}
 

Это также помогло бы другим помочь вам, если бы вы объяснили, как вы ожидаете, что это будет работать. Не все свободно владеют теорией графов.

Комментарии:

1. 1) Я получаю входные данные в случайном порядке, поэтому я не могу зависеть от порядка входных значений. Проблема связана с порядком самих элементов, как вы упомянули. 3) Я уже придерживаюсь подхода, о котором вы упомянули, добавляя элемент в set .

2. ОК. Но, как я уже сказал, если вы предоставите некоторые подробности о том, что представляют данные и что вы ищете (не вывод, а то, что означает вывод), мы были бы в лучшем положении, чтобы помочь.

3. Я добавил некоторые подробности в раздел редактирования в моем сообщении, пожалуйста, посмотрите, поможет ли это. Я не уверен, можем ли мы назвать это графом или нет.