循環する言葉
問題文
循環単語は、円で書かれた単語と考えることができます。循環単語を表すには、任意の開始位置を選択し、時計回りに文字を読み取ります。したがって、「picture」と「turepic」は同じ循環する単語の表現です。
String []ワードが与えられ、その各要素は巡回ワードの表現です。表現されている異なる巡回単語の数を返します。
最速の勝利(Big O、n =文字列内の文字数)
循環する言葉
問題文
循環単語は、円で書かれた単語と考えることができます。循環単語を表すには、任意の開始位置を選択し、時計回りに文字を読み取ります。したがって、「picture」と「turepic」は同じ循環する単語の表現です。
String []ワードが与えられ、その各要素は巡回ワードの表現です。表現されている異なる巡回単語の数を返します。
最速の勝利(Big O、n =文字列内の文字数)
回答:
これが私の解決策です。まだO(n 2)かもしれませんが、平均的なケースはそれよりはるかに良いと思います。
基本的には、各文字列を正規化することで機能し、どの回転も同じ形式になります。例えば:
'amazing' -> 'mazinga'
'mazinga' -> 'mazinga'
'azingam' -> 'mazinga'
'zingama' -> 'mazinga'
'ingamaz' -> 'mazinga'
'ngamazi' -> 'mazinga'
'gamazin' -> 'mazinga'
正規化は、最小文字を(文字コードで)探し、文字が最後の位置になるように文字列を回転させることによって行われます。その文字が複数回出現する場合は、各出現後の文字が使用されます。これにより、各巡回単語に正規表現が与えられ、マップのキーとして使用できます。
正規化は最悪の場合(文字列内のすべての文字が同じである場合など)はn 2ですが、aaaaaa
ほとんどの場合、数回しか発生せず、実行時間はに近づきn
ます。
私のラップトップ(デュアルコアIntel Atom @ 1.66GHzおよび1GBのRAM)でこれを実行すると/usr/share/dict/words
(平均文字数9.5文字の234,937ワード)、約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")
私が使用した方法は、文字列の各文字で始まる各単語のローリングハッシュを計算することでした。これはローリングハッシュであるため、すべてのn個のハッシュを計算するのにO(n)(nは語長)の時間がかかります。文字列はbase-1114112番号として扱われ、ハッシュが一意であることを保証します。(これはHaskellソリューションに似ていますが、文字列を2回通過するだけなので、より効率的です。)
次に、各入力ワードについて、アルゴリズムはその最小ハッシュをチェックして、ハッシュのセット(Pythonセットなので、ルックアップはセットのサイズでO(1)です)にすでにあるかどうかを確認します。そうである場合、その単語またはそのローテーションの1つはすでに確認されています。それ以外の場合は、そのハッシュをセットに追加します。
コマンドライン引数は、1行に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])
これの効率についてはよくわからない、おそらくかなり悪い。アイデアは、最初にすべての単語の可能なローテーションをすべて作成し、文字列を一意に表す値をカウントして、最小値を選択することです。これにより、循環グループに固有の数値が得られます。
この数でグループ化し、これらのグループの数を確認できます。
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
ゲームのルールがわかったので、もう一度始めることにしました(そう思います)。
長さ3のランダムに構成された一意の「単語」(小文字のみ)の10000語辞書。同様に、長さ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]]]
5402
語長の関数としてのタイミング。各辞書に10000語。
調査結果をOでどのように解釈するかは特にわかりません。簡単に言えば、3文字の辞書から4文字の辞書へとタイミングがおよそ2倍になります。タイミングは4文字から8文字までほとんど無視できます。
これは、二次時間を回避するO(n)で実行できます。アイデアは、ベース文字列を2回横断する完全な円を作成することです。したがって、「amazingamazin」を完全な円ストリングとして構成し、「amazing」に対応するすべての循環ストリングをチェックします。
以下は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
これが非常に効率的かどうかはわかりませんが、これが私の最初の亀裂です。
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;
}
私が問題を理解しているのかわかりませんが、これは少なくともコメントに投稿された@dudeの例と一致します。私の確かに間違った分析を訂正してください。
文字列リストの指定されたNワードの各ワードWについて、最悪の場合、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), $/;