Cuenta la cantidad de palabras cíclicas en una entrada

9

Palabras cíclicas

Planteamiento del problema

Podemos pensar en una palabra cíclica como una palabra escrita en un círculo. Para representar una palabra cíclica, elegimos una posición de inicio arbitraria y leemos los caracteres en el sentido de las agujas del reloj. Entonces, "imagen" y "turepic" son representaciones para la misma palabra cíclica.

Se le da una cadena de palabras [], cada elemento de los cuales es una representación de una palabra cíclica. Devuelve el número de diferentes palabras cíclicas que se representan.

Victorias más rápidas (Big O, donde n = número de caracteres en una cadena)

piernas de eggon
fuente
3
Si está buscando críticas a su código, entonces el lugar para ir es codereview.stackexchange.com.
Peter Taylor
Frio. Editaré para enfatizar el desafío y moveré la parte de crítica a la revisión de código. Gracias Peter
eggonlegs
1
¿Cuál es el criterio ganador? ¿El código más corto (Code Golf) o cualquier otra cosa? ¿Hay alguna limitación en la forma de entrada y salida? ¿Necesitamos escribir una función o un programa completo? ¿Tiene que estar en Java?
ugoren
1
@eggonlegs ¿Ha especificado big-O, pero con respecto a qué parámetro? ¿Número de cadenas en la matriz? ¿Es la comparación de cadenas entonces O (1)? ¿O el número de caracteres en la cadena o el número total de caracteres? ¿O algo más?
Howard
1
@dude, seguramente son las 4?
Peter Taylor

Respuestas:

4

Pitón

Aquí está mi solución. Creo que todavía podría ser O (n 2 ), pero creo que el caso promedio es mucho mejor que eso.

Básicamente funciona normalizando cada cadena para que cualquier rotación tenga la misma forma. Por ejemplo:

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

La normalización se realiza buscando el carácter mínimo (por código de caracteres) y girando la cadena para que ese carácter esté en la última posición. Si ese carácter aparece más de una vez, se utilizan los caracteres después de cada aparición. Esto le da a cada palabra cíclica una representación canónica, que puede usarse como clave en un mapa.

La normalización es n 2 en el peor de los casos (donde cada carácter de la cadena es el mismo, por ejemplo aaaaaa), pero la mayoría de las veces solo habrá unas pocas ocurrencias, y el tiempo de ejecución estará más cerca n.

En mi computadora portátil (Intel Atom de doble núcleo a 1.66 GHz y 1 GB de RAM), ejecutar esto /usr/share/dict/words(234,937 palabras con una longitud promedio de 9.5 caracteres) toma alrededor de 7.6 segundos.

#!/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")
Gordon Bailey
fuente
3

Python (3) otra vez

El método que utilicé fue calcular un hash rodante de cada palabra comenzando en cada carácter de la cadena; Como es un hash rodante, se necesita tiempo O (n) (donde n es la longitud de la palabra) para calcular todos los n hashes. La cadena se trata como un número base-1114112, lo que garantiza que los hashes sean únicos. (Esto es similar a la solución de Haskell, pero más eficiente ya que solo pasa por la cadena dos veces).

Luego, para cada palabra de entrada, el algoritmo verifica su hash más bajo para ver si ya está en el conjunto de hashes vistos (un conjunto de Python, por lo tanto, la búsqueda es O (1) en el tamaño del conjunto); si es así, entonces la palabra o una de sus rotaciones ya se ha visto. De lo contrario, agrega ese hash al conjunto.

El argumento de la línea de comandos debe ser el nombre de un archivo que contiene una palabra por línea (como /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])
Lowjacker
fuente
1

Haskell

No estoy seguro de la eficacia de esto, lo más probable es que sea bastante malo. La idea es crear primero todas las rotaciones posibles de todas las palabras, contar los valores que representan de forma única las cadenas y seleccionar el mínimo. De esa forma obtenemos un número que es exclusivo de un grupo cíclico.
Podemos agrupar por este número y verificar el número de estos grupos.

Si n es el número de palabras en la lista ym es la longitud de una palabra, entonces calcular el 'número de grupo cíclico' para todas las palabras es O(n*m), ordenar O(n log n)y agrupar 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
shiona
fuente
1

Mathematica

Decidí comenzar de nuevo, ahora que entiendo las reglas del juego (creo).

Un diccionario de 10000 palabras de "palabras" únicas compuestas al azar (solo minúsculas) de longitud 3. De manera similar, se crearon otros diccionarios que consisten en cadenas de longitud 4, 5, 6, 7 y 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];

gtoma la versión actual del diccionario para verificar. La palabra superior se une con variantes cíclicas (si existe alguna). La palabra y sus coincidencias se agregan a la lista de salida outde palabras procesadas. Las palabras de salida se eliminan del diccionario.

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 recorre todas las palabras del diccionario.

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

Ejemplo 1 : palabras reales

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

{{"filete", "té"}, {"mano"}, {"ollas", "mancha"}, {"espada", "palabras"}}
4


Ejemplo 2 : palabras artificiales. Diccionario de cadenas de longitud 3. Primero, sincronización. Luego el número de palabras del ciclo.

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

d3

5402


Tiempos en función de la longitud de la palabra . 10000 palabras en cada diccionario.

tiempos

No sé cómo interpretar los resultados en términos de O. En términos simples, el tiempo se duplica aproximadamente del diccionario de tres caracteres al diccionario de cuatro caracteres. El tiempo aumenta de manera insignificante de 4 a 8 caracteres.

DavidC
fuente
¿Puedes publicar un enlace al diccionario que usaste para que pueda compararlo con el tuyo?
eggonlegs
El siguiente enlace a dictionary.txt debería funcionar: bitshare.com/files/oy62qgro/dictionary.txt.html (Perdón por el momento en que tendrá que esperar a que comience la descarga). Por cierto, el archivo tiene 3char, 4char ... 8char diccionarios todos juntos, 10000 palabras en cada uno. Querrás separarlos.
DavidC
Increíble. Muchas gracias :)
eggonlegs
1

Esto se puede hacer en O (n) evitando el tiempo cuadrático. La idea es construir el círculo completo atravesando la cadena base dos veces. Así que construimos "amazingamazin" como la cadena de círculo completo para verificar todas las cadenas cíclicas correspondientes a "amazing".

A continuación se muestra la solución 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
Azee
fuente
0

No sé si esto es muy eficiente, pero esta es mi primera grieta.

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;
}
piernas de eggon
fuente
0

Perl

No estoy seguro de entender el problema, pero esto coincide con el ejemplo @dude publicado en los comentarios al menos. por favor corrija mi análisis seguramente incorrecto.

para cada palabra W en las N palabras dadas de la lista de cadenas, debe recorrer todos los caracteres de W en el peor de los casos. Tengo que asumir que las operaciones hash se realizan en tiempo constante.

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), $/;
ardnew
fuente