Задать вопрос
@MaxLich
java developer

Как эффективно сгруппировать строки?

Здравствуйте. Нужно решить такую задачу, и не получается.

На вход поступает набор строк вида:
A;B;C
X;Y;Z
J;A;Z
U;V;W
E;E;E
D;F;G

Если две строки имеют общие элементы в одной позиции (один и больше), то считается, что они принадлежат одной группе. Если две группы пересекаются, то считается, что это одна группа. То есть ниже представленные строки составят одну группу:
F;I;J
F;X;A
D;X;P
У первой и второй строки общий элемент - "F", у второй и третьей - "X". И они все принадлежат одной группе.
Нужно найти все такие группы, подсчитать их количество, и вывести эти группы в порядке убывания их размер (размер группы - это количество строк в ней). Почти два дня уже бьюсь, и не могу решить эту задачу. Одно решение придумал, но оно не работает, когда строк около 1 000 000: вычисление идёт слишком долго. Ну соответственно, при большом количестве данных встаёт вопрос об экономном расходовании памяти.

Сначала у меня был такой алгоритм:
1. Создаю список списков строк: List> , он будет хранить группы
2. Кидаю в него первую строку (соответственно, создаю первую группу).
3. Перебираю строки из изначального списка:
3.1. Беру очередную строку, удаляю её из списка
3.2 Получаю итератор для перебора групп
3.3. Перебираю группы
3.3.1 Беру очередную группу
3.3.2 Перебираю строки в ней
3.3.2.1 Для каждой строки из группы смотрю, есть ли совпадающие элементы с элементами строки из первоначального списка:
а) если есть, то смотрю была ли уже добавлена строка в какую группу:
1) если была добавлена, то беру текущую группу и её полностью добавляю в ту, куда была добавлена строка, после этого текущую группу удаляю
2) если не была добавлена, то добавляю в текущую группу строку и запоминаю эту группу
после этого выхожу из цикла перебора строк группы
б) если нет, то перехожу к следующей итерации
3.4 После выхода из цикла групп, проверяю есть ли данные о группе, в которой обнаружена строка, имеяющая общие элементы с текущей строкой, если информации о такой группе нет, то тогда создаю новую группу и добавляю туда текущую строку.
  • Вопрос задан
  • 521 просмотр
Подписаться 3 Средний Комментировать
Решения вопроса 1
@MaxLich Автор вопроса
java developer
Нашёл одно решение. Алгоритм:
  1. храним результат в виде списка списков: [номер_группы -> [строки_группы]]
  2. используем вспомогательный список хэш-таблиц: [позиция_слова -> { слово -> номер_группы }] и вспомогательную хэш-таблицу для хранения какая группа в какую была влита
  3. каждое слово строки ищем в соответствующей (позиции слова в строке) хэш-таблице
    а) если слово есть, запоминаем номер группы (значение из хэш-таблицы), в которой оно найдено
    б) если слова нет, то добавляем его в список новых слов
  4. если строка (а точнее её слова) найдена в группах, то берём первую из "живых" (объяснение этого позже) групп, иначе создаём новую группу
  5. добавляем новые слова в соответствующие хэш-таблицы с номером найденной/созданной группы
  6. объединяем найденные группы в одну, выбранную ранее. Так как группы хранятся в виде списка строк, то просто объединяем списки строк в один у выбранной группы, а более ненужные группы отмечаем как "мёртвые" (присваиваем null, дабы не перемещать элементы внутри списка)
  7. добавляем строку в список строк группы


