Объединить два перечисляемых числа, сохраняя порядок, определенный любым источником

Мне нужно объединить два перечислимых числа, сохраняя при этом относительный порядок элементов, определенный в исходных перечислениях. Например, если enumerable1 содержит «фокстрот», «uniform» и «кило», а enumerable2 содержит «uniform», «charlie» и «kilo», результирующее перечисляемое должно содержать «фокстрот», «uniform», «charlie», « килограмм».

Объяснение: из enumerable1 мы знаем, что «фокстрот» стоит перед «uniform», а из enumerable2 мы знаем, что «uniform» появляется перед «charlie». Поэтому можно предположить, что слово «фокстрот» появляется раньше, чем «униформа» появляется перед «чарли». Кроме того, из enumerable1 мы знаем, что «uniform» появляется перед «kilo», а из enumerable2 мы знаем, что «charlie» появляется после «uniform» и перед «kilo». Объединив все это, мы можем определить окончательный порядок слов «фокстрот», «униформа», «чарли» и «кило».

Однако бывают случаи, когда это не сработает, поскольку невозможно определить порядок или оба исходных перечисления противоречат друг другу. Например, если enumerable3 содержит «фокстрот» и «чарли», а enumerable4 содержит «uniform» и «kilo», их невозможно объединить, поскольку невозможно определить какой-либо порядок. Или, если enumerable5 содержит «фокстрот» и «uniform», а enumerable6 содержит «uniform» и «фокстрот», их нельзя объединить, и должно быть возвращено пустое перечисляемое.

Хотя это легко представить в моей голове, мне трудно превратить это в код. С# желательно. В этом очень конкретном случае максимальное количество элементов в каждом перечислимом равно 4, но я думаю, что не помешало бы иметь код, который работал бы независимо от этого.

Есть идеи у кого-нибудь?

Что, если оба перечислимых числа противоречат друг другу? Например, если в enumerable1 за буквой «A» следует «B», а в enumerable2 за буквой «B» следует «A», как это следует обрабатывать?

Johnathan Barclay 26.06.2024 17:03

@JohnathanBarclay это должен быть случай «невозможно объединить», т.е. е. вернуть пустую коллекцию. Я обновлю вопрос, включив эту информацию.

wexman 26.06.2024 17:07

Они должны быть в «естественном» порядке, например enumerable1.OrderBy(s => s), или в том порядке, в котором они были в коллекции?

Tim Schmelter 26.06.2024 17:10

«Хотя это легко представить в моей голове, мне трудно превратить это в код». Это часть проблемы. Вы не переходите от идеи прямо к коду. Вы идете от идеи к алгоритму и от алгоритма к коду. Уберите клавиатуру, возьмите ручку и бумагу и придумайте, как сделать это вручную. Уточните эти шаги в формальный алгоритм, а затем, как только у вас появится алгоритм, работающий с несколькими различными входными данными, возьмите клавиатуру и реализуйте этот алгоритм в коде. Если вы хотите спросить нас, как написать код, вы уже должны точно знать, что он должен делать.

jmcilhinney 26.06.2024 17:10

@TimSchmelter нет, не в «естественном» порядке. Но я заметил, что выбрал пример, в котором это так, чего, вероятно, не следовало делать.

wexman 26.06.2024 17:11

@jmcilhinney ну, думаю, здесь сегодня слишком жарко. Похоже, я не могу придумать алгоритм, поэтому и спрашиваю. Если бы он у меня был, я бы смог написать для него код.

wexman 26.06.2024 17:18

chatgpt.com/share/ed3dd0ca-0c9a-4852-8b9f-4147609884b6

user2051770 26.06.2024 17:22

@ user2051770: это страшно, но он действительно работает и кажется хорошим алгоритмом.

Tim Schmelter 26.06.2024 17:33

@user2051770 user2051770 Любое неоднозначное упорядочение просто выбирает один из них, а не дает сбой, как предполагалось. И это логично: в коде нет механизма поиска такого случая и принятия каких-либо мер.

Servy 26.06.2024 17:52

@user2051770 user2051770, это на самом деле довольно близко, но оно не обрабатывает «отключенный» случай (фокстрот, Чарли против униформы, килограмм) правильно (и, к сожалению, мне не удалось убедить чатгпт в обратном). Эти вещи пугающе хороши, но все же недостаточно хороши.

wexman 26.06.2024 17:57

