#c# #algorithm
#c# #алгоритм
Вопрос:
Предположим, у вас есть миллион последовательных целых чисел. Возвращает все возможные значения a, b и c такие, что
a b c<=d.
d will be provided to you.
ex: if the numbers are 1,2,3,4,5,6,7
and d=7
[1,2,3]
[1,2,4]
[1,2,3] will be same as [1,3,2] and [3,2,1]...
Поскольку один миллион слишком большой, в примере я принял 1000 в качестве примера. Также для удобства я использовал от 1 до 1000 в качестве набора данных.
Предположим
`a<b<c`
Таким образом 3*a<1000
, ==> a<333.33, поэтому я извлекаю a
от 1 до 333.
static void Main(string[] args)
{
int d = 519;
var setA = Enumerable.Range(1, 333);
IEnumerable<int> value = Enumerable.Range(1, 1000);
var result = (from a in setA
from b in value
from c in value
where a!=b amp;amp; a!=c amp;amp; b!=c amp;amp; a b c <= d
select new {a,b,c}).ToList().Distinct().ToList();
foreach (var item in result)
{
Console.WriteLine(item);
}
Console.ReadKey();
}
Это происходит медленно и выдает System.OutOfMemoryException
исключение…..
Комментарии:
1. codereview.stackexchange.com
2. Все ли числа положительные? Если это так, то вы можете сначала отфильтровать только where
i<d
, а затем wherea < d/3
3. Да, мы можем предположить все положительные числа в наборе.
4. @DStanley больше похоже на a > 2 и a < d — 3
5. @Serpiton не if
a < b < c
(поскольку1,2,3
и3,2,1
являются эквивалентными множествами).
Ответ №1:
Сужение a
до 1...333
— хорошая идея. Вы можете еще больше улучшить свой код, используя тот факт, что a < b < c
, so b,c in Enumerable.Range(1, 1000)
является неоптимальным.
Вы можете определить нижние и верхние границы для b
и c
в зависимости от заданных чисел a
и b
соответственно:
a < b => b >= a 1, b in Enumerable.Range(a 1, ...)
b < c => c must be in Enumerable.Range(b 1, ...)
Кроме того, вы можете определить границы для a
и b
тому подобное:
- Поскольку
b >= a 1
иa b c <= total
,a (a 1) ((a 1) 1) <= total
также должно иметь значение true . Этогоa < total / 3
недостаточно. Этоa <= (total - 3) / 3
- Аналогично
a b (b 1) <= total
, это,b <= (total - a - 1) / 2
- И, конечно же,
a b c <= total
переводится вc <= total - a - b
Вы можете использовать это, вложив итерации и используя SelectMany
для выравнивания списка результатов:
var result = Enumerable.Range(1, (total - dba - dca) / 3)
.SelectMany(
a => Enumerable.Range(a 1, (total - a - dcb) / 2 - a)
.SelectMany(
b => Enumerable.Range(b 1, (total - a - b) - b)
.Select(c => new { a, b, c })));
Что касается вашей производительности и проблемы с нехваткой памяти:
Удалите ToList()
из вашего запроса LINQ. Это приводит к тому, что все результаты загружаются в память до того, как вы начнете их обрабатывать. Поскольку вы хотите распечатать только кортежи, вам не нужно загружать их все в память. Это большая сила LINQ — он просто возвращает перечислитель без фактического вычисления результата. Если вы удалите ToList()
из запроса LINQ, цикл for-each вычислит один результат за итерацию, распечатает его и снова забудет.
В качестве пояснительного ответа на ваши комментарии:
Реализация Enumerable.Range
выглядит следующим образом:
private static IEnumerable<int> RangeIterator(int start, int count)
{
for (int i = 0; i < count; i)
yield return start i;
}
Реализация SelectMany
:
private static IEnumerable<TResult> SelectManyIterator<TSource, TResult>(IEnumerable<TSource> source, Func<TSource, IEnumerable<TResult>> selector)
{
foreach (TSource source1 in source)
{
foreach (TResult result in selector(source1))
yield return result;
}
}
Так, например,
foreach (var item in Enumerable.Range(1, 10).SelectMany(n => Enumerable.Range(1, n)))
{ /* do something */ }
концептуально переводится как:
for (int i = 0; i < 10; i)
{
var n = 1 i;
for (int j = 0; j < n; j)
{
var result = 1 j;
/* do something */ // <- processes the current result and forgets it again
}
}
Однако, когда вы добавляете ToList
:
foreach (var item in Enumerable.Range(1, 10).SelectMany(n => Enumerable.Range(1, n)).ToList())
{ /* do something */ }
это означает:
var list = new List<int>();
for (int i = 0; i < 10; i)
{
var n = 1 i;
for (int j = 0; j < n; j)
{
var item = 1 j;
list.Add(item); // <- puts the current result in a list
}
}
// list content: 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 6...
foreach (var item in list)
{ /* do something */ }
Комментарии:
1. -@chiccodoro, это хорошая идея. И вы думаете, что LINQ работает медленно? Я не уверен, что мы могли бы использовать
for
цикл для его обработки.2. -@chiccodoro, ваш код не работает. Необработанное исключение типа ‘System. Исключение OutOfMemoryException’ произошло в mscorlib.dll . Я думаю, из-за SelectMany . Пожалуйста, проверьте это в своем поле.
3. @Love, честно говоря, нет, я тестировал его с числом 10 только для того, чтобы убедиться, что он работает. Я просто хотел указать вам на несколько улучшений. «медленное» и исключение нехватки памяти немного изменяются. Чтобы определить «медленный», вам нужен лимит времени, а для исключения нехватки памяти вам нужен лимит памяти. Вы не предоставили ни того, ни другого, но я уверен, что мой ответ занимает меньше памяти и меньше времени, чем ваш исходный код.
4. -@chiccodoro, это работает после того, как я удалил
ToList()
. Просто интересно, как перечислитель хранит каждый результат? Циклforeach
кода находится под результатом. Я предполагаю, что он его вычислил.5. @Love: Я рад это слышать! Далее мне интересно, насколько это отличается от производительности AD.Net это ответ. Я ожидаю, что разница будет довольно небольшой.
Ответ №2:
Вы можете поиграть с index
и вашим выбором для каждого из for
циклов, предполагая, что входные данные отсортированы. Первый цикл должен составлять одну треть основного массива, второй цикл должен составлять не более половины, но начинаться с индекса первого цикла. что-то вроде этого:
const int totalElements = 1000;
const int target = 1000;
var value = Enumerable.Range(1, totalElements).OrderBy(dd => dd).ToList();
var sw = new Stopwatch();
var result = new List<int[]>();
sw.Start();
for (int i = 0; i < value.Count / 3; i )
{
var a = value[i];
for (int j = i 1; j < value.Count / 2; j )
{
var b = value[j];
for (int k = j 1; k < value.Count; k )
{
var c = value[k];
if (a b c <= target)
{
var newR = new[] { a, b, c };
result.Add(newR);
}
}
}
}
sw.Stop();
Console.WriteLine("Time Taken: " sw.Elapsed.TotalSeconds);
Console.WriteLine("Total result count: " result2.Count);
Для 1000 это занимает 4,9 секунды на моей паршивой машине, где, поскольку LINQ выдает outofmemory exception
. Кроме того, вам нужно добавить какое-то сравнение равенства в LINQ, иначе вы получите такие результаты, как 1 2 3 и 3 2 1 , и используя только индекс, вы избавляетесь от этого.
Вы также можете настроить диапазон, выполнив что-то вроде этого:
for (int i = 0; i < value.Count / 3; i )
{
var a = value[i];
var index = value.IndexOf(target - a) 1;
for (int j = i 1; j < index; j )
{
var b = value[j];
var remainingIndex = value.IndexOf(target - (a b)) 1;
for (int k = j 1; k < remainingIndex; k )
{
var c = value[k];
if (a b c <= target)
{
var newR = new[] { a, b, c };
result.Add(newR);
}
}
}
}
Возможно, было бы лучше, если бы значения были последовательными, как в этом примере, и вы могли бы легко найти индекс. Если это не так, вам нужно найти индекс по Binary Search
, но, вероятно, производительность в любом случае будет аналогичной.
Комментарии:
1. «Первое должно составлять одну треть основного массива». Почему?
2. @Tarik Как объяснено в вопросе
3. Должны ли мы предположить, что числа являются последовательными?
4. Да, это предположение было сделано в некоторых случаях, как упоминалось выше
5. Тот факт, что вам нужно проверять каждый результат, показывает, что учитываются числа, которые не являются кандидатами — это можно дополнительно оптимизировать 🙂
Ответ №3:
Я предлагаю хранить числа в классе SortedSet . Вы можете сделать следующее (псевдокод):
For each n in GetViewBetween (1, d)
For each m in GetViewBetween (n 1, d-n)
For each k in GetViewBetween (m 1, d-n-m)
Store or print or callback or return with continuation the Tuple object with n, m, k
Loop
Loop
Loop
Комментарии:
1. Тем, кто голосует за даунвотер, оставьте сообщение, чтобы дать возможность исправить ответ.
2. (Я не сторонник понижения). Ваш ответ в основном такой же, как у меня, только в другой нотации: GetViewBetween (1, d) == Enumerable . Диапазон (1, d), а вложенный for-each эквивалентен вложенному SelectMany. Поэтому я думаю, что ваш ответ хорош. Не очень понятно, какое значение это добавляет к уже существующим ответам, хотя, возможно, вы захотите прояснить этот момент.
3. @chiccodoro за исключением того, что я не предполагал, что числа были последовательными. Кроме того, использование отсортированного набора позволяет получить доступ к диапазону чисел за линейное время. Теперь, предполагая последовательные целые числа, три вложенных цикла do выполняют работу без накладных расходов LINQ или Enumerators .
Ответ №4:
Я бы предложил вам геометрическое представление: представьте, что ваш ‘d’ является суммой границ треугольника. Таким образом, вам нужно найти все подходящие треугольники. В вашем коде вы удваиваете варианты: (a b c) = (b c a), например. Но если вы сделаете 3 цикла для каждой стороны треугольника, тогда это будет быстрее.
Комментарии:
1. И вы не можете перечислить все возможные варианты быстрее, тогда O (d), где d = — максимальная длина границ треугольника.