Код метода поиска групп:
code
private static List<List<String>> findLineGroups(List<String> lines) {
        class NewLineElement {
            private String lineElement;
            private int columnNum;

            private NewLineElement(String lineElement, int columnNum) {
                this.lineElement = lineElement;
                this.columnNum = columnNum;
            }
        }

        if (lines == null)
            return Collections.emptyList();

        List<List<String>> linesGroups = new ArrayList<>(); //список групп, каждый элемент вида "номер группы - список строк группы"
        if (lines.size() < 2) {
            linesGroups.add(lines);
            return linesGroups;
        }

        List<Map<String, Integer>> columns = new ArrayList<>(); // список стобцов, каждый столбец - мапа с парами "элемент строки/столбца-номер группы"
        Map<Integer, Integer> unitedGroups = new HashMap<>(); //мэп с парами "номер некоторой группы - номер группы, с которой надо объединить данную"
        for (String line : lines) {
            String[] lineElements = line.split(";");
            TreeSet<Integer> groupsWithSameElems = new TreeSet<>(); //список групп, имеющих совпадающие элементы
            List<NewLineElement> newElements = new ArrayList<>(); //список элементов, которых нет в мапах столбцов

            for (int elmIndex = 0; elmIndex < lineElements.length; elmIndex++) {
                String currLnElem = lineElements[elmIndex];
                if (columns.size() == elmIndex)
                    columns.add(new HashMap<>());
                if ("".equals(currLnElem.replaceAll("\"","").trim()))
                    continue;

                Map<String, Integer> currCol = columns.get(elmIndex);
                Integer elemGrNum = currCol.get(currLnElem);
                if (elemGrNum != null) {
                    while (unitedGroups.containsKey(elemGrNum)) // если группа с таким номером объединена с другой,
                        elemGrNum = unitedGroups.get(elemGrNum); //то сохраняем номер группы, с которой была объединена данная
                    groupsWithSameElems.add(elemGrNum);
                } else {
                    newElements.add(new NewLineElement(currLnElem, elmIndex));
                }
            }
            int groupNumber;
            if (groupsWithSameElems.isEmpty()) {
                linesGroups.add(new ArrayList<>());
                groupNumber = linesGroups.size() - 1;
            } else {
                groupNumber = groupsWithSameElems.first();
            }
            for (NewLineElement newLineElement : newElements) {
                columns.get(newLineElement.columnNum).put(newLineElement.lineElement, groupNumber);
            }
            for (int matchedGrNum : groupsWithSameElems) { //перебираем все группы с таким же элементом
                if (matchedGrNum != groupNumber) {
                    unitedGroups.put(matchedGrNum, groupNumber); //сохраняем инф-цию об объединённых группах
                    linesGroups.get(groupNumber).addAll(linesGroups.get(matchedGrNum)); //объединяем группы
                    linesGroups.set(matchedGrNum, null); //помечаем группу с текущим номер, как несуществующую
                }
            }
            linesGroups.get(groupNumber).add(line);
        }
        linesGroups.removeAll(Collections.singleton(null)); //удаляем несуществующие группы
        return linesGroups;
    }
Ответ написан
Комментировать
Пригласить эксперта
Ответы на вопрос 2
leahch
@leahch
3D специалист. Dолго, Dорого, Dерьмово.
Как ни странно, итерировать группы совсем не обязательно!
1) группа у вас состоит из одного элемента. В вашем примере F и X - две группы, в которые нужно положить номера строк.
2) за один проход бежим по строкам и добавляем их в соответствующие группы термов, которые держим в hashtable, где ключом у нас сам терм, а значением - массив из номеров строк.
3) после того, как заполнили хеш, пробегаемся по нему один раз и смотрим, у кого длина массива больше единицы, это и будут исходные группы.

Если нам нужно дополнительно сформировать группы из двух-трех термов, то делает все тоже самое, но ключом ставим treeset из этих элементов.

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map.Entry;
import java.util.TreeSet;

public class Groups {

	public static void main(String[] args) {
		String[] myData = {
				"F;I;J", 
				"F;X;A",
				"X;D;P",

				"A;B;C",
				"X;Y;Z",
				"J;A;Z",
				"U;V;W",
				"E;E;E",
				"D;F;G",
		};
		
		HashMap<String, TreeSet<Integer>> groups = new HashMap<String, TreeSet<Integer>>();
		
		for(int line=0; line< myData.length; line++ ) { // бежим по строкам
			
			List<String> terms = Arrays.asList(myData[line].split(";")); // разбиваем на термы
			
			for(String term: terms) { // пробегаем по термам
				TreeSet<Integer> group = groups.get(term); // выдергиваем группу
				
				if(group == null) { // если группы нет
					group = new TreeSet<Integer>();
					groups.put(term, group);
				}
				group.add(line); // добваляем строку
			}
		}
		
		// выводим результат
		for(Entry<String, TreeSet<Integer>> group: groups.entrySet()) {
			if(group.getValue().size() >1)
				System.out.printf("%s - %s\n", group.getKey().toString(), group.getValue().toString());
		}
	}
}


И результат

A - [1, 3, 5]
D - [2, 8]
F - [0, 1, 8]
J - [0, 5]
X - [1, 2, 4]
Z - [4, 5]
Ответ написан
dimonchik2013
@dimonchik2013
non progredi est regredi
фильтр Блума
Ответ написан
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы