Tenho necessidade de criar uma lista de combinações de números. Os números são muito pequenos para que eu possa usar byte
em vez de int
. No entanto, requer muitos loops aninhados para obter todas as combinações possíveis. Estou me perguntando se existe uma maneira mais eficiente de fazer o que procuro. O código até agora é:
var data = new List<byte[]>();
for (byte a = 0; a < 2; a++)
for (byte b = 0; b < 3; b++)
for (byte c = 0; c < 4; c++)
for (byte d = 0; d < 3; d++)
for (byte e = 0; e < 4; e++)
for (byte f = 0; f < 3; f++)
for (byte g = 0; g < 3; g++)
for (byte h = 0; h < 4; h++)
for (byte i = 0; i < 2; i++)
for (byte j = 0; j < 4; j++)
for (byte k = 0; k < 4; k++)
for (byte l = 0; l < 3; l++)
for (byte m = 0; m < 4; m++)
{
data.Add(new [] {a, b, c, d, e, f, g, h, i, j, k, l, m});
}
Eu estava pensando em usar algo como um, BitArray
mas não tenho certeza de como poderia incorporá-lo.
Qualquer recomendação seria grandemente apreciada. Alternativamente, talvez esta seja a maneira mais rápida de fazer o que desejo?
EDITAR Alguns pontos rápidos (e desculpas por não colocá-los na postagem original):
- Os números e a ordem deles (2, 3, 4, 3, 4, 3, 3 etc) são muito importantes, então usar uma solução como Gerar Permutações usando LINQ não ajudará porque os máximos em cada 'coluna' são diferente
- Não sou um matemático, então peço desculpas se não estou usando os termos técnicos como 'permutações' e 'combinações' corretamente :)
- Eu não preciso preencher todas estas combinações de uma vez - eu não pode simplesmente pegar um ou outro com base em um índice
- Usar
byte
é mais rápido do que usarint
, garanto . Também é muito melhor no uso de memória ter mais de 67 milhões de matrizes de bytes em vez de ints - Meu objetivo final aqui é procurar uma alternativa mais rápida aos loops aninhados.
- Eu considerei usar programação paralela, mas devido à natureza iterativa do que estou tentando alcançar, não consegui encontrar uma maneira de fazer isso com sucesso (mesmo com
ConcurrentBag
) - no entanto, estou feliz por estar errado :)
CONCLUSÃO
Caramiriel forneceu uma boa micro-otimização que economiza algum tempo dos loops, então eu marquei essa resposta como correta. Eric também mencionou que é mais rápido pré-alocar a Lista. Mas, neste estágio, parece que os loops aninhados são de fato a maneira mais rápida possível de fazer isso (deprimente, eu sei!).
Se você quiser tentar exatamente o que eu estava tentando fazer benchmark StopWatch
, vá com 13 loops contando até 4 em cada loop - isso perfaz cerca de 67m + linhas na lista. Na minha máquina (i5-3320M 2.6GHz) leva cerca de 2.2s para fazer a versão otimizada.
fonte
Respostas:
Como um lembrete: você provavelmente não precisa desse tipo de código ao desenvolver sua própria solução. Isso pode e deve ser usado apenas em situações muito específicas. A legibilidade geralmente é mais importante do que a velocidade.
Você pode usar as propriedades de uma estrutura e alocar a estrutura com antecedência. Cortei alguns níveis no exemplo abaixo, mas tenho certeza de que você será capaz de descobrir os detalhes. É executado cerca de 5 a 6 vezes mais rápido do que o original (modo de liberação).
O bloco:
struct ByteBlock { public byte A; public byte B; public byte C; public byte D; public byte E; }
O laço:
var data = new ByteBlock[2*3*4*3*4]; var counter = 0; var bytes = new ByteBlock(); for (byte a = 0; a < 2; a++) { bytes.A = a; for (byte b = 0; b < 3; b++) { bytes.B = b; for (byte c = 0; c < 4; c++) { bytes.C = c; for (byte d = 0; d < 3; d++) { bytes.D = d; for (byte e = 0; e < 4; e++) { bytes.E = e; data[counter++] = bytes; } } } } }
É mais rápido porque não aloca uma nova lista toda vez que você a adiciona à lista. Além disso, como está criando essa lista, ele precisa de uma referência para todos os outros valores (a, b, c, d, e). Você pode assumir que cada valor é modificado apenas uma vez dentro do loop, então podemos otimizá-lo para fazer isso (localidade dos dados).
Leia também os comentários para efeitos colaterais.
Editou a resposta para usar um em
T[]
vez de aList<T>
.fonte
List<T>.Add
método.List<T>.Add()
método vai além do que ele pode armazenar. Isso fará com que ele seja redimensionado (dobra de tamanho), o que ultrapassa 2 GB de memória. Tente pré-alocar usando o novo List <ByteBlock> (maxPerLevel.Aggregate (1, (x, y) => x * y)), embora já seja 'aleatório' que você precise de um bloco completo de 2 GB desses dados na memória. Observe também que data.ToArray (); é caro porque mantém os itens na memória duas vezes nesse ponto. [reformulada]O que você está fazendo é contar (com raiz variável, mas ainda contando).
Como você está usando C #, suponho que não queira brincar com o layout de memória útil e as estruturas de dados que permitem que você realmente otimize seu código.
Então aqui estou postando algo diferente, que pode não se adequar ao seu caso, mas é importante notar: Caso você realmente acesse a lista de uma forma esparsa, aqui está uma classe que permite calcular o i-ésimo elemento em tempo linear (ao invés do que exponencial como as outras respostas)
class Counter { public int[] Radices; public int[] this[int n] { get { int[] v = new int[Radices.Length]; int i = Radices.Length - 1; while (n != 0 && i >= 0) { //Hope C# has an IL-opcode for div-and-reminder like x86 do v[i] = n % Radices[i]; n /= Radices[i--]; } return v; } } }
Você pode usar esta classe desta forma
Counter c = new Counter(); c.Radices = new int[] { 2,3,4,3,4,3,3,4,2,4,4,3,4};
agora
c[i]
é o mesmo que a sua lista, nomeá-lol
,l[i]
.Como você pode ver, você pode facilmente evitar todos esses loops :) mesmo quando pré-calcula toda a lista, já que pode simplesmente implementar um contador Carry-Ripple.
Contadores são um assunto muito estudado, eu recomendo fortemente que você procure alguma literatura, se você sentir.
fonte
Método 1
Uma maneira de torná-lo mais rápido é especificar a capacidade se você planeja continuar usando
List<byte[]>
, assim.var data = new List<byte[]>(2 * 3 * 4 * 3 * 4 * 3 * 3 * 4 * 2 * 4 * 4 * 3 * 4);
Método 2
Além disso, você pode usar
System.Array
diretamente para obter acesso mais rápido. Eu recomendo essa abordagem se sua pergunta insiste que cada elemento seja preenchido fisicamente na memória, de antemão.var data = new byte[2 * 3 * 4 * 3 * 4 * 3 * 3 * 4 * 2 * 4 * 4 * 3 * 4][]; int counter = 0; for (byte a = 0; a < 2; a++) for (byte b = 0; b < 3; b++) for (byte c = 0; c < 4; c++) for (byte d = 0; d < 3; d++) for (byte e = 0; e < 4; e++) for (byte f = 0; f < 3; f++) for (byte g = 0; g < 3; g++) for (byte h = 0; h < 4; h++) for (byte i = 0; i < 2; i++) for (byte j = 0; j < 4; j++) for (byte k = 0; k < 4; k++) for (byte l = 0; l < 3; l++) for (byte m = 0; m < 4; m++) data[counter++] = new[] { a, b, c, d, e, f, g, h, i, j, k, l, m };
Método 3
Como alternativa, você pode usar a seguinte técnica para inicialização de baixo custo que se adapte ao acesso de forma esparsa. Isso é especialmente favorável quando apenas alguns elementos podem ser necessários e determinar todos eles antecipadamente é considerado desnecessário. Além disso, técnicas como essas podem se tornar a única opção viável ao trabalhar com elementos mais vastos quando a memória escasseia.
Nesta implementação, cada elemento deve ser determinado preguiçosamente, em tempo real, no momento do acesso. Naturalmente, isso tem um custo de CPU adicional incorrido durante o acesso.
class HypotheticalBytes { private readonly int _c1, _c2, _c3, _c4, _c5, _c6, _c7, _c8, _c9, _c10, _c11, _c12; private readonly int _t0, _t1, _t2, _t3, _t4, _t5, _t6, _t7, _t8, _t9, _t10, _t11; public int Count { get { return _t0; } } public HypotheticalBytes( int c0, int c1, int c2, int c3, int c4, int c5, int c6, int c7, int c8, int c9, int c10, int c11, int c12) { _c1 = c1; _c2 = c2; _c3 = c3; _c4 = c4; _c5 = c5; _c6 = c6; _c7 = c7; _c8 = c8; _c9 = c9; _c10 = c10; _c11 = c11; _c12 = c12; _t11 = _c12 * c11; _t10 = _t11 * c10; _t9 = _t10 * c9; _t8 = _t9 * c8; _t7 = _t8 * c7; _t6 = _t7 * c6; _t5 = _t6 * c5; _t4 = _t5 * c4; _t3 = _t4 * c3; _t2 = _t3 * c2; _t1 = _t2 * c1; _t0 = _t1 * c0; } public byte[] this[int index] { get { return new[] { (byte)(index / _t1), (byte)((index / _t2) % _c1), (byte)((index / _t3) % _c2), (byte)((index / _t4) % _c3), (byte)((index / _t5) % _c4), (byte)((index / _t6) % _c5), (byte)((index / _t7) % _c6), (byte)((index / _t8) % _c7), (byte)((index / _t9) % _c8), (byte)((index / _t10) % _c9), (byte)((index / _t11) % _c10), (byte)((index / _c12) % _c11), (byte)(index % _c12) }; } } }
fonte
Na minha máquina, isso gera as combinações em 222 ms vs 760 ms (os 13 loops for):
private static byte[,] GenerateCombinations(byte[] maxNumberPerLevel) { var levels = maxNumberPerLevel.Length; var periodsPerLevel = new int[levels]; var totalItems = 1; for (var i = 0; i < levels; i++) { periodsPerLevel[i] = totalItems; totalItems *= maxNumberPerLevel[i]; } var results = new byte[totalItems, levels]; Parallel.For(0, levels, level => { var periodPerLevel = periodsPerLevel[level]; var maxPerLevel = maxNumberPerLevel[level]; for (var i = 0; i < totalItems; i++) results[i, level] = (byte)(i / periodPerLevel % maxPerLevel); }); return results; }
fonte
var numbers = new[] { 2, 3, 4, 3, 4, 3, 3, 4, 2, 4, 4, 3, 4 }; var result = (numbers.Select(i => Enumerable.Range(0, i))).CartesianProduct();
Usando o método de extensão em http://ericlippert.com/2010/06/28/computing-a-cartesian-product-with-linq/
public static IEnumerable<IEnumerable<T>> CartesianProduct<T>(this IEnumerable<IEnumerable<T>> sequences) { // base case: IEnumerable<IEnumerable<T>> result = new[] { Enumerable.Empty<T>() }; foreach (var sequence in sequences) { // don't close over the loop variable (fixed in C# 5 BTW) var s = sequence; // recursive case: use SelectMany to build // the new product out of the old one result = from seq in result from item in s select seq.Concat(new[] { item }); } return result; }
fonte
List possui um array internamente onde armazena seus valores, com comprimento fixo. Quando você chama List.Add, ele verifica se há espaço suficiente. Quando não for possível adicionar o novo elemento, ele criará uma nova matriz de tamanho maior, copiará todos os elementos anteriores e adicionará um novo. Isso leva alguns ciclos.
Como você já sabe o número de elementos, pode criar a lista do tamanho correto, que já deve ser muito mais rápido.
Além disso, não tenho certeza de como você acessa os valores, mas você pode criar esta coisa e salvar a imagem em código (carregá-la do disco provavelmente será mais lento do que o que você está fazendo agora. Quantas vezes você lê / escreve neste coisa?
fonte
Aqui está uma maneira diferente que só precisa de 2 laços. A ideia é aumentar o primeiro elemento e se esse número ultrapassar, aumentar o próximo.
Em vez de exibir os dados, você pode usar currentValues.Clone e adicionar essa versão clonada à sua lista. Para mim, isso funcionou mais rápido do que a sua versão.
byte[] maxValues = {2, 3, 4}; byte[] currentValues = {0, 0, 0}; do { Console.WriteLine("{0}, {1}, {2}", currentValues[0], currentValues[1], currentValues[2]); currentValues[0] += 1; for (int i = 0; i <= maxValues.Count - 2; i++) { if (currentValues[i] < maxValues[i]) { break; } currentValues[i] = 0; currentValues[i + 1] += 1; } // Stop the whole thing if the last number is over // } while (currentValues[currentValues.Length-1] < maxValues[maxValues.Length-1]); } while (currentValues.Last() < maxValues.Last());
fonte
Todos os seus números são constantes de tempo de compilação.
Que tal desenrolar todos os loops em uma lista (usando seu programa para escrever o código):
data.Add(new [] {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}); data.Add(new [] {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}); etc.
Isso deve, pelo menos, eliminar a sobrecarga dos loops for (se houver).
Não estou muito familiarizado com C #, mas parece haver alguns meios de serializar objetos. E se você apenas gerasse essa lista e a serializasse de alguma forma? Não tenho certeza se a desserialização é mais rápida do que criar a Lista e adicionar os elementos, no entanto.
fonte
Você precisa que o resultado seja uma matriz de matrizes? Com a configuração atual, o comprimento das matrizes internas é fixo e pode ser substituído por estruturas. Isso permitiria que tudo fosse reservado como um bloco contínuo de memória e forneceria acesso mais fácil aos elementos (não tenho certeza de como usar isso mais tarde).
A abordagem abaixo é muito mais rápida (41ms vs 1071ms para o original na minha caixa):
struct element { public byte a; public byte b; public byte c; public byte d; public byte e; public byte f; public byte g; public byte h; public byte i; public byte j; public byte k; public byte l; public byte m; } element[] WithStruct() { var t = new element[3981312]; int z = 0; for (byte a = 0; a < 2; a++) for (byte b = 0; b < 3; b++) for (byte c = 0; c < 4; c++) for (byte d = 0; d < 3; d++) for (byte e = 0; e < 4; e++) for (byte f = 0; f < 3; f++) for (byte g = 0; g < 3; g++) for (byte h = 0; h < 4; h++) for (byte i = 0; i < 2; i++) for (byte j = 0; j < 4; j++) for (byte k = 0; k < 4; k++) for (byte l = 0; l < 3; l++) for (byte m = 0; m < 4; m++) { t[z].a = a; t[z].b = b; t[z].c = c; t[z].d = d; t[z].e = e; t[z].f = f; t[z].g = g; t[z].h = h; t[z].i = i; t[z].j = j; t[z].k = k; t[z].l = l; t[z].m = m; z++; } return t; }
fonte
Que tal usar
Parallel.For()
para executá-lo? (Elogios de otimização de Struct para @Caramiriel ). Eu modifiquei levemente os valores (a é 5 em vez de 2), então estou mais confiante nos resultados.var data = new ConcurrentStack<List<Bytes>>(); var sw = new Stopwatch(); sw.Start(); Parallel.For(0, 5, () => new List<Bytes>(3*4*3*4*3*3*4*2*4*4*3*4), (a, loop, localList) => { var bytes = new Bytes(); bytes.A = (byte) a; for (byte b = 0; b < 3; b++) { bytes.B = b; for (byte c = 0; c < 4; c++) { bytes.C = c; for (byte d = 0; d < 3; d++) { bytes.D = d; for (byte e = 0; e < 4; e++) { bytes.E = e; for (byte f = 0; f < 3; f++) { bytes.F = f; for (byte g = 0; g < 3; g++) { bytes.G = g; for (byte h = 0; h < 4; h++) { bytes.H = h; for (byte i = 0; i < 2; i++) { bytes.I = i; for (byte j = 0; j < 4; j++) { bytes.J = j; for (byte k = 0; k < 4; k++) { bytes.K = k; for (byte l = 0; l < 3; l++) { bytes.L = l; for (byte m = 0; m < 4; m++) { bytes.M = m; localList.Add(bytes); } } } } } } } } } } } } return localList; }, x => { data.Push(x); }); var joinedData = _join(data);
_join()
é um método privado, definido como:private static IList<Bytes> _join(IEnumerable<IList<Bytes>> data) { var value = new List<Bytes>(); foreach (var d in data) { value.AddRange(d); } return value; }
No meu sistema, esta versão é executada aproximadamente 6 vezes mais rápido (1,718 segundos contra 0,266 segundos).
fonte
Parallel.For
VS travou!Alguns de seus números cabem inteiramente em um número inteiro de bits, então você pode "compactá-los" com o número de nível superior:
for (byte lm = 0; lm < 12; lm++) { ... t[z].l = (lm&12)>>2; t[z].m = lm&3; ... }
Claro, isso torna o código menos legível, mas você salvou um loop. Isso pode ser feito sempre que um dos números for uma potência de dois, que é sete vezes no seu caso.
fonte
Aqui está outra solução. Fora do VS, ele roda 437,5 ms, o que é 26% mais rápido que o código original (593,7 no meu computador):
static List<byte[]> Combinations(byte[] maxs) { int length = maxs.Length; int count = 1; // 3981312; Array.ForEach(maxs, m => count *= m); byte[][] data = new byte[count][]; byte[] counters = new byte[length]; for (int r = 0; r < count; r++) { byte[] row = new byte[length]; for (int c = 0; c < length; c++) row[c] = counters[c]; data[r] = row; for (int i = length - 1; i >= 0; i--) { counters[i]++; if (counters[i] == maxs[i]) counters[i] = 0; else break; } } return data.ToList(); }
fonte