web-dev-qa-db-fra.com

Générer une matrice contenant toutes les combinaisons d'éléments tirés de n vecteurs

Cette question apparaît assez souvent sous une forme ou une autre (voir par exemple ici ou ici ). J'ai donc pensé le présenter sous une forme générale et fournir une réponse qui pourrait servir de référence future.

Étant donné un nombre arbitraire n de vecteurs de tailles éventuellement différentes, générez une matrice de colonnes n- dont les lignes décrivent toutes les combinaisons d'éléments tirées de ces vecteurs (produit cartésien).

Par exemple,

vectors = { [1 2], [3 6 9], [10 20] }

devrait donner

combs = [ 1     3    10
          1     3    20
          1     6    10
          1     6    20
          1     9    10
          1     9    20
          2     3    10
          2     3    20
          2     6    10
          2     6    20
          2     9    10
          2     9    20 ]
52
Luis Mendo

La fonction ndgrid donne presque la réponse, mais a une mise en garde: n les variables de sortie doivent être explicitement définies pour l'appeler. Puisque n est arbitraire, la meilleure façon est d'utiliser une liste séparée par des virgules (générée à partir d'un tableau de cellules avec ncells) pour servir de sortie. Les matrices n résultantes sont ensuite concaténées dans la matrice de colonne n- souhaitée:

vectors = { [1 2], [3 6 9], [10 20] }; %// input data: cell array of vectors

n = numel(vectors); %// number of vectors
combs = cell(1,n); %// pre-define to generate comma-separated list
[combs{end:-1:1}] = ndgrid(vectors{end:-1:1}); %// the reverse order in these two
%// comma-separated lists is needed to produce the rows of the result matrix in
%// lexicographical order 
combs = cat(n+1, combs{:}); %// concat the n n-dim arrays along dimension n+1
combs = reshape(combs,[],n); %// reshape to obtain desired matrix
46
Luis Mendo

Un peu plus simple ... si vous avez la boîte à outils Neural Network, vous pouvez simplement utiliser combvec :

vectors = {[1 2], [3 6 9], [10 20]};
combs = combvec(vectors{:}).' % Use cells as arguments

qui renvoie une matrice dans un ordre légèrement différent:

combs =

     1     3    10
     2     3    10
     1     6    10
     2     6    10
     1     9    10
     2     9    10
     1     3    20
     2     3    20
     1     6    20
     2     6    20
     1     9    20
     2     9    20

Si vous voulez la matrice qui est dans la question, vous pouvez utiliser sortrows :

combs = sortrows(combvec(vectors{:}).')
% Or equivalently as per @LuisMendo in the comments: 
% combs = fliplr(combvec(vectors{end:-1:1}).') 

qui donne

combs =

     1     3    10
     1     3    20
     1     6    10
     1     6    20
     1     9    10
     1     9    20
     2     3    10
     2     3    20
     2     6    10
     2     6    20
     2     9    10
     2     9    20

Si vous regardez les internes de combvec (tapez edit combvec dans la fenêtre de commande), vous verrez qu'il utilise un code différent de la réponse de @ LuisMendo. Je ne peux pas dire lequel est globalement le plus efficace.

S'il vous arrive d'avoir une matrice dont les lignes s'apparentent au tableau de cellules précédent, vous pouvez utiliser:

vectors = [1 2;3 6;10 20];
vectors = num2cell(vectors,2);
combs = sortrows(combvec(vectors{:}).')
27
horchler

J'ai fait quelques analyses comparatives sur les deux solutions proposées. Le code d'analyse comparative est basé sur la fonction timeit , et est inclus à la fin de cet article.

Je considère deux cas: trois vecteurs de taille n et trois vecteurs de taille n/10, n et n*10 respectivement (les deux cas donnent le même nombre de combinaisons). n varie jusqu'à un maximum de 240 (J'ai choisi cette valeur pour éviter l'utilisation de mémoire virtuelle sur mon ordinateur portable).

Les résultats sont donnés dans la figure suivante. La solution basée sur ndgrid est considérée comme prenant systématiquement moins de temps que combvec. Il est également intéressant de noter que le temps pris par combvec varie un peu moins régulièrement dans le cas de tailles différentes.

enter image description here


Code de référence

Fonction pour la solution basée sur ndgrid:

function combs = f1(vectors)
n = numel(vectors); %// number of vectors
combs = cell(1,n); %// pre-define to generate comma-separated list
[combs{end:-1:1}] = ndgrid(vectors{end:-1:1}); %// the reverse order in these two
%// comma-separated lists is needed to produce the rows of the result matrix in
%// lexicographical order
combs = cat(n+1, combs{:}); %// concat the n n-dim arrays along dimension n+1
combs = reshape(combs,[],n);

Fonction pour la solution combvec:

function combs = f2(vectors)
combs = combvec(vectors{:}).';

Script pour mesurer le temps en appelant timeit sur ces fonctions:

nn = 20:20:240;
t1 = [];
t2 = [];
for n = nn;
    %//vectors = {1:n, 1:n, 1:n};
    vectors = {1:n/10, 1:n, 1:n*10};
    t = timeit(@() f1(vectors));
    t1 = [t1; t];
    t = timeit(@() f2(vectors));
    t2 = [t2; t];
end
13
Luis Mendo

Voici une méthode à faire soi-même qui m'a fait rire avec plaisir, en utilisant nchoosek, bien que ce soit pas mieux que accepté par @Luis Mendo Solution.

Pour l'exemple donné, après 1 000 exécutions, cette solution a pris ma machine en moyenne 0,00065935 s, contre la solution acceptée 0,00012877 s. Pour les vecteurs plus gros, suivant le post de benchmarking de @Luis Mendo, cette solution est toujours plus lente que la réponse acceptée. Néanmoins, j'ai décidé de le poster dans l'espoir que vous y trouverez peut-être quelque chose d'utile:

Code:

tic;
v = {[1 2], [3 6 9], [10 20]};

L = [0 cumsum(cellfun(@length,v))];
V = cell2mat(v);

J = nchoosek(1:L(end),length(v));
J(any(J>repmat(L(2:end),[size(J,1) 1]),2) | ...
  any(J<=repmat(L(1:end-1),[size(J,1) 1]),2),:)  = [];

V(J)
toc

donne

ans =

 1     3    10
 1     3    20
 1     6    10
 1     6    20
 1     9    10
 1     9    20
 2     3    10
 2     3    20
 2     6    10
 2     6    20
 2     9    10
 2     9    20

Elapsed time is 0.018434 seconds.

Explication:

L obtient les longueurs de chaque vecteur en utilisant cellfun. Bien que cellfun soit fondamentalement une boucle, c'est efficace ici étant donné que votre nombre de vecteurs devra être relativement faible pour que ce problème soit même pratique.

V concatène tous les vecteurs pour un accès facile plus tard (cela suppose que vous avez entré tous vos vecteurs en tant que lignes. v 'fonctionnerait pour les vecteurs de colonne.)

nchoosek obtient toutes les façons de choisir les éléments n=length(v) parmi le nombre total d'éléments L(end). Il y aura plus de combinaisons ici que ce dont nous avons besoin.

J =

 1     2     3
 1     2     4
 1     2     5
 1     2     6
 1     2     7
 1     3     4
 1     3     5
 1     3     6
 1     3     7
 1     4     5
 1     4     6
 1     4     7
 1     5     6
 1     5     7
 1     6     7
 2     3     4
 2     3     5
 2     3     6
 2     3     7
 2     4     5
 2     4     6
 2     4     7
 2     5     6
 2     5     7
 2     6     7
 3     4     5
 3     4     6
 3     4     7
 3     5     6
 3     5     7
 3     6     7
 4     5     6
 4     5     7
 4     6     7
 5     6     7

Puisqu'il n'y a que deux éléments dans v(1), nous devons jeter toutes les lignes où J(:,1)>2. De même, où J(:,2)<3, J(:,2)>5, etc ... En utilisant L et repmat nous pouvons déterminer si chaque élément de J est dans sa plage appropriée, puis utilisez any pour supprimer les lignes qui contiennent un élément incorrect.

Enfin, ce ne sont pas les valeurs réelles de v, juste des indices. V(J) renverra la matrice souhaitée.

2
Geoff