これは実際にはコンピュータサイエンスではありません...
k = 1からMまでのkの約数の合計を格納するテーブルdを作成します。ここでM = 5 ⋅106。それが時間的に重要な部分です。次に、すべての1≤j≤kの約数の合計を格納するテーブルsを作成します。k= 1からMまでです。これは簡単です。s0= 0、 sk + 1=sk+dk + 1。そしてf(L、R)=sR−sL − 1。
最初の表は問題です。あなたはこれを処理しますO (n ログn )。そして、あなたはファクター2だけが必要だとあなたは言う...
500万のエントリを持つ配列dができます。おそらく、エントリあたり4バイト= 20メガバイトです。家庭用コンピューターに搭載されている一般的なプロセッサーでは、20メガバイトはどのキャッシュにも適合しません。そして、あなたのコードはその配列の要素に多くのランダムな順序でアクセスします。潜在的な約数kごとに、kで割り切れるすべての数値にアクセスし、約数の合計をkずつ増やします。
より少ない訪問でそれをしましょう:kで割り切れるjを訪問するとき、2つの除数kとj / kを追加します。しかし、それを行うときは、j =k2、kのみを追加します(k = j / kであり、除数を2回カウントする必要がないため)。その後、kおよびj / kを追加して、さらにjにします。j / kはk + 1、k + 2、k + 3などに等しいため、除算する必要はありません。k= 1の場合の配列を初期化します。つまり、A [j] = 1 + j /を設定します。 j≥2の場合は1。
A [1] = 1
for (j = 2; j ≤ M; j += 1)
A [j] = 1 + j
for (k = 2; k*k ≤ M; k += 1)
j = k*k
A [j] += k
j += k
s = k + (k + 1)
while j ≤ M
A [j] += s
j += k
s += 1 // s equals k + j / k
操作を保存しません。ただし、配列Aにはるかに規則的なパターンでアクセスしているため、項目へのアクセスが高速になるため、時間を節約できます。jは小さくなり、各jの反復数が大きくなるため、分岐予測がよりうまく機能します。
さらに改善するには、コンピューターのプロセッサキャッシュに適合する配列項目の数を調べ、配列の部分範囲のみに対してコード全体を実行します(たとえば、A [0]からA [99999]のみを変更し、次にAを変更します。 [100000]からA [199999]など)。このように、ほとんどのメモリアクセスはキャッシュメモリのみにアクセスします。
サイズMのテーブルでN回のルックアップを行っています。MがNより大幅に大きい場合は、このテーブルを構築しないアプローチを検討する必要があります。ルックアップごとにかなり遅くなる可能性がありますが、少数のルックアップ。ここでN≤100,000でM = 5,000,000の場合でも、表の約数1、2、3、4、j / 1、j / 2、j / 3、j / 4を数えないことがあります(これにより、ビルドするのが少し速くなります)、そしてルックアップ中にそれを処理します。
または、奇数の除数の合計のみを追加してから、偶数の除数の合計を計算することもできます(奇数kの除数の合計がsの場合、2kの合計は3s、4kの場合は7sになります。 、8kの場合は15秒など)、ほぼ2倍の節約になります。
PS。私はそれを測定しました... jとk / jの両方を追加することにより、除数のすべての合計をカウントするアルゴリズムをよりキャッシュフレンドリーにし、速度を2倍にしました。最初に奇数kの除数の合計を計算し、次に奇数の値から偶数kを計算すると、合計で7倍速くなります。明らかにすべてが一定の要因です。