Мне нужно объединить два перечислимых числа, сохраняя при этом относительный порядок элементов, определенный в исходных перечислениях. Например, если 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, но я думаю, что не помешало бы иметь код, который работал бы независимо от этого.
Есть идеи у кого-нибудь?
@JohnathanBarclay это должен быть случай «невозможно объединить», т.е. е. вернуть пустую коллекцию. Я обновлю вопрос, включив эту информацию.
Они должны быть в «естественном» порядке, например enumerable1.OrderBy(s => s), или в том порядке, в котором они были в коллекции?
«Хотя это легко представить в моей голове, мне трудно превратить это в код». Это часть проблемы. Вы не переходите от идеи прямо к коду. Вы идете от идеи к алгоритму и от алгоритма к коду. Уберите клавиатуру, возьмите ручку и бумагу и придумайте, как сделать это вручную. Уточните эти шаги в формальный алгоритм, а затем, как только у вас появится алгоритм, работающий с несколькими различными входными данными, возьмите клавиатуру и реализуйте этот алгоритм в коде. Если вы хотите спросить нас, как написать код, вы уже должны точно знать, что он должен делать.
@TimSchmelter нет, не в «естественном» порядке. Но я заметил, что выбрал пример, в котором это так, чего, вероятно, не следовало делать.
@jmcilhinney ну, думаю, здесь сегодня слишком жарко. Похоже, я не могу придумать алгоритм, поэтому и спрашиваю. Если бы он у меня был, я бы смог написать для него код.
chatgpt.com/share/ed3dd0ca-0c9a-4852-8b9f-4147609884b6
@ user2051770: это страшно, но он действительно работает и кажется хорошим алгоритмом.
@user2051770 user2051770 Любое неоднозначное упорядочение просто выбирает один из них, а не дает сбой, как предполагалось. И это логично: в коде нет механизма поиска такого случая и принятия каких-либо мер.
@user2051770 user2051770, это на самом деле довольно близко, но оно не обрабатывает «отключенный» случай (фокстрот, Чарли против униформы, килограмм) правильно (и, к сожалению, мне не удалось убедить чатгпт в обратном). Эти вещи пугающе хороши, но все же недостаточно хороши.
Если вы спрашиваете об алгоритме, то в этом вопросе не должно быть тега C#, поскольку алгоритмы не зависят от языка. Не думайте с точки зрения алгоритмов. Сначала делайте это интуитивно. Как только вы получите что-то, что начнет казаться правильным, начните углубляться в конкретную логику. Люди часто делают сложные вещи на ощупь: бросить мяч другому человеку — это сложный физический расчет, но полные идиоты могут сделать это с относительной легкостью — и это должно быть вашим первым шагом.
@wexman, конечно, но теперь у тебя есть хорошая отправная точка. Теперь просто решите проблему «отключения». Я просто показывал, что быстрый запрос с вашим собственным вопросом дает довольно хороший ответ для начала.
Похоже, вам может понадобиться топологическая сортировка , где каждый элемент зависит от своего непосредственного преемника в первом перечислимом или втором перечислимом или в обоих, если они присутствуют в обоих. Если да, см., возможно, Топологическую сортировку с использованием LINQ и/или Топологическую сортировку с поддержкой циклических зависимостей. Вам нужно будет подумать о том, как обрабатывать дубликаты внутри данного перечислимого.
Ваш вопрос было бы гораздо проще понять, если бы вместо "foxtrot", "uniform", "charlie", "kilo" у вас было "A", "B", "C", "D" или 1, 2, 3, 4. Кратковременная память человека имеет довольно небольшой размер, и то, как вы представили проблему, занимает большую ее часть. Я предлагаю отредактировать вопрос и соответствующим образом упростить пример.
@TheodorZoulias Я действительно сделал это в исходном вопросе, но это заставило бы людей предположить «естественный порядок» элементов, который в данном случае не указан. Вот почему я выбрал «фокстрот, униформа, Чарли, килограмм», потому что это не отсортировано по алфавиту, но большинство людей знают порядок корректоров еще со времен банды ищейок...





Я отредактировал свой ответ, чтобы проверить наличие дубликатов и противоречий. Теперь это должно работать во всех случаях. Проверка каждого из них заключалась в том, чтобы убедиться, что ни исходные списки, ни возвращенный список не содержат дубликатов. (Если бы было противоречие, то в отсортированном возвращаемом списке были бы дубликаты)
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;
}
}
Забыл упомянуть, что он не предполагает дублирования и противоречий, но я не думаю, что это будет так сложно исправить.
Спасибо за Ваш ответ! С отключенным случаем я смог справиться, но не знаю, как исправить случай «нет схваток». Буду продолжать попытки!
@wexman добавил простую проверку на дубликаты и теперь должен работать во всех случаях. Если вас устраивает, не забудьте отметить ответ.
Цена этого подхода — очень высокая сложность.
Сложность?! Это как 10 строк кода
Помечено как ответ, поскольку оно делает именно то, что я просил. Спасибо! Отдельное спасибо @RotundChincilla за объяснение!
@publicwireless Сколько раз он перебирает IEumerables. Что это значит, если это не тривиальный список? Сколько вы могли бы сэкономить, просто предварительно материализовав списки?
Несмотря на вложенные циклы for, это не O (N-квадрат) из-за операторов прерывания. Если вам трудно это увидеть, попробуйте выполнить это в отладчике VS.
Первое, что пришло мне в голову при чтении задачи, было: «Это похоже на задачу с графом». В этом случае элементы ваших перечислимых элементов являются узлами, а порядок перечисляемых элементов образует ребра.
Например, рассмотрим «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#.
@TheodorZoulias Хорошая мысль, я был занят разработкой конструкции алгоритма и забыл вернуться, чтобы закончить это. Добавим в пост.
Спасибо за Ваш ответ. Мне удалось справиться с делами «фокстрот, Чарли», «униформа, килограмм» (отключено). Однако это не работает для случая «фокстрот, Чарли, килограмм», «униформа, Чарли, килограмм», где возвращается «фокстрот, униформа, Чарли, килограмм», но это не должно быть. Я не знаю, как с этим справиться...
@wexman Каков ваш ожидаемый результат в этом случае? (Переименование узлов, потому что сокращенный порядок дает некоторый... явный язык): «ABC» в сочетании с «ZBC» образует Y-образный граф, в котором оба начальных элемента («A» и «Z») указывают на цепочку. компании «Би-С». Предполагая, что вы хотите, чтобы первое перечисляемое имело приоритет, результатом должно быть «A-Z-B-C», потому что и «A», и «Z» предшествуют «BC», что соответствует полученному вами выводу.
Извините, что так поздно поддержал меня, меня не было на работе пару дней. Прежде всего, я хотел бы отметить, что вы, @RotundChinchilla, заслуживаете дополнительных баллов за подробное объяснение здесь. Вы бы тоже заслужили оценку за ответ, но, честно говоря, общедоступная беспроводная связь была быстрее. Что касается случая A-B-C Z-B-C, то в моем особом случае желаемый результат - это пустое перечислимое число, но в целом я думаю, что A-Z-B-C будет таким же действительным, как и Z-A-B-C.
@wexman Спасибо за подробности. Не волнуйтесь, я просто рад, что могу внести свой вклад.
Что, если оба перечислимых числа противоречат друг другу? Например, если в enumerable1 за буквой «A» следует «B», а в enumerable2 за буквой «B» следует «A», как это следует обрабатывать?