Если вы спрашиваете об алгоритме, то в этом вопросе не должно быть тега C#, поскольку алгоритмы не зависят от языка. Не думайте с точки зрения алгоритмов. Сначала делайте это интуитивно. Как только вы получите что-то, что начнет казаться правильным, начните углубляться в конкретную логику. Люди часто делают сложные вещи на ощупь: бросить мяч другому человеку — это сложный физический расчет, но полные идиоты могут сделать это с относительной легкостью — и это должно быть вашим первым шагом.

jmcilhinney 26.06.2024 18:11

@wexman, конечно, но теперь у тебя есть хорошая отправная точка. Теперь просто решите проблему «отключения». Я просто показывал, что быстрый запрос с вашим собственным вопросом дает довольно хороший ответ для начала.

user2051770 26.06.2024 18:25

Ваш вопрос было бы гораздо проще понять, если бы вместо "foxtrot", "uniform", "charlie", "kilo" у вас было "A", "B", "C", "D" или 1, 2, 3, 4. Кратковременная память человека имеет довольно небольшой размер, и то, как вы представили проблему, занимает большую ее часть. Я предлагаю отредактировать вопрос и соответствующим образом упростить пример.

Theodor Zoulias 26.06.2024 21:27

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

wexman 27.06.2024 08:56
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
15
135
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

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

class Program {
    static void Main(string[] args) {
        IEnumerable<string> l1 = new List<string> { "uniform", "charlie", "kilo" };
        IEnumerable<string> l2 = new List<string> { "kilo", "uniform", "charlie" };
        try {
            IEnumerable<string> result = Merge(l1, l2);
            foreach (string s in result) {
                Console.WriteLine(s);
            }
        }
        catch (InvalidOperationException e) {
            Console.WriteLine(e.Message);
        }
        Console.ReadKey();
    }

    public static IEnumerable<T> Merge<T> (IEnumerable<T> list1, IEnumerable<T> list2) {
        if ((list1.Distinct().Count() != list1.Count()) || (list2.Distinct().Count() != list2.Count()) ) {
            throw new InvalidOperationException("Couldn't merge.  One or both lists contained duplicates.");
        }
        
        if ((list1.Count() == 0)) {
            return list2;
        }
        if ((list2.Count() == 0)) {
            return list1;
        }
        List<T> newList = new List<T>();       
        bool matchfound = false;
        for(int i = 0; i< list1.Count(); i++) {
            for (int j = 0; j < list2.Count(); j++) {
                if (list1.ElementAt(i).Equals(list2.ElementAt(j))) {
                    matchfound = true;
                    newList.AddRange(Merge(list1.Take(i), list2.Take(j)));
                    newList.Add(list1.ElementAt(i));
                    newList.AddRange(Merge(list1.Skip(i + 1), list2.Skip(j + 1)));
                    break;
                }
            }
            if (matchfound) {
                break;
            }
        }
        if (!matchfound) {
            throw new InvalidOperationException("Could not merge");
        }
        if (newList.Distinct().Count() != newList.Count()) {
            throw new InvalidOperationException("Couldn't merge.  Contradiction in lists.");

        }
        return newList;
    }

}

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

public wireless 26.06.2024 20:25

Спасибо за Ваш ответ! С отключенным случаем я смог справиться, но не знаю, как исправить случай «нет схваток». Буду продолжать попытки!

wexman 27.06.2024 10:03

@wexman добавил простую проверку на дубликаты и теперь должен работать во всех случаях. Если вас устраивает, не забудьте отметить ответ.

public wireless 27.06.2024 16:31

Цена этого подхода — очень высокая сложность.

user1937198 27.06.2024 16:31

Сложность?! Это как 10 строк кода

public wireless 27.06.2024 16:39

Помечено как ответ, поскольку оно делает именно то, что я просил. Спасибо! Отдельное спасибо @RotundChincilla за объяснение!

wexman 27.06.2024 17:15

@publicwireless Сколько раз он перебирает IEumerables. Что это значит, если это не тривиальный список? Сколько вы могли бы сэкономить, просто предварительно материализовав списки?

user1937198 27.06.2024 17:40

Несмотря на вложенные циклы for, это не O (N-квадрат) из-за операторов прерывания. Если вам трудно это увидеть, попробуйте выполнить это в отладчике VS.

public wireless 27.06.2024 17:54

Обзор

Первое, что пришло мне в голову при чтении задачи, было: «Это похоже на задачу с графом». В этом случае элементы ваших перечислимых элементов являются узлами, а порядок перечисляемых элементов образует ребра.

