Contar o número de palavras cíclicas em uma entrada


9

Palavras cíclicas

Declaração do Problema

Podemos pensar em uma palavra cíclica como uma palavra escrita em círculo. Para representar uma palavra cíclica, escolhemos uma posição inicial arbitrária e lemos os caracteres no sentido horário. Portanto, "imagem" e "turepic" são representações para a mesma palavra cíclica.

Você recebe as palavras String [], cada elemento do qual é uma representação de uma palavra cíclica. Retorne o número de palavras cíclicas diferentes que são representadas.

Vitórias mais rápidas (Big O, onde n = número de caracteres em uma string)


3
Se você está procurando críticas ao seu código, o melhor lugar é codereview.stackexchange.com.
Peter Taylor

Legal. Vou editar para enfatizar o desafio e mover a parte da crítica para a revisão do código. Obrigado Peter.
eggonlegs

11
Qual é o critério de vencimento? Código mais curto (Code Golf) ou qualquer outra coisa? Existem limitações na forma de entrada e saída? Precisamos escrever uma função ou um programa completo? Ele precisa estar em Java?
Ugoren

11
@eggonlegs Você especificou big-O - mas com relação a qual parâmetro? Número de strings na matriz? A comparação de strings é O (1)? Ou número de caracteres na string ou número total de caracteres? Ou qualquer outra coisa?
Howard

11
@ cara, certamente são 4?
Peter Taylor

Respostas:


4

Pitão

Aqui está a minha solução. Acho que ainda pode ser O (n 2 ), mas acho que o caso médio é muito melhor que isso.

Basicamente, ele funciona normalizando cada string para que qualquer rotação tenha a mesma forma. Por exemplo:

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

A normalização é feita procurando o caractere mínimo (por código char) e girando a string para que o caractere esteja na última posição. Se esse caractere ocorrer mais de uma vez, serão utilizados os caracteres após cada ocorrência. Isso fornece a cada palavra cíclica uma representação canônica, que pode ser usada como uma chave em um mapa.

A normalização é n 2 no pior caso (onde todos os caracteres da string são iguais, por exemplo aaaaaa), mas na maioria das vezes haverá apenas algumas ocorrências e o tempo de execução será mais próximo n.

No meu laptop (Intel Atom dual core de 1,66 GHz e 1 GB de RAM), a execução /usr/share/dict/words(234.937 palavras com um comprimento médio de 9,5 caracteres) leva cerca 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")

3

Python (3) novamente

O método que eu usei foi calcular um hash de rolagem de cada palavra começando em cada caractere na string; como é um hash contínuo, o O (n) (onde n é o comprimento da palavra) leva tempo para calcular todos os n hashes. A sequência é tratada como um número base-1114112, o que garante que os hashes sejam exclusivos. (Isso é semelhante à solução Haskell, mas mais eficiente, pois só passa pela cadeia de caracteres duas vezes.)

Então, para cada palavra de entrada, o algoritmo verifica seu hash mais baixo para ver se ele já está no conjunto de hashes visto (um conjunto Python, portanto, a pesquisa é O (1) no tamanho do conjunto); se for, a palavra ou uma de suas rotações já foi vista. Caso contrário, ele adiciona esse hash ao conjunto.

O argumento da linha de comando deve ser o nome de um arquivo que contém uma palavra por linha (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])

1

Haskell

Não tenho certeza sobre a eficiência disso, provavelmente muito ruim. A idéia é criar primeiro todas as rotações possíveis de todas as palavras, contar os valores que representam exclusivamente as seqüências e selecionar o mínimo. Dessa forma, obtemos um número exclusivo para um grupo cíclico.
Podemos agrupar por esse número e verificar o número desses grupos.

Se n for o número de palavras na lista e m for o comprimento de uma palavra, calcule o 'número do grupo cíclico' para todas as palavras O(n*m), classificando O(n log n)e agrupando 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

Mathematica

Decidi começar de novo, agora que entendo as regras do jogo (acho).

Um dicionário de 10000 palavras de "palavras" únicas compostas aleatoriamente (somente em minúsculas) de comprimento 3. De maneira semelhante, outros dicionários foram criados, consistindo em cadeias de comprimento 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];

gleva a versão atual do dicionário para verificar. A palavra superior é associada a variantes cíclicas (se houver alguma). A palavra e suas correspondências são anexadas à lista de saída out, de palavras processadas. As palavras de saída são removidas do dicionário.

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 percorre o dicionário de todas as palavras.

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

Exemplo 1 : palavras reais

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

{{"bife", "teca"}, {"mão"}, {"panelas", "ponto"}, {"espada", "palavras"}}
4


Exemplo 2 : Palavras artificiais. Dicionário de cadeias de comprimento 3. Primeiro, tempo. Então o número de palavras do ciclo.

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

d3

5402


Tempos em função do comprimento da palavra . 10000 palavras em cada dicionário.

horários

Não sei particularmente como interpretar as descobertas em termos de O. Em termos simples, o tempo praticamente dobra do dicionário de três caracteres para o dicionário de quatro caracteres. O tempo aumenta quase de forma insignificante de 4 a 8 caracteres.


Você pode postar um link para o dicionário que você usou para que eu possa comparar com o seu?
eggonlegs

O seguinte link para dictionary.txt deve funcionar: bitshare.com/files/oy62qgro/dictionary.txt.html (Desculpe o minuto em que você terá que esperar o início do download). BTW, o arquivo tem o 3char, 4char ... 8char dicionários todos juntos, 10.000 palavras em cada um. Você vai querer separá-los.
21813 DavidC

Impressionante. Muito obrigado :)
eggonlegs

1

Isso pode ser feito em O (n), evitando o tempo quadrático. A idéia é construir o círculo completo atravessando a corda base duas vezes. Então, construímos "amazingamazin" como a sequência de círculos completa para verificar todas as seqüências cíclicas correspondentes a "amazing".

Abaixo está a solução 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

Não sei se isso é muito eficiente, mas esse é meu primeiro crack.

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

Não sei se entendi o problema, mas isso corresponde ao exemplo @dude postado nos comentários pelo menos. corrija minha análise certamente incorreta.

para cada palavra W nas N palavras fornecidas da lista de cadeias, você deve percorrer todos os caracteres de W no pior caso. Eu tenho que assumir que as operações de hash são feitas em tempo 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), $/;
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.