2025年の抱負

学業

  • 修了する
  • 学外発表する

仕事

  • 週5日8時間労働に慣れる(体調を崩さない)
  • 同期やチームのメンバーなど、会社の方々と仲良くなる

社会人1年目でできないことが多いと思うので、基本的なことから取り組みます。

競プロ

  • コンテストに出る
  • AtCoder Algorithm 2050
  • AtCoder Heuristic 1600
  • AC Count 4000
  • 作問してyukicoderに投稿
  • JAGでお手伝いする

就活生の中には、社会人になっても競プロを続けている人が会社にいるかを気にする人もいると思います。 自分がコンテストに出ることで、その一例になれたら良いと考えています。

その他

  • TOEIC800↑
  • 仕事とも競プロとも関係のない技術関係のアウトプットを出す
  • 体を鍛えて登山する

特に英語を頑張りたいです。

2024年の振り返り

もう2024年ではないどころか2025年が始まってから1週間以上経過していますが、2024年を振り返っていこうと思います。

下記の記事で書いた2024年の抱負と対応させながら振り返ります。

学業

M1の間に授業は取り切る

そもそもこの抱負を書いていたタイミングでM1の間に取る予定の授業は終わっており、あとは成績発表を待つだけという状況だったような気もしますが、とりあえずM1の間に授業を取り切ることはできました。 おかげでM2の1年間はかなり自由な時間が多かったと思うので良かったです。 (なお、自由な時間に何をしていたかというと…。)

1週間に1本以上論文を読む

年間で約50本と考えると達成できなかったように思います。 この抱負を書いたくせに結局1〜3月はほぼ何もせず、3月末に以下のjoisinoさんの記事を読んで真似しようと思ったのですが、結局1本の論文を一通り読むだけでも(Readableを使っても)平均で5〜6時間ほどかかってしまい、しかも内容が理解・消化しきれずに後で何度も読み直すことが続いて、習慣化することはできませんでした。

ただ、10月末に修論の進捗状況に危機感を感じて以降は、ChatGPTに論文の要約や背景知識の補足をしてもらいながら、論文を紙に印刷して読むようになりました。 現在は平均で2〜3日に1本程度読んでいます。 今となっては論文を読むという行為は知識の吸収効率が非常に高いと感じます。1つの論文はページ数が10ページ前後のものが多く、背景知識も適切な分量でまとめられており、詳しく知りたければ参考文献を再帰的に参照していくことができます。 もともと自分は知識を積み上げていくボトムアップ型の学習をすることが多かったのですが、論文を読む場合は必要になった知識をその都度学ぶトップダウン型の学習をすることになるため、そのような学習の仕方に慣れるという意味でも非常に有意義でした。 よく考えると自分は競プロでもコンテスト参加とその復習を繰り返すトップダウン型の練習をしていたので、実はそっちのほうが向いているのではないかということもわかりました。

成果を出して学外発表をする

結局学外発表をすることはできませんでした。 ただ、最近まで書いていた修士論文の内容はどこかで発表するつもりです。 (ちなみにこの記事を書くのが遅くなった原因のほとんどは修士論文の執筆に追われていたためです。)

競プロ

AtCoder Algorithm 2100

AtCoder Heuristic 1600

Codeforces 2100

AC Count 3500

Codeforces 2100とAC Count 3500は達成できましたが…。

結局AtCoderについてはAlgorithmもHeuristicもRatedにほぼ出ず、ABCにたまに出る程度になってしまいました。 最近は競プロのコンテストに出るモチベが下がってしまっていると感じます。

  • ICPC 2023 Asia Yokohama Regionalを最後にICPCを引退した。
  • ChatGPTの台頭などにより以前より高パフォーマンスを取ることが難しくなった。
  • そもそもARC以降のコンテストを楽しめるレベルではない。
  • 他の趣味が楽しい。

あたりが理由でしょうか。

一応問題を解くことは継続しています。 今年はEDPCをすべて埋めたり、競プロ典型 90 問の★6まですべて+★7半分程度を埋めたりしました。 最近はARCの過去問を、新体制になったARC104から順番に黄diff下位くらいまで埋める取り組みを始めました。今はARC115まで埋まっています。 少しずつARCの問題を楽しめるようになりたいです。

ライブラリを作るのも多少はやっています。

作問してyukicoderに投稿

JAGでお手伝いする

作問はアイデア自体はあるのですが放置しています。そもそも作問作業の流れみたいなものがあまり良くわかっていないのもあります。ITF.PCに運営側で参加すれば良かった…。 JAGに関してはそもそも入会手続きを忘れていました。今この振り返りを書いているタイミングで入会手続きのメールを送信しました。

ICPC国内予選では老害ムーブをしました。

その他

就活で内定を得る

内定は得ることができ、4月から東京都内で働きます。 自分の就活は本当にこれ(↓)だったので、競プロに感謝してもしきれません。

競プロ就活はポテンシャルを評価されているのだと(勝手に)思っているので、今後はキャッチアップに真剣に取り組もうと思います。

1か月に1本以上ブログ記事を書く

2024年は25本の記事を書きました。 大半が競プロで解いた問題の解説記事で、ブログ記事を書こうとすると正確に記述する意識が高まり、結果として自分の思考の整理にもなるというのは感じます。 ただし、2024年の後半は、Algorithmの問題を解いた後に解説記事を書くのではなく、自分だけが見られるメモ(Notion)にポイントを2〜3行で書くようになったこともありほぼ書かなくなってしまいました。 来年はAHCの過去問を解いたら書く程度になるのかなと思います。

制作物などのアウトプットを増やす

結局このブログの記事と競プロのライブラリくらいしかまともなアウトプットはなかった気がします。反省。

Kaggleのコンペに参加する

参加できませんでした。残念。 参加したい気持ちはありますが、今の自分の関心度的には AHC > CodinGame > Kaggle という感じなので、2025年もあまり期待できなさそうです。 Santaコンペくらいは参加したいです。

資格を取る(データベーススペシャリスト、統計検定2級〜1級、数学検定1級、TOEIC800↑)

そもそも申し込みさえしていませんでした…。 研究室に留学生が多いこともあり、英語の重要性を毎日ひしひしと感じているので、TOEIC800↑を来年は目指そうと思っています。

VALORANTプラチナ

プラチナどころかダイヤモンドを達成してしまいました。 詳しくはこちら。

趣味

抱負とはあまり関係ないですが趣味についても書きます。

ゲーム

長くなってしまったので以下の記事に書きました。

音楽

2024年の4月に人生ではじめて音楽アーティストのライブに参加しました。 想像していたよりかなり素晴らしい体験で、また参加したいと思って2025年のライブにも申し込んだのですが、落選してしまいました。 参加したときの感想は記事にしています。

Apple MusicのReplay'24によると、サカナクション以外ではヨルシカ、milet、Aimer、米津玄師などの邦楽を聴いている時間が長かったようです。 また、VCT EMEAでマップ間の待機時間に流れているこの曲(Makes Us Come Alive)が好きでよく聴いていました。

その他

2024年の夏にはコミックマーケット104に参加しました。 こういうイベントは年を取るとどんどん参加するのが難しくなると思うので、参加できるうちは参加したいですね。(もうすでに夏コミはかなりしんどいと感じました。)

ちなみにネタがあればサークル参加もしたいと思っているのですが…。

振り返りは以上です。 2024年もいろいろありましたが、個人的には新しいことに取り組んだというよりもやり残したことに取り組んだ年だったと思います。 本文中ではあえて詳しく書きませんでしたが、実はあまり調子が良い1年ではなかったような気もします。(というかコロナが始まってからの生活スタイルに未だに順応できていないと感じます。) ですが、秋から年末にかけて自分なりの対処方法が少しずつわかってきたのは良かったです。

来年もほどほどに頑張ります。

2024年に遊んだゲーム

2024年はVALORANTを継続的にプレイしました。以下の記事で語っています。

ただ、他にもさまざまなゲームを遊んだので紹介します。

一部のゲームではネタバレが含まれるので注意。

Baba Is You

ルールが動的に変化する倉庫番型のパズルゲームです。 購入自体は2021年なのですが、途中までやって難しくて放置していました。 結局表面はほぼヒントなしで解けたのですが、裏面はそもそも方針さえ思い浮かばないものも多く、非常に難しくて結構ヒントを見てしまいました。 CRUSHERS、BOOBY TRAPが特に難しかった記憶があります。 THE RETURN OF SCENIC PONDはとあるヒント集では最難関とされていますが、自力で解けました。

個人的な理由もあり、今年最も印象に残ってるゲームです。制作者が天才すぎる…。

Monster Hunter World: Iceborne

大人気モンスターハンターシリーズです。ワールドとアイスボーンどちらもやりました。 導きの地が解放されたあたりで飽きてやめてしまいましたが、実はそこからも結構いろんなモンスターが登場するらしいです。 3G以降ほぼ使っていない太刀を使ったら操作性が違いすぎてびっくりしました。(XXでは大剣を、Rise: Sunbreakではガンランスを使っていました。) 今年は新作も発売予定で、ますます楽しみなシリーズだと思います。

Phasmophobia

廃墟に侵入し、手がかりを元にゴーストを特定するゲームです。 基本的に友人らとマルチプレイでやりました。 高難度のモードをプレイする場合は結構知識が必要ですが、簡単なモードから進めていくと自然に知識量が増えていくのでおすすめです。 ホラーゲームには抵抗がありほとんどプレイしたことがなかったのですが、複数人でやる場合はホラー要素はかなり軽減されるので楽しいと感じました。(それはもはやホラーゲームなのか…?)

Lethal Company

ホラーゲームその2。モンスターが蔓延る惑星を探索してお宝を集めるゲームです。 基本的に友人らとプレイしました。 MODなども多く開発されており、パーティーゲームとしてはかなり面白いと思います。 YouTubeでソロスコアアタックをしている動画もあり、多様な遊び方ができると感じます。 ただ、最近プレイヤー側が有利な仕様やグリッチが次々にアップデートで修正されているため、ソロではかなり難しいだろうなとも感じます。

Eternal Return

バトロワ型のLoL(MOBA)です。ソロではやっておらず、友人に誘われてたまにプレイしていますが、未だにキャラの名前さえほぼ覚えられていません。 MOBA系のゲームも今年初めてプレイしましたが、知識と判断力と操作の正確性が必要でかなり難しいゲームジャンルだと感じます。 ちなみにヴァーニャ専です。

Inscryption

カードゲームをプレイしながら進める謎解き脱出ゲームです。 ややホラー要素があります。 かなり面白くて20時間くらいぶっ通しでプレイしてクリアしました。 あまりにも短期間でクリアしたので正直内容をあまり覚えていないのですが、逆に言えばそのくらい面白いです。

Undertale

あまりにも有名なゲームですが、ついにやりました。 NルートとPルートはクリアしましたが、Gルートはアンダイン戦がクリアできずに放置しています。

スプラトゥーン3

あまりにも有名なイカのゲームです。 グランドフェスティバルの直前に購入し、自分は割とすぐに飽きてしまいましたが、弟が今でもかなりハマっているようです。

Keep Talking and Nobody Explodes

2人以上で遊ぶゲームで、1人が爆弾を解除しつつ、他の人はマニュアルを見ながら指示を出します。 研究室の忘年会でやったらかなり反響が良かったので、パーティーゲームとしておすすめです。

Street Fighter 6

10時間くらいやってオンライン対戦でボコボコにされてやめてしまいました。 こういう本格的な対戦ゲームをVALORANTと並行して進めるほどの時間的余裕も熱量もありませんでした。 ただ友人らを見ていると本当に面白いゲームなのだろうなとは思います。

【VALORANT】ダイヤモンド1になりました

今年(2024年)の10月にVALORANTでプラチナ1になり、以下の記事を投稿しました。

まだ2ヶ月しか経っていないですが、ついにダイヤモンド1になることができたので、プラチナ1になったときに比べると簡単にですが取り組みをまとめておこうと思います。

プレイ時間の内訳

モード ゴールド1達成時 プラチナ1達成時 ダイヤモンド1達成時
コンペティティブ 154 283 316
アンレート 229 302 306
デスマッチ 115 174 189
チームデスマッチ 33 125 168
スイフトプレイ 2 25 33
その他 5 7 7
合計 538 916 1019

プレイ時間の内訳を見てもわかる通り、プラチナ1になった直後と比べると30時間程度しかコンペティティブモードは回していません。 実はプラチナ1になった後、割とトントン拍子でプラチナ3まで上がることができました。 それからは2ヶ月間くらいプラチナ3の中でランクの上下を繰り返し(本当にずっとプラチナ3で一度もプラチナ2には下がらなかった)、ついにダイヤモンド1を達成できました。

練習法

自分よりうまい人とプレイした

プラチナ3になるまでは完全ソロで回していたのですが、プラチナ3になってからはランク制限などを満たしたことにより、友人と2人でランクを回すことが増えました(ソロとデュオの割合が2:1くらい)。 その友人はランク制限を満たしているとはいえ、自分より内部レートが高いということもあり、デュオでプレイしている際は強制的に自分の適性ランクよりやや高いレベルのプレイヤーと戦うことになりました。 相手のレベルが高いと自分のプレイの悪いところを突いてくることも増える上、味方のプレイなどから学べる点も増えるので、自分の足りない部分を見つける効率が良いと感じました。 また、格上マッチで良い成績を残すことができると、内部レーティング(MMR)が上がりやすい気がします。

