こんばんはSekia the Liarです。更新頻度についての釈明はさておきえーとP.E. 29でしたね。はい、すいません。
問題
-
Consider all integer combinations of a^(b) for 2 ≤ a ≤ 5 and 2 ≤ b ≤ 5: (引用者による省略) How many distinct terms are in the sequence generated by ab for 2 ≤ a ≤ 100 and 2 ≤ b ≤ 100?
-
2 ≤ a ≤ 5 と 2 ≤ b ≤ 5について, abを全て考えてみよう: (引用者による省略) 2 ≤ a ≤ 100, 2 ≤ b ≤ 100 で同じことをしたときいくつの異なる項が存在するか?
解答
値の重複を取り除くにはハッシュを使うのが定石です。
use strict;
use warnings;
use feature qw/say/;
use Math::BigInt;
my %pows;
for my $n (2 .. 100) {
for my $i (2 .. 100) {
$pows{ Math::BigInt->new($n) ** $i } = 1;
}
}
say scalar keys %pows;
しかしPerlのメソッド解決オーバヘッドは結構でかいので、10,000個のMath::BigIntインスタンス生成は割と時間を食います。毎回Math::BigIntというのも芸がないし、少し頭を使って解いてみることにしました。
ab = (an)b/nであることに着目しましょう。これは中学だか高校だかで習った通りです。ただし問題の範囲は整数なので、指数は2 ≤ b/n ≤ 100なる整数でなければなりません。つまりnはbの約数(ただしb自身を除く)です。
この等号で結ばれたべき乗は同じ(つまり重複した)値を持ちます。
例えば212 = 4(=22)6 = 8(=23)4 = 16(=24)3 = 64(=26)2 = 4,096であり、他に4,096となるようなべき乗は整数の範囲ではなさそうです(証明してないので間違ってたら教えてください)。
これをふまえると、等式が成り立つa、bの組のうちaが最小のもの(ここでa0、b0とおきましょう)が分かれば、残りのものはすべてa = a0n、b = b0/nの形をしているはずです。したがって同じ値を持つa、bの組はすべてa0、b0から求められます。
よってa0、b0のみを数えて、残りは重複として印をつけてスキップするようにすれば、重複のないべき乗の個数を数えることができます。
ところで2 ≤ an ≤ 100、2 ≤ b/n ≤ 100という拘束条件にちょっとした注意が必要です。というのも、n = 1のときにこの条件を満足せず、nがある程度大きくなれば満足するa、bが存在するからです。 このような数を取りこぼさないためにはb/n ≤ 100 × floor(logn100)(floorは床関数)までを探索し、b/n ≤ 100である間はnに1加えて何もせずスキップする、というような処理が必要になります。
#!/usr/bin/env perl
use strict;
use warnings;
use feature qw/say/;
use POSIX qw/floor/;
{
my %memos;
#returns divisors of $n (except $n itself) in numeric order
sub divisors($) {
my $n = shift;
return @{ $memos{$n} } if exists $memos{$n};
my @early = grep { $n % $_ == 0 } 2 .. floor sqrt $n;
my @later = reverse map { $n / $_ } @early;
shift @later if @later != 0 and $later[0] ** 2 == $n;
return @{ $memos{$n} = [1, @early, @later] };
}
}
sub logn($$) { my ($b, $n) = @_; (log $n) / (log $b) }
my %passed;
my $num_distincts = 0;
for my $n (2 .. 100) {
my $max_i = 100 * floor logn($n, 100);
for my $i (2 .. $max_i) {
my $is_dist = 0;
for my $d (divisors $i) {
my ($m, $j) = ($n ** $d, $i / $d);
next if $passed{$m, $j};
next if $j > 100;
last if $m > 100;
$passed{$m, $j} = $is_dist = 1;
}
$num_distincts++ if $is_dist;
}
}
say $num_distincts;
蛇足な補足
実装にちょっと珍しい多次元配列のエミュレート機能を使っています。$passed{$m, $j}
という部分です。
ぱっと見ハッシュ・スライスのようですが、シジルが$なことから分かるようにスカラ値を返します。
これは次のように解釈されます:
$passed{ join($;, $m, $j) }
Perl 5のハッシュが文字列しかキーにできないという制約からこのような形になっているのでしょう。$;
(use English
すると$SUBSEP
)は特殊変数で、デフォルトでは"¥034"
が入っています。これはUS-ASCIIではFS(File Separator)という制御文字なので、キーの値と混同されることはまずありません。
Pythonなどのタプルをキーとする方法に比べると美しくありませんが、使い勝手に差はないので案外と重宝します。疎な多次元配列を作るときなどは配列リファレンスよりこちらの方がいいでしょう。
コメント
コメントを投稿