web-dev-qa-db-fra.com

Trouver le nombre d'occurrences d'une sous-séquence dans une chaîne

Par exemple, supposons que la chaîne soit constituée des 10 premiers chiffres de pi, 3141592653, et que la sous-séquence soit 123. Notez que la séquence se produit deux fois:

3141592653
 1    2  3
   1  2  3

C'était une question d'entrevue à laquelle je ne pouvais pas répondre et je ne pouvais pas penser à un algorithme efficace et cela me dérange. Je pense qu'il devrait être possible de le faire avec une simple expression rationnelle, mais ceux comme 1.*2.*3 ne renvoient pas toutes les sous-séquences. Mon implémentation naïve en Python (compter les 3 pour chaque 2 après chaque 1) a fonctionné pendant une heure et ce n’est pas fait.

57
Jake

C'est un problème classique programmation dynamique (et qui n'est généralement pas résolu à l'aide d'expressions régulières).

Mon implémentation naïve (comptez les 3 pour chaque 2 après chaque 1) fonctionne depuis une heure et ce n'est pas terminé.

Ce serait une approche de recherche exhaustive qui s'exécute dans le temps exponentiel. (Je suis surpris qu'il dure des heures cependant).


Voici une suggestion pour une solution de programmation dynamique:

Esquisse d'une solution récursive:

(Toutes mes excuses pour la description longue, mais chaque étape est vraiment simple alors supportez-moi ;-)

  • Si la sous-séquence est vide, une correspondance est trouvée (il ne reste plus de chiffres à faire correspondre!) Et nous retournons 1

  • Si la séquence d'entrée est vide, nous avons épuisé nos chiffres et nous ne pouvons trouver aucune correspondance, nous retournons donc 0.

  • (Ni la séquence ni la sous-séquence ne sont vides.)

  • (Supposons que " abcdef " indique la séquence d'entrée et " xyz "désigne la sous-séquence.)

  • Définissez result sur 0

  • Ajoutez à la result le nombre de correspondances pour bcdef et xyz (c.-à-d., ignore le premier chiffre entré et renvoie)

  • Si les deux premiers chiffres correspondent, c'est-à-dire, a = x

    • Ajoutez à la result le nombre de correspondances pour bcdef et yz (c'est-à-dire, faire correspondre le premier chiffre de la sous-séquence et effectuer une récurrence sur les autres chiffres de la sous-séquence)

  • Retourne result


Exemple

Voici une illustration des appels récursifs pour l’entrée 1221/ 12 . (La sous-séquence est en gras, · représente une chaîne vide.)

enter image description here


Programmation dynamique

Si implémenté naïvement, certains (sous-) problèmes sont résolus plusieurs fois (·/2 par exemple dans l'illustration ci-dessus). La programmation dynamique évite de tels calculs redondants en se souvenant des résultats des sous-problèmes précédemment résolus (généralement dans une table de recherche).

Dans ce cas particulier, nous avons créé une table avec

  • [longueur de la séquence + 1] rangées, et
  • [longueur de la sous-séquence + 1] colonnes:

enter image description here

L'idée est que nous devions indiquer le nombre de correspondances pour 221/ 2 dans la ligne/colonne correspondante. Une fois cela fait, nous devrions avoir la solution finale dans la cellule 1221/ 12 .

Nous commençons à renseigner la table avec ce que nous savons immédiatement (les "cas de base"):

  • Quand il ne reste plus de chiffres de sous-séquence, nous avons 1 correspondance complète:

enter image description here

  • Quand il ne reste plus de chiffres de séquence, nous ne pouvons avoir aucune correspondance:

    enter image description here

Nous procédons ensuite en renseignant le tableau de haut en bas/de gauche à droite selon la règle suivante:

  • Dans la cellule [ rangée ] [ col ] écrivez la valeur trouvée entre [ rangée -1] [col].

    Intuitivement, cela signifie "Le nombre de correspondances pour 221/ 2 inclut toutes les correspondances pour 21/ 2 . "

  • Si séquence à la ligne , la ligne et le sous-programme à la colonne col commencent par le même chiffre, ajoutez la valeur trouvée à [ rangée - 1] [ col - 1] à la valeur qui vient d'être écrite dans [ rangée ] [ col ].

    Intuitivement, cela signifie "Le nombre de correspondances pour 1221/ 12 inclut également toutes les correspondances pour 221/ 12 . "

enter image description here

Le résultat final se présente comme suit:

enter image description here

et la valeur dans la cellule en bas à droite est bien 2.


Dans du code

Pas en Python, (mes excuses).

class SubseqCounter {

    String seq, subseq;
    int[][] tbl;

    public SubseqCounter(String seq, String subseq) {
        this.seq = seq;
        this.subseq = subseq;
    }

    public int countMatches() {
        tbl = new int[seq.length() + 1][subseq.length() + 1];

        for (int row = 0; row < tbl.length; row++)
            for (int col = 0; col < tbl[row].length; col++)
                tbl[row][col] = countMatchesFor(row, col);

        return tbl[seq.length()][subseq.length()];
    }

    private int countMatchesFor(int seqDigitsLeft, int subseqDigitsLeft) {
        if (subseqDigitsLeft == 0)
            return 1;

        if (seqDigitsLeft == 0)
            return 0;

        char currSeqDigit = seq.charAt(seq.length()-seqDigitsLeft);
        char currSubseqDigit = subseq.charAt(subseq.length()-subseqDigitsLeft);

        int result = 0;

        if (currSeqDigit == currSubseqDigit)
            result += tbl[seqDigitsLeft - 1][subseqDigitsLeft - 1];

        result += tbl[seqDigitsLeft - 1][subseqDigitsLeft];

        return result;
    }
}

Complexité

Un avantage supplémentaire de cette approche de type "à compléter" est qu'il est trivial de comprendre la complexité. Une quantité constante de travail est effectuée pour chaque cellule et nous avons des lignes de longueur de séquence et des colonnes de longueur de sous-séquence. La complexité est donc O (MN) M et N désignent les longueurs des séquences.

116
aioobe

Super réponse, aioobe ! Pour compléter votre réponse, quelques implémentations possibles en Python:

# straightforward, naïve solution; too slow!

def num_subsequences(seq, sub):
    if not sub:
        return 1
    Elif not seq:
        return 0
    result = num_subsequences(seq[1:], sub)
    if seq[0] == sub[0]:
        result += num_subsequences(seq[1:], sub[1:])
    return result

# top-down solution using explicit memoization

def num_subsequences(seq, sub):
    m, n, cache = len(seq), len(sub), {}
    def count(i, j):
        if j == n:
            return 1
        Elif i == m:
            return 0
        k = (i, j)
        if k not in cache:
            cache[k] = count(i+1, j) + (count(i+1, j+1) if seq[i] == sub[j] else 0)
        return cache[k]
    return count(0, 0)

# top-down solution using the lru_cache decorator
# available from functools in python >= 3.2

from functools import lru_cache

def num_subsequences(seq, sub):
    m, n = len(seq), len(sub)
    @lru_cache(maxsize=None)
    def count(i, j):
        if j == n:
            return 1
        Elif i == m:
            return 0
        return count(i+1, j) + (count(i+1, j+1) if seq[i] == sub[j] else 0)
    return count(0, 0)

# bottom-up, dynamic programming solution using a lookup table

def num_subsequences(seq, sub):
    m, n = len(seq)+1, len(sub)+1
    table = [[0]*n for i in xrange(m)]
    def count(iseq, isub):
        if not isub:
            return 1
        Elif not iseq:
            return 0
        return (table[iseq-1][isub] +
               (table[iseq-1][isub-1] if seq[m-iseq-1] == sub[n-isub-1] else 0))
    for row in xrange(m):
        for col in xrange(n):
            table[row][col] = count(row, col)
    return table[m-1][n-1]

# bottom-up, dynamic programming solution using a single array

def num_subsequences(seq, sub):
    m, n = len(seq), len(sub)
    table = [0] * n
    for i in xrange(m):
        previous = 1
        for j in xrange(n):
            current = table[j]
            if seq[i] == sub[j]:
                table[j] += previous
            previous = current
    return table[n-1] if n else 1
14
Óscar López

Une façon de le faire serait d'utiliser deux listes. Appelez-les Ones et OneTwos.

Parcourez la chaîne, caractère par caractère.

  • Lorsque vous voyez le chiffre 1, saisissez une entrée dans la liste Ones.
  • Chaque fois que vous voyez le chiffre 2, parcourez la liste Ones et ajoutez une entrée à la liste OneTwos.
  • Chaque fois que vous voyez le chiffre 3, parcourez la liste OneTwos et générez un 123.

Dans le cas général, cet algorithme sera très rapide puisqu'il s'agira d'un seul passage dans la chaîne et de plusieurs passages dans des listes normalement beaucoup plus petites. Les cas pathologiques vont le tuer, cependant. Imaginez une chaîne comme 111111222222333333, mais avec chaque chiffre répété des centaines de fois.

7
Jim Mischel
from functools import lru_cache

def subseqsearch(string,substr):
    substrset=set(substr)
    #fixs has only element in substr
    fixs = [i for i in string if i in substrset]
    @lru_cache(maxsize=None) #memoisation decorator applyed to recs()
    def recs(fi=0,si=0):
        if si >= len(substr):
            return 1
        r=0
        for i in range(fi,len(fixs)):
            if substr[si] == fixs[i]:
                r+=recs(i+1,si+1)
        return r
    return recs()

#test
from functools import reduce
def flat(i) : return reduce(lambda x,y:x+y,i,[])
N=5
string = flat([[i for j in range(10) ] for i in range(N)])
substr = flat([[i for j in range(5) ] for i in range(N)]) 
print("string:","".join(str(i) for i in string),"substr:","".join(str(i) for i in substr),sep="\n")
print("result:",subseqsearch(string,substr))

sortie (instantanément):

string:
00000000001111111111222222222233333333334444444444
substr:
0000011111222223333344444
result: 1016255020032
2
Luka Rahne

Une réponse Javascript basée sur la programmation dynamique de geeksforgeeks.org et la réponse de aioobe :

class SubseqCounter {
    constructor(subseq, seq) {
        this.seq = seq;
        this.subseq = subseq;
        this.tbl = Array(subseq.length + 1).fill().map(a => Array(seq.length + 1));
        for (var i = 1; i <= subseq.length; i++)
          this.tbl[i][0] = 0;
        for (var j = 0; j <= seq.length; j++)
          this.tbl[0][j] = 1;
    }
    countMatches() {
        for (var row = 1; row < this.tbl.length; row++)
            for (var col = 1; col < this.tbl[row].length; col++)
                 this.tbl[row][col] = this.countMatchesFor(row, col);

        return this.tbl[this.subseq.length][this.seq.length];
    }
    countMatchesFor(subseqDigitsLeft, seqDigitsLeft) {
            if (this.subseq.charAt(subseqDigitsLeft - 1) !=     this.seq.charAt(seqDigitsLeft - 1)) 
            return this.tbl[subseqDigitsLeft][seqDigitsLeft - 1];  
            else
            return this.tbl[subseqDigitsLeft][seqDigitsLeft - 1] +     this.tbl[subseqDigitsLeft - 1][seqDigitsLeft - 1]; 
    }
}
0

Ma tentative rapide:

def count_subseqs(string, subseq):
    string = [c for c in string if c in subseq]
    count = i = 0
    for c in string:
        if c == subseq[0]:
            pos = 1
            for c2 in string[i+1:]:
                if c2 == subseq[pos]:
                    pos += 1
                    if pos == len(subseq):
                        count += 1
                        break
        i += 1
    return count

print count_subseqs(string='3141592653', subseq='123')

Edit: Celui-ci devrait être correct également si 1223 == 2 et les cas plus compliqués:

def count_subseqs(string, subseq):
    string = [c for c in string if c in subseq]
    i = 0
    seqs = []
    for c in string:
        if c == subseq[0]:
            pos = 1
            seq = [1]
            for c2 in string[i + 1:]:
                if pos > len(subseq):
                    break
                if pos < len(subseq) and c2 == subseq[pos]:
                    try:
                        seq[pos] += 1
                    except IndexError:
                        seq.append(1)
                        pos += 1
                Elif pos > 1 and c2 == subseq[pos - 1]:
                    seq[pos - 1] += 1
            if len(seq) == len(subseq):
                seqs.append(seq)
        i += 1
    return sum(reduce(lambda x, y: x * y, seq) for seq in seqs)

assert count_subseqs(string='12', subseq='123') == 0
assert count_subseqs(string='1002', subseq='123') == 0
assert count_subseqs(string='0123', subseq='123') == 1
assert count_subseqs(string='0123', subseq='1230') == 0
assert count_subseqs(string='1223', subseq='123') == 2
assert count_subseqs(string='12223', subseq='123') == 3
assert count_subseqs(string='121323', subseq='123') == 3
assert count_subseqs(string='12233', subseq='123') == 4
assert count_subseqs(string='0123134', subseq='1234') == 2
assert count_subseqs(string='1221323', subseq='123') == 5
0
Jakub

psh. Les solutions O(n) sont bien meilleures.

Pensez-y en construisant un arbre:

itérer le long de la chaîne si le caractère est '1', ajoutez un noeud à la racine de l'arbre . si le caractère est '2', ajoutez un enfant à chaque noeud de premier niveau . if le caractère est '3', ajoutez un enfant à chaque nœud de second niveau.

renvoie le nombre de nœuds de troisième couche.

ce serait inefficace, alors pourquoi ne pas stocker le nombre de nœuds à chaque profondeur:

infile >> in;
long results[3] = {0};
for(int i = 0; i < in.length(); ++i) {
    switch(in[i]) {
        case '1':
        results[0]++;
        break;
        case '2':
        results[1]+=results[0];
        break;
        case '3':
        results[2]+=results[1];
        break;
        default:;
    }
}

cout << results[2] << endl;
0
datdo

Comment compter toutes les séquences à trois membres 1..2..3 dans le tableau de chiffres.

Rapidement et simplement

Remarquez, nous n'avons pas besoin de TROUVER toutes les séquences, nous avons seulement besoin de les COMPTER. Ainsi, tous les algorithmes qui recherchent des séquences sont excessivement complexes.

  1. Jetez tous les chiffres, ce n'est pas 1,2,3. Le résultat sera char array A
  2. Faire parallèle int tableau B de 0. En parcourant A à partir de la fin, comptez pour chaque 2 dans A le nombre de 3 dans A après eux. Mettez ces nombres dans les éléments appropriés de B.
  3. Faites en sorte que le tableau int parallèle C soit égal à 0. Si vous exécutez A à partir du nombre final, pour chaque 1 dans A, la somme de B après sa position. Le résultat mis à la place appropriée dans C.
  4. Comptez la somme de C.

C'est tout. La complexité est O (N). Vraiment, pour une ligne de chiffres normale, le raccourcissement de la ligne source prend environ deux fois plus de temps. 

Si la séquence est plus longue, de M membres par exemple, la procédure peut être répétée M fois. Et la complexité sera O (MN), où N sera déjà la longueur de la chaîne source raccourcie.

0
Gangnus