ちなみに、これがブースティングかそうでないかの区別は結構難しいと考えており、気にしすぎかもしれませんがデュオの割合が高くなりすぎないようにしていました。あまりにも実力が離れたプレイヤー同士でマッチすることはお互いにとって不幸である上、レーティングシステムの存在意義を損なう行為だと感じています。

(その友人の最高ランクはアセンダント2なのですが、一応弁解しておくと、PCが壊れて約半年間VALORANTをしていないうちにEP切り替え時のランクリセットによってプラチナ3に下がっていたので回せるという状況でした。半年間でどのくらい実力が下がるのかは微妙なところですが、友人も自分も本来はセンチネル専なのにほぼ自分がセンチネルをピックしていたこともあり、パフォーマンスは自分とそんなに変わらなかった気がします。その上、勝率は30%程度でした。)

なお、設定やデバイスやウォーミングアップの内容はほぼ変わっていません。 ダイヤモンド1になってから射撃練習場のアスレチックをするようになったくらいでしょうか。 相変わらずWooting 80HEの効果は大きいと感じています。

できるようになったこと

低リスク高リターンな撃ち合い(ミクロ面)

  • タイミングでピークして勝負して撃ち合う
  • ミリ置きしてヘッショ1発以外では死なないようにする
  • ラークなど1人の相手に対してオフアングルで戦う

セットアップ(マクロ面・ラウンド開始前)

主にセンチネルを使っているときの話ですが、

  • プッシュを狩れたりラークが刺さった次のラウンドは本隊についていく(攻め)
  • 前のラウンドでセットアップが刺さったら次のラウンドでは逆サイトを守る(守り)
  • 交互にサイトを攻めてくるパターンはかなり多いのでそれを意識してセットアップを組む(守り)
  • ラップ系を最初は広く設置して情報を取ることで味方を寄らせ、壊してくるようになったら配置を偏らせる(守り)

などです。K/Dなどの直接的な成績ではわかりませんが、勝敗には結構影響があると思います。同じことをし続けないというイメージです。

1人で広範囲のエリアを管理(マクロ面・ラウンド中)

攻めで相手の配置を予測することは難しくてあまりできていないのですが、守りで取られているエリアがどこであるかを意識して、必要に応じて死なない程度にプッシュするように心がけています。

エコラウンドでのギャンブルプレイ

  • センチネルだけどシェリフを買って前目で撃ち合う(相手が舐めていて意外に刺さる)
  • アビスのBデンジャーなどの思い切ったポジションに行く

キルジョイやサイファーなどのセンチネルがバイラウンドでガジェットが壊される前に死ぬのは良くない動きだと思いますが、エコラウンドはもとから勝てる可能性が低いのでこのようなギャンブルプレイは有効だと感じました。

まとめ

正直上振れ感が否めないですが、それでも tracker.gg によればダイヤモンド1はだいたい上位15%らしいです。

ハイライトは素材がたまったら作成しようかな…。

【VALORANT】プラチナ1になりました

去年(2023年)の12月に以下の記事を投稿しました。

この記事を書いてから約10ヶ月経ちましたが、ついにプラチナ1になることができたので、10ヶ月間を振り返る記事を書こうと思います。

プレイ時間の内訳

モード ゴールド1達成時 プラチナ1達成時
コンペティティブ 154 283
アンレート 229 302
デスマッチ 115 174
チームデスマッチ 33 125
スイフトプレイ 2 25
その他 5 7
合計 538 916

期間別振り返り(自分語り)

ゴールド1達成~EP7終了

実はゴールド1達成後、年末にゴールド3まで達成しました。 ただこの時は運よく勝ちが連続しただけで、個人の撃ち合いや立ち回りではまだゴールド3レベルではないと感じていました。

EP8

ランクリセットでシルバー2まで下がったり、EP8のACT2で10連敗した結果モチベが下がり、プレイ頻度が落ちました。(2連敗したらやめる、は続けていたのですが…)

EP8はゴールド2で終了しました。 キャラはセンチネル(キルジョイ・サイファー)やコントローラー(オーメン・ヴァイパー)を中心にすべてのロールのキャラを使っていました。

EP9~プラチナ1達成

EPが切り替わったこともあり、プレイを再開しました。 9月末には予約していたWooting 80HEが届いたこともあり、プレイ時間が徐々に増え、10月にはプラチナ1を達成できました。

序盤はキャラとしてはセンチネルか4月に実装されたクローヴを使っていましたが、諸事情(後述)で途中からほぼサイファーのみを使うようになりました。

意識したこと・変わったこと

特に最近の自分に当てはまることが多いと思います。

大きく分けると撃ち方と立ち回りの2点があります。

撃ち方

切り返し撃ちを練習しました。

練習方法として、30体bot撃ちのハードモードや100体bot撃ちでbotを撃ってから次のbotが出るまでの間にキーボードでの左右移動を挟む練習をしました。 Kovaak'sについては、もともとトラッキング系のエイム練習としてやっており、実際に効果もあったのですが、最近はキーボードを使ったエイムの練習にならないと考えてあまりしていません。

シルバー帯ではしゃがみ撃ちをする相手が多いため比較的安定して撃ち合いに勝てるようになりました。 ゴールド帯から徐々に切り返し撃ちをする相手も増えてきましたが、100体bot撃ちのbotに左右移動を追加して、キーボードで左右の微調整を行いながら撃つ練習をしたら比較的撃ち合いに勝ちやすくなったと思います。

なお、自分の逆キーストッピングはまだ徹底できてないようで、Wooting 80HEを使い始めてからは特に混戦時の切り返し撃ちの精度が向上したように感じます。

立ち回り

最近特に成長したと感じるのはこちらです。練習方法としては3つあります。

  1. 上手な人のプレイを参考にした
  2. スイフトプレイを活用した
  3. キャラピックを絞った

1. 上手な人のプレイを参考にした

もともとセットアップや定点の座学は行っていたのですが、上手な人のプレイ動画を見て立ち回りを勉強するということはあまり行っていなかったため、立ち回りを見るようにしました。 主にZETA DIVISION所属のLazさんのセンチネルのプレイ動画を参考にしていましたが、単にエイムが良いと結論付けず、攻めのラークのタイミングや守りの寄りのタイミングなどを自分のプレイと比較しながら見ると多くの改善点が発見できました。特にサイファーのワイヤーをラウンド中に張り直すことによるエリア管理は攻めでも守りでも効果がありました。(今までやっていなかったのがおかしいというのはその通り)

2. スイフトプレイを活用した

平日で忙しいときなどはデスマッチやチームデスマッチだけやるようにしていたのですが、これらの量を減らして代わりにスイフトプレイをやり、立ち回りの練習量を増やすようにしました。 スイフトプレイだと変な構成になることも多いのですが、なぜかセンチネルをピックすると味方が正気になってまともな構成をしてくれることが多い気がします。

3. キャラピックを絞った

スキルを含めた立ち回りを練習するためにアイスボックス以外のすべてのマップでサイファーをプレイするようにしました。 キャラを固定するとスキルの使い方や立ち回りがどのマップでも似通ってくるので練習効率が上がると感じます。 ただし他のキャラがしたいことがわからなくなる可能性もあるため、これはあくまでも「今はそういうことをしたら効果がある時期だった」というだけの可能性が高いです。

やや別の話題ですが、ソロランクではセンチネルが一番ランクを上げやすいと感じています。 体感の話になりますが、最近アカウントのレベルが50にも満たないアカウントが多く、彼らは高確率でデュエリストかクローヴをピックするためです。 イニシエーターのスキル合わせは難しいため、余りがちなセンチネルをピックして本隊がローテしやすい・寄りやすい動きをすると良い気がします。 難点としては自分で試合を動かしている感覚があまりないことでしょうか。

設定など

  • クロスヘア:基本的には1420(白またはシアン)、たまにサイズ3輪郭ありのドット(白)
  • 感度:800DPIの0.3
  • マウス:G Pro X Superlight レッド(2024年1月~)
  • マウスパッド:ARTISAN 零 XSOFT L(2023年11月~)
  • キーボード:Wooting 80HE(2024年10月~)
  • アップ:30体bot撃ちハードモード20体以上→100体bot撃ち移動なし→100体bot撃ち移動あり→チームデスマッチ→デスマッチ→スイフトプレイ

まとめ

tracker.gg によればプラチナ1はだいたい上位30%ということで、アイアン3スタートで一時はアイアン2まで下がったことを考えるとかなり成長したなと感じます。

最後にプラチナになるまでのハイライトを作成したので載せておきます。こういう形で自分のプレイを見返すと、プロの選手が異次元であることを思い知らされますね…!

AtCoder Heuristic Contest 002 A - Walking on Tiles

問題

問題概要

 50 \times 50 マスの床に  1 \times 1 または  1 \times 2 のタイルが敷き詰められている。

同じタイルは 2 回以上踏むことはできず、各マスごとに踏むことで得られる得点が決まっている。

上下左右に好きな回数だけ移動し、通過したマスに対応する得点の総和を最大化せよ。

やったこと

  • 4 方向のうち移動できるマスに移動するという DFS を時間の許す限り繰り返す→3865159点

ここで面白かったのは、探索順をランダムにするとスコアが下がってしまうということだった。おそらく方向を偏らせることで開始マスから遠いところに行けるため、広いスペースを使えるのではないかと考えている。

  • 上の DFS を 1500 ms くらい行って発見したルートのうち、→と移動している部分について↑→↓か↓→↑と移動することが可能な部分について、そうするように変更する→4126905点

ここまでが自力の実装で、どちらにしても青パフォ中位くらい。

ここから解説記事やツイートを少しずつ読みながら実装していった。

  • 経路から 1 マス選んで元の経路に戻ってくる焼きなまし→4751950点

  • 経路から 2 マス選んで別の繋ぎ方をする焼きなまし→5625038点

なおこれらの経路探索では 4 方向の探索順はランダムにしてある。(初期解の生成では探索順は固定)

ここで 2 マス選んだほうがスコアが良い理由について考察すると、改善前の経路における 2 マスの間のマスを使って新しいルートを探索できることで、より密な(無駄のない)ルートを構築できるからであると考えている。 自分の実装では、1 マス選んで元の経路に戻ってくる方法では、最終的にどのマスに戻ってくるかわからないため、最終的に戻ってきたマスと最初に選んだマスの間のマスを新しいパス探索では使えていないという弱点がある。

その後しばらく定数倍高速化などの小手先の改善を行っていたがスコアとして変化はあまりなかった。

  • 2 次元配列から 1 次元配列に変更
  • 周囲 4 マスのうち行ける方向を前計算(余談だが、1 次元配列にすると実装が大変かと思いきや、「行ける方向を前計算」するとあまり実装が大変ではないことがわかった)
  • 別の繋ぎ方をする際に 2 マスのうち両方から探索を行う(パスの終点近くはさまざまな方向を試すが、始点近くは方向が偏りがちという問題の対策)

以下では説明のため、2 マスの間の距離の最大値を  D_{max} とし、探索する最大の深さを  DFSCALL_{max} とする

若干の定数倍高速化や実装の変更は行っていたとしても、最終的なスコアの上昇に大きく寄与したのはハイパラの調整であった。

感想

今回の問題は解(移動列)が前の状態に依存しており、単純な操作列のスワップでは valid な解を得られないため、近傍をどう設計すれば良いのか解説記事を読むまでわからなかった。 実際には今回のような問題の場合は依存性が局所的であるため、選んだ 2 マスの間についてのみ依存性を考え直すことで近傍を設計できるのだが、これは割と汎用的なテクニックだと思った。

一般的には近傍の計算を高速化すると焼きなましの反復回数が増えて得点が上がることが多い。 近傍計算の高速化方法として以下のようなものがある。

  • ある解の評価値を一から計算するのではなく、前の解との差分を考えることで高速に計算する
  • ある解から別の解を計算するときに変化のみをメモしておき、ダメだったら逆の操作をして戻すことで状態のコピーに必要な計算コストを減らす

これらの工夫を考えるにあたって、解をどのように表現するか(状態の持ち方)は重要だと感じた。今回の問題ではもともと移動方向を状態として持っていたが、移動方向ではなく実際に通過したマスの列を持つと実装が楽になる。(別に計算コストはあまり変わらないが。)

良いハイパラの勘みたいなものはなかなか身につけることができなさそうで難しい。 短期コンテストだと Optuna などを回す時間もなさそうである。

参考文献

本番 1 位の ats5515 さんのツイート

AHC典型解法シリーズ第2弾「焼きなまし法」]

実装コード

#pragma GCC target("avx2")
#pragma GCC optimize("O3")
#pragma GCC optimize("unroll-loops")

#include "my_template.hpp"
#include "misc/timer.hpp"
#include "misc/xor_shift.hpp"
using namespace std;

#ifdef LOCAL
constexpr f64 SCALE = 3;
#else
constexpr f64 SCALE = 1;
#endif
constexpr f64 TL = 1950 * SCALE;
constexpr f64 TL1 = 100 * SCALE;

// param
#ifdef PARAM
f64 T0;
f64 T1;
int D_MAX;
int DFS_MAX_CALL;
#else
constexpr f64 T0 = 180;
constexpr f64 T1 = 2;
constexpr int D_MAX = 40;  // D_MAX >= 1
constexpr int DFS_MAX_CALL = 800;
#endif

// const param
constexpr int N = 50;
constexpr char dir[4] = {'D', 'R', 'U', 'L'};
// input
array<int, N * N> t, p;
int sx, sy;
int start;
int M;
// 各マスに対して隣接するマスのうち到達可能なマスを列挙
array<vector<int>, N * N> adj;
// 長さ 1~4 の順列をすべて前計算
vector<vector<vector<int>>> fac;

struct Solver {
    // 状態
    // スコア
    int cst_score;
    int nst_score;
    // 通ったマスを (x, y) とすると, x * N + y の形で保存
    vector<int> cst_ps;
    vector<int> nst_ps;

    Solver() {
        // input
        cin >> sx >> sy;
        REP(i, N * N) cin >> t[i];
        REP(i, N * N) cin >> p[i];
        // start, M
        start = sx * N + sy;
        REP(i, N * N) chmax(M, t[i]);
        M++;
        // 4 方向のうち行ける可能性があるマスを前計算
        REP(x, N) {
            REP(y, N) {
                const int a = x * N + y;
                REP(k, 4) {
                    const int nx = x + dx[k], ny = y + dy[k];
                    if (!(0 <= nx and nx < N and 0 <= ny and ny < N)) continue;
                    const int b = nx * N + ny;
                    if (t[b] != t[a]) {
                        adj[a].push_back(b);
                    }
                }
            }
        }
    }

    void init_dfs() {
        cst_score = 0;
        cst_ps.clear();

        vector<int> tile(M, 0);
        vector<int> ps;

        tile[t[start]] = 1;
        ps.push_back(start);

        auto dfs = [&](auto f, int score, int cp) -> void {
            if (timer.elapsed() >= TL1) return;
            if (cst_score < score) {
                cst_score = score;
                cst_ps = ps;
            }
            FORE(np, adj[cp]) {
                if (tile[t[np]] == 1) continue;

                tile[t[np]] = 1;
                ps.push_back(np);

                f(f, score + p[np], np);

                tile[t[np]] = 0;
                ps.pop_back();
            }
        };
        dfs(dfs, p[start], start);
    }

    void output(vector<int>& ps) {
        string ans;
        REP(i, LEN(ps) - 1) {
            int d = ps[i + 1] - ps[i];
            if (d == N) ans += dir[0];
            if (d == 1) ans += dir[1];
            if (d == -N) ans += dir[2];
            if (d == -1) ans += dir[3];
        }
        print(ans);
    }

    void update() {
        // 2 マス選んでその間を別のルートで結ぶ
        int mi = rnd.rand_int(LEN(cst_ps) - 1);
        int mj = rnd.rand_int(mi + 1, min(LEN(cst_ps) - 1, mi + D_MAX));
        vector<int> tile(M, 0);
        int constant_score = 0;
        {
            REP(i, LEN(cst_ps)) {
                if (!(mi < i and i < mj)) {
                    const int cp = cst_ps[i];
                    tile[t[cp]] = 1;
                    constant_score += p[cp];
                }
            }
        }

        vector<int> ps;

        int dfs_count = 0;
        bool found = false;
        auto dfs = [&](auto f, int score, int cp, bool rev) -> void {
            if (timer.elapsed() > TL) return;
            dfs_count++;
            if (dfs_count > DFS_MAX_CALL) return;
            const int len = LEN(adj[cp]);
            const int kr = rnd.rand_int(LEN(fac[len]));

            FORE(k, fac[len][kr]) {
                const int np = adj[cp][k];
                if (tile[t[np]] == 0) {
                    // まだ踏んでないタイル
                    tile[t[np]] = 1;
                    ps.push_back(np);

                    f(f, score + p[np], np, rev);
                    if (found) break;

                    tile[t[np]] = 0;
                    ps.pop_back();
                } else {
                    // もう踏んでいるタイルの場合, ps[mj] と一致するか判定
                    if ((rev == 0 and np == cst_ps[mj]) or (rev == 1 and np == cst_ps[mi])) {
                        nst_score = constant_score + score;
                        found = true;
                        nst_ps.clear();
                        // nst_ps に通過したマスを追加
                        REP(i, mi + 1) nst_ps.push_back(cst_ps[i]);
                        if (rev) {
                            RREP(i, LEN(ps)) nst_ps.push_back(ps[i]);
                        } else {
                            REP(i, LEN(ps)) nst_ps.push_back(ps[i]);
                        }
                        REP(i, mj, LEN(cst_ps)) nst_ps.push_back(cst_ps[i]);
                        break;
                    }
                }
            }
        };

        if (rnd.rand_double() < 0.5) {
            // mi -> mj
            dfs(dfs, 0, cst_ps[mi], false);
        } else {
            // mj -> mi
            dfs(dfs, 0, cst_ps[mj], true);
        }
    }

    void solve() {
        init_dfs();
        show("finish init_dfs()");
        int count = 0;
        f64 t, T;
        while (true) {
            if (count % 100 == 0) {
                t = timer.elapsed() / TL;
                if (t >= 1.0) break;
                T = T0 * (1.0 - t) + T1 * t;
            }
            count++;
            // nst を計算
            nst_score = cst_score;
            nst_ps = cst_ps;
            update();
            // 反映させる or そのまま
            if (rnd.rand_double() < exp((nst_score - cst_score) / T)) {
                cst_score = nst_score;
                cst_ps = nst_ps;
            }
        }
        // print(count);
        output(cst_ps);
        // print(cst_score);
    }
};

int main(int argc, char* argv[]) {
    timer.reset();

#ifdef PARAM
    if (argc == 1 + 4) {
        T0 = atof(argv[1]);
        T1 = atof(argv[2]);
        D_MAX = atoi(argv[3]);
        DFS_MAX_CALL = atoi(argv[4]);
    }
#endif

    // 前計算
    fac.resize(5);
    REP(sz, 1, 5) {
        vector<int> perm(sz);
        iota(ALL(perm), 0);
        do {
            fac[sz].push_back(perm);
        } while (next_permutation(ALL(perm)));
    }

    Solver solver;
    solver.solve();
    return 0;
}

Rainbow Six Siege が Trend Micro Apex One によって起動できなくなる不具合

Rainbow Six Siege を遊んでいたときに発生した不具合に関するメモ。

状況

  • 環境:Windows10 であり、steam 版ではなく Ubisoft
  • 初めは何の問題もなくゲームを起動でき、プレイもできるが、以下のような内容が書かれたタブが出現する。
hh:mm:ss: BattlEyeサービスの開始...
hh:mm:ss: 更新中...
hh:mm:ss: ゲームを起動する...
hh:mm:ss: 注:ファイルブロックは、ゲームに問題がなければ無視しても構いません。
hh:mm:ss: [INFO] ファイルの読み込みがブロックされました。"C:\Program Files (x86)\Trend Micro\Security Agent\AMSI\TmAMSIProvider64.dll".
  • クイックマッチを数戦プレイすると友人らのパーティーから追い出されてしまう。(というか友人らのパーティーが解散してしまう)
  • その後ゲームを起動しようとすると起動できない。
  • Ubisoft Connect の再起動などは効果なし。
  • PC を再起動すれば数試合はプレイ可能だが、再度同じ状況が発生する。

解決策

Trend Micro Apex One(大学で配布されているウイルスバスター)が原因のようで、以下のように検索除外に設定を加えれば良い。

  1. タスクバーの ^ から Trend Micro Apex One を起動
  2. 左下の歯車のマークから設定を開く
  3. 保護→検索除外→ディレクトリ とし、以下のパスを追加
C:\Program Files (x86)\Ubisoft
C:\Users\username\AppData\Local\Ubisoft

username は自分の PC の名前に置き換えること。

おそらく一番上のパスが効果があった。

BattlEye というのはアンチチートソフトのことらしく、これが C:\Program Files (x86)\Ubisoft\Ubisoft Game Launcher\games\Tom Clancy's Rainbow Six Siege\BattlEye にあるので、これを Trend Micro Apex One が無視するように変更できる。

BattlEye が安全かは一旦信じるしかない…。

感想

ウイルス対策ソフトとアンチチートソフト系の相性が割と悪く、こういうことはよく発生する印象がある。チートがなくなることはないだろうし、こういうのはゲームを遊んでいくうえで付き合っていくべき問題なのだろうなと感じる。