Например, рассмотрим «ABCD» как одно из входных перечислимых чисел. Мы можем преобразовать это в такой график:

A --> B --> C --> D

Если вторым входом является «B-X-Y-C», он сам по себе формирует график:

B --> X --> Y --> C

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

A --> B -----------> C --> D 
      |              ^       
      |              |       
      +-> X --> Y ---+       

Исходя из этого, мы можем построить «объединенный порядок», идя слева направо, добавляя узел в порядок только в том случае, если все узлы, ведущие к нему, уже присутствуют в порядке. В этом примере объединенный порядок: «A-B-X-Y-C-D».

Как отметил в комментарии пользователь «dbc», это соответствует определению топологической сортировки, упорядочивания узлов графа, при котором каждый узел появляется только после своих зависимостей.

Более того, используя конструкцию графа, мы теперь можем формально определить «противоречие» порядка как цикл в графе. Если узел «I» указывает на «J», а «J» указывает на «I» (как на входах «I-J» и «J-I»), вы можете представить, как в результирующем графе есть узлы «I» и «J». ", указывая друг на друга в цикле.

Мы также должны рассмотреть ситуацию, когда существует несколько точек «входа» и «выхода», например. с учетом входных данных «M-N-O-P» и «Z-N-X-Y», который образует X-образный граф, где «M» и «Z» указывают на «N», а «N» разветвляется на две цепочки «OP» и «XY». Это можно решить, разрешив первому вводу иметь приоритет и последовательно объединяя отдельные элементы в равных ветвях. Это сформирует порядок: «M-Z-N-O-X-P-Y». Альтернативно, мы могли бы объединить сами ветки последовательно: «M-Z-N-O-P-X-Y». Обе являются допустимыми топологическими сортировками. На момент написания этой статьи первоначальный вопрос не касался того, какой порядок предпочтителен.

Наконец, у нас есть еще один случай, когда два графа не пересекаются, например. заданы входы «ABC» и «XYZ». Эти две цепочки не имеют общих элементов. Согласно задаче, это должно привести к сбою и возврату пустого заказа. Это можно рассматривать как отдельный случай, возможно, путем сканирования перечислимых элементов на наличие хотя бы одного общего узла перед выполнением какой-либо другой работы.


Разработка алгоритма

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

В этом случае я буду использовать Алгоритм Кана, который имеет очень чистый шаблон псевдокода, расположенный в Википедии. Алгоритм Кана требует двух вещей: графа, отслеживающего входящие и исходящие ребра, и списка «начальных узлов» (узлов без входящих ребер).

Структура данных для графа может представлять собой модифицированный список смежности, в котором каждый узел хранится в качестве ключа в словаре, а значение для каждого представляет собой список непосредственно соседних узлов. Хотя типичный список смежности хранит только исходящие ребра, мы также можем отслеживать входящие ребра, добавляя второй список для каждого ключа. Это будет выглядеть так:

"B" : {
    "nodes that point into this one": [
        "A"
    ],
    "nodes that this one points to" : [ 
        "C"
    ]
},
...

Чтобы избежать необходимости сканирования графа после построения «начальных узлов», мы можем просто отслеживать потенциальные начальные узлы во время построения графа и удалять их как кандидатов, если они когда-либо получат входящее ребро. Учитывая контекст проблемы, мы знаем, что единственные потенциальные начальные узлы — это элементы в начале перечислимого — все, что происходит дальше, обязательно имеет узел, указывающий на него.

После этого давайте разработаем код для построения графа и списка «начальных узлов». Концептуально нам просто нужно перебрать каждое перечисляемое, добавить каждый элемент в качестве узла и нарисовать границу между ним и предыдущим элементом. Если узел в графе уже существует, мы добавляем ребро к существующему узлу вместо того, чтобы добавлять его дубликат.

(Я написал свой код на Python для простоты тестирования, преобразовать его в C# должно быть довольно просто.) Обновлено: версия C# указана в конце.

def ConstructGraph(*args):
    graph = {}
    start_points = []
    for enumerable in args:
        AttachToGraph(graph, enumerable, start_points)
    return graph, start_points

        
def AttachToGraph(graph, enumerable, start_points):
    seen_nodes = set()
    previous_node = None

    ## Tentatively add the first element as a start point.
    if enumerable[0] not in graph and enumerable[0] not in start_points:
        start_points.append(enumerable[0])

    for current_node in enumerable:
        ## Handle the obvious error case where a node is repeated in the same enumerable (e.g. "A, B, A")
        if current_node in seen_nodes:
            raise f"Found duplicate in enumerable."

        ## Add the element as a node.
        if current_node not in graph:
            graph[current_node] = {"outgoing": [], "incoming": []}

        ## Draw the edge between the current node and the previous one.
        if previous_node != None:
            graph[previous_node]["outgoing"].append(current_node)
            graph[current_node]["incoming"].append(previous_node)

            ## If we find a start point with an incoming edge, it's not a start point.
            if current_node in start_points:
                start_points.remove(current_node)

        seen_nodes.add(current_node)
        previous_node = current_node

Например, вызов ConstructGraph("ACFG", "BCDEF") сформирует следующий граф: Граф с узлами A и B, указывающими на C, который указывает на F и D. Узел D указывает на E, который указывает на F. Узел F указывает на G.

Ожидаемый порядок после завершения топологической сортировки будет (очень удобно): «ABCDEFG».

Вот моя реализация алгоритма Кана на Python с использованием псевдокода из Википедии в качестве шаблона. Я назвал переменные с именами переменных псевдокода в качестве префикса, а также более понятным именем.

def Kahn(graph, start_points):
    S_queue = start_points[:] ## We could re-use start_points, but let's clone it for clarity.
    L_ordering = []
    while len(S_queue) > 0:
        N_node = S_queue.pop(0)
        L_ordering.append(N_node)

        ## Clone the edge list to avoid problems when iterating
        destinations = graph[N_node]["outgoing"][:] 

        for M_destination in destinations:
            ## Remove the edge both ways.
            graph[N_node]["outgoing"].remove(M_destination)
            graph[M_destination]["incoming"].remove(N_node)

            ## Once all the dependencies are in the ordering, enqueue the destination node.
            if len(graph[M_destination]["incoming"]) == 0:
                S_queue.append(M_destination)

    ## Check for cycles.
    for N_node in graph:
        if len(graph[N_node]["outgoing"]) > 0 or len(graph[N_node]["incoming"]) > 0:
            return []

    return L_ordering

Объединив функции, мы можем сделать следующее:

graph, start_points = ConstructGraph("ACFG", "BCDEF")
print(Kahn(graph, start_points))
## prints "ABCDEFG"

Этот код не обрабатывает непересекающиеся перечислимые элементы, поскольку эту проблему можно решить заранее, реализовав проверку на то, имеют ли данные перечислимые элементы общий элемент или нет.


Обновлено: Версия С#

(Возможно, я откусил больше, чем могу переварить, пытаясь сделать его универсальным; этот код предполагает, что TObject не является сложным ссылочным типом. Пожалуйста, измените код в соответствии со своими потребностями.)

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

public static class Merge
{
    public static List<TObject> MergeLists<TObject>(params IEnumerable<TObject>[] enumerables)
    {
        (Dictionary<TObject, Dictionary<string, List<TObject>>> graph, List<TObject> startPoints) = ConstructGraph(enumerables);
        return (graph == null) ? new List<TObject>() : Kahn(graph, startPoints);
    }


    private static (Dictionary<TObject, Dictionary<string, List<TObject>>> Graph, List<TObject> StartPoints) ConstructGraph<TObject>(params IEnumerable<TObject>[] args)
    {
        Dictionary<TObject, Dictionary<string, List<TObject>>> graph = new Dictionary<TObject, Dictionary<string, List<TObject>>>();
        List<TObject> startPoints = new List<TObject>();

        foreach (IEnumerable<TObject> enumerable in args)
        {
            try
            {
                AttachToGraph(ref graph, enumerable, ref startPoints);
            }
            catch // I advise defining a custom exception type, or alternatively pre-sanitizing the enumerables.
            {
                graph = null;
                break;
            }
        }
        return (graph, startPoints);
    }

    private static void AttachToGraph<TObject>(
        ref Dictionary<TObject, Dictionary<string, List<TObject>>> graph,
        IEnumerable<TObject> enumerable,
        ref List<TObject> startPoints)
    {
        HashSet<TObject> seenNodes = new HashSet<TObject>();
        TObject previousNode = default;
        bool isStartOfEnumerable = true;

        if (!graph.ContainsKey(enumerable.ElementAt(0)) && !startPoints.Contains(enumerable.ElementAt(0)))
        {
            startPoints.Add(enumerable.ElementAt(0));
        }

        foreach (TObject currentNode in enumerable)
        {
            // Sanity check the enumerable.
            if (seenNodes.Contains(currentNode))
            {
                throw new Exception("Found an obviously invalid IEnumerable."); // If you're going with this, define a custom exception.
            }

            if (!graph.ContainsKey(currentNode))
            {
                graph.Add(currentNode, new Dictionary<string, List<TObject>>{
                    {"outgoing", new List<TObject>()},
                    {"incoming", new List<TObject>()}
                });
            }

            if (!isStartOfEnumerable)
            {
                graph[previousNode]["outgoing"].Add(currentNode);
                graph[currentNode]["incoming"].Add(previousNode);

                if (startPoints.Contains(currentNode))
                {
                    startPoints.Remove(currentNode);
                }
            }

            seenNodes.Add(currentNode);
            previousNode = currentNode;

            // Update flag.
            if (isStartOfEnumerable)
            {
                isStartOfEnumerable = false;
            }
        }
    }

    private static List<TObject> Kahn<TObject>(
        Dictionary<TObject, Dictionary<string, List<TObject>>> graph,
        List<TObject> startPoints)
    {
        // Note that this creates a shallow copy, update this to deep-copy if needed.
        Queue<TObject> queue = new Queue<TObject>(startPoints);
        List<TObject> ordering = new List<TObject>();

        while (queue.Count > 0)
        {
            TObject node = queue.Dequeue();

            ordering.Add(node);

            // Modify to deep-copy if TObject is a complex reference type.
            List<TObject> destinations = new List<TObject>(graph[node]["outgoing"]);

            foreach (TObject destination in destinations)
            {
                graph[node]["outgoing"].Remove(destination);
                graph[destination]["incoming"].Remove(node);

                if (graph[destination]["incoming"].Count == 0)
                {
                    queue.Enqueue(destination);
                }
            }
        }

        foreach (TObject node in graph.Keys)
        {
            if (graph[node]["outgoing"].Count > 0 || graph[node]["incoming"].Count > 0)
            {
                return new List<TObject>();
            }
        }

        return ordering;
    }
}

Используйте так:

List<string> list1 = new List<string> { "A", "B", "D", "F" };
List<string> list2 = new List<string> { "B", "C", "D", "E", "F" };
List<string> result = Merge.MergeLists(list1, list2);

// 'result' has the order "ABCDEF".

«конвертировать его в C# должно быть довольно просто». Если это просто, почему вы тогда этого не делаете? Это вопрос C#, и не все разработчики C# знают, как конвертировать код Python в C#.

Theodor Zoulias 26.06.2024 21:30

@TheodorZoulias Хорошая мысль, я был занят разработкой конструкции алгоритма и забыл вернуться, чтобы закончить это. Добавим в пост.

RotundChinchilla 26.06.2024 22:03

Спасибо за Ваш ответ. Мне удалось справиться с делами «фокстрот, Чарли», «униформа, килограмм» (отключено). Однако это не работает для случая «фокстрот, Чарли, килограмм», «униформа, Чарли, килограмм», где возвращается «фокстрот, униформа, Чарли, килограмм», но это не должно быть. Я не знаю, как с этим справиться...

wexman 27.06.2024 09:23

@wexman Каков ваш ожидаемый результат в этом случае? (Переименование узлов, потому что сокращенный порядок дает некоторый... явный язык): «ABC» в сочетании с «ZBC» образует Y-образный граф, в котором оба начальных элемента («A» и «Z») указывают на цепочку. компании «Би-С». Предполагая, что вы хотите, чтобы первое перечисляемое имело приоритет, результатом должно быть «A-Z-B-C», потому что и «A», и «Z» предшествуют «BC», что соответствует полученному вами выводу.

RotundChinchilla 27.06.2024 17:19

Извините, что так поздно поддержал меня, меня не было на работе пару дней. Прежде всего, я хотел бы отметить, что вы, @RotundChinchilla, заслуживаете дополнительных баллов за подробное объяснение здесь. Вы бы тоже заслужили оценку за ответ, но, честно говоря, общедоступная беспроводная связь была быстрее. Что касается случая A-B-C Z-B-C, то в моем особом случае желаемый результат - это пустое перечислимое число, но в целом я думаю, что A-Z-B-C будет таким же действительным, как и Z-A-B-C.

wexman 03.07.2024 14:04

@wexman Спасибо за подробности. Не волнуйтесь, я просто рад, что могу внести свой вклад.

RotundChinchilla 03.07.2024 17:13

Другие вопросы по теме