Conta il numero di parole cicliche in un input


9

Parole cicliche

Dichiarazione problema

Possiamo pensare a una parola ciclica come a una parola scritta in un cerchio. Per rappresentare una parola ciclica, scegliamo una posizione iniziale arbitraria e leggiamo i caratteri in senso orario. Quindi, "immagine" e "turepico" sono rappresentazioni della stessa parola ciclica.

Ti viene data una stringa [] parole, ogni cui elemento è una rappresentazione di una parola ciclica. Restituisce il numero di diverse parole cicliche rappresentate.

Vince più velocemente (Big O, dove n = numero di caratteri in una stringa)


3
Se stai cercando critiche al tuo codice, allora il posto dove andare è codereview.stackexchange.com.
Peter Taylor,

Freddo. Modificherò per enfatizzare la sfida e sposterò la parte critica alla revisione del codice. Grazie Peter.
eggonlegs,

1
Quali sono i criteri vincenti? Il codice più breve (Code Golf) o qualcos'altro? Ci sono delle limitazioni nella forma di input e output? Dobbiamo scrivere una funzione o un programma completo? Deve essere in Java?
ugoren,

1
@eggonlegs Hai specificato big-O - ma rispetto a quale parametro? Numero di stringhe nell'array? Il confronto delle stringhe è quindi O (1)? O il numero di caratteri nella stringa o il numero totale di caratteri? O qualsiasi altra cosa?
Howard,

1
@dude, sicuramente sono 4?
Peter Taylor,

Risposte:


4

Pitone

Ecco la mia soluzione Penso che potrebbe essere ancora O (n 2 ), ma penso che il caso medio sia molto meglio di così.

Fondamentalmente funziona normalizzando ogni stringa in modo che qualsiasi rotazione abbia la stessa forma. Per esempio:

'amazing' -> 'mazinga'
'mazinga' -> 'mazinga'
'azingam' -> 'mazinga'
'zingama' -> 'mazinga'
'ingamaz' -> 'mazinga'
'ngamazi' -> 'mazinga'
'gamazin' -> 'mazinga'

La normalizzazione viene eseguita cercando il carattere minimo (tramite il codice char) e ruotando la stringa in modo che il carattere si trovi nell'ultima posizione. Se quel personaggio compare più di una volta, vengono usati i caratteri dopo ogni occorrenza. Ciò conferisce a ogni parola ciclica una rappresentazione canonica, che può essere utilizzata come chiave in una mappa.

La normalizzazione è n 2 nel caso peggiore (in cui ogni carattere nella stringa è lo stesso, ad es. aaaaaa), Ma il più delle volte ci saranno solo alcune occorrenze e il tempo di esecuzione sarà più vicino n.

Sul mio laptop (Intel Atom dual core a 1,66 GHz e 1 GB di RAM), l'esecuzione su /usr/share/dict/words(234.937 parole con una lunghezza media di 9,5 caratteri) richiede circa 7,6 secondi.

#!/usr/bin/python

import sys

def normalize(string):
   # the minimum character in the string
   c = min(string) # O(n) operation
   indices = [] # here we will store all the indices where c occurs
   i = -1       # initialize the search index
   while True: # finding all indexes where c occurs is again O(n)
      i = string.find(c, i+1)
      if i == -1:
         break
      else:
         indices.append(i)
   if len(indices) == 1: # if it only occurs once, then we're done
      i = indices[0]
      return string[i:] + string[:i]
   else:
      i = map(lambda x:(x,x), indices)
      for _ in range(len(string)):                       # go over the whole string O(n)
         i = map(lambda x:((x[0]+1)%len(string), x[1]), i)  # increment the indexes that walk along  O(m)
         c = min(map(lambda x: string[x[0]], i))    # get min character from current indexes         O(m)
         i = filter(lambda x: string[x[0]] == c, i) # keep only the indexes that have that character O(m)
         # if there's only one index left after filtering, we're done
         if len(i) == 1:
            break
      # either there are multiple identical runs, or
      # we found the unique best run, in either case, we start the string from that
      # index
      i = i[0][0]
      return string[i:] + string[:i]

def main(filename):
   cyclic_words = set()
   with open(filename) as words:
      for word in words.readlines():
         cyclic_words.add(normalize(word[:-1])) # normalize without the trailing newline
   print len(cyclic_words)

if __name__ == '__main__':
   if len(sys.argv) > 1:
      main(sys.argv[1])
   else:
      main("/dev/stdin")

3

Python (3) di nuovo

Il metodo che ho usato era di calcolare un hash rolling di ogni parola a partire da ciascun carattere della stringa; dal momento che è un hash rolling, ci vuole O (n) (dove n è la lunghezza della parola) tempo per calcolare tutti gli n hash. La stringa viene trattata come un numero di base 1114112, il che garantisce che gli hash siano univoci. (Questo è simile alla soluzione Haskell, ma più efficiente poiché attraversa la stringa solo due volte.)

Quindi, per ogni parola di input, l'algoritmo controlla il suo hash più basso per vedere se è già nel set di hash visti (un set di Python, quindi la ricerca è O (1) nella dimensione del set); se lo è, allora la parola o una delle sue rotazioni è già stata vista. Altrimenti, aggiunge quell'hash all'insieme.

L'argomento della riga di comando dovrebbe essere il nome di un file che contiene una parola per riga (come /usr/share/dict/words).

import sys

def rollinghashes(string):
    base = 1114112
    curhash = 0
    for c in string:
        curhash = curhash * base + ord(c)
    yield curhash
    top = base ** len(string)
    for i in range(len(string) - 1):
        curhash = curhash * base % top + ord(string[i])
        yield curhash

def cycles(words, keepuniques=False):
    hashes = set()
    uniques = set()
    n = 0
    for word in words:
        h = min(rollinghashes(word))
        if h in hashes:
            continue
        else:
            n += 1
            if keepuniques:
                uniques.add(word)
            hashes.add(h)
    return n, uniques

if __name__ == "__main__":
    with open(sys.argv[1]) as words_file:
        print(cycles(line.strip() for line in words_file)[0])

1

Haskell

Non sono sicuro dell'efficienza di questo, molto probabilmente piuttosto male. L'idea è di creare prima tutte le possibili rotazioni di tutte le parole, contare i valori che rappresentano in modo univoco le stringhe e selezionare il minimo. In questo modo otteniamo un numero unico per un gruppo ciclico.
Possiamo raggruppare per questo numero e controllare il numero di questi gruppi.

Se n è il numero di parole nell'elenco e m è la lunghezza di una parola, calcolare il "numero di gruppo ciclico" per tutte le parole è O(n*m), ordinamento O(n log n)e raggruppamento O(n).

import Data.List
import Data.Char
import Data.Ord
import Data.Function

groupUnsortedOn f = groupBy ((==) `on` f) . sortBy(compare `on` f)
allCycles w = init $ zipWith (++) (tails w)(inits w)
wordval = foldl (\a b -> a*256 + (fromIntegral $ ord b)) 0
uniqcycle = minimumBy (comparing wordval) . allCycles
cyclicGroupCount = length . groupUnsortedOn uniqcycle

1

matematica

Ho deciso di ricominciare, ora che ho capito le regole del gioco (penso).

Un dizionario di 10000 parole di "parole" univoche composte casualmente (solo lettere minuscole) di lunghezza 3. In modo simile sono stati creati altri dizionari costituiti da stringhe di lunghezza 4, 5, 6, 7 e 8.

ClearAll[dictionary]      
dictionary[chars_,nWords_]:=DeleteDuplicates[Table[FromCharacterCode@RandomInteger[{97,122},
chars],{nWords}]];
n=16000;
d3=Take[dictionary[3,n],10^4];
d4=Take[dictionary[4,n],10^4];
d5=Take[dictionary[5,n],10^4];
d6=Take[dictionary[6,n],10^4];
d7=Take[dictionary[7,n],10^4];
d8=Take[dictionary[8,n],10^4];

gprende la versione corrente del dizionario per verificare. La parola in cima è unita a varianti cicliche (se ne esistono). La parola e le sue corrispondenze vengono aggiunte all'elenco di output outdelle parole elaborate. Le parole di output vengono rimosse dal dizionario.

g[{wds_,out_}] := 
   If[wds=={},{wds,out},
   Module[{s=wds[[1]],t,c},
   t=Table[StringRotateLeft[s, k], {k, StringLength[s]}];
   c=Intersection[wds,t];
   {Complement[wds,t],Append[out,c]}]]

f scorre attraverso tutte le parole del dizionario.

f[dict_]:=FixedPoint[g,{dict,{}}][[2]]

Esempio 1 : parole reali

r = f[{"teaks", "words", "spot", "pots", "sword", "steak", "hand"}]
Length[r]

{{"bistecca", "teaks"}, {"mano"}, {"pentole", "spot"}, {"spada", "parole"}}
4


Esempio 2 : parole artificiali. Dizionario di stringhe di lunghezza 3. Primo, tempismo. Quindi il numero di parole del ciclo.

f[d3]//AbsoluteTiming
Length[%[[2]]]

d3

5402


Tempi in funzione della lunghezza della parola . 10000 parole in ciascun dizionario.

temporizzazioni

Non so particolarmente come interpretare i risultati in termini di O. In termini semplici, i tempi raddoppiano approssimativamente dal dizionario dei tre caratteri al dizionario dei quattro caratteri. Il tempismo aumenta quasi trascurabilmente da 4 a 8 caratteri.


Puoi forse pubblicare un link al dizionario che hai usato in modo da poterlo confrontare con il tuo?
eggonlegs,

Il seguente link a dizionari.txt dovrebbe funzionare: bitshare.com/files/oy62qgro/dictionary.txt.html (Mi dispiace per il minuto in cui dovrete attendere l'inizio del download.) A proposito, il file ha il 3char, 4char ... dizionari 8char tutti insieme, 10000 parole in ciascuno. Ti consigliamo di separarli.
DavidC

Eccezionale. Grazie mille :)
eggonlegs

1

Questo può essere fatto in O (n) evitando il tempo quadratico. L'idea è quella di costruire il cerchio completo che attraversa due volte la stringa di base. Quindi costruiamo "amazingamazin" come la stringa del cerchio completo per controllare tutte le stringhe cicliche corrispondenti a "amazing".

Di seguito è la soluzione Java:

public static void main(String[] args){
    //args[0] is the base string and following strings are assumed to be
    //cyclic strings to check 
    int arrLen = args.length;
    int cyclicWordCount = 0;
    if(arrLen<1){
        System.out.println("Invalid usage. Supply argument strings...");
        return;
    }else if(arrLen==1){
        System.out.println("Cyclic word count=0");
        return;         
    }//if

    String baseString = args[0];
    StringBuilder sb = new StringBuilder();
    // Traverse base string twice appending characters
    // Eg: construct 'amazingamazin' from 'amazing'
    for(int i=0;i<2*baseString.length()-1;i++)
        sb.append(args[0].charAt(i%baseString.length()));

    // All cyclic strings are now in the 'full circle' string
    String fullCircle = sb.toString();
    System.out.println("Constructed string= "+fullCircle);

    for(int i=1;i<arrLen;i++)
    //Do a length check in addition to contains
     if(baseString.length()==args[i].length()&&fullCircle.contains(args[i])){
        System.out.println("Found cyclic word: "+args[i]);
        cyclicWordCount++;
    }

    System.out.println("Cyclic word count= "+cyclicWordCount);
}//main

0

Non so se questo è molto efficace, ma questa è la mia prima crepa.

private static int countCyclicWords(String[] input) {
    HashSet<String> hashSet = new HashSet<String>();
    String permutation;
    int count = 0;

    for (String s : input) {
        if (hashSet.contains(s)) {
            continue;
        } else {
            count++;
            for (int i = 0; i < s.length(); i++) {
                permutation = s.substring(1) + s.substring(0, 1);
                s = permutation;
                hashSet.add(s);
            }
        }
    }

    return count;
}

0

Perl

non sono sicuro di aver capito il problema, ma questo corrisponde almeno all'esempio @dude pubblicato nei commenti. per favore correggi la mia analisi sicuramente errata.

per ogni parola W nelle N parole indicate nell'elenco di stringhe, è necessario scorrere tutti i caratteri di W nel caso peggiore. devo presumere che le operazioni di hash vengano eseguite in tempo costante.

use strict;
use warnings;

my @words = ( "teaks", "words", "spot", "pots", "sword", "steak", "hand" );

sub count
{
  my %h = ();

  foreach my $w (@_)
  {
    my $n = length($w);

    # concatenate the word with itself. then all substrings the
    # same length as word are rotations of word.
    my $s = $w . $w;

    # examine each rotation of word. add word to the hash if
    # no rotation already exists in the hash
    $h{$w} = undef unless
      grep { exists $h{substr $s, $_, $n} } 0 .. $n - 1;
  }

  return keys %h;
}

print scalar count(@words), $/;
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.