最初に、頻度の実行中の合計の表を作成できます。したがって、次のデータがある場合:
%freq = (
a => 15,
b => 25,
c => 30,
d => 20
);
現在の合計は次のようになります。
%running_sums = (
a => 0,
b => 15,
c => 40, # 15 + 25
d => 70, # 15 + 25 + 30
);
$max_sum = 90; # 15 + 25 + 30 + 20
重み付けされた頻度で単一の文字を選択するには、 の間の数値を選択する必要があります[0,90)
。その後、その文字を含む範囲について running_sum テーブルで線形検索を実行できます。たとえば、乱数が 20 の場合、適切な範囲は 15 ~ 40 で、これは文字 'b' です。O(m*n)
線形検索を使用すると、m が必要な文字数、n がアルファベットのサイズ (したがって、m=16、n=26)の合計実行時間が得られます。これは基本的に @default ロケールが行うことです。
線形検索の代わりに、running_sum テーブルで二分検索を実行して、切り捨てられた最も近い数値を取得することもできます。これにより、合計実行時間が になりO(m*log(n))
ます。
ただし、 m 文字を選択する場合はO(m*log(n))
、特に ifよりも高速な方法がありn < m
ます。まずm
、ソートされた順序で乱数を生成します (これは でソートせずに実行できます)。次に、ソートさO(n)
れた乱数のリストと実行中の合計のリストの間の範囲について線形マッチングを行います。これにより、総実行時間はO(m+n)
. コード全体が Ideone で実行されています。
use List::Util qw(shuffle);
my %freq = (...);
# list of letters in sorted order, i.e. "a", "b", "c", ..., "x", "y", "z"
# sorting is O(n*log(n)) but it can be avoided if you already have
# a list of letters you're interested in using
my @letters = sort keys %freq;
# compute the running_sums table in O(n)
my $sum = 0;
my %running_sum;
for(@letters) {
$running_sum{$_} = $sum;
$sum += $freq{$_};
}
# generate a string with letters in $freq frequency in O(m)
my $curmax = 1;
my $curletter = $#letters;
my $i = 16; # the number of letters we want to generate
my @result;
while ($i > 0) {
# $curmax generates a uniformly distributed decreasing random number in [0,1)
# see http://repository.cmu.edu/cgi/viewcontent.cgi?article=3483&context=compsci
$curmax = $curmax * (1-rand())**(1. / $i);
# scale the random number $curmax to [0,$sum)
my $num = int ($curmax * $sum);
# find the range that includes $num
while ($num < $running_sum{$letters[$curletter]}) {
$curletter--;
}
push(@result, $letters[$curletter]);
$i--;
}
# since $result is sorted, you may want to use shuffle it first
# Fisher-Yates shuffle is O(m)
print "", join('', shuffle(@result));