Подсчитайте количество циклических слов на входе


9

Циклические Слова

Постановка задачи

Мы можем думать о циклическом слове как о слове, написанном по кругу. Чтобы представить циклическое слово, мы выбираем произвольную начальную позицию и читаем символы по часовой стрелке. Таким образом, «картинка» и «турепик» являются представлениями одного и того же циклического слова.

Вам дается слово String [], каждый элемент которого является представлением циклического слова. Возвращает количество различных циклических слов, которые представлены.

Самые быстрые победы (Big O, где n = количество символов в строке)


3
Если вы ищете критику своего кода, то вам стоит посетить codereview.stackexchange.com.
Питер Тейлор

Круто. Я отредактирую акцент на вызове и перейду часть критики к обзору кода. Спасибо, Питер.
eggonlegs

1
Каковы критерии победы? Кратчайший код (Code Golf) или что-нибудь еще? Есть ли ограничения на форму ввода и вывода? Нужно ли нам написать функцию или полную программу? Это должно быть в Java?
Угорен

1
@eggonlegs Вы указали big-O - но по какому параметру? Количество строк в массиве? Сравнение строк тогда O (1)? Или количество символов в строке или общее количество символов? Или что-нибудь еще?
Говард

1
@ чувак, конечно, это 4?
Питер Тейлор

Ответы:


4

питон

Вот мое решение. Я думаю, что это все еще может быть O (n 2 ), но я думаю, что средний случай намного лучше, чем это.

В основном это работает путем нормализации каждой строки, так что любое вращение будет иметь одинаковую форму. Например:

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

Нормализация выполняется путем поиска минимального символа (по коду символа) и поворота строки, чтобы символ находился в последней позиции. Если этот символ встречается более одного раза, используются символы после каждого вхождения. Это дает каждому циклическому слову каноническое представление, которое можно использовать в качестве ключа на карте.

Нормализация равна n 2 в худшем случае (где каждый символ в строке одинаков, например aaaaaa), но в большинстве случаев будет только несколько случаев, и время выполнения будет ближе к n.

На моем ноутбуке (двухъядерный Intel Atom @ /usr/share/dict/words1,66 ГГц и 1 ГБ оперативной памяти) его запуск (234937 слов со средней длиной 9,5 символов) занимает около 7,6 секунды.

#!/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) снова

Метод, который я использовал, заключался в вычислении скользящего хэша каждого слова, начиная с каждого символа в строке; поскольку это скользящий хеш, требуется O (n) (где n - длина слова), чтобы вычислить все n хешей. Строка обрабатывается как число base-1114112, что гарантирует уникальность хэшей. (Это похоже на решение Haskell, но более эффективно, поскольку оно проходит через строку только дважды.)

Затем для каждого входного слова алгоритм проверяет свой самый низкий хеш, чтобы увидеть, находится ли он уже в наборе увиденных хэшей (набор Python, таким образом, поиск равен O (1) в размере набора); если это так, то слово или один из его поворотов уже видели. В противном случае он добавляет этот хэш к набору.

Аргумент командной строки должен быть именем файла, содержащего одно слово в строке (например /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 - это количество слов в списке, а m - это длина слова, тогда вычисляется «номер циклической группы» для всех слов: O(n*m)сортировка O(n log n)и группировка 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

Решили начать снова, теперь, когда я понимаю правила игры (я думаю).

Словарь из 10000 слов уникальных случайно составленных «слов» (только в нижнем регистре) длиной 3. Аналогичным образом были созданы другие словари, состоящие из строк длиной 4, 5, 6, 7 и 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];

gпринимает текущую версию словаря для проверки. Верхнее слово объединяется с циклическими вариантами (если они есть). Слово и его совпадения добавляются в список вывода outобработанных слов. Выходные слова удаляются из словаря.

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 пробегает словарь всех слов.

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

Пример 1 : фактические слова

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

{{"steak", "teaks"}, {"hand"}, {"pots", "spot"}, {"sword", "words"}}
4


Пример 2 : Искусственные слова. Словарь строк длины 3. Во-первых, время. Тогда количество слов цикла.

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

d3

5402


Время как функция длины слова . 10000 слов в каждом словаре.

тайминги

Я не особенно знаю, как интерпретировать результаты в терминах О. Проще говоря, время примерно удваивается от словаря из трех символов до словаря из четырех символов. Время увеличивается почти незаметно с 4 до 8 символов.


Можете ли вы опубликовать ссылку на словарь, который вы использовали, чтобы я мог сравнить с вашим?
eggonlegs

Должна работать следующая ссылка на dictionary.txt: bitshare.com/files/oy62qgro/dictionary.txt.html (извините за минуту, которую вам придется ждать, чтобы начать загрузку.) Кстати, файл имеет 3char, 4char ... 8чар словари все вместе, по 10000 слов в каждом. Вы хотите отделить их.
DavidC

Потрясающие. Большое спасибо :)
eggonlegs

1

Это можно сделать за O (n), избегая квадратичного времени. Идея состоит в том, чтобы построить полный круг, пересекающий основную строку дважды. Таким образом, мы строим «amazingamazin» как строку полного круга, чтобы проверить все циклические строки, соответствующие «удивительным».

Ниже приведено решение 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

Я не знаю, насколько это эффективно, но это мой первый крэк.

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

не уверен, что я понимаю проблему, но это соответствует примеру @dude, опубликованному в комментариях, по крайней мере. пожалуйста, исправьте мой заведомо неверный анализ.

для каждого слова W в заданных N словах списка строк вы должны пройти все символы W в худшем случае. Я должен предположить, что хэш-операции выполняются в постоянное время.

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), $/;
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.