tag:crieit.net,2005:https://crieit.net/tags/%E7%B4%84%E6%95%B0/feed
「約数」の記事 - Crieit
Crieitでタグ「約数」に投稿された最近の記事
2020-06-29T02:47:22+09:00
https://crieit.net/tags/%E7%B4%84%E6%95%B0/feed
tag:crieit.net,2005:PublicArticle/15986
2020-06-29T02:39:09+09:00
2020-06-29T02:47:22+09:00
https://crieit.net/posts/koya-abc172-d
AtCoder Beginner Contest 172 D - Sum of Divisors
<h1 id="問題文"><a href="#%E5%95%8F%E9%A1%8C%E6%96%87">問題文</a></h1>
<p>要約:<br />
k=1からNまで、<img src="http://chart.apis.google.com/chart?cht=tx&chl=k%20*%20f(k)" alt="k * f(k)" /> を足し上げる。<br />
ただし、<img src="http://chart.apis.google.com/chart?cht=tx&chl=f(k)" alt="f(k)" />とは、正の整数であって、kの約数であるものの個数。</p>
<p><a target="_blank" rel="nofollow noopener" href="https://atcoder.jp/contests/abc172/tasks/abc172_d">https://atcoder.jp/contests/abc172/tasks/abc172_d</a></p>
<h1 id="O(N^2)"><a href="#O%28N%5E2%29">O(N^2)</a></h1>
<p><img src="http://chart.apis.google.com/chart?cht=tx&chl=1%20\le%20j%20\le%20k" alt="1 ≦ j ≦ k" /> なるすべての j について、ループを回して k の約数であるかを調べると <img src="http://chart.apis.google.com/chart?cht=tx&chl=O%28N^2%29" alt="O(N^2)" />になります。<br />
<a href="https://crieit.now.sh/upload_images/5b556d92f0305f87aa876d79c8c86a875ef8bb4e364c6.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/5b556d92f0305f87aa876d79c8c86a875ef8bb4e364c6.png?mw=700" alt="image" /></a></p>
<pre><code class="cpp">#include <iostream>
using ll = long long;
ll f_n2(int N)
{
ll ans = 0;
for (int k = 1; k <= N; ++k)
{
// 約数カウンタ
int c = 0;
for (int j = 1; j <= N; ++j)
{
// 割り切れるなら増やす
if (k % j == 0) ++c;
}
ans += (ll)k * c;
}
return ans;
}
int main()
{
int N;
std::cin >> N;
ll ans = f_n2(N);
std::cout << ans << '\n';
}
</code></pre>
<p><code>N <= 10^7</code> ですので、当然、3secには間に合いません。</p>
<h1 id="O(N√N)"><a href="#O%28N%E2%88%9AN%29">O(N√N)</a></h1>
<p>約数を数える部分は<img src="http://chart.apis.google.com/chart?cht=tx&chl=O%28\sqrt%20N%29" alt="O(√N)" /> にできます。</p>
<p>たとえば 20 の約数がいくつあるか数えることを考えましょう。<br />
20 = 1 * 20<br />
20 = 2 * 10<br />
20 = 4 * 5</p>
<p>このように、割り切れたときには必ず相方がいることが分かります。<br />
左側は必ず√20 より小さく、右側は必ず√20 より大きくなっているので、<br />
(そうしないと、掛けたときに20になりません)<br />
√20 = 4.47... まで探索して2倍すれば十分であることが分かります。</p>
<p>ただし、16のような平方数では、<br />
16 = 1 * 16<br />
16 = 2 * 8<br />
16 = 4 * 4</p>
<p>このようになることがあるので、<br />
√k ぴったりのときは1つしか数えないような工夫が必要です。<br />
<a href="https://crieit.now.sh/upload_images/d288b0b0de53bab5a2c736d5d6b63b955ef8bd84e9ace.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/d288b0b0de53bab5a2c736d5d6b63b955ef8bd84e9ace.png?mw=700" alt="image" /></a></p>
<pre><code class="cpp">#include <iostream>
#include <cmath>
using ll = long long;
ll f_nsqn(int N)
{
ll ans = 0;
for (int k = 1; k <= N; ++k)
{
// 約数カウンタ
int c = 0;
int sqk = (int)sqrt(k);
for (int j = 1; j <= sqk; ++j)
{
if (k % j == 0)
{
// k = j * j
if (k / j == j)
c++;
// それ以外
else
c += 2;
}
}
ans += (ll)k * c;
}
return ans;
}
int main()
{
int N;
std::cin >> N;
ll ans = f_nsqn(N);
std::cout << ans << '\n';
}
</code></pre>
<p>N = 10^7 のとき、N√N = 3 * 10^10 くらいになるので、これでも間に合いません。</p>
<h1 id="O(NlogN)"><a href="#O%28NlogN%29">O(NlogN)</a></h1>
<p>k の約数を探しにいくのではなく、k 自身が将来の数の約数である、というふうに考えてみます。</p>
<p>たとえば、k=2 とすると、<br />
2, 4, 6, 8, 10 はすべて 2 の倍数であり、<br />
言い換えると、これらはすべて 2 を約数にもつ数でもあります。<br />
したがって、f(2), f(4), f(6), f(8), f(10) は 2 が約数であることを知ることができます。<br />
約数の数がひとつ増えたことになるので、+1 しておきます。<br />
このようにすると、たとえば f(10) は、k=1, 2, 5, 10 のときに +1 されることになり、<br />
これはつまり約数の数になっています。<br />
<a href="https://crieit.now.sh/upload_images/3069b2ecca771b8ee9b57dee6b0b69425ef8c147ace0b.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/3069b2ecca771b8ee9b57dee6b0b69425ef8c147ace0b.png?mw=700" alt="image" /></a></p>
<p>図を見ると分かるように、これのループ回数は<br />
N + N/2 + N/3 + ...... + N/N<br />
になっています。</p>
<p>これの計算量を見積もるのは難しそうですが、<br />
1/1 + 1/2 + 1/3 + ...... + 1/N<br />
の形の和を調和級数と呼び、なんとその値はだいたいlogNになります。<br />
したがって、この解法の計算量は<img src="http://chart.apis.google.com/chart?cht=tx&chl=O(N%5Clog%20N)" alt="O(NlogN)" />です。</p>
<p>約数列挙の方針では「約数かどうかを判定してみたものの約数ではなかった」という場合があるのに対して、<br />
こちらでは必ず約数になっています。直感的にも、こちらのほうが効率がよさそうです。</p>
<pre><code class="cpp">#include <iostream>
#include <vector>
using ll = long long;
ll f_nlogn_memo(int N)
{
std::vector<int> p(N+1, 0);
for (int k = 1; k <= N; ++k)
{
for (int j = k; j <= N; j += k)
{
// k, 2k, 3k, ... の約数カウントを増やす
++p[j];
}
}
ll ans = 0;
for (int k = 1; k <= N; ++k)
{
ans += (ll)k * p[k];
}
return ans;
}
int main()
{
int N;
std::cin >> N;
ll ans = f_nlogn_memo(N);
std::cout << ans << '\n';
}
</code></pre>
<p>C++でギリギリ通ります。<br />
空間計算量がO(N)なので、10^7 ものメモリを確保するのが遅いのかもしれません。</p>
<h1 id="O(NlogN) その2"><a href="#O%28NlogN%29+%E3%81%9D%E3%81%AE2">O(NlogN) その2</a></h1>
<p>もとの問題に立ち返ってみると、求めたいものは k * f(k) の総和でした。</p>
<p>ここまでの図をじーっと見ていると、<br />
そもそも○を使う必要はなく、数字で埋めてしまっていいことがわかります。<br />
この表中の数字をすべて足しさえすればよいので、<br />
配列を使わなくても実装できます。<br />
<a href="https://crieit.now.sh/upload_images/dc494292a387e6a735bd8357d917d82a5ef8c6a103de3.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/dc494292a387e6a735bd8357d917d82a5ef8c6a103de3.png?mw=700" alt="image" /></a></p>
<pre><code class="cpp">#include <iostream>
using ll = long long;
ll f_nlogn(int N)
{
ll ans = 0;
for (int k = 1; k <= N; ++k)
{
for (int j = k; j <= N; j += k)
{
// 数字を直接足し込む
ans += j;
}
}
return ans;
}
int main()
{
int N;
std::cin >> N;
ll ans = f_nlogn(N);
std::cout << ans << '\n';
}
</code></pre>
<p>C++では余裕をもって通ります。<br />
言語によってはこれでもまだ通らないことがあるようです。<br />
(Rubyで6300ms前後でした)</p>
<h1 id="O(N)"><a href="#O%28N%29">O(N)</a></h1>
<p>「表中の数字をすべて足し上げる」という方針をもってさらに図を見つめていると、<br />
横方向の足し算は等差数列の和であり、O(1) で計算できてしまうことが分かります。<br />
横方向がO(1) で計算できたので、全体ではO(N)になります。<br />
<a href="https://crieit.now.sh/upload_images/b8bcba0c3337ab7fa5a44a20edcdd8a45ef8cc92edc77.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/b8bcba0c3337ab7fa5a44a20edcdd8a45ef8cc92edc77.png?mw=700" alt="image" /></a></p>
<pre><code class="cpp">#include <iostream>
using ll = long long;
// 1からnまでの和
ll sn(ll n)
{
return (ll)n * (n+1) / 2;
}
ll f_n(int N)
{
ll ans = 0;
for (int k = 1; k <= N; ++k)
{
ans += sn(N/k) * k;
}
return ans;
}
int main()
{
int N;
std::cin >> N;
ll ans = f_n(N);
std::cout << ans << '\n';
}
</code></pre>
<h1 id="O(√N)"><a href="#O%28%E2%88%9AN%29">O(√N)</a></h1>
<p>表中の数字をすべて足し上げればよいので、もはや見た目の順番通りに足す必要はありません。<br />
表の数字を左に詰めると、対称性が見てとれます。<br />
√N 個のL字型について、1つのL字は O(1) で計算できるので、<br />
全体で O(√N) で計算ができます。<br />
<a href="https://crieit.now.sh/upload_images/20514c2e76826449995092ca734237295ef8d09be4d0a.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/20514c2e76826449995092ca734237295ef8d09be4d0a.png?mw=700" alt="image" /></a></p>
<pre><code class="cpp">#include <iostream>
#include <cmath>
using ll = long long;
ll sn(ll n)
{
return (ll)n * (n+1) / 2;
}
ll f_sqn(int N)
{
int sqN = (int)sqrt(N);
ll ans = 0;
for (int k = 1; k <= sqN; ++k)
{
ans += (sn(N/k) - sn(k)) * k * 2 + k * k;
}
return ans;
}
int main()
{
int N;
std::cin >> N;
ll ans = f_sqn(N);
std::cout << ans << '\n';
}
</code></pre>
<h1 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h1>
<p>いろんなオーダーの解法があって、少しずつ改善されていく様子が面白かったです。</p>
<p>なお、<img src="http://chart.apis.google.com/chart?cht=tx&chl=O(N%5E%7B%5Cfrac%7B1%7D%7B3%7D%7D)" alt="O(N^1/3)" />の方法もあるらしいです。</p>
ドッグ