Algorithme de produit cartésien efficace
quelqu'un peut-il me démontrer un algorithme de produit cartésien plus efficace que celui que j'utilise actuellement (en supposant qu'il y en ait un)? J'ai cherché et j'ai cherché un peu sur Google mais je ne vois rien d'évident donc je pourrais rater quelque chose.
foreach (int i in is) {
foreach (int j in js) {
//Pair i and j
}
}
C'est une version très simplifiée de ce que je fais dans mon code. Les deux entiers sont des clés de recherche qui sont utilisées pour récupérer un / plusieurs objets et tous les objets des deux recherches sont appariés ensemble dans de nouveaux objet.
ce petit bloc de code dans un système beaucoup plus grand et plus complexe devient un goulot d'étranglement majeur de performance comme l'ensemble de données qu'il fonctionne sur des échelles. Une partie de cela pourrait probablement être atténuée en améliorant les structures de données utilisées pour stocker les objets et les recherches impliquées, mais le principal problème que je pense est toujours le calcul du produit cartésien lui-même.
Modifier
Donc un peu plus sur mon spécifiques d'utilisation de l'algorithme pour voir s'il peut y avoir des trucs que je peux utiliser en réponse au commentaire de Marc. Le système global est un moteur de requête SPARQL qui traite les requêtes SPARQL sur des ensembles de données de graphe, SPARQL est un langage basé sur un motif donc chaque requête se compose d'une série de motifs qui sont appariés par rapport au(S) graphe (s). Dans le cas où les deux modèles ont aucune commune variables (ils sont disjoints), il est nécessaire de calculer le produit Cartésien des solutions produites par les deux modèles pour obtenir le ensemble de solutions possibles pour la requête globale. Il peut y avoir n'importe quel nombre de modèles et je peux devoir calculer des produits cartésiens plusieurs fois qui peuvent conduire à une expansion assez exponentielle dans des solutions possibles si la requête est composée d'une série de modèles disjoints.
D'une façon ou d'une autre à partir des réponses existantes je doute qu'il y ait des trucs qui pourraient s'appliquer
mise à Jour
alors j'ai pensé que je voudrais poster une mise à jour sur ce que j'ai mis en œuvre dans afin de minimiser la nécessité de faire des produits cartésiens et optimiser ainsi le moteur de requête en général. Notez qu'il n'est pas toujours possible d'éliminer complètement la nécessité pour les produits, mais il est presque toujours possible d'optimiser la taille de deux ensembles de joints est beaucoup plus petite.
puisque chaque modèle de graphe de base (BGP) qui est un ensemble de trois modèles est exécuté comme un bloc (essentiellement) le moteur est libre de réordonner des modèles dans un BGP pour une performance optimale. Exemple considérons le BGP suivant:
?a :someProperty ?b .
?c :anotherProperty ?d .
?b a :Class .
exécutée comme la requête nécessite un produit cartésien puisque les résultats du premier pattern sont disjoints du second pattern de sorte que les résultats des deux premiers patterns sont un produit cartésien de leurs résultats individuels. Ce résultat contiendra beaucoup plus de résultats que nous avons réellement besoin puisque le troisième modèle restreint les résultats possibles du premier modèle mais nous n'appliquons cette restriction qu'après. Mais si nous réarrangeons comme donc:
?b a :Class .
?a :someProperty ?b .
?c :anotherProperty ?d .
nous aurons encore besoin d'un produit cartésien pour obtenir les résultats finaux puisque les 2nd et 3rd patterns sont toujours disjoints mais en réordonnant nous restreignons la taille des résultats du 2nd pattern, ce qui signifie que la taille de notre produit cartésien sera beaucoup plus petite.
il y a plusieurs autres optimisations que nous faisons mais je ne vais pas les poster ici car cela commence à entrer dans une discussion assez détaillée des internes du moteur SPARQL. Si quelqu'un est intéressé à plus de détails laissez un commentaire ou envoyez-moi un tweet @RobVesse
6 réponses
La complexité du produit cartésien est O( n2), il n'y a pas de raccourci.
Dans certains cas, l'ordre vous parcourez vos deux axes est important. Par exemple, si votre code est de visiter chaque fente dans un tableau ou chaque pixel dans une image, alors vous devriez essayer de visiter les fentes dans l'ordre naturel. Une image est typiquement présentée dans ' scanlines’, donc les pixels sur n'importe quel O sont adjacentes. Par conséquent, vous devriez itérer sur le O X sur l'intérieure.
Si vous avez besoin du produit cartésien ou wherther est un algorithme plus efficace dépend du problème à résoudre.
vous ne pouvez pas vraiment changer la performance d'une boucle imbriquée sans quelques connaissances supplémentaires, mais cela serait spécifique à l'utilisation. Si vous avez n
éléments is
et m
éléments js
, il est toujours en O(n*m).
Vous pouvez changer le de bien:
var qry = from i in is
from j in js
select /*something involving i/j */;
C'est toujours en O(n*m), mais a nominal extra au-dessus de LINQ (vous ne le remarquerez pas dans l'usage normal, Cependant).
Qu'êtes-vous faire dans cas? Il y a peut être des trucs...
Une chose à certainement eviter est tout ce qui oblige une jonction croisée à amortir-le foreach
approche est fine et n'a pas de tampon, mais si vous ajoutez chaque élément à un List<>
, alors méfiez-vous des implications de mémoire. Idem OrderBy
etc (si utilisé de manière inappropriée).
je ne peux pas proposer quelque chose de mieux, que O(n^2), parce que c'est le taille de la sortie, et l'algorithme ne peut donc pas être plus vite.
ce que je peux suggérer est d'utiliser une autre approche à savoir si vous avez besoin de calculer produit. Par exemple, vous n'avez peut-être même pas besoin de l'ensemble de produits P
si seulement vous allez à se demander si certains éléments font partie. Vous n'avez besoin que des informations sur les décors.
en Effet (pseudo-code)
bool IsInSet(pair (x,y), CartesianProductSet P)
{
return IsInHash(x,P.set[1]) && IsInHash(y,P.set[2])
}
où le produit Cartésien est calculé comme ceci:
// Cartesian product of A and B is
P.set[1]=A; P.set[2]=B;
si vous implémentez des sets comme hashes, alors cherchez dans un produit cartésien de m
ensembles est juste une recherche dans m
hachages vous obtenez gratuitement. Construction du produit cartésien et IsInSet
rechercher chaque prise O(m)
fuseau, où m
est un nombre de sets que vous multipliez, et c'est beaucoup moins que n
--taille de chaque ensemble.
des renseignements supplémentaires ont été ajoutés à la question.
les doublons peuvent être évités si vous enregistrez ceux que vous avez déjà calculés afin d'éviter de les dupliquer à nouveau - il est supposé que le coût d'une telle comptabilité - un hashmap ou une simple liste - est moins que le coût de calcul d'un duplicata.
le C # runtime est vraiment très rapide, mais pour le levage extrême lourd, vous pourriez vouloir tomber dans le code natif.
vous pourriez aussi remarquer l'essentiel parallelness de ce problème. Si le calcul d'un produit n'a pas d'impact sur le calcul de tout autre produit, vous pouvez directement utiliser un processeur multiples noyaux pour faire le travail en parallèle. Regardez pool de threads.QueueUserWorkItem.
si la localisation du cache (ou la mémoire locale requise pour maintenir les j) est un problème, vous pouvez rendre votre algorithme plus convivial en divisant les tableaux d'entrées de façon récursive. Quelque chose comme:
cartprod(is,istart,ilen, js,jstart,jlen) {
if(ilen <= IMIN && jlen <= JMIN) { // base case
for(int i in is) {
for(int j in js) {
// pair i and j
}
}
return;
}
if(ilen > IMIN && jlen > JMIN) { // divide in 4
ilen2= ilen>>1;
jlen2= jlen>>1;
cartprod(is,istart,ilen2, js,jstart,jlen2);
cartprod(is,istart+ilen2,ilen-ilen2, js,jstart,jlen2);
cartprod(is,istart+ilen2,ilen-ilen2, js,jstart+jlen2,jlen-jlen2);
cartprod(is,istart,ilen2, js,jstart+jlen2,jlen-jlen2);
return;
}
// handle other cases...
}
notez que ce pattern d'accès va automatiquement profiter assez bien de tous les niveaux de cache automatique; ce genre de technique s'appelle cache-inconscients conception de l'algorithme.
Je ne sais pas comment écrire Java-like-Iterators en C#, mais peut-être que vous savez et pouvez transférer ma solution ici pour C# vous-même.
il peut être intéressant si vos combinaisons sont trop grandes pour les garder complètement en mémoire.
cependant, si vous filtrez par attribut sur la collection, vous devriez filtrer avant de construire la combinaison. Exemple:
si vous avez des nombres de 1 à 1000 et des mots aléatoires et les combiner, puis filtrer ces combinaisons, où le nombre est divisible par 20 et le mot commence par "d", vous pouvez avoir 1000*(26*x)=26000*x combinaisons à rechercher.
ou vous filtrez les nombres en premier, ce qui vous donne 50 nombres, et (si distribués également) 1 caractère, qui sont seulement 50*x éléments à la fin.