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> ドッグ