Alternativa mais rápida para loops aninhados?

85

Tenho necessidade de criar uma lista de combinações de números. Os números são muito pequenos para que eu possa usar byteem 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, BitArraymas 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 usar int, 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.

Benpage
fonte
1
Tente usar o linq e se você estiver usando um processador multicore, então Parrallel.for
Jalpesh Vadgama
1
com base no que vejo, essas não são as permutações, mas as combinações de alguns conjuntos muito pequenos (2-4 elementos), está certo ou você realmente deseja todas / algumas permutações de um conjunto?
Carsten,
Suponho que você já pesquisou bing.com/search?q=c%23+permutation+enumerable e por algum motivo (não mencionado na postagem) decidiu contra as respostas existentes como stackoverflow.com/questions/4319049/… ... Considere listar opções que você olhou e decidiu contra para tornar esta questão melhor.
Alexei Levenkov
3
Se for sobre desempenho: Você pode pré-alocar a lista (construtor) e desenrolar alguns loops, mas acho que é só isso ... além de pré-calcular e armazenar esses números. Os loops (overhead) são provavelmente os mais caros de todos, já que há pouca quantidade de operações dentro do corpo.
Caramiriel
5
@benpage: Por que você precisa gerar todas as combinações antecipadamente? Por que não gerar uma combinação de seu índice quando você precisar?
Pieter Witvoet

Respostas:

61

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 a List<T>.

Caramiriel
fonte
1
É uma estrutura, então você deve estar bem =) eles são todos únicos. É copiado ao chamar o List<T>.Addmétodo.
Caramiriel,
4
É ainda mais rápido se você alocar a capacidade para a Lista ()
Eric,
5
Cuidado com as exceções stackoverflow ao alocar muitos objetos na pilha.
Andrei Tătar
7
@Andrew: não entendi. Este código não é recursivo e tem uso mínimo da pilha.
CodesInChaos
3
@Andrew: Isso está sem memória, não stackoverflow. Isso ocorre porque o 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]
Caramiriel
33

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á-lo l, 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.

Adão
fonte
4
Gosto da sua resposta, mas dizer que todas as outras respostas são exponenciais não é verdade.
Biscoitos de
1
Qual é a velocidade disso em comparação com a resposta de Caramiriel?
John Odom
17
"C-kiddy- #", realmente? Isso parece totalmente desnecessário.
KChaloux
2
E é assim: Math.DivRem
Caramiriel
1
Acho que além de algum nível, a Otimização é uma questão de uso. Por exemplo, se cada array for usado apenas uma vez, você pode evitar a alocação intensiva de memória, que é o gargalo crítico em minha opinião. Além disso, se você quiser calcular todos os valores, deve explorar o fato de fazer incrementos únicos (ou seja, +1 incremento) evitando uma divisão. Isso é mais para ser uma
14

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.Arraydiretamente 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 };

Isso leva 596 ms para ser concluído no meu computador, o que é cerca de 10,4% mais rápido do que o código em questão (que leva 658 ms).

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)
            };
        }
    }
}

Isso leva 897 ms para ser concluído no meu computador (também criando e adicionando a um Arraycomo no Método 2 ), o que é cerca de 36,3% mais lento do que o código em questão (que leva 658 ms).

biscoitos
fonte
1
Sua segunda sugestão também é uma economia significativa em termos de consumo de memória. (Mas eu observaria que isso pressupõe que a lista não deve mudar)
Taemyr,
Preciso que toda a lista seja criada de uma vez - não consigo me referir a um índice dentro da lista.
benpage
Obrigado @Taemyr. Vou atualizar para observar isso de acordo. Se a implementação realmente insiste em que você tenha toda a lista preenchida antecipadamente, esta terceira opção obviamente não funcionará para você.
Biscoitos de
3
@benpage Por que você precisa que a lista seja preenchida?
Taemyr
14

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;
}
Andrei Tătar
fonte
Esta é uma ótima resposta! Infelizmente, ele é executado mais lentamente do que os loops aninhados. Alguma chance de você editar usando TPL?
benpage
ainda um pouco mais lento, infelizmente.
benpage
1
@benpage Existe uma maneira fácil de torná-lo pelo menos 2 vezes mais rápido. Você apenas tem que alterar o tipo de resultado para int [,]. Isso alocará toda a memória do array em uma chamada. Não tenho certeza de como isso atende às suas necessidades (alterando o tipo de retorno).
Andrei Tătar
8
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;
}
Eric
fonte
1
isso é muito mais lento :(
benpage de
8

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?

gjvdkamp
fonte
Na verdade, tentei pré-alocar um array regular e, acredite ou não, é mais lento. Como eu disse acima, isso precisa ser criado na hora, não posso calcular uma vez e deixá-lo.
benpage
mesmo? uau - você está executando com otimizações habilitadas, certo? (apenas perguntando)
Carsten,
Ah, esse é outro problema, arrays regulares [x, y] são bons de usar, mas um array de arrays será mais rápido. stackoverflow.com/questions/597720/… por causa de como eles estão implementados sob o capô em IL
gjvdkamp
5

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());
  • Espero que este código funcione, converti-o de vb
the_lotus
fonte
3

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.

nulo
fonte
A serialização é realmente uma ótima abordagem fora da caixa!
Joel B
Infelizmente, os máximos na lista são dinâmicos, não posso digitar isso estaticamente. Boa ideia!
benpage
2

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;
}
gjvdkamp
fonte
Boa ideia - na verdade, foi isso que fiz em meu projeto do mundo real - só não coloquei na solução original por causa da simplicidade. Eu estava procurando principalmente uma alternativa melhor para os loops aninhados.
benpage
1

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).

jdphenix
fonte
1
Isso é praticamente garantido para dar a você um falso compartilhamento e provavelmente será muito mais lento.
gjvdkamp
Nada mal - infelizmente, ele roda mais devagar do que os loops for. FWIW eu tentei com ALL se Parallel.ForVS travou!
benpage
@gjvdkamp Eu atualizei minha resposta com uma versão paralela que eu acredito que elimina o problema de falso compartilhamento.
jdphenix
0

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.

Fabien Dupont
fonte
Eu gostaria de saber mais sobre esta resposta - você poderia expandi-la?
benpage
Desculpe pela resposta tardia. m vai de 0 a 3, que em binário faz de 00 a 11, l de 0 a 2, o que faz de 00 a 10, então se você imprimi-los separadamente, isso resultaria em: 00 00 00 01 00 10 00 11 01 00 .. . 10 11 Você pode juntá-los em um único número de 4 bits, indo de 0000 a 1011, e selecionar os bits apropriados usando uma máscara lm & 3 torna o biwise e entre lm e (11) b lm & 12 faz o mesmo com lm e (1100) b então mudamos dois bits para ter o número "real". A propósito, acabei de perceber que é suficiente fazer lm >> 2 neste caso.
Fabien Dupont
0

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();
}
Henrik
fonte