ICPC 2025 横浜 参加記(type-p)

hiryuNです。千葉大学からtype-pというチームでICPCに参加しました。

自分は現在B4でICPCへの参加は3回目、ICPCのRegionalへの参加は2回目です。

ICPCについて知らない方もいると思うので軽く説明しておくと、ICPCは同大学の学生3人がチームとなって協力しながら競プロの問題を解くコンテストで、大学生を参加対象としている競プロのコンテストの中では一番規模が大きい大会だと思っています。

すでに国内予選は突破しており、この記事の内容はアジア地区予選(とはいっても日本のチームがほとんど)の内容です

チームメンバー紹介

hiryuN

Python B4 Rating2182 (highest 2197)

ABCが苦手でARC,AGCの方が好き。青になってからはABCから逃げるようになり、そのまま入黄してからまともにABCをやっていなかったので典型が解けない。最近、大学内の競プロerと話題を共有するためにABCを再開したがなかなか黄パフォが取れない

タイピングが遅い

bigsea 

C++ B3  Rating1312 (highest1390)

チーム内で唯一英語ができる。会場に入る時には一番前に居てもらいチーム紹介もやってもらった。問題文を読んだのも主にこの人

「DはDPのD」と唱えるだけでこのレートまで登り詰めた、最近、同大学内の後輩の実力やAtCoder参加者のインフレに怯えている

miya

Python B3 Rating1585 (highest1594)

ABCが得意でARC,AGCからは逃げている。典型が得意で自分と違ってちゃんとライブラリを持ってきた。

ICPC前日に入青をかけたABCに出て負けたので水色でICPCに出ることとなる。

戦略

今回からABが一番簡単な問題である保証が無くなったので新たに戦略を考える必要があった。

結局、我々のチームは最初、miyaにPCの準備をしてもらって、bigseaには問題をAから順に訳して、順位表情報が得られしだい簡単そうな問題から訳してもらい、その間自分はお菓子を食べるという戦略となった。

本番

問題文:https://icpc.jp/2025/docs/all_contest_en_with_cover.pdf

無事、遅刻せずに本番当日を迎えることができ、事前の戦略通りPCの準備と問題の翻訳をしてもらっていた。自分も問題を読む努力はしていたが結局わからなかったので、bigseaが訳したA問題を考えていた。

PCの準備も無事に終わり、PCが空いていてもったいなかったのでライブラリの写経を始めた。miyaも自分と比較すれば英語ができるのでmiyaには後ろの方の問題を読んでもらっていた。

慣れないキー配列でのタイピングだったので元来のタイピングの遅さも相まって写経が全然進まないうちに他のチームが一番簡単な問題であろうD問題を通し始めたのでbigseaからD問題の内容が伝えられる。

Problem D Decompose Concatenate

どちらか片方は10の冪乗になりそうでbigseaもそういっていたのでそれを実装

めんどくさかったので10の冪乗や結合順は全探索をした

0:24 AC

 

 

その後はC問題が簡単そうだという話を聞いたのでそれを解いていたが嘘解法(だったので一旦放棄して、miyaにその時点でD問題の次に解かれていたE問題を書いてもらうために席を代わりその間にH問題を読む、しばらくしてE問題を誤読していたことが発覚したので自分がH問題を書く

Problem H U-Shaped Panels

U字のタイルで目標盤面が作れるかの判定問題。タイルの大きさ k が5以上だったら嬉しいのになと思いながら制約を見るとそうだったので実装。

実装は4つの辺の位置を走査するような処理を書いてそれをコピペしてそれぞれ違う行をコメントアウトする芸のない実装。角の部分も2回見てるし

k = 5 で解いてしまい 1 ペナ

1:13 AC

 

Problem E Cutting Tofu

H問題AC時にまだE問題の考察が進んでいなそうだったので引き受ける。

直方体の面に対して平行に切る必要があるという考察を聞いて、切り取る立方体の集合のどの面も直方体に面していなければ立方体を大きくできるので立方体の一辺は直方体のいずれかの辺を整数で割った値であると考察し、その整数で二分探索。これを3回繰り返し一番大きいものをとる。

分数の比較に手間取るがペナ無しで通した

1:49 AC

 

Problem J ICPC Board

順位表的に簡単そうなJ問題の訳をもらう

sampleやOutputの制約のきつさ的にかなりパターンは限られているように感じられた。

まず、任意の連続する縦2マスとそこから右に2つ移動した縦2マスは同じ文字の集合になりそこから縦列に含まれる文字は列数の偶奇が同じであればある特定の2文字が交互に並んでいることがわかる。と思っていたが必要十分条件をちゃんと確認できておらず、90度回転したものも存在することを見落としていた。

縦だけ見て1ペナ、そこから90度回転させるときにn,mをswapせず配列外参照して1ペナで合計2ペナ

2:45 AC

 

 

J問題に苦戦している間に bigsea がA問題を miya がC問題を見ていた。

miyaがとりあえずC問題の方針を建てていたようなので軽く聞いて正当性とかはよくわからなかったけど任せることにした。今考えるとこの采配が良くなくて少し考えれば自分と同じような嘘にハマっていることがわかったのでここが自分の一番の反省点だと思っている。

その間自分はA問題の解法に難癖をつけたり、F問題やI問題を考察していた。

miyaはセグ木が必要らしいのでセグ木を書いていたがどうやら写経ミスでバグっていたらしく印刷クエリを出して自分が交代した

 

Problem I Game of Names

2人が交互に自分の名前が書いているマスに隣接していないマスで白紙のマスに自分の名前を書くゲーム。書けなくなったら負け

片方が書けなくなってももう片方は書き続けるというルールのもと最終盤面を考えると、2つの同じ名前の間は1マス以上空いていることからそこにもう片方の名前が書けることがわかる。2つの名前が交互に書いてあるので両端がどうなっているかが決まれば2人が書ける回数の差は決定するので両者とも両端をできるだけ取れるようにすればあとは何をしても同じ。あとは両端に名前が書けるかどうかを丁寧に考えていけば良い

長さが1のときに配列外参照で1ペナ

場合分けしていた部分のどこがミスっていて1ペナ

結局場合分けの部分は細かく分けずに書いた方が実装ミスしづらい

3:56 AC

 

 

miyaがまだ写経ミス箇所を見つけていなかったのでbigseaに代わる

A 問題の解法の正当性はなかったが自分も今書ける問題がなかったので嘘でもいいからと適当に書いてもらう。嘘だったので落ちる

そうこうしている間にmiyaが写経ミス箇所を見つけてmiyaに交代。前述の通り嘘解だったので落ちる。

ここで残り15分程度だったので最後の望みをかけてF問題の嘘乱択を書き始める。

書き終わる前にコンテスト終了となったが後でやってみた感じではうまく行かなそうであった。

 

 

 

結果は5完707分で20位

全て凍結前に解いていたのでYesNoのときは順位が落ちることしかないので少し憂鬱だった

サイトスコアとかの詳しいことはよくわかっていませんがおそらくplay-offにはいけないのでまた来年頑張りたいと思っています。

https://cdn.discordapp.com/attachments/1105823194335678509/1448201174682435604/IMG_1364.jpg?ex=693a65f2&is=69391472&hm=f7e2f9a36a454837e693f756c5b156a025c0a8e2ab79eb81746a7e7a208702bf&

 

 

 

 

 

JAG 夏合宿 2025 参加記 by hiryuN (type-p)

千葉大学のtype-pというチームでICPCに参加したhiryuNです。

ICPC2025の国内予選を突破できたのでJAGが開催する夏合宿に参加しました。参加記を書くと助かる命があるとのことなので書きます。

Day1

この日は ABEFGL の6完で10位でした。十進数の世界の住人としては10位以内に入れたのは嬉しいです。

このうちBEFLを自分が解きました。これについて大まかな思考の過程を書いていきます。

B-JAG Box

まず、第一感で思いついた解法はAをソートすることで最適解が得られるというものでした。転倒数を減らすswapを行うことでコストが小さくなることを示すことができればソートしたものが最適解となるのはよく見る手法だし置かれている位置的にもそんなに難しい問題でも無いだろうというメタ読みもあり未証明で通しました。

 

E-Pole

https://atcoder.jp/contests/abc405/tasks/abc405_f
この問題が頭に浮かびました

この問題では円に互いに交差しない弦を引いて円盤を分割すると面を頂点、弦を辺とした木が得られるということを用いて解いています。

今回の問題でも同様に球面を分割したものを頂点、円を辺とすることで木が得られます。問題設定を木に関する問題に言い換えると木の半径の長さを求める問題になります。これはBFS2回のやつで得られるのであとはグラフを構築するだけです。

ある円が他の円に包含されているかが分かればいいのですが、実装の少ない方針を引きたいところ。

各円は中心の座標が与えられているのでそれを用いて判定する方法を考えると円が包含されているとき包含する側の円を含む平面で球全体を分割した時の小さい方に中心が含まれることがわかります

適当に角度を変えてあげると2つの円なら線分に見える

平面の式は容易に求まるのでこれである頂点が他の頂点の子孫かどうかがわかります。

円の中心が原点から遠いほど円は小さくなるので原点からの距離でソートしておくとトポロジカルソートをしたことになるので実装が楽かもしれません。

F-Walking on Binary Tree

まず、LCAに移動してから目的地に行くのが最短になる。

LCAに移動するまでは制約無く行けるので行くとしてそれ以降は L と R の欲しい方が来るまで暇つぶしをしなければいけない2部グラフなので奇数回の暇つぶしはできないし、2回分の暇つぶしはいつでもできるので偶奇が合うところで一番近い L と R と欲しい方を使えば良い

これは後ろから1個飛ばしで見ていって L があればその index で答えを更新するという方法で偶奇が一致する次の L の index が得られる。答えの初期値は最も小さい L の index

偶奇ごと L R ごとに 4 回処理を書いたので面倒だった。実装前に考えてから実装した方がいいと思う

あと N が奇数の時は N と S を2倍にした

L-Sum of Floor(N/ij)

図を書いて考えてみると 1 以上の値が得られる部分は左上のみで非常に少なく、さらにi と j の対称性から紫の部分の和が分かれば2倍することで答えがわかる

黄色の境界部分のみ1回だけ足すことに注意

ということで√n回の一次元の場合の問題を解けば良いことになる

これはΘ(√n)でできるが i 行目の問題は n の大きさが i 分の1になってるので調和級数の和を計算するノリで積分すると全体でΘ(n^(3/4))になりそう

黄色線より右は計算してないので実際の計算量はこれよりも小さい可能性もあるがよくこれがPythonで通ったなといった感想

Day2

この日はABFGの4完19位と低めの成績

言い訳をすると、主な原因は FA したときに貰える風船が欲しくてまだ誰も解いていなかった D に特攻してたからだと考えています。あと純粋に眠かった

自分が書いたのはAG

A-Mix Condiments

シンプルに全探索が効くので特定の2つの薬について考えれば良い

普通に片方を始点としてもう片方と目標物を終点とするベクトルを見て向きが同じかどうかと長さの比を見れば良い

実装をサボってx 座標の比だけ見たら x 座標が同じケースで落ちることになった。サンプルの時点で落ちるのだがこの時に限って試すのをサボったのでダメだった

ちゃんと書いたら通る

G-Driving Playlist

一旦曲の最初からしか再生できないことを忘れて考える。ついでに曲の長さの制約を見てないので総和に対して線形で通ると思い込んでいる

各人について乗り込んだ直後に聞きたいポイントが流れるような再生開始地点というのが必ず一つ存在し、開始地点を1秒前倒しにするごとに目標達成が1秒遅れる。

これは円環で考えなければいけないがベストなタイミングで切ると2つの一次関数的な値になって考えやすい

これを各人について考えて最大を取ることによって各再生地点での答えがわかる。

これは累積maxを適宜±1しつつ取ればベストなタイミングより左側と右側それぞれについて求まるのでこれのmaxを取る

曲の最初からしか再生できないことを思い出しながら可能な地点についてminを取ると曲の長さが短ければ通る

曲の長さの制約を見つけたので考え直すとさっきの解法は全ての開始地点について考えなくてもいいことに気づく、歯抜けになる分 ±1 のところを飛んだ index 分に増やせばOK
曲の開始地点だけあれば良いが各人のベストなタイミングについても考えることによって実装が楽になっている(気がする)

 

Day3

この日はチームメイトが1人帰ったので2人での参加となりました。

ABFHJKの6完で13位です。

ABFHKを自分が書きました。

 

A-Spray on Surface

なぜか # と . の間を数える発想より前に # と # の間を数えて # の数の6倍からその2倍を引く発想を得たので早めに解くことができた

 

B-Funini Adventure

二分探索が思いつく

あるコスト以下の操作だけで目標回数を達成できるかを考える

あるコスト以下の操作を全部行うと目標回数を超える可能性があるので操作回数はちゃんと数え上げて余剰分を削る必要がある。余剰分は二分探索で得られた答えを引けばOK (それ未満だったら矛盾するので)

操作回数を計算するときナイーブに計算すると操作回数がマイナスになるケースに引っかかりペナを出す

 

F-Ants Collision

一番端にいる蟻は高々1回しか衝突できない

二番目にいる蟻についても考えると3回まで次は5回までと増えていく

なぜ中の蟻ほど衝突できる回数が増えるのか考えるとより外側にいる蟻の衝突回数に依存してぶつかる回数が増えるからでこれについて考えるのは左向きで衝突する時と右向きで衝突する時で分けて考えるとわかりやすいだろうと考える。

そうすると隣り合う蟻の向かい合う方向での衝突回数は同じであることに気づける。左端のありが左向きでぶつかる回数は0であることから各蟻の左右の衝突回数は一意に定まる。この時点で衝突回数がマイナスになったり右端の右向き衝突が0以外なら解なし(右端について1ペナ)

さらに各蟻について向きを考えると左右の衝突回数の差は1以下となる(ここまでの条件で十分だと思い1ペナ)

もし、左の方が多ければ初め左向きで最後も左向き、逆も同じ。左右の衝突回数が同じであれば最初と最後で違う向きとなる

最後の盤面を考える、衝突しなくなったということはある右向きの蟻より右側に左向きの蟻がいることがない

ということはある部分より右にいた蟻は全部右向き左にいた蟻は全部左向きになる

ということでそのある部分を適切に定められれば良い、前述の通り一部の蟻は最後に左右のどっちを向いているかを決められているのでそれらについて条件を満たしていればあとは適当に割り当てていけば良い

H-Max of Mod

できるだけ大きい数を保った方が良さそうだと考えた。

最大値の半分より大きい数で割るのが0を含まない条件なのでその中で最小値を選び続ける

指数的に数が小さくなるので操作回数は対数的なので通ると思って出す。

 

K-Bomb Game

M-1個取った時点で残る石は確定するのでどちらが勝つか残る石の数の偶奇でわかる

残る石は連続するはずなのでM-1個取るパターン数は高々Bの数で十分少ないです。

そこで各パターンをそれぞれ左から何回以上、右から何回以上取ると勝利という条件だと思って一番早く達成できる勝利条件を2人とも目指すことにします。

 

ある条件に対して達成したい側は残り回数が大きい方を取り、したくない側は小さい方を取り続けることが最善で、大きい方の回数が条件達成までにかかる時間だとわかります。

よってこの時間が最も短いものが最初に達成でき、それで勝てる方が勝ちです。両者同じ時間の条件があれば先手の勝ちです。これでACできました

しかしこれは真っ赤な嘘で例えば Aliceが(3,3)のBobが(2,4)と(4,2)の勝利条件を持っていればBobの初手でAliceの初手と同じ側を選択すればBobの勝利条件が先に達成されます。また複数の条件が相互作用する状況で正当な解法かどうかはこれだけでは言えないはずです

まあとはいえこのようなケースは理論上構築できないはずなので考察が嘘なだけで解法自体は問題がないと考えています。証明はまだしてません。

おわりに

今回の合宿を通して競い合う相手がいて緊張感のある良質なチーム練習ができたと思っています。本番に向けて練習を重ねてより良い結果が得られるように頑張りたいなと思っています。

 

 

 

必見!! 大富豪必勝法 3選

誰もが一度はプレイしたことがある有名なトランプゲーム「大富豪」。地域によって多種多様なローカルルールが存在します。ときには「大貧民」と呼ばれることもあり、名称にまで多様性が溢れ出しています。それは「大富豪」というゲームが様々な場所で愛されている証拠でもあり、これを読んでいる方の大部分は「大富豪」について少なくとも存在ぐらいは知っていることでしょう。

本記事ではそんな大人気ゲーム「大富豪」の必勝法についてお伝えできればと考えております。

 

必勝法を語る前にまず皆様に知っておいていたただきたいのは、前述の通り「大富豪」には多種多様なローカルルールが存在するということです。場合によっては自らルールを追加で作成することも是とされます。なので極端なことを言ってしまえばトランプを用いた一連の動作を一切無視して、個々人の持つ年齢人種性別といった特徴を持って自分が必ず勝てるようなルールを作成することも不可能ではありません。しかしそのような限定的なローカルルールを用いて勝利する方法を必勝法と呼ぶのは少し強引であるようにも感じます。ですので本記事では特殊なローカルルールを極力使用せずに勝利する方法について紹介していきたいと思います。

勿論大富豪のルールのどこからどこまでがローカルルールであるかは人によって様々な考え方があると思いますので、万人が納得できるルール設定ではない可能性がございますがその点はご了承ください。

 

1. 1人で行う方法

大富豪を1人で行います。

カードを配り終えた後、自分の手札を確認して見ましょう。するとほぼ確実に「♢3」が存在するため最初にカードを出す権利を得ます。「♡3」やその他のカードを所持しているプレイヤーから始めるというルールの場合も同様に権利が得られます。

万が一「♢3」が存在しなかった場合はまず落ち着いて周りを見渡して見ましょう。不思議なことに「♢3」を所持していると宣言するプレイヤーは存在しません。ここで「今回は自分から手番を始める取り決めにさせていただけないだろうか」と提案しましょう。このとき他のゲーム参加者から反対意見が出ることはないため最初にカードを出す権利が得られます。余談ですがこの場合トランプを後で買い換えることを推奨します。

この後の必勝戦略は複数存在しますが一例を挙げますと

1. 任意のカードを1枚場に出す。

2.パスをする

3.まだ手札があれば1に戻る

といった戦略で必勝となります。「禁止上がり」のルールが設けられている場合指定されているカードを優先的に出すように注意しましょう。

 

2. 55人で行う方法

まず54人で大富豪を行なっている集団を探し、頃合いを見てゲームに参加させていただきましょう。

55人でゲームが開始されますが Joker2枚を含む54枚のトランプを55人で均等に分けることは不可能です。

「大富豪」というゲームは別に手札の枚数が均等でなければできない訳ではありませんが、このような大人数になると必然的に個々人の手札の枚数は少なくなり、手札1枚の差の与える影響が大きくなってしまいます。ここは均等に配るため端数は使用しないというのが筋でしょう。

1人あたり、 \lceil \frac{54}{55} \rceil 枚の手札でゲームが開始されます。

ここで全員の手札枚数の合計を計算すると \lceil \frac{54}{55} \rceil \times 55 = 0 となりゲームが終了します。

通常「大富豪」では一番早い段階で手札を無くしたプレイヤーが勝者となります。

より正確には今までの人生の中で「一番最後に手札をなくしたタイミング」が一番早いプレイヤーが勝者となります。

普段はそのゲームが始まる以前の情報が参照されることはありませんが今回の場合はゲーム内で手札がなくなる事象が発生しなかったため例外的に参照されることとなります。

自分以外の54人は前のゲームで手札がなくなる事象を経験しているため自分が勝者となります。

Jokerの採用枚数によって求められる参加人数が変化することに注意してください。

3. 5人で行う方法

以下の手順で必ず勝つことができます。

1. 「大富豪」を行う

2.自分が勝者でない場合1に戻る

競プロ

こんにちは hiryuN ないしは ひりゅー です。

今回は競技プログラミングという今僕がハマっているオンラインゲームについて紹介して行きたいと思います。

 

現在日本において一番有名だと思う競技プログラミングサイトAtCoderのリンクを貼っておきます。この記事を見て興味を持ってくださった方はぜひ登録して競技プログラミングの世界に突入してみてはいかがでしょうか?

atcoder.jp

 

競技プログラミングとは

競技プログラミングとは

決められた条件のもとで与えられた問題、課題をプログラミングを用いて解決し、その過程や結果を競うものを競技プログラミングといいます。
様々なジャンルの出題がされますが、プログラミングや思考力、数学力、知識を活用します。

競技プログラミングとは? - AtCoderInfo  より抜粋

 

これはAtCoder 公式ページに書いてある競技プログラミングについての説明です。

抽象的でとっつきにくいかと思うので例題を挙げてみます

atcoder.jp

問題概要を説明しますと 6 桁の整数が与えられるのでその中に 1 が 1 つ、2 が 2 つ、3 が 3 つ含まれているかどうかを判定して「Yes」か「No」を出力せよという問題です。

要するに「122333」や「321233」みたいなのには「Yes」を「111111」や「123456」みたいなのには「No」を出力すれば正解となります。

この問題に対するアプローチはいくつかあり一番すなおな方法は純粋に"1"、"2"、"3"の数を数えていく方法だと思います。プログラミングに触れたことのある人であればご存じかと思いますが多くの言語には for 文と呼ばれる繰り返し処理を行える機能がありそれを用いてこの問題を解くことができます。

他にもテクニカルな方法として数字の列をソートしてみるという方法があります。Yesを出力するような整数は各桁の数字が小さい順に並び替えたとき全て「122333」という数字となるので一致するかの判定を行えばそれで終了となります。

Pythonで書いてみたものがこちらになります

s = input()
if "".join(sorted(s)) == "122333":
    print("Yes")
else:
    print("No")


これをAtCoderの提出ページにコピペして提出することでこの問題に正解することができます。

AtCoderの提出確認画面

画面下部の「AC」という文字が正解している証拠となります。

単純に答えの出力を間違えた場合「WA」にコードを実行した際にエラーが出た場合は「RE」や「CE」と出てきます

そして競技プログラミングでは正しい出力を出す以外にも満たすべき要件があり、最も重要なことの一つに実行時間制限というものがあります。これは提出したプログラムが実行に使って良い時間の制限であり大体 2秒 だったり 3秒 だったりします。これを超過すると「TLE」と出てきてしまい、これもまた不正解として扱われてしまいます。

では次にこの実行時間制限について詳しく書いていけたらなと思います。

実行時間とアルゴリズムについて

実行時間について説明するためにまた別の問題を挙げたいと思います。(これはさっきの問題よりも難しいと思います)

atcoder.jp

問題概要を説明しますと正整数 N が与えられます。( N の最大値は 200,000 )

このNに対して a \times b + c \times d = N となるような (a,b,c,d) の組み合わせが何種類あるかを答えてくださいという問題です。(a,b,c,dは全て正整数)

例えば N = 5 のとき (a,b,c,d) = (1,1,2,2)1 \times 1 + 2 \times 2 = 5 となるため条件を満たします。他にも 13 種類の組み合わせがあり、答えるべき数字は 14 となります。

具体的な14種類の答え

方針A

さて、いきなりこんな問題を出されたとて何から始めれば良いのやらといった感じの方も多いことでしょう。

右辺の1 変数の情報から 左辺の4 変数の情報を得ようとしてるので難しいのは当たり前です。

逆に 左辺の情報から右辺について考えるのは簡単でただ単に四則演算の計算を行えば良いだけです。

そこで a,b,c,d に対して色々な値を当てはめてみたときに N の値がいくつになるのかを考えてその値が正しい値かを判断して、正しい値となるようなケースが何通りあるかを調べてみようという方針が立ちます。

この方法で正しい答えを得るには  N の値が正しい値になる可能性のある a,b,c,d の組み合わせ全てについて考える必要があります。

少し考えるとa,b,c,dのうちどれか一つでもNを超えた場合には等式が成り立たないことがわかりますので、a,b,c,d それぞれについて 1N までの数字を全て確かめていくという方法が思い付きます。

これを先ほども紹介した for 文を使って書いてみることを考えてみましょう。

N=int(input())
count = 0
for a in range(1, N+1):
    for b in range(1, N+1):
        for c in range(1, N+1):
            for d in range(1, N+1):
                if a*b + c*d == N:
                    count += 1
print(count)

さてこのプログラムは「AC」を勝ち取ることができるでしょうか?

試してみましょう

試してみました!

結果はTLEとなってしまいました。何がいけなかったのかプログラムの中身について触れていきましょう

方針A のプログラムでは 1N までの数字を試すという作業を 4 重に行っています。これはN \times 4回の作業を行っているわけではなく九九の表のように 2 重の処理を行うだけでも N × N 回分の作業量となり、4 重で行うことでなんと N \times N \times N \times N 回もの作業量となってしまいます。

これに冒頭に挙げました N の最大値である 200,000 を入れて計算してみますと脅威の

1,600,000,000,000,000,000,000(=1.6 \times 10^{21})回*1となります。

電子計算機というものは人間と比べて計算がはるかに得意で、言語にもよりますが 1秒間に 10^8回程度の処理をすることができます。しかしそんな電子計算機にも限界というものがあり今回のプログラムでは 1秒間に 10^8 回もの処理を行えたとしても単純計算で1.6 \times 10^{13}秒もの時間がかかります

これは約50万年*2であり北京原人が現代人にまで進化できるほどの途方もない時間です。

そんな時間がかかってしまうプログラムは実用的なプログラムであるとは言えないですね

AtCoder内で「AC」を勝ち取るためにはこの問題の場合 2秒 以内にプログラムが終了しなければいけないので逆算すると 大体 2 \times 10^8工程以下で答えを出さなければいけない訳です。

ここで競技者たちの工夫が必要になってくるわけです。

例えば今はa,b,c,d全てについて毎回調べていますが本当に全て調べる必要があるのでしょうか?これについて考えていきます。

 

方針B

実は a,b,c について値が決まった時 a \times b + c \times d = N という式を満たすと仮定すると式変形を行うと d = \dfrac{N-a \times b} {c} となり、残りの d の値も定まってしまうことがわかります。

この d の値が正整数という条件を満たしているなら count を +1 するといった方法をとるとさっきは a,b,c,d の4つの変数に対して行っていた繰り返しを a,b,c の3つの変数に対して行うだけで良くなり、作業量が N \times N \times N 回まで減ります。N に 200,000 を代入すると

8,000,000,000,000,000(=8 \times 10^{15})回 となります。

#これは p/q が正整数になるかを判定する関数
def Is_PositivInt(p, q):
    if p % q == 0:
        if p // q > 0:
            return True
        else:
            return False
    else:        
        return False

N = int(input())
count = 0
for a in range(1, N+1):
    for b in range(1, N+1):
        for c in range(1, N+1):
            if Is_PositivInt(N - (a*b), c):
                count += 1
print(count)

 

これを実行するには 約460日 かかります。北京原人北京原人のままですがさすがに誕生日を 1, 2 回迎えられるような時間をこのような単純な問題に費やすのはいかがなものかと思います。

 

方針C

そこで更なる高速化ができないかを考えるわけです。

そのための下準備として新たに変数を 2 つ増やし、

a \times b = x , c \times d = y , x + y = N

とします。正整数同士を掛け合わせたものなのでxy も正整数となります。

考える変数を増やしてどうするんだと思われるかもしれませんが容赦なく増やします。考えるべき式も 3 つに増えました。

1 つずつ順番に見ていきたいと思います。

 

まずは x + y = N

新たに増やした変数が 2 つも入っており非常にわかりにくそうですが一旦xyが新たに増やした変数ということを忘れて単純に正整数を入れて等式が成立する条件を考えてみるとただの足し算であるこの式は単純明快で

(x,y) = (1,N-1),(2,N-2),...,(N-1,1)

N - 1 個の正解の組み合わせがあります。

もちろんこの問題の正解として N - 1 を出力すれば良いわけではないです。

例えば x = 4の場合 (a,b) = (1,4),(2,2),(4,1) の3種類の組み合わせが考えられ y の値も x と同様に  y = 4であれば (c,d) の値に関しても 3種類の組み合わせが考えられます。よってこの場合は (a,b,c,d)の値の組み合わせは 3通り  \times 3通り で 9 通り考えられることになります。

このように ある x についてその値のとき条件式を満たす(a,b) の値の組が何通りあるかと、ある y についてそれを満たす (c,d) の値の組が何通りあるかが分かればある (x,y) を満たす (a,b,c,d) の値の組が全体で何通りあるかがわかります。これを(x,y) = (1,N-1),(2,N-2),...,(N-1,1)N - 1 個の組み合わせについて全部考えてそれを足し合わせてやれば答えが出てきます。

例えば N = 5 のときであれば考えるべき (x,y) の組は (1,4),(2,3),(3,2),(4,1)の 4つ であり、x,yがそれぞれの値のときありうる値の組が何通りあるかを調べてみると答えが

 1  \times 3 + 2 \times 2 + 2 \times 2 + 3 \times 1

の式で求められることがわかります。当然計算した答えは 14 です。

この部分は N-1 回の掛け算を行った後 N-1 個の数字の合計を求めるので最低でも合計で 2 \times N - 3 回の演算が必要です。これは最大で399,997回となります。

X= #ここに x を固定したときの(a, b)の組の数を記録する配列を作る
Y= #ここに y を固定したときの(c, d)の組の数を記録する配列を作る
count = 0
for i in range(1, N):
    count += X[i] * Y[N-i]
print(count)

 

次に a × b = x の式について考えます。

まず x については前述の通り 1N - 1 の N-1 個の値を代入してみたものそれぞれについて考え、その値を代入したときの式を成り立たせる (a,b) の組み合わせを考えます。

これには前述の (a,b,c) が定まれば d が自動で定まるのと同じ考え方が適用でき、a の値を定めると  b = \frac {x} {a} と式変形でき  b の値が自動で定まります。

a の値については x の値を超えると解がなくなるので 1 ~ x の範囲で考えてあげればOKです

ですので各 x について 調べなければいけない a の種類数は x 個であり、それらを合計すると

 1 + 2 + 3 + ... + (N - 1) = \displaystyle \sum_{i=1}^{N-1} i = \frac {(N-1) \times (N)}{2}

であり、最大で 19,999,900,000( \simeq 2.0 \times 10^{10}) 回です。

X = [None] + [0] * (N-1)
for x in range(1, N):
    for a in range(1, x+1):
        if Is_PositivInt(x, a):#b = x/a が正整数かどうかの判定
            X[x] += 1

最後に c × d = y の式についてですが、 見ての通り a × b = x の式と同じ形式であり、a × b = x の式について考えたときに得た結果をそのまま流用することができます。操作回数は 1 です。

Y = X

最後に全ての工程で必要な操作の回数を足し合わせてみると

20,000,299,998( \simeq 2.0 \times 10^{10})回となります。

#これは p/q が正整数になるかを判定する関数
def Is_PositivInt(p, q):
    if p % q == 0:
        if p // q > 0:
            return True
        else:
            return False
    else:
       return False

N = int(input())

X = [None] + [0] * (N-1)
for x in range(1, N):
    for a in range(1, x+1):
        if Is_PositivInt(x, a):#b = x/a が正整数かどうかの判定
            X[x] += 1
            
Y = X

count = 0
for i in range(1, N):
    count += X[i] * Y[N-i]
print(count)

これを実行するのにかかる時間は約 200 秒です。カップラーメンが作れるくらいの時間

今まで出てきた数字から考えるとお手頃で現実的なかわいい数字ですが、今回の制限時間である 2 秒には遠く及びません。

最後にもう少しだけ作業を効率化する必要があります。

 

方針D

ここで注目するのは a × b = x の式について考えた際の「各 x について 調べなければいけない a の種類数は x 個であり」という部分です。

具体的に x = 24 としてみます

このとき (a,b) としてあり得る組み合わせは (1,24),(2,12),(3,8),(4,6),(6,4),(8,3),(12,2),(24,1) の 8 個です。

ここで注目して欲しいのは (3,8)(8,3) の組のようにある組に対してそれの ab の値を交換したものもまたあり得る組となるという所です。

これは掛け算が可換*3であることから正しいとわかります。

このことから a \leq b となるような組についてだけ考えて  a <  b *4となる組の数だけ 2 倍すると正しい答えとなります。

例えば x = 24 の例であれば  a <  b となる組が (1,24),(2,12),(3,8),(4,6) の部分まで数えると 4 個なのでこの部分だけでこれを 2 倍した 8 個が正しい答えとなることがわかります。

a \leq b となる組み合わせを全て考えるために a をどの範囲にして調べる必要があるかについてですが a の値が  \sqrt{x} を超えると a \leq b より

 x = \sqrt{x} \times \sqrt{x} < a \times a \leq a \times b

となって x a \times b の間に等号が成立しなくなってしまうことがわかります。

よって 各 x に対して調べなければいけない  a の範囲は 1 \lfloor \sqrt{x} \rfloor ( \sqrt{x} を超えない最大の整数)までであり、その個数の総和は \displaystyle \sum_{i=1}^{N-1}   \lfloor \sqrt{i} \rfloor となります。

これに N の最大値を代入すると

59,528,927( \simeq 6 \times 10^7) となります。

X = [None] + [0] * (N-1)
for x in range(1, N):
    for a in range(1, x+1):
        if a * a > x:
            break
        elif a * a == x:
            X[x] += 1
        else:
            if Is_PositivInt(x, a):
                X[x] += 2

これに x+y=N の式について考える工程で必要な操作回数399,997を足すと

59,928,924( \simeq 6 \times 10^7)

となります

def Is_PositivInt(p, q):
    if p % q == 0:
        if p // q > 0:
            return True
        else:
            return False
    else:
       return False

N = int(input())

X = [None] + [0] * (N-1)
for x in range(1, N):
    for a in range(1, x+1):
        if a * a > x:
            break
        elif a * a == x:
            X[x] += 1
        else:
            if Is_PositivInt(x, a):
                X[x] += 2
            
Y = X

count = 0
for i in range(1, N):
    count += X[i] * Y[N-i]
print(count)

これを実行するのにかかる時間はだいたい 0.6 秒程度であり、これにて目標であった 2 秒を切ることができめでたくAC を勝ち取りました。やったね

 

このように計算の仕方を工夫してただ何も考えずにプログラムを作ったときよりも格段に速く動作するプログラムを作る過程こそが競技プログラミングの醍醐味です。少しでも競技プログラミングの楽しさが伝わっていたらいいなと思っています。

少しでも興味がある方はぜひ競技プログラミングを始めてみてください!

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

*1:どこまでを1つの処理とみなすかなどによってこの回数は変化する上、操作の種類が複数種ある場合操作ごとに実行時間にバラつきがあるため操作回数と実行時間は完全に比例するわけではないため1の位まで操作回数を出すことにあまり意味は無く指数表記のみで充分なのですが見たときのインパクトのためだけにあえてこのような表記をしていますし、これ以降もします。

*2:この実行時間はあくまで目安であってプログラミング言語の種類やマシンスペックによって変動します。これ以降に出てくる実行時間についても同じ

*3: a × b = b × a になるってこと

*4:a = b の場合 a と b の値を入れ替えても元の組と変わらないためそれを省くために = を除いている