マイファニーウィークエンド

ゲームの感想とか、哲学の話とか

【祝7周年!】ポケマスデビューからの3.5年を振り返る!【パート1?】

ポケモンマスターズ EX 7周年おめでとうございます!!!!!!!

 開幕突然の 自分語り どうした?なんですが、自分がポケマスを始めたのは3.5周年(ネオチャンガラル)のころだったので、これでポケマスの歴史の半分は知っているということになりました。いや本当に感慨深いです。

 折角なので何かしらお祝いに記事を書きたいなと思いまして、いろいろ考えていたんですけど、書きたいことが多すぎてまとまりません。考えたこと(読み飛ばしてくれていいです)↓

 

  • 好きなバディーズを語る
    • 性能とか、ビジュとか、フレーバーとか
    • 挙げだすとどう考えてもキリがない
  • 好きなコンテンツを語る
    • タワーとかラリーとか覇者とか
    • みっちり語ろうとすると準備が大変そうすぎる
  • 好きなゲームシステムを語る
    • そもそもポケマスのバトルが面白いという話をする
    • みっちり語ろうとすると準備が大変そうすぎる
  • 好きなストーリー・イベントを語る
    • なんぼでもある。キリがない
  • ポケマスの歴史を振り返る
    • 流石に書きたいことが多すぎるし、自分が始まる前のことはあまり書けなさそう
    • 逆に言えば自分が遊んだ3.5年分ならギリギリ書けるのでは?

 うーん、まだしも書けそうなのは自分のポケマスデビューからの3.5年を振り返ることかもしれない。そうは言っても3.5年、正直書いているうちにギブアップするかもしれないんですけど、一旦書き始めてみようと思います。ダメだったらボツになります。幸いスクショは無限に(ザっと数えて5,000枚以上)あるので思い出すだけならそんなに難しくはないはず。

 開幕の自分語りどころか全編自分語りになるのが確定しましたが、まあブログなんて自分語りをする場所ですから。いちポケマスプレイヤーの3.5年の歩みを少し見ていってもらえると幸いです。自分にとっても記録を残しておけばあとから読み返して懐かしい気持ちになれるはず(そういう意味では3.5年ってちょっと遅かったかもという気はしますが)。一応、注意点としては自分のスクショとか過去ツイとかを漁りながら書いていく記事なので全ての出来事を総ざらいできるわけではもちろんないですし、事実誤認や記憶違いがある可能性もあります。何かお気づきの点があればX(現Twitter)までご連絡いただければと思います。

【追記】

 と思っていたのですが、普通に半年分で力尽きてしまいました。みっちり書き上げてから投稿することも考えたのですが、流石に長くなりすぎるし完成する前にどう考えてもボツにしてしまうのが目に見えていたので今回の記事はポケマスデビューから半年後の4周年までの内容になります。それでもなんと約2万字あります。なんで??

ポケマスを始めるまで~ポケマスデビュー(~2023/03)

 もちろん、実際にポケマスを始めるまでもポケマスの存在を知らなかったわけではなかったです。ポケモン本編はほぼ全作遊んできましたし、ポケカなどの外伝コンテンツにもどっぷりで、Twitterのポケモンのコミュニティにはいたわけですから、定期的にウワサ話は流れてきていました。

 特に『世界を彩る数式』が公開されたころの反響は凄まじかったです。ただし、今までソシャゲをほとんど遊んでいなかったこともあり、まともに遊んでいける気がしなかったのでなかなか手を出せていませんでした。

 そんな自分にとって転機となったのはやはり2023年2月のPokémon Presentsで発表されたネオ・チャンピオンのマリィ・ホップ・ビートでした。

 ちょっと今見ても衣装良すぎるな……。当時も今も、ソード・シールドがポケモン本編で一番好きな私にとって、これはあまりにもポケマスを始めるのに一番のタイミングでした。フォロワーさんが背中を押してくれたこともあり、ついにポケマスEXデビューをすることになりました。2023年3月1日未明、午前1時のことでした。

うちの最古のポケマスのスクショ

2023/03~2023/04

 右も左もわからないまま始めたポケマスEX、ゲンカイ突破? ☆アップ? わざレベル? 多すぎる強化要素*1、いろいろあって何から進めたらいいのかわからないコンテンツ、右往左往していた自分がなんとか一番大変だった序盤期を乗り切れたのはDiscord鯖のみなさまの支えがあってのことでした。思えばあの鯖が立ち上がったのも自分がポケマスデビューしたころのことですから、鯖も3.5周年なわけですね。本当に感慨深いです。

 当時は悪の組織編カロス編の真っただ中で、チャレンジバトルのパキラに勝てなかったのが印象によく残っています*2。まあ当時は今みたいにフェス限いっぱいもらえたりもなかったですし、ネオチャンガラル3組+恒常ではそりゃ難しかったよなと思います(自分のポケマス理解・練度もまだまだでしたし)。ただ、逆に言えば初期で苦しかったのはそこくらいで、WPM編とかのコンテンツは全部マリィチャンがなぎ倒していってくれました。

 マリィチャンの火力とタフネスは本当にすごくて、バトルヴィラ*3なんかでも相当お世話になりました。最近でも完凸ボードやジムポテのおかげで意外と前線でやれてるいいバディーズだなと思います(パッシブがシンプルなステータスアップなのでレベル上限突破の恩恵が大きいのもあるかも)。ヴィラだとビートチャンも活躍してくれましたね。Bわざのたびにわざの使用回数が回復するのでマリィチャンと並べてバフをかけ続けてくれたのが心強かったです。現代ではやや器用貧乏な印象もありますが、どんな編成にも入るポテンシャルがあるのでラリーなどで頼りにさせてもらっています。ホップチャンはこの2組と比べると少し癖が強いところがあるなという印象はあります。WTZの維持・デバフ・シンクロわざ、毎ターン最適な技を選択する必要がある難しさ・忙しさがありました。ただ、後々語る(予定の)覇者であったりトリプルピックアップの副産物だったりでいつの間にか完凸になった今ではまたラリーなどで活躍してくれています。あと、ホップはドドン台詞が本当に好きで、ポケマスからは離れちゃうんですけど、ソード・シールドのエンディング後のホップとのラストバトルはポケモン全体でも1, 2を争うくらい好きなシーンなんですよ。それまでの旅の総括として本当に切なくなってしまって号泣してしまって。だから初めてホップをお迎えしたときのこのドドン台詞も本当に心が揺さぶられました。

 ネオチャンガラル、どちらかというと3組とも単体で完結している寄りの性能をしていて3組並べてすごくシナジーが生まれるわけじゃないですけど、それでも3組並べるとぎゅんぎゅんBC加速して次々シンクロ技が解放されて、並べて楽しいデザインになってたのが良かったなと今になっても思います。

 そんなガラル好きの自分なのですが、最初に☆6EXにしたのもなんとソニア&ワンパチでした。当時はそもそも☆6EXがなにかもあんまりよくわかっていなかったんですけど、「サポートを優先してあげていくのがおすすめ」と聞いていたのと、「戦闘!ソニア」がお気に入りだったのもあります。決め手になったのはEXカラーの黒スキニーがセクシーすぎたからでしたが。

当時この衣装を「黒タイツ」と表現してしまい途轍もない恥をかいたことがいまだにトラウマ(?)になっています。いやでも今見ても質感はどちらかというとタイツっぽいですよね?? そりゃもちろん本当にタイツにコートだったらとんでもない衣装ということになってしまうのですが……

 懐かしの第一回パシオ殿堂入りもソニア入りで達成していました(当時はまだ☆5)。

 この当時の一番のメインコンテンツはやはりチャンバトでしたね。圧倒的に所持状況が足りていない中で毒耐久したり試行錯誤したりで毎週頭をひねりながら、鯖のみなさんにも相談しながらコツコツクリアしていきました。当時は自分も含めて鯖で定期的に(縛り)チャンバト配信があったのも懐かしいです。あの頃お世話になった恒常・配布系バディーズの話だけでも余裕で1記事書けそうですが、今回は割愛します。いつか機会があれば。

当時(厳密には半年くらい先)のチャンバト配信で使っていた素材です。我ながらなかなか凝ってますね

 そろそろストーリーについても触れておきたいです。WPM編は各地方ごとのトレーナーの夢の掛け合い要素こそあれ若干の薄味感を覚えざるを得なかったのですが*4、伝説ポケモン編~悪の組織編はずっと興奮しっぱなしでした。今更自分が語るまでもなく語りつくされていますけれど、それでも語るとするならやはり『正しき心に舞い降りる翼』でしょうか。この先のシルバーの話をいくつも読んだ今だからこそという思い出補正もあるのかもしれませんが、やはり自分はこういう「報われる」話が大好きなんです。

 もうひとつ、この頃のメインストーリーで触れておくとするならやはりヒガナの存在でしょう。自分は基本的に多くのコンテンツで箱推しになるタイプなのでポケマスでも「このトレーナーが一番好き!」というのはあまり決められないのですが、「ポケマスを遊んで好きになったトレーナー」という意味では間違いなくヒガナになると思います。

 まあもともと「ミステリアスで底が知れないけど妙にフレンドリーなおねーさんキャラ」が割とツボだったのですが、ヒガナのその魅力に気づかせてくれたのがポケマスでした。あとおっぱいがとても大きい

 余談ですが、自分が今のポケマス用のX(旧Twitter)アカウントをつくったのもこの頃ですね。5,000万DL記念のキャンペーンに参加するためでした。

 そしてこの伝説ポケモン編からの悪の組織編『大地と海と空のデルタ』ですよ。

「ダイゴが」「ヒガナに」「メガレックウザと一緒に隕石を砕くことを依頼する」、あまりにも文脈が乗りすぎている。絶対にプレイヤーたちの脳を焼いてやるというポケマスくんの魂が感じられるシナリオだと思います

 凄まじい。これは凄まじいです。今見ても心が震える「凄味」があります。自分は見事にこのシナリオで脳を焼かれました。シンプルにヒガナの魅力を引き出すのがうますぎますし、ヒガナとダイゴの会話から取れる出汁を無限にとっています*5。

 ポケマスくんの強火に当てられ、ヒガナに脳を焼かれてしまった自分なのですが、その余韻も冷めない4月……

ま さ か の ヒ ガ ナ 新 規 供 給

 めちゃくちゃびっくりしました。決してメジャーとは言えないキャラだしそうそう新規供給があるものではないと思っていましたから*6。あまりにも自分に都合の良すぎるタイミングで本当に夢かと思いました。シナリオも本当によく、ダイゴとヒガナの関係性からさらにさらに出汁を取り、さらにこのやり取り

 もとより焼かれていた脳はもう消し炭になりました。本当にポケマスくんはヒガナの魅力を引き出すのがうますぎる。衣装も、野性味あふれる通常衣装とのギャップがもう最高ですし、性能としても未だに個性があるのでEXR(アタッカー)がもらえれば十分やれる性能をしているんじゃないかなと思っています(ちょうど悪物理でセレナが来ちゃいましたけど)。スぺコストウコにもEXRが追加されたので近々(選べるのタイミング?)ほかのスぺコスにもEXR追加あるんじゃないかと思っています。本当にお願いします。

 ポケマスくん、やっぱり原作で決してめちゃくちゃ有名というわけではないキャラからでも見事に滋味を掬い上げて一本のシナリオに昇華するテクニックが凄まじくて、それは(謎解きイベントと時系列が巻き戻っちゃうんですけど)自分がポケマスを始めてから初の通常イベント『ガラルのボールマニア?』でポケモンマスターズ EXの常識を変えたボールガイのシナリオでもいかんなく発揮されていたと思います。

 ボールガイって基本的にはネタキャラとしての扱いが多くて、実際この時だってそういう存在としての魅力も発揮されていたんですけど、それだけで終わらなかったのがすごくて、パシオという環境とボールガイという存在を衝突させてこんなにも見事なシナリオを出力するポケマスくんの手腕には舌を巻かざるを得ませんでした。

こういう本編だと基本的に語られない「主人公からの感情」が語られるのがポケマスのいいところですよね

 まさかボールガイを主役にしてこんな味わいあるイベントが展開されるとは思っていなかったです。個人的には今でもすごいお気に入りのイベントのひとつです。ヒガナの時もなんですけど、やっぱり「報われる」話が好きなんでしょうね。

2023/05~2023/06

 スクリーンショットを振り返っているととても懐かしいイベントが出てきました。

どうしてもシンクロわざ中心の編成だとタイムに限界があったなあというのをうっすらと覚えています

 そう言えばありましたね、タイムアタックイベント。まあ今のダメチャレとかの前身のようなものと考えることもできるかもですね(そうかな)。

 これがちょうど悪の組織編アローラ編の頃でした。当時はポケマスデビューしてから初のメインストーリー更新ということでお祭り騒ぎにワクワクしていたような気がします。

これは当時「何かと使い勝手が良さそう」と思って撮ったスクショ

 個人的にソード・シールドの次に好きなポケモン本編がサン・ムーンだったこともあり(やりこんだ世代という意味では一番かも)、当時のマジコスキャプテンズの実装はとても嬉しかったです。今思い返すとカプたちのように明確に一貫したコンセプトで実装された例って意外と少ない気がしますね。性能的にも面白くて当時はとてもわくわくしていました。

 ただ、当時の自分は(今も?)ダイヤを使う勇気がなかなか出せない臆病者なもので、結局この4組は全員涙を呑んで見送ることにしたのでした。結局全員そろったのは5周年の選べるフェス限だったのでここから1年以上後になるのでした。

 言いたいことはわかるけど当時の自分ちょっと面倒くさすぎますね。

 

 他にこのころ印象深かった出来事と言えばやはりセキ・カイの実装でしょうか。待望のヒスイ勢の実装です! 自分はまだ当時ポケマス始めたてだったのでそこまでピンと来ていなかったのですが、周りの盛り上がりのすさまじさはよく覚えています。セキの「P技B技BD技急所化」からの「急所時次ゲージ消費無9」のインパクトで笑ってしまったのもよく覚えています。もう今ではそこまで無法というほどの印象ではなくなってしまいましたね。時が流れるのは早い(セキだけに)。カイは当時は相方不足感がありましたけれど、のちのちレッドチャンという最強の相棒を得てラニュイ相手などに大暴れしてくれました。Bサーチの66連めで2組来ていきなりわざ2になったのでそのまま勢いでわざ3にしたのも思い出深いです。ダイヤもですがアメもめちゃくちゃケチケチする人だったのでマスター(というか限定バディーズ全般?)でわざ3なのはカイだけみたいな時期が結構長かったような気がします。

 シナリオも良かったですよねえ。最後の着地、「セキとカイが現代にやってきたとしたら、どうする?」というシナリオを「相棒をモンスターボールに入れる」という回答で〆るのは本当に痺れました。

 まあメタ的には「サトシのような特例があまり増えると工数がかかりすぎる」みたいな事情があったのかもしれないのですが、だとしてもそれをこうして納得感のある形で話のオチに組み込む腕前に脱帽するほかありません。

 ポケマス本編のイベントからは少し離れてちょっと余談なのですが、この頃、次に来るバディーズの検討をつけたくて実装履歴を自力でスプレッドシートに書き起こしたりしていたののスクリーンショットも見つかりました。我ながらちょっと引くほどの情熱です。

 

タイプごと、世代ごととかに分けてどこが実装時期が空いていて次きそうかなとか考えてました

 ただ、この頃はまだ始めたてでマスターの周期とかメインシナリオの更新周期とかが全然わかってなかったのでお粗末な予想ではありました。でもこれ考えてるときはめっちゃ楽しかったですね……。

 

 さて、セキカイイベのあとはジョウトのターンでした! ジョウトは毎年これくらいの時期に何かしら供給が来ることが多かったような気がします。これもまた自分で言う必要もないくらい多くのプレイヤーの共通認識だとは思うのですが、ポケマスのジョウトイベントは積み重ねがすごくて、この年は『黄金色に輝く未来』の復刻からの流れもあって非常に印象的でした。

 当時の自分も今と似たようなことを言っていますね。ミナキはBGMアレンジも良くて、本当にお迎えしたかったのですが、この時も結局涙を呑んで見送りました(これも約1年後にお迎え)。でも復刻のマツバはちゃっかり確保していました。鯖の皆さんに見守られながら。

当時の配信用の素材です。わざわざマツバは反転させたりしています。無駄に凝っている

 当時のツイートを発掘してたらめちゃくちゃ悩みながらも回して11連で2枚抜きしてるツイートが出てきました。それは犯罪だろ。ともかく、カイの時と同じくマツバも2枚抜きできたのでそのままわざ3に上げました。晴れ編成でなくてもすさまじい耐久力の本当に頼れるサポートで、EXRの追加もあって現在でも信頼感がすごくあるバディーズのひと組ですね。ていうかこの人、わざ1でもT技回数回復あるのおかしくないですか?

2023/07

 いきなり余談から始めるんですが、2023年7月はコシヒカリ(マジコスヒカリのごく一部界隈での俗称)にひたすら苦しめられていました。

 あまり身内ネタばかり書くのもよくないと思うんですけど、やっぱり記録としては残しておきたくて……。

 閑話休題。2023年、夏イベ『離れ離れの双子星』もとても良かったですよね。さっきから良かったとしか言っていないんですが。

 当時の自分のツイートちょっと良すぎますねこれ。しかしこのイベントシナリオは本当に良かった。『ポケットモンスター』の原点のひとつ(だと勝手に思っている)「ひと夏のちょっとした冒険」の味わいがとてもよく出ていたと思うんですよね。自分はこういう「世界を救ったとかではないし、周りの人々はほとんど起きたことすら知らないけれど、当事者たちの中にだけ確かに思い出として残るちょっとしたイベント」みたいなお話が本当に大好きなんです(いやまあこの話は割ともうちょっと規模の大きな話な気もしますけど)。フウランの話としても良かったですし、アオギリの使い方もうまかった。半年前にリラを実装していたのもこれへの補助線だったのかなあというくらいリラもいい味わいを出していました。思えばマジコスネモの時もいい味を出していましたしポケマスくんはリラの扱いも上手いですね。リラの新規イベください(強欲)。ウシオの素手太鼓も良かった(?)。

 でも夏イベという意味では2022夏『真夏の空に願いをこめて』も負けていません(何が?)。

よ、良すぎる……。もう言葉なんて要りませんよね?

そしてこれは原作を読んだら全然笑えないシーンだった石コナ

 『世界を彩る数式』はトウヤとNの話で、もちろんトウヤが主人公であるという正史を踏まえるとそっちがストーリーとして本筋になるのは当たり前なんですけど、今回はそれを踏まえたうえで「じゃあ正史で関わりがないトウコとNの間にならどういう話が生まれる?」というところを膨らませたシナリオで、そこからこの着地に至るのはやっぱりすごいなと。当時の自分はたぶんまだ世界を彩る数式を読んではいないんですけど、それでも本当に感激したので、ずっと追っていた人たちの感動はまた格別だったろうなと思います。でも、ブレイク団のイカサマを暴くくだりはなんかこう、ゴツゴツしてたと思う。

 この年は夏ユウリ目当てでステップアップも回しました。ビジュがめっちゃ好きなんですよね。衣装もですけど、ヘアスタイル(ギブソンタック)もソード・シールドの中では一番好きな髪形でして。インテレオンがすごく思い入れのあるポケモンなのもありました(ソードでのパートナーでした)。キョダイインテレオンもめっちゃ好きなんでいつか実装してほしいところです。この時は天井で夏ユウリ、道中で夏ダイゴと夏Nもお迎えできていたので一気に夏イベのシナリオを開放できたので相当運が良かったと思います。2021, 2020の夏イベも良かったことは言うまでもありません。ガスボンベ担当の主人公も見れますし(?)。

 話を現代に戻しまして、過去の夏バディーズはかなりお迎えできたんですが夏フウランはやはりお迎えできずでした。夏フウは1年後のステップアップでお迎えすることになるんですが、個人的にすごく好きなバディーズのひと組ですね。デバッファー・麻痺撒きとしても優秀なうえにはめつのねがいの演出・火力が気持ちいいですしB技砲台としても魅力的です。今年EXアタッカーが追加されたのも嬉しかったですね。

 さて、この夏にはもうひとつどデカいイベントがありました。そう、スコアアタックイベント「みずを統べるもの」です。

 第一回のスコアアタックイベントということもあり、みんな手探りで攻略を進めていたのが思い出深いです。この頃の自分も所持状況も腕前も全然足りていないながらに全力で試行錯誤していました。

試行錯誤のあしあと

 個人的には、ポケマスのコンテンツの中だとスコアアタックイベントはかなり上位に入るくらい好きなコンテンツのひとつです(他はタワーとかジムバトルとか)。ある程度緩い縛りのある中で最適解を探して試行錯誤して編成パズルをガチャガチャし続けるのが本当に沼が深いと感じます。ただ、本気でやろうと思うと無限に時間が必要なコンテンツにはなるのでもう今では本気でやりこむのは無理かもなあとは思っています。いつかまたあの沼に飛び込みたいとは思っているのですが……。とは言えもう今となってはスコアアタックイベントも研究が進みすぎていて自分くらいのプレイヤーがちょっとやりこんでどうこうできるような世界ではないのかもなという気もします。スコアアタックには本当にずっと未練があるのですが、それはまた別の機会に(あるかなあ)。

 これはスコアタ攻略中に現れた不審者。

 

 水スコアタと言えば念願の道着ユウリ復刻も思い出深いです。今ではイベント合わせの復刻は選べるのが当たり前になりましたね。選べる方がいいっちゃいい気もするんですけれど、当時の誰が復刻するか予想する時間も楽しかったので少し寂しい思いもあります。

当時の配信用素材。恒常3人がうっすら映りこんでいるのがお気に入り

 この時は確かユウリもマジコスフラダリもお迎えできた気がします。直前に夏ユウリの話もしたばかりですけど、道着ユウリもめちゃくちゃ好きなんですよね。性能としてはややポジションの被るセレナチャンと比べられがちな印象がありますが、タワポテのおかげで結構火力が出せるようになったのはいいところだな、と思ってます。あとすいりゅうれんだの音が気持ちいい。

 さて、この辺ほぼ同時進行なので厳密な時系列はもうわからんのですが、前回の悪の組織編(アローラ)から2か月がたち、ガラル編が開幕しました! 前述のとおり自分はポケモン本編だとソード・シールドが一番好きなタイトルということもあり非常に期待していたのですが、その期待を大きく上回る素晴らしいシナリオでした。ソード・シールドは本当に好きなタイトルではあるんですけど、やっぱりローズとかブラックナイトのあたりってちょっと言葉足らずだった部分もあったよなと正直今でも思っています。悪の組織編ガラル編は本編で向き合いきれなかった問いについて、ポケマスという場で改めて同じ問いを投げなおし、そして今の彼ら(ダンデたち)だからこそたどり着ける答えに至ったシナリオだったと思います。

 これは点と点を線で勝手に結んでいるだけのようにも思えますけど、ポケマスくんだとありえなくはないラインな気もしますね。

 ここのオリーヴも本当にいい味を出している。このわずかな「間」に文脈をきっちり乗せられるポケマスくんは本当にすごいなと思います。

 そして、何よりもやはり特筆すべきはこのシーンでしょう。

 自分はポケマスくんのこういう原作再現に本当に弱いんです。Switch初の完全新作であるソード・シールドをワクワクしながら起動して最初に流れたあのシーンを、明らかに今までのポケモンとは違う新時代のポケモンの幕開けを告げるあのシーンを、ここで再現するというのはあまりにも卑怯というものです。しかもこのイベントにおいては、「バディストーンの力はバディーズたちに預けてこそ輝く」ことを認めたローズがこの口上を述べることによる力強い文脈が乗っているわけです。いまだに自分の中でポケマス全体で五本指に入るくらいには好きなシナリオです。

 こんなことも言ってましたが、実際に実現したのは1年半後、ポプラ登場はさらに約1年後でしたねえ。ポプラさんプレイアブル化待ってます😭

4周年!(2023/08)

 さて、そして8月、ついにポケモンマスターズ EXは4周年を迎えます。前夜祭はまさかのパルデアから一番乗りでやってきたネモでしたね。

 ガラルやヒスイの例と比べたときにパルデアはかなり早かったですよねえ。今となってはゼロの秘宝組やZ-A組も遜色ない速度かな?という感じではありますが。そして同時に第四のロール・スピード、第五のロール・フィールドの追加でもありました。

 この時の情報公開はすごく変則的で、22時公開のPokémon Presentsに先だってネモ&パーモットの情報だけ公開されたんですよね。正直自分でもどういう流れだったかあまり覚えていないのですが、当時のツイートをさかのぼった感じPVとかイラストとか一切なしにレターが公開されがっぽいですかね?(もしも当時の情報を覚えてらっしゃる方がいらっしゃったらご連絡ください) だとしたら本当に異例ですね。お昼時点での情報ではネモとスピードロールの情報だけが公開されていたのでPokémon Presentsでまさかのフィールドロールとマサルの情報が公開されて度肝を抜かれたように記憶しています。二段構えのサプライズになっていてあれは(いい意味で)ずるい情報の出し方だったなあと思います。

 当時のツイートを遡ると、4周年の前夜祭期間は結構プロモーションが豪華だったような気がしますね。珍しくカナちゃんが直接情報をお届けしてくれる動画が公開されたりして。個人的にはカナちゃんがすごく好きなのでもっと積極的に出てきてくれたらいいのになと思ったりしています。当時はWCSが日本で開催されていて「控室」で盛り上がっていたりもして、なんかポケモン界隈全体がずっとお祭り騒ぎだったような気がしますね。懐かしいなあ……。

 話がロールに戻りますが、思えばもうロールが3つだった期間の方が短くなるのも目の前なんですね。恐ろしい話です。ただまあ、後付けのロールということもあってスピード・フィールドロールには思うところがないでもないよなとは当時も今も思っています(特にスピード)。

 ここから約3年たちますが、やっぱりスピードロールのEXBわざ効果はちょっと地味だよなという印象は変わらない気がします。マジコスライヤーとかミヅキチャンとか、めっちゃ強いスピードバディーズはもちろんいっぱいいるんですが、それはロールだけじゃなくて全体の性能込みでという話であって。裏を返せば別に全員がスピードロール前提でその分ほかで下駄を履かせてもらえるならいいというわけなんですが、これが結局のちのEXロール格差の話にもつながってくるのかな、という気はしますね。

 ということで4周年本祭、前夜祭のネモからうっすら匂わされてはいましたがネオチャンカントーでしたね!

www.youtube.com

 TwitterとYouTubeで微妙に動画違うんですね。『選べる☆最強のトレーナートレーナーEX』の情報なんかは動画としてはYouTubeの方にしかありませんでした。『選べる☆最強のトレーナートレーナーEX』って何ですか???? まあ今年のメガ盛りどんぶりみたいなもんか……。

 PV本編の話に戻りまして、なんかひとりだけ特に他とかかわりなく勝手に悪夢に引きこもってるアカギとかアクロマに「キライですからね!」されるゲーチスとか、味がするところの多すぎる前半部はさておき、3周年から連続のアニバーサリーレッドには驚かされました(まあ今振り返ると6周年6.5周年もレッドで7周年はグリーンなのでまだまだ序の口みたいなもんだったんだなあという感じですが)。ただ、リーフは本当に念願の3着目ですし、グリーンも何気に当時はポケマスオリジナル衣装はマジコスだけだったんじゃないかなあという感じで、自分としては素直に嬉しかったですね。衣装もおそろいでEXカラーも込みですごくかわいくてお気に入りです(個人的にはやっぱりリーフがエレベーターガールっぽくて一番好きですかね)。

 性能面でもネオチャンカントーの3組には本当にずっとお世話になりました。レッドチャンは相棒に焦がれていたカイと組んでシンプルな超火力で猛威を振るっていた印象がやはり強いです。現代では流石に純粋な火力では少し物足りなさを感じないこともないですが、氷抵抗ダウン・S技の行動阻害などで十分仕事を果たしてくれる頼れるバディーズだと思います。リーフチャンは当初は単体だとにらみつけるのデバフ速度がやや遅くS技で倍化するまでに時間がかかるなど、ちょっと難しいところもあるバディーズという印象だったのですが、ラリーなどでセレナチャンのような他のデバッファーと組むことで真価を発揮すると理解してからは活躍の幅がぐっと広がったように感じています。ただ、やはりその2組と比べてもグリーンチャンは本当に頭一つ抜けて無法で、特殊寄り万能タンクとして全然今でも現役だなと思います(その後のアニバタンクともなんだかんだできちんと差別化できているのが本当に偉い)。

本当にこの通りだと思います。無法すぎる。

 

 ???

 

 4周年の目玉のひとつだったEXロールについては、当時結構賛否両論ではあったように思います。EXロール自体もですけど、当時はロールケーキの入手手段があまりにも限られすぎていたことが指摘されていた気がします。まあ、その頃から「こういうのは後々入手手段が増えていくからあわてないのが一番」と古参勢に言われていて、実際その通りにラリーなどである程度解消されましたが。そしてEXロール自体についてですね。やっぱりどうしてもロールの組み合わせで相対的な格差が結構大きい感じになっちゃうのは後付けゆえの仕方なさこそあるもののちょっともやもやした気持ちになってしまう要素でしたね(特にあとからEXRが追加されたバディーズについては)。いまだに「このバディーズのEXRが〇〇だったらなあ」はときどき思ってしまうところではあります。もちろんEXRによる面白みもいっぱい生まれてはいると思います。シンプルにバディーズの性能に多様性が生まれることは面白みですし、バトルラリーとかもEXRなくては成り立たないコンテンツですからね。さらにもう一歩踏み込むなら、多様性と格差って表裏一体というかどうしても避けがたいトレードオフではあるんですよね。もう少し調整の余地があったのでは、という気持ち自体はあるんですが、とはいえ全くの平等なんてのには当然絶対できない、とも思っています。

 4周年といえば、シナリオもなかなか強烈でした。主に新人相手にすさまじいスパルタ教育を施すレッドさんが。自分はあの指導をされたらまあ間違いなく心が折れちゃうだろうなとは思いますね😅

 でも、シナリオとしては三者三様のマサラのトレーナーの在り方を改めて表現した良いシナリオだったと思います。同じキャラクターでも立場を変えて別の見方をすることでまた新たな面が浮かび上がって見えてくるというのは、やはり良いものですよね。

 

 そして後夜祭(という表現は当時なかったですし時期的には本祭②という感じもしますが)はキリヤ・リッカ、そしてバラエティロケット団でしたね。キリヤは当時も今も貴重なスピサポで火力役もこなせるし加速力・支援性能も高い万能型なのが偉くて、なんだかんだラリーでしょっちゅうお世話になってます。ちょうどこの7周年でわざ3になったので今月のラリーでも最終階アザミ相手に火力役を務めてもらいました。

 ロケット団の方は、一気に5組実装は結構話題になりましたよね。アテナのテクアタなど当時としては結構レアなEXRの組み合わせもあったりして。ちなみに、この時のバラエティロケット団確定サーチは自分はランスでした。

悪の組織の最も冷酷と呼ばれている幹部のやることが「にせヤドンのしっぽ密造」なことないだろ

ドドン背景もヤドンのいどだし

 ストーリーに話を戻すと、長かった(自分がリアタイしたのは半年ですが)悪の組織編が堂々完結ということでやはり感慨深い思いがありました。前夜祭・本祭前半からの盛り上げ方も良かったですし、最終的にキリヤと主人公にきちんとフィーチャーして締めてくれたのがとても良かったと思います。クルティたちにスポットライトが当たったのもメインストーリーの締めとして、満を持してという感じで良かったですよねえ。結局サカキに吠え面をかかせることはできず割と余裕の顔で逃げられてしまったのは若干の消化不良感はありましたけれど*7。悪の組織編が完結して始まったアルセウス編もそろそろ2年になるので7周年後夜祭で何かしら大きな動きがある可能性はありますね。アルセウス編自体がそれぞれのイベントの独立性が割と高くてあんまり縦軸の話が進んでいないなという印象はあるのですが……。

 

おわりに

 まさか1日かけて、2万字かけて半年分しか進められないとは思っていませんでした。まあ始めてからの半年ってそりゃ一番濃かった時期だとは思うのですが……。これでも結構触れられなかったなあという話題とかも結構あるんですよね。やっぱりまとめて一気にとかじゃなくて定期的に感想はまとめないといけないなあと思いました。

 (過去のブログ記事を見ていただければわかりますけれど)自分はとても飽き性な人間で、正直この先書き続けられる気は全くしていないのですが、もしも需要があれば、そして気が向けばパート2以降も書いていきたいなと思います。ミヅキの話とかも全然できてませんしね!!

 まさかこの伏線を3年越しに回収することになろうとは。

 

 

OFUSEで応援を送る

 

*1:アプデで改善されるそうなので良かったです

*2:「推奨レベルが全然アテにならないんですけど!」って鯖で泣きついて「それアテにならないんですよ」と苦笑いされました

*3:ヴィラも今月いっぱいで引退ですね。少し寂しい

*4:これはポケマスを始めたばかりの初心者にとって少しつらいというか、自分が他人に勧める際にも「最初ちょっと薄味だけど頑張って進めてくれ」と言わざるを得ないのが厳しい要素だなと思っていたのですが、アプデで改善されるとのことです。期待

*5:このシナリオ、もちろんヒガナの魅力もすさまじいのですが、アオギリマツブサの魅力も非常に良かったと思います。新規衣装ください(強欲)

https://x.com/Kbt_Another/status/1655173156037099520?s=20

*6:実際これ以降の新規供給はほとんどありません(タワー報酬のシナリオくらい?)。マジコスとか待ってます(メガマンダはちょうどユウキで実装されちゃいましたけど)

*7:これ以降も『緑は永遠の色』とか『エムゼット団 参上!』とか、ロケット団の出てくるシナリオは大体ぬるっとサカキに逃げられる展開が続いているなあとは感じます。まあ捕まっちゃったら終わりですし改心させるわけにもいかんので仕方ないわけですが……

【AtCoder参加記録】ABC457(2026/05/09回)に参加した

  • 8回目の挑戦
  • 恐怖の土曜労働が発生したりして参加が危ぶまれたものの21時20分ごろから参加
    • 若干パフォーマンスは下がったものの、まあ誤差でしょう
  • 前回

【AtCoder参加記録】ABC456(2026/05/02回)に参加した - マイファニーウィークエンド

結果

atcoder.jp

  • 今回も無事3完
  • 安定しているといえばしている
  • 今更ながら知ったけど、毎回のパフォーマンスでだいたい最終的に安定するであろうレートがわかるのね(因果が逆?)
  • 今のペースだと茶色にはおおよそ入れるっぽい
    • 水色は正直きつそうだし4完を安定させて緑を取るのが目標かなあ

各問題感想

A問題

atcoder.jp

  • ほぼほぼ語ることもないような気もするけど
  • 前回同様Bの布石のようになっていた
fn main() {
    input! {
        n: usize,
        a: [i32; n],
        x: usize,
    }

    println!("{}", a[x - 1]);
}
  • x - 1にしないといけないのがポイントで、こういうので一つずれると毎回頭が混乱する
    • そういえば前回geminiに教えてもらったやつを使えばこういうずらさないといけないやつを解消できるのでは?

B問題

atcoder.jp

  • 入力の取り出し方でかなり苦戦
  • ロジック自体は本当にどうってことないんだけど
fn main() {
    input! {
        n: usize,
    }

    let mut arr = Vec::new();

    for _i in 0..n {
        input! {
            l: usize, a: [i64; l],
        }
        arr.push(a);
    }

    input! {
        x: usize, y: usize,
    }

    println!("{}", arr[x - 1][y - 1]);
}
  • 入力値の取り方がちょっとゴリ押しすぎるな
  • geminiに訊いてみた
use proconio::input;
use proconio::marker::Usize1;

fn main() {
    input! {
        n: usize,
        arr: [[i64]; n], // これだけで最終的にVec<Vec<i64>>にできるらしい
        x: Usize1, // 前回もあったやつ。最初から0始まりで取れる
        y: Usize1,
    }

    // すでに0始まりになっているので、そのままアクセスできる
    println!("{}", arr[x][y]);
}
  • [i64]だけで「行の先頭に長さLがあり、その後にL個の要素が続く」パターンが読み込めるらしい。便利すぎか?
  • んでやっぱり-1は前回やったやつでいけた。忘れないようにしないとな

C問題

atcoder.jp

  • 最初問題見たときはややこしくてあっこれは無理となったが冷静になったら意外といけた
    • 久しぶりに見たΣに無駄におびえてしまったというのもある
  • わざわざご丁寧にループさせる必要なんてなくて、出来上がった数列のK番目の数字にしか興味がないから、Kまでは実際の数字を知る必要がない
fn main() {
    input! {
        n: usize, k: usize,
    }

    let mut arr = Vec::new();

    for _i in 0..n {
        input! {
            l: usize, a: [i64; l],
        }
        arr.push(a);
    }

    input! {
        c: [usize; n],
    }

    let mut count: usize = 0;  // 現時点の位置を表す。countじゃないほうがよかったかな
    let mut ans: i64 = 0;

    for (i, num) in c.iter().enumerate() {  // Cでループを回す
        if k <= count + (*num * arr[i].len()) {  // 現時点の位置にC一回で追加される要素を足してKを追い抜くか確認
            let mut rest = k - count;  // 追い抜いてしまうならそこまでいくつ必要か計算
            while rest > arr[i].len() {  // これ割った余りで良かったのでは?
                rest -= arr[i].len();
            }
            ans = arr[i][rest - 1];
            break;
        }
        count += *num * arr[i].len();  // countを増やすんじゃなくて最初からrestを減らすようにしておいた方がきれいだったのでは
    }
    
    println!("{}", ans);
}
  • これも入力の受け取りをもっとシンプルにできるね
  • せっかくだから現時点の思い付きだけでもうちょっときれいに書き直してみる
fn main() {
    input! {
        n: usize, mut k: Usize1,
        arr: [[i64]; n],
        c: [usize; n],
    }

    let mut ans: i64 = 0;

    for (i, num) in c.iter().enumerate() {
        if k < *num * arr[i].len() {
            let m = k % arr[i].len();
            ans = arr[i][m];
            break;
        }
        k -= *num * arr[i].len();
    }
    
    println!("{}", ans);
}
  • うん、かなりすっきりした
  • ついでにさらにgeminiに訊いてみたらこんな感じ
fn main() {
    input! {
        n: usize, mut k: usize, // kはインデックスではなく「残り回数」的な意味ならusizeでそのまま取るのもアリとのこと
        arr: [[i64]; n],
        c: [usize; n],
    }

    let mut ans = 0;

    // c と arr を「ペア」にしてループ回す
    for (&num, row) in c.iter().zip(&arr) { 
        let total_len = num * row.len();
        
        if k < total_len {
            ans = row[k % row.len()];
            break;
        }
        k -= total_len;
    }
    
    println!("{}", ans);
}
  • 一番のミソはおそらくzipで、複数の配列を同じインデックスで参照して回すことができる
    • 片方にはあるけどもう一方にないインデックスでパニックみたいなことを防げるっぽい
  • ちなみにこういう書き方もできるらしい
let ans = c.iter().zip(&arr)
    .find_map(|(&num, row)| {
        let total = num * row.len();
        if k < total {
            Some(row[k % row.len()])
        } else {
            k -= total;
            None
        }
    })
    .unwrap_or(0);
  • 詳しくはまた時間ある時に見てみる

D問題

atcoder.jp

  • なんとなくやること自体はわかるんだけど、最大値を求めるというのが難しくてギブアップ
  • Gemini先生に回答の方針だけまず訊いてみた
    • 最大を求めるのは困難だが「特定の値よりも大きくなりうるか」なら容易に判定できる
    • 「小さい値からひとつずつ更新して……」だとキリがないが、「取りうる最大値と最小値をレンジとして取って二分探索」なら十分に可能
  • それを踏まえて自力で作った回答がこれ
fn check(x: i64, a: &Vec<i64>, k: &i64) -> bool {  // 特定の数を超えられるか判定する関数
  let mut rest = *k;  // 残りの選択可能回数
  for (i, &num) in a.iter().enumerate() {  // それぞれの数値が指定の数を超えられるか判定
    if x <= num {  // 最初から超えていたら早期continue(x - num < 0を回避)
      continue;
    }
    let diff = x - num;
    let divisor = (i + 1) as i64;
    let cnt = (diff + divisor - 1) / divisor;  // 何回選択すれば指定の数を超えられるか?
    if rest >= cnt {  // たぶんrestがマイナスになると困るとかの理由でこうしたけど普通に早期リターンの方がきれいな気がする
      rest -= cnt;
    } else {
      return false;  // 残り回数が足りない時点で超えられないのでfalse
    }
  }
  true  // 数列のすべての数で指定の数値を超えられるならtrue
}

fn main() {
  input! {
    n: usize, k: i64,
    a: [i64; n]
  }

  let mut ok: i64 = 0;  // 最小(絶対に超えられる)
  let mut ng: i64 = 1_000_000_000_000_000_000; // 最大(絶対に超えられない)(詳しくは後述)

  while (ok - ng).abs() > 1 {  // okとngが隣接する(<= 1)ならその間にはない
    let mid = (ok + ng) / 2;

    if check(mid, &a, &k) {
      ok = mid;
    } else {
      ng = mid;
    }
  }

  println!("{}", ok);  // okとngが隣接しているなら最大値はその時のok
}
  • これは惜しくて、最大の値が足りていなかった
  • 十分に大きくとった結果が下の通り(そのほか微修正している)
fn check(x: i128, a: &[i128], k: i128) -> bool {  // 引数はスライスにするのが望ましい
  let mut rest = k;
  for (i, &num) in a.iter().enumerate() {
    if x <= num {
      continue;
    }
    let diff = x - num;
    let divisor = (i + 1) as i128;
    let cnt = (diff + divisor - 1) / divisor;
    if rest >= cnt {
      rest -= cnt;
    } else {
      return false;
    }
  }
  true
}

fn main() {
  input! {
    n: usize, k: i128,
    a: [i128; n]
  }

  let mut ok: i128 = 0;
  let mut ng: i128 = 10_i128.pow(18) + 10_i128.pow(18) * 200_000 + 1;  // 最大値はこれくらいとる必要があった(実際にはもう少し小さくてもいいのかも)

  while ng - ok > 1 {  // 絶対にng > okなので絶対値じゃなくてもいい
    let mid = (ok + ng) / 2;

    if check(mid, &a, k) {
      ok = mid;
    } else {
      ng = mid;
    }
  }

  println!("{}", ok);
}
  • 厳密には初期値と加算値(配列の長さ)の最大値が2 * 105で、それを最大1018回加算できるので、1023くらいあればいいっぽい
  • あと、切り上げ割り算をdiv_ceilで書けるともっとすっきりするんだけど、なにがしかの問題でうまくいかない。Geminiに訊いても判然としないのだけれど、どうやら環境構築で間違っている模様

最後に

  • D問題、聞けば全然簡単という場合も多いのだけれど、いかんせんまだまだ引き出しが少ないのが問題だなと感じる
    • 逆に言えば経験を積んで引き出しを増やしていけば難なく解けるようになるんじゃないかなあ
  • とりあえずは目指せ茶色ですね

【AtCoder参加記録】ABC456(2026/05/02回)に参加した

  • ゴールデンウイークが挟まってしまったせいで解いてから少し時間が経ってしまった
  • 7回目の挑戦
  • 前回

kubotat.hatenablog.com

結果

atcoder.jp

  • 引き続きの3完!
  • ただしD問題はやはり手も足も出ず
  • A, B問題でも割と悩んだこともあって時間は前回よりかかったためパフォーマンスはやや落ちているが、まあ安定して3完できているのはよいこと

各問題感想

A問題

atcoder.jp

  • 恥ずかしいことに少し迷った(3分くらい)
    • プログラミングというよりも数学力の衰えを感じた
fn main() {
    input! {
        x: i32,
    }

    let mut ans = "No";
    if x >= 3 && x <= 18 {
      ans = "Yes";
    };
    
    println!("{}", ans);
}

B問題

atcoder.jp

  • さらに迷った(15分くらい)
  • これもプログラミングという以上に数学の部分で迷った
fn main() {
    input! {
        a: [[i32; 6]; 3],
    }

    let mut ans = 0.0;

    for prem in (4..=6).permutations(3) {  // nPnができるやつ
      let mut prob = 1.0;
      for i in 0..3 {
        let count = a[i].iter().filter(|&&x| x == prem[i]).count() as f64;  // ここが何かあんまりわかってないので復習が必要かも
        prob *= count/6.0;
      }
      ans += prob;
    }

    println!("{}", ans);
}
  • 整数型と浮動小数点型との間を行ったり来たりして苦戦した跡が見える
  • もっとスマートな解き方があるはず
  • 参考

yiskw713.hatenablog.com

qiita.com

C問題

atcoder.jp

  • それなりに難産だったが最終的にACをもぎ取れてよかった(45分くらい)

最初の方針

  • 前回のD問題の連結リストを使えるのでは? と思った結果迷走
    • 最近知ったやつを使おうとするとこういうのは迷走しがち
  • 「いちいち次のやつと被ってるか判定してたら間に合わなくなるけど事前に判定しておいて結果を持たせておけばなんとかなるのでは?」と思ったが全然そんなことはなくTLE
    • 結局2重ループしちゃってるのでそれはそう
fn main() {
    input! {
        s: String,
    }

    let mut next = vec![false; s.len()];
    let bytes = s.as_bytes();

    for i in 0..bytes.len() - 1 {
      if bytes[i] == bytes[i + 1] {
        next[i] = true;
      }
    }

    let mut count: i64 = 0;

    for i in 0..next.len() {
      for j in i .. next.len() {
        if next[j] {
          count += 1;
          break;
        } else {
          count += 1;
        }
      }
    }
    
    println!("{}", count % 998244353);
}

うまくいった方針

  • こういうのはダメそうならすっぱり諦めて別の解き方を探るのが大事
  • 次と被ってる数字同士の間が何個分か?が大事な気がしたので、「何番目の数字が次と被ってるか」の情報を持たせるようにした
    • これが一番スマートというわけではないとは思う
  • 例えば最初に次と被ってる数字が3番目(abccとか)だったら、abcの3文字からa, ab, abc, b, bc, cの6通りの部分文字列が作れる
    • なんとなくこれはn文字に対して1からnまでの総和が作れる部分文字列になりそうな気がした
      • ここマジで直感でしかないので本当に数学力の衰え
      • さらに言えばそこから1からnまでの総和の求め方は思い出せなくてググった。ヤバい
fn main() {
    input! {
        s: String,
    }

    let mut dupl = Vec::new();  // 「次と被ってる数字のインデックス」を集めた配列
    let bytes = s.as_bytes();

    for i in 0..bytes.len() - 1 {
      if bytes[i] == bytes[i + 1] {
        dupl.push(i);
      }
    }

    let mut count: i64 = 0;

    if dupl.is_empty() {
      count += (s.len() * (s.len() + 1) / 2) as i64; // 次と被ってる数字がひとつもない場合は全体をnとして計算
    }

    for (i, point) in dupl.iter().enumerate(){
      if i == 0 {
        count += ((point + 1) * (point + 2) / 2) as i64;
      } else {
        count += ((point - dupl[i - 1]) * (point - dupl[i - 1] + 1) / 2) as i64;
      }
    }

    let last = dupl.last();
    if let Some(x) = last {
      count += ((s.len() - *x - 1) * (s.len() - *x) / 2) as i64;
    }
    
    println!("{}", count % 998244353);
}
  • なんか条件分岐とかも全体的にかなり汚いのでお見せするのも恥ずかしいというのが正直なところではある
  • 1からnまでの総和を求める部分は関数として切り出したほうがよかったかな
  • 最初は次と被ってる数字がひとつもない場合の処理が抜けていてWAになって絶望した
    • テストケースは通ってるんだしおそらくエッジケースだろうと思ってコードテストで次と被ってる数字がひとつもない場合を試したら案の定誤答になったのでわかった
  • 最終的にACをとれたとは言えなんとなくの直感とか山勘でなんとかした部分も大きいので正直かなり怪しい

D問題

atcoder.jp

  • 正直手も足も出ず(全然方針も浮かばなかった)
  • そのうちGeminiに訊いてみるかもだけど正直今週は時間ないかもという感じ

最後に

  • なんとなく3完はできて当たり前みたいな空気になってきているけど全然B問題で苦戦するときもあることを考えるとそんなことはないとは思う
  • とは言え少しずつ引き出しが増えてる感覚とか嗅覚がついてきた感覚はあるっちゃあるのでコツコツ頑張っていきたい

【AtCoder参加記録】ABC455(2026/04/25回)に参加した

  • 6回目の挑戦
  • 前回

kubotat.hatenablog.com

結果

https://atcoder.jp/users/KN1403/history/share/abc455atcoder.jp

  • 先週に引き続きの3完! とてもうれしい
  • この調子で安定して3完できるようにしていきたい
  • ただまあ人というのは欲張りなもので、憧れのC問題が解けるようになってくると今度はCまででは満足できなくなってきてD問題も解きたくなってきている……
    • 今回は40分余っていたのでD問題にもしっかりめに挑戦したんだけど、なんかワンチャン行けそうな気もしたんだよなあ

各問題感想

  • A問題は省略(2分半くらい)

B問題

atcoder.jp

  • 意外と苦戦した(16分くらい)
  • なんかいい解法無いかと思いつつ結局マス目の数が少ないので全探索でゴリゴリにゴリ押した
fn main() {
    input! {
        h: usize, w: usize,
        s: [Bytes; h],  // 読み込み時にVec<Vec<u8>>で読み込めるようになる
    }

    let mut ans: i64 = 0;

    for i in 0..h {  // 開始地点(縦)
      for j in 0..w {  // 開始地点(横)
        for k in i..h {  // 終了地点(縦)
          for l in j..w {  // 終了地点(横)
            let mut flag = true;
            'loop_m: for m in i..=k {  // 長方形領域の全探査(縦)
              for n in j..=l {  // 長方形領域の全探査(横)
                if s[m][n] != s[i + k - m][j + l - n] {  // 点対象になってるかどうかを判定
                  flag = false;
                  break 'loop_m;
                }
              }
            }
            if flag {
              ans += 1;  // すべてのマスが条件を満たしていた場合にのみカウント
            }
          }
        }
      }
    }
    
    println!("{}", ans);
}
  • 鬼の6重ループ
  • Rustでbreakで多重ループをまとめて抜けたい場合にはループに名前(ラベル)を付けるといい
    • 参考

enu23456.hatenablog.com

  • もうちょっとほかの解法がないかは探ってみたい

5/7追記

  • Geminiに訊いてみた
use itertools::iproduct;
use proconio::input;
use proconio::marker::Bytes;

fn main() {
    input! {
        h: usize, w: usize,
        s: [Bytes; h],
    }

    let mut ans = 0;

    // 長方形の「左上(i, j)」と「右下(k, l)」の全組み合わせ
    for (i, j, k, l) in iproduct!(0..h, 0..w, 0..h, 0..w) {
        if i > k || j > l { continue; }  // iがkより右、jがlより下にならないようにする

        // 長方形内部の全マス (m, n) について点対称かチェック
        // all を重ねることで「全てのマスで条件が真か」を 1 行で書ける
        let is_symmetric = (i..=k).all(|m| {
            (j..=l).all(|n| s[m][n] == s[i + k - m][j + l - n])
        });

        if is_symmetric {
            ans += 1;
        }
    }

    println!("{}", ans);
}
  • iproduct!: 直積集合を作ることができる

qiita.com

  • all: イテレータに対して、コールバック関数の条件すべてに合致するならtrueを、そうでないならfalseを返す

qiita.com

C問題

atcoder.jp

  • 先週に引き続きのクリア(約40分)
    • 前回は約60分かかっているのでスピードも上がっている
  • 整数列のそれぞれの整数に対して合計の数値を出して、高いものから順に指定回数分除去するという方針(これが最適なのかは知らん)
    • 整数がラベルでもあり値でもあるのでそこをこんがらがらないように気を付ける
fn main() {
    input! {
        n: i64, k: i64,
        a: [i64; n]
    }

    let mut numbers = HashMap::new();  // aが非常に大きくなり得てとびとびになる可能性もあるからVec<Vec<i64>>ではなくHashMapにしたけど諸説

    for i in a {
      *numbers.entry(i as usize).or_insert(0) += i;  // それぞれの整数について、整数をラベルとしてキーに当てつつ、値ともして合計を出してバリューに格納する
    }

    let mut numbers_vec: Vec<(&usize, &i64)> = numbers.iter().collect();  // バリューでソートしたいのでVecに変換

    numbers_vec.sort_by(|a, b| a.1.cmp(&b.1));  // バリューでソート(popは後ろから取るので昇順でいい)

    for _i in 0..k{
      numbers_vec.pop();  // 指定回数分大きい方から排除
    }

    if numbers_vec.is_empty() {  // 空配列になってるときに後の処理がバグりそうで怖かったのでとりあえず早期リターン
      println!("0");
      return;
    }

    let mut ans = 0;

    for (_i, number) in numbers_vec {
      ans += number;
    } 
    
    println!("{}", ans);

}
  • 多重ループにこそなってないけど変換したりソートしたりしてる過程でTLEになってそうだなーと思いつつダメもとで提出したらそのままAC
  • コレクションでなにを選択するかが難しい(最初からベクタでよかったのでは?)
  • あとはソートのコールバックの書き方がまだちょっと慣れない
    • なんとなくのまま写経しているけどcmpが何かもわかってないのでそこも調べなければ
  • 参考

dottrail.codemountains.org

yiskw713.hatenablog.com

D問題

https://atcoder.jp/contests/abc455/tasks/abc455_datcoder.jp

  • なんかソリティア(クロンダイクとかスパイダーとか)みたいなことをやらされている
  • クエリ(移動)のたびに上に乗っているカード全部の情報を更新しないといけないのか? と思いつつ、すべてのカードに対してそんなことやってたらTLE一直線だと思うので考えどころ
  • アイデアとしては、Option型を使って「一番下ではない(None)または上に乗っているカードの情報(Vec)(一番下のカードにのみ持たせる)」をまとめて持たせるというアイデア
    • ただしそれでは一番下のカードからその上に乗っているカードの情報は参照できても、移動させるカードや移動先のカードから一番下のカードの情報を参照できないので、自分の現在地の一番下のカードの情報(=自分が今どの山にいるか)の情報も持たせた方がいいと思う
  • 流れとしてはこんな感じか?
    1. 動かす対象のカード(Cで指定されたカード)の情報から一番下のカードの情報を取得
    2. 一番下のカードの情報から移動させるカードすべての配列を取得
    3. 配列の情報から、移動対象のカードすべての一番下のカードの情報を移動先のカードの一番下のカードの情報で更新
    4. 移動先の一番下のカードの情報を移動させたカードの情報で更新
  • ただ正直これでもTLEになりそうな気がせんでもない
  • とりあえず一旦これで何かしら形になるまで試行錯誤してみて、ダメなようならまたGeminiに相談してみようと思う

4/28追記① 当初の方針での実装

  • 若干Geminiに相談しながらとりあえずこの方向性で形にしてみた
fn main() {
    input! {
        n: usize, q: usize,
        moving: [(usize, usize); q],
    }


    let mut cards: Vec<(usize, Option<Vec<usize>>)> = Vec::with_capacity(n);

    for i in 0..n {
      cards.push((i, Some(vec![i])));  // 一番下のカードの情報と、上に乗っているカードの情報の配列(上に乗っているカードの情報はボトムの時にだけあればいいからOption)
    }

    for (c, p) in moving {
      let move_target = c - 1;
      let move_to = p - 1;
      let departure_deck = cards[move_target].0;
      let destination_deck = cards[move_to].0;
      let pile = cards[departure_deck].1.as_mut().unwrap();  // 配列から要素を一つだけ引っこ抜いたら怒られるので.as_mut()をつける必要があった

      if let Some(position) = pile.iter().position(|&x| x == move_target) {  // positionで該当する値のインデックスをとってくることができる
        let mut moving_cards = pile.split_off(position);  // split_offで特定のインデックス以降を切り取ることができる
        for card_number in &moving_cards {  // このループがギルティ
          cards[*card_number].0 = destination_deck;
        }
        cards[destination_deck].1.as_mut().unwrap().append(&mut moving_cards);  // appendで配列に配列をくっつけられる
      };
      cards[move_target].1 = None;
    }

    let mut ans = Vec::new();

    for (_bottom, pile) in cards {
      match pile {
        Some(deck) => {
          ans.push(deck.len());
        }
        None => {
          ans.push(0);
        }
      }
    }
    
    println!("{:?}", ans.iter().format(" "));   // イテレータの要素を指定した区切り文字でつないで出力できる
}
  • なんとなくうすうす感じてたけどやっぱりTLE
    • 結局のところ愚直に動かすカードすべてを操作しているのがアウト

参考

zenn.dev

rs.nkmk.me

4/28追記② 配列による双方向連結リストを使った解法

  • 結局方向性をガラッと変えないといけなさそうだったのでGeminiに相談
  • それぞれのカードのひとつ前とひとつ後ろにいるカードだけを情報として保持するようにする
    • 途中似たようなことは思ってたんだけどなあ。悔しい
fn main() {
    input! {
        n: usize, q: usize,
        moving: [(usize, usize); q],
    }


    let mut next: Vec<Option<usize>> = vec![None; n];  // 次のカードの情報を持つリスト
    let mut prev: Vec<Option<usize>> = vec![None; n];  // 前のカードの情報を持つリスト
    let mut buttom: Vec<Option<usize>> = Vec::with_capacity(n);  // 一番下のカードの情報を持つリスト

    for i in 0..n {
      buttom.push(Some(i));
    }

    for (c, p) in moving {
      if let Some(x) = prev[c - 1] {
        next[x] = None;  // ひとつ前のカードがあるならそのカードは次のカードがない状態にする
      } else {
        buttom[c - 1] = None;  // ひとつ前のカードがないなら、動かすカードは一番下だったはずなので、動かした後、そこには一番下のカードはない状態にする
      }
      prev[c - 1] = Some(p - 1);
      next[p - 1] = Some(c - 1);
    }

    let mut ans = Vec::with_capacity(n);

    for pile in buttom {
      match pile {
        Some(x) => {  // 一番下にカードがあるならそこから山をたどって数えていく
          let mut count = 1;
          let mut current = next[x];
          while let Some(next_card) = current {
            count += 1;
            current = next[next_card];
          }
          ans.push(count);
        }
        None => {
          ans.push(0);
        }
      }
    }

    println!("{:?}", ans.iter().format(" "));
}
  • やってみると本当に想像以上にあっさり
  • とは言え自力でできる気はしないので虚無い
  • まあ慣れだと思って我慢するしかない

  • 追加でさらにブラッシュアップできないかGeminiに相談した結果がこれ

fn main() {
    input! {
        n: usize, q: usize,
        moving: [(Usize1, Usize1); q],  // Usize1を使えば最初から-1できる
    }


    let mut next: Vec<Option<usize>> = vec![None; n];
    let mut prev: Vec<Option<usize>> = vec![None; n];
    let mut bottom: Vec<Option<usize>> = (0..n).map(Some).collect();  // map(Some)はmap(|i| Some(i))の省略形。bottomの初期化がシンプルになる

    for (c, p) in moving {
      if let Some(x) = prev[c] {
        next[x] = None;
      } else {
        bottom[c] = None;
      }
      prev[c] = Some(p);
      next[p] = Some(c);
    }

    let mut ans = Vec::with_capacity(n);

    for pile in bottom {
      match pile {
        Some(x) => {
          let mut count = 1;
          let mut current = next[x];
          while let Some(next_card) = current {
            count += 1;
            current = next[next_card];
          }
          ans.push(count);
        }
        None => {
          ans.push(0);
        }
      }
    }

    println!("{:?}", ans.iter().format(" "));
}
  • C問題でも出てきたcollect()が便利そうだなと思った
  • あとbottomのスペルを間違えていたのがとても恥ずかしい

最後に

  • 2週連続で3完できたのはうれしい
  • とは言え今回妙にB問題でてこずったように、C問題も安定できるとは限らないので解けて当たり前とは思わないようにしたい
  • あとはいい感じで毎週参加する習慣がついているのでこの習慣を続けていきたい
  • D問題が解けるようになると嬉しいな
    • そのためにはC問題までをもっと速く解けるようにならないといけないかも
  • あとやっぱりそもそもの書けるコードの引き出しが少ないので公式ドキュメントを読んでいく必要がありそう(英語きついなあ)

4/28追記

  • D問題もなんだかんだ慣れれば行けるかもなという気がしてきた
  • とは言えとにかく定石をいろいろ知って引き出しを増やしていかないといけなそうだなと感じた
    • 知らん定石に出会ったときの無力感はえぐいけど頑張って続けていくしかない

【AtCoder参加記録】ABC454(2026/04/18回)に参加した

  • 5回目の挑戦
  • 前回

kubotat.hatenablog.com

結果

atcoder.jp

  • 初の3完(C問題までAC)達成!!!!
  • 流石にうれしすぎる。ドーパミンえぐい
    • B問題ACまでも10分程度だったので、着実な成長を感じている
  • やはり順位もかなり上がっている
    • ただ、レートは意外とそこまで大きな変化は無し。一回のスコアよりもコンスタントに参加し続ける方が大切ということか
  • とは言えかなり問題が易しかったのもあると思うので、3完を安定させつつ、あわよくば4完を狙っていきたい

各問題感想

  • A問題は流石にスキップ(forするだけ)

B問題

atcoder.jp

  • HashSetを使えば重複を排除できるので、重複を排除して減ってないなら重複はない(全員違う服)
  • 重複は排除されているんだからその数が実際に着られている服の種類の数で、これがM以上ならよい
    • 実際にはMより大きくなることはないけど
  • Rustのコレクション型に慣れてきたのが如実に出ていて、成長を感じる
  • ただ、1つめが==nでふたつめが>=mなのはキモいかも
fn main() {
    input! {
        n: usize, m: usize,
        f: [usize; n]
    }

    let mut data = HashSet::new();

    for cloth in f {
      data.insert(cloth);
    }

    if data.len() == n {
      println!("Yes");
    } else {
      println!("No")
    }

    if data.len() >= m {
      println!("Yes");
    } else {
      println!("No")
    }
}

C問題

atcoder.jp

  • 毎度鬼門のC問題をついにクリア
  • わらしべ長者でたどっていけるところまでたどりつつ、すでに通った道の場合はスキップする感じでDFSで解いた
  • これが最適解なのかはわからないけど、しっかりスタンダードなDFSを書けたのは成長とポジティブに捉えたい
    • 先週背伸びしてD問題まで手を伸ばしておいて本当に良かった(先週のDFSをかなり参考にしたので)
  • visitedが2次元配列にならないあたりかなり易しいなとは感じる
fn main() {
    input! {
        _n: usize, m: usize,
        trade: [(usize, usize); m],
    }

    let mut trade_list: BTreeMap<usize, Vec<usize>> = BTreeMap::new();

    for (a, b) in trade {
      trade_list.entry(a).or_insert(Vec::new()).push(b);
    }

    let mut available = HashSet::new();

    available.insert(1 as usize);

    let start = trade_list.get(&1);

    match start {
      None => {}
      Some(value) => {
        for item in value {
          dfs (
            item,
            &trade_list,
            &mut available,
          )
        }
      }
    }
    
    println!("{}", available.len());
}

fn dfs (
  current: &usize,
  trade_list: & BTreeMap<usize, Vec<usize>>,
  available: &mut HashSet<usize>,
) -> () {
  if available.contains(current) {
    return;
  }
  available.insert(*current);
  match trade_list.get(current) {
    None => {return;}
    Some(v) => {
      for item in v {
        dfs (
          item,
          trade_list,
          available,
        );
      }
    }
  };
}
  • BTreeMapにしたけど多分これ普通にHashMapでよかったっぽいな
  • BTreeMapの理解もある程度進んできたとは思うものの、使いこなせてない感はかなりある
    • わざわざmatchでNoneをはじいたりしてるけど、多分entryを使って何かしらできると思う
  • というところまで自分で考えてからGeminiに相談してみた(1週間後)
use proconio::input;
use std::collections::HashSet;

fn main() {
    input! {
        n: usize, m: usize, // nも使うことが多いので受け取っておく
        trade: [(usize, usize); m],
    }

    // 1. 隣接リストを Vec で作る (1-indexed なので n+1 サイズ確保)
    let mut graph = vec![vec![]; n + 1];
    for (a, b) in trade {
        graph[a].push(b);
    }

    let mut visited = HashSet::new();

    // 2. 最初のノードからDFSを開始(中身があるかは関数内で判定される)
    dfs(1, &graph, &mut visited);
    
    println!("{}", visited.len());
}

fn dfs(
    current: usize,
    graph: &Vec<Vec<usize>>,
    visited: &mut HashSet<usize>,
) {
    // 3. 訪問済みなら即終了
    if visited.contains(&current) {
        return;
    }

    // 4. 訪問マークをつける
    visited.insert(current);

    // 5. 隣接リストを辿る(if let や match 不要でスッキリ)
    for &next in &graph[current] {
        dfs(next, graph, visited);
    }
}
  • 数がわかっているしそもそもHashMapですらなくVec<Vec<usize>>でいい
    • matchもif letも使わなくてよくなる
  • 正直自力で解いてから1週間経ってしまっているのでGeminiの回答例を見てもかなりピンとこない
    • 次回自力でこれに近いものが書けるかというとかなり怪しい気がする
    • やっぱり復習はできるだけ早くした方がいい

D問題

atcoder.jp

  • C問題ACの時点でまだ30分ほど残っていたのでワンチャンあるかと思って問題を見てみたが糸口もつかめずという感じでリタイア
  • いずれはDも解けるようになりたいし、Geminiに相談してみた
  • ざっくりした方針としては、「それぞれを究極まで縮めてみて同じ結果にたどり着くなら互いに変換可能」という考え方
use proconio::input;
use proconio::marker::Chars;

fn main() {
    input! {
        t: usize,
    }

    for _ in 0..t {
        input! {
            a: Chars,
            b: Chars,
        }

        if reduce(a) == reduce(b) {
            println!("Yes");
        } else {
            println!("No");
        }
    }
}

/// 文字列を限界まで「(xx) -> xx」のルールで短縮する関数
fn reduce(s: Vec<char>) -> Vec<char> {
    let mut stack = Vec::new();

    for c in s {
        stack.push(c);

        // スタックの末尾4つが '(', 'x', 'x', ')' かチェック
        // whileを使っているのは、消したことで新たに (xx) が完成する場合があるため
        while stack.len() >= 4 {
            let n = stack.len();
            if stack[n-4] == '(' && stack[n-3] == 'x' && stack[n-2] == 'x' && stack[n-1] == ')' {
                // (xx) を取り除く
                stack.pop(); // ')'
                stack.pop(); // 'x'
                stack.pop(); // 'x'
                stack.pop(); // '('
                // 代わりに xx を入れ直す
                stack.push('x');
                stack.push('x');
            } else {
                break;
            }
        }
    }
    stack
}
  • 問題から受ける印象よりもはるかにシンプルな答え
  • スタックを使って直近4つが縮められる形になってたら縮めるというロジック

最後に

  • とりあえず今晩は初の3完の喜びを噛み締めながらぐっすり寝たいと思います
  • ただ、今回解けたのは運の問題も大きそうだし、次回以降も解けるとは限らないので次回解けなくても過剰に落ち込まないようにしないとなあという感じ(予防線張りすぎ?)
  • あと、そろそろ解いて終わりにしてばかりでもいけないので、Geminiに相談しつつ振り返りというか、もっといい解き方を考える復習の時間を設けるようにしていきたい
  • ここまで当日に書いたけど結局公開が1週間後になってしまったので変な感じになってしまった(もろもろ追記済み)
  • 結局大した復習もできなかったので次回(というか今日)以降は極力鉄は熱いうちに打っておきたい

【AtCoder参加記録】ABC453(2026/04/11回)に参加した

  • 4回目の挑戦
  • 前回

kubotat.hatenablog.com

近況

  • 結局我慢できなくて書籍を買った

gihyo.jp

  • かなり難しい

    • 何度かRustで問題に挑戦した経験があるからぎりぎり理解できているみたいなところがある
    • 完全ミリしらからいきなりこの本は無理だと思う
  • だんだん各種の型とかもわかってきたし、そろそろ問題の内容によってはCまで解ける可能性もワンチャンあるんじゃないかなという淡い期待を抱き始めている

    • 直前に解いてみた過去問では大筋のアルゴリズム部分を自力で考えて解くことができているので

結果

atcoder.jp

  • 結局今回も2完のみで終了
  • 今回はなんとなくC問題も解けそうな気がしていただけに悔しい

各問題感想

A問題

atcoder.jp

  • まさかの苦戦
  • 問題自体は大したことがないのだが、全部oだった場合がうまく判定されず
  • 結局のところ実は回答自体は問題なくてcargo-competeの設定を変える必要があったらしい
    • テストが通らないだけなのでそのまま提出したらACになった
      • それなりに時間が溶けてしまった(結局C問題は時間関係なく解けなかったけど)
fn main() {
    input! {
        _n: usize,
        s: String,
    }

    let mut ans = s.clone();

    for (i, character) in s.chars().enumerate() {
      if character == 'o' {
        ans.remove(0);
      } else {
        break;
      }
    }

    println!("{}", ans);
}
  • enumerateしてforするのにも慣れてきた(このiは全部oの時の判定を何とかやろうとしてた時の名残だから結局別にenumerateする必要はなかったけど)
  • テストが通らない問題についてはabc453/testcases/a.ymlを以下のように修正する必要があった
  • 変更前
---
type: Batch
timelimit: ~
match: Lines
  • 変更後
---
type: Batch
timelimit: ~
match: SplitWhitespace
  • 参考:

qiita.com

B問題

atcoder.jp

  • 特に言うことはないかも
fn main() {
    input! {
        t: i64, x: i64, 
        a: [i64; t + 1]
    }

    let mut now: i64 = 0;

    for (i, value) in a.iter().enumerate() {
      if i == 0 {
        println!("{} {}", i, value);
        now = *value;
      } else {
        if (now - value).abs() >= x {
          println!("{} {}", i, value);
          now = *value;
        }
      }
    }
    
}
  • .abs(): 絶対値をとる
    • 参考

mebee.info

  • aには0も含まれるからt + 1個になることにだけ注意

C問題

atcoder.jp

  • 一旦すげえ愚直に解いてみた(欲張り法、貪欲法と言うらしい)が流石にWA
fn main() {
    input! {
        n: usize,
        l: [f64; n]
    }

    let mut count: i64 = 0;
    let mut point: f64 = 0.5;

    for d in l {
      if point > 0.0 {
        point = point - d;
        if point < 0.0 {
          count = count + 1;
        }
      } else {
        point = point + d;
        if point > 0.0 {
          count = count + 1;
        }
      }
    }

    println!("{}", count);
}
  • ポイントはf64型と比較するために0を0.0にするとこくらいか
  • 原点方向に近づくよりも、あえていったん離れる方が最終的なスコアが伸ばせるパターンがあるんだろうけど具体的な例が全く思い浮かばず
    • 提示されているテストケースにはなかった
    • 今でもどういう例かわかってないしGeminiに訊いても得られなかった
  • なんとなく動的計画法っぽく見えるなと思って調べたものの具体的なコードへの落とし込み方が全く分からず時間切れ
  • 実のところ制約上移動回数は20回だから全探索しても220≒106程度なので計算量としては許容範囲内だった
    • 解いてる間に一瞬頭をよぎったものの全探索のやり方もあんまりわかってなかったりして諦めていたけど、もっと調べれば制限時間内にAC取れてた可能性は結構あったと思う。悔しすぎる
    • むしろLiの値がめちゃくちゃでかいから動的計画法を使うのは厳しいらしい
fn main() {
    input! {
        n: usize,
        l: [f64; n],
    }

    let mut maximum = 0;

    for i in 0..(1 << n) {
      let mut count: i64 = 0;
      let mut point : f64 = 0.5;
      for j in 0..n {
        let rest = (n - j) as i64;
        if count + rest < maximum {
          break;
        }
        let current = point;
        if (i >> j) & 1 == 1 {
          point = point + l[j  as usize];
          if current < 0.0 && point > 0.0 {
            count = count + 1;
          }
        } else {
          point = point - l[j as usize];
          if current > 0.0 && point < 0.0 {
            count = count + 1;
          }
        }
      }
      maximum = max(maximum, count);
    }

    println!("{}", maximum);
}
  • カギは先週に引き続いてビット演算
    • 1~Nの試行でそれぞれ正の方向に向かうか負の方向に向かうかの2択なのでそれをビットの1or0に対応させてあげればあとはそれをforで回して全探索できる
  • でも意外と他は言うことあんまりないかも
    • 1 << n: ビットシフトを使ってる。2進法で表記したときに1がn個並んでる数+1になる
    • (i >> j) & 1: これもビットシフト、j番目のビットが立っているかどうか判定できる
  • 可能な限り整数のみで扱ったほうが確実なので、すべての数を2倍にして計算するというテクニックもあるらしい

D問題

atcoder.jp

  • 制限時間内が終わってからだけどDにも挑戦してみた
  • 最短じゃなくてもいいからDFSかなーと思ったけどBFSの方がよかったらしい
    • DFSでもキューを使うならいけるらしいが再帰関数だと「一度通ったところももう一度通ってよい」仕様があるためメモリエラーを回避できず(うまいことやればいけるのかも?)
  • もちろんDFSも自力ではできなかったのでGeminiに訊きながら書いた
    • MLE以外はすべてACだったのでロジック自体はあってると思う
  const WALL: u8 = b'#';
  const ROAD: u8 = b'.';
  const START: u8 = b'S';
  const GOAL: u8 = b'G';
  const STRAIGHT: u8 = b'o';
  const TURN: u8 = b'x';

  struct Direction {
    dimension: (isize, isize),
    label: char,
  }

  const UP: Direction = Direction {dimension: (-1, 0), label: 'U'};
  const DOWN: Direction = Direction {dimension: (1, 0), label: 'D'};
  const LEFT: Direction = Direction {dimension: (0, -1), label: 'L'};
  const RIGHT: Direction = Direction {dimension: (0, 1), label: 'R'};
  const DYDX: [Direction; 4] = [UP, DOWN, LEFT, RIGHT];


fn main() {
  input! {
    h: usize, w: usize,
    mut g: [Bytes; h],
  }

  // 外側を壁で囲んでおくと「端にいるかどうか」をいちいち個別のロジックで確認する必要がなくなる
  let mut grid = vec![vec![b'#'; w + 2]; h + 2];
  // 座標と向きの情報を持った三次元配列として定義
  let mut visited: Vec<Vec<[bool; 5]>> = vec![vec![[false; 5]; w + 2]; h + 2];
  let mut start: (usize, usize) = (0, 0);
  let mut goal: (usize, usize) = (0, 0);
  let mut ans: String = "".to_string();

  for i in 0..h {
    for j in 0..w {
      grid[i + 1][j + 1] = g[i][j];
      match grid[i + 1][j + 1] {
        START => {
          start = (i + 1, j + 1);
          // start地点は場所さえ控えておけば性質自体は普通の道と同じなので置き換えてしまう
          grid[i + 1][j + 1] = b'.';
        }
        GOAL => {
          goal = (i + 1, j + 1);
        }
        _ => {}
      }
    }
  }

  let origin = Direction{ dimension: (0, 0), label: ' '};

  let result = dfs(
    start,
    origin,
    &goal,
    &grid,
    &mut visited,
    &mut ans
  );

  println!("{}", if result {"Yes"} else {"No"});
  if result {println!("{}", ans);}
}

fn dfs (
  current: (usize, usize),
  before: Direction,
  goal: &(usize, usize),
  grid: &Vec<Vec<u8>>,
  visited: &mut Vec<Vec<[bool; 5]>>,
  ans: &mut String
) -> bool {
  let mut before_idx = 0;
  match before.label {
      'U' => {before_idx = 1;}
      'D' => {before_idx = 2;}
      'L' => {before_idx = 3;}
      'R' => {before_idx = 4;}
      _ => {}
  }
  if visited[current.0][current.1][before_idx] {return false;}
  visited[current.0][current.1][before_idx] = true;

    match grid[current.0][current.1] {
        GOAL => {
          return true;
        }
        ROAD => {
          for d in DYDX {
            let new_y = (current.0 as isize + d.dimension.0) as usize;
            let new_x: usize = (current.1 as isize + d.dimension.1) as usize;
            if grid[new_y][new_x] != WALL {
              ans.push(d.label);
              if dfs(
                (new_y, new_x),
                d,
                goal,
                grid,
                visited,
                ans,
              ) {
                return true;
              };
              ans.pop();
            }
          }
        }
        STRAIGHT => {
          let d = before;
          let new_y = (current.0 as isize + d.dimension.0) as usize;
          let new_x = (current.1 as isize + d.dimension.1) as usize;
            if grid[new_y][new_x] != WALL {
              ans.push(d.label);
              if dfs(
                (new_y, new_x),
                d,
                goal,
                grid,
                visited,
                ans,
              ) {
                return true;
              };
              ans.pop();
            }
        }
        TURN => {
          for d in DYDX {
            if d.label == before.label {continue;} 
            let new_y = (current.0 as isize + d.dimension.0) as usize;
            let new_x: usize = (current.1 as isize + d.dimension.1) as usize;
            if grid[new_y][new_x] != WALL {
              ans.push(d.label);
              if dfs(
                (new_y, new_x),
                d,
                goal,
                grid,
                visited,
                ans,
              ) {
                return true;
              };
              ans.pop();
            }
          }
        }
        _ => {}
    }
    false
}
  • mut g: [Bytes; h],: グリッドをVec<Vec<u8>>として読み込めるようになるらしい
  • 「一度通った道をもう一度通る」ことはあり得るが、「一度通った道を同じ向きでもう一度通る」ことはあり得ないのがミソ

    • visitedを単なる二次元配列でなく向きの情報も持たせた三次元配列として定義する
  • 最終的にBFSで解いたのはこんな感じ(もちろんGeminiに訊きながら)

  const WALL: u8 = b'#';
  const ROAD: u8 = b'.';
  const START: u8 = b'S';
  const GOAL: u8 = b'G';
  const STRAIGHT: u8 = b'o';
  const TURN: u8 = b'x';

  struct Direction {
    dimension: (isize, isize),
    label: char,
  }

  const UP: Direction = Direction {dimension: (-1, 0), label: 'U'};
  const DOWN: Direction = Direction {dimension: (1, 0), label: 'D'};
  const LEFT: Direction = Direction {dimension: (0, -1), label: 'L'};
  const RIGHT: Direction = Direction {dimension: (0, 1), label: 'R'};
  const DYDX: [Direction; 4] = [UP, DOWN, LEFT, RIGHT];

  #[derive(Clone, Copy)]
  struct Parent {
    y: usize,
    x: usize,
    dir_idx: usize,
    label: char,
  }

fn main() {
  input! {
    h: usize, w: usize,
    mut g: [Bytes; h],
  }

  let mut grid = vec![vec![b'#'; w + 2]; h + 2];
  let mut start: (usize, usize) = (0, 0);
  let mut goal: (usize, usize) = (0, 0);
  let mut prev: Vec<Vec<[Option<Parent>; 5]>> = vec![vec![[None; 5]; w + 2]; h + 2];
  let mut queue = VecDeque::new();

  for i in 0..h {
    for j in 0..w {
      grid[i + 1][j + 1] = g[i][j];
      match grid[i + 1][j + 1] {
        START => {
          start = (i + 1, j + 1);
          grid[i + 1][j + 1] = b'.';
        }
        GOAL => {
          goal = (i + 1, j + 1);
        }
        _ => {}
      }
    }
  }

  let mut ans = Vec::new();
  queue.push_back((start.0, start.1, 0));

  while let Some((y, x, b_idx)) = queue.pop_front() {

    if (y, x) == goal {
      let mut curr_y = y;
      let mut curr_x = x;
      let mut idx = b_idx;
      while let Some(p) = prev[curr_y][curr_x][idx] {
        ans.push(p.label);
        curr_y = p.y;
        curr_x = p.x;
        idx = p.dir_idx;
      }

      ans.reverse();
      println!("Yes");
      println!("{}", ans.into_iter().collect::<String>());
      return;
    }

    let current_tile = grid[y][x];
    for (i, d) in DYDX.iter().enumerate() {
      let next_idx = i + 1;

      if !is_movable(current_tile, b_idx, next_idx) {continue;}

      let ny = (y as isize + d.dimension.0) as usize;
      let nx = (x as isize + d.dimension.1) as usize;

      if grid[ny][nx] != WALL && prev[ny][nx][next_idx].is_none() {
        prev[ny][nx][next_idx] = Some(Parent {y, x, dir_idx: b_idx, label: d.label});
        queue.push_back((ny, nx, next_idx));
      }

    }
  }

  println!("{}", "No");
}

fn is_movable (current_tile: u8, b_idx: usize, next_idx: usize)-> bool {
  match current_tile {
    ROAD => {
      return true;
    }
    STRAIGHT => {
      if b_idx == next_idx {return true;}
      return false;
    }
    TURN => {
      if b_idx == next_idx {return false;}
      return true;
    }
    _ => {}
  }
  false
}
  • #[derive(Clone, Copy)]: 標準のトレイトを実装させられる

doc.rust-jp.rs

  • VecDeque: frontとbackの両方からpopできる
  • Some周りの挙動がぼんやりとしかわかっていないのでもうちょっと調べたほうがよさそう
    • 仕様自体はうっすらわかってるけどそれがここでどうしてこの書き方になっているのかはよくわからない

最後に

  • 最終的な回答に書いてることを読んでなんとなく理解することはできるけど、自力でこれをかけるかと言えばまあ無理だなという感じ
  • とは言えちょっとずつわかるようになってきている気はするのであきらめずコツコツ続けるしかないかなという印象
  • シンプルに楽しいのは楽しいので続けていきたい
  • また来週も(参加できそうなら)頑張ります

【AtCoder参加記録】ABC452(2026/04/04回)に参加した

 3回目の挑戦

 前回: kubotat.hatenablog.com

各問題感想

 流石にA問題はもう省略してもいいと思うのでB問題から(そのうちBも省略するかも)

B問題

atcoder.jp

  • やること自体は簡単だけどRustで文字列を動的に生成する方法を知らず調べるのに時間がかかった
fn main() {
    input! {
        h: usize, w: usize,
    }

    for i in 0..h {
      if i == 0 || i == h - 1 {
        let draw = '#'.to_string().repeat(w);
        println!( "{}", draw);
      } else {
        let mut draw = '#'.to_string();
        let blanc = '.'.to_string().repeat(w - 2);
        draw.push_str(&blanc);
        draw.push_str(&'#'.to_string());
        println!( "{}", draw);
      }
    }
}
  • 文字列を繰り返すにはrepeat()を使えばいい
    • ただしrepeat()するためにはどの型にする必要があるのかがあまりわかっていない(charとかStringとか&strとかがかなりふわふわ)
      • このへんは前回の整数系の型と合わせてもっときっちり基礎を固めないといけなさそう
  • 文字列の後ろに連結したい場合はpush_str()メソッドを使えばいい
    • これも使える型使えない型渡す型が全然わかってない
    • あとなんかrepeat()で作った文字列をそのまま渡す方法がよくわからなくていったん変数に代入したりかなり回りくどいことをしている

参考にした記事

mebee.info

qiita.com

rs.nkmk.me

C問題

atcoder.jp

  • 今まで挑戦したC問題の中で一番キツかった
  • まず問題の意味を理解するのにかなり苦労した
  • なんとなくやらないといけないことはわかったが、TLEにならない方法が全く思いつかなかった
    • まずS1~SMのM回分のループは絶対に必要(M行出力しないといけないから)
    • それぞれの文字列Sjについて肋骨の本数ぶんループするまでならそこまででかくならない(数が10本までだから)
    • それぞれの肋骨について該当するものがあるかどうかS1~SMまでループするとアウト(Mは最大で20万とかだから)
    • 前回とかそのほか過去問を踏まえると、先に『文字数がlの文字列のp番目にaからzのうちどの文字が入りうるか』の情報をまとめておけばループ数をM2からM*2に減らせそうだというのはなんとなく見当がついた
      • 過去問から英小文字は整数で表現できるということは知っていた
      • おそらくHashMapあたりを使うのだろうというところも見当はついた
      • ただ、そこから先にその情報をどのような型で持たせるべきかと考えたときに「文字数lがキーでその値がさらに『文字の位置pがキーでその値が持ちうる文字の整数の配列』」という型をどう表現すればいいのかわからなかった。というかネストが深くて一発でTLEに飛んでいきそうな予感しかしなかった
  • 30分くらい悩んだが現在の自分の引き出しでこの問題をどうこうすることはできなさそう(調べようにもどんな検索ワードで検索すればいいのかもわからない)と判断して、残り40分ほど制限時間は残っていたがギブアップした
  • 試験時間終了後におとなしくgemini先生に相談した
  • そして最終的にACを取ったコードがこちら(ほぼ自分で書いた部分はないけど)
fn main() {
    input! {
        n: usize,
        d: [(usize, usize); n],
        m: usize,
        s: [String; m],
    }

    let mut map: HashMap<(usize, usize), u32> = HashMap::new();
    for str in &s {
      let length = str.len();
      for (pos, ch) in str.bytes().enumerate() {
        let bit = 1u32 << (ch -b'a');
        *map.entry((length, pos)).or_insert(0u32) |= bit;
      }
    }

    let mut rib_allowed: Vec<u32> = Vec::with_capacity(n);
    for i in 0..n {
      let (a, b) = d[i];
      let letter = *map.get(&(a, b - 1)).unwrap_or(&0);
      rib_allowed.push(letter);
    }

    for string in &s {
      if string.len() != n {
        println!("No");
        continue;
      }

      let mut ok = true;
      for (i, ch) in string.bytes().enumerate() {
        let bit = 1u32 << (ch - b'a');
        if (bit & rib_allowed[i]) == 0 {
          ok = false;
          break;
        }
      }
      println!("{}", if ok {"Yes"} else {"No"});
    }
}
  • 文字数をキーにしてさらにその値も文字の位置をキーにして……とすると大変なので(文字数, 文字の位置)みたいなタプルにするとよかった(最大102なのでこれでいいということだと思う)
  • 一番カギになる概念はたぶんビット演算の概念
    • サブネットマスクでなんとなく概念としては知ってたので飲み込むことはできた
    • 文字列は整数として表現できるので、桁数に対応させればビット演算を利用してフラグ管理できる
    • <<: 整数の桁を左にずらすことができる
    • 1u32: 普通に1とすると型推論でu8にされてしまうところをu32型にすることができる
    • |=: OR代入。ORをとった結果を代入できる。整数でORをとると勝手にビット演算してくれるっぽい
  • enumerate(): イテレータに使うことでインデックスと要素をペアとしてタプルにできる
    • 直前のbytes()でたぶんイテレータにしてるんだと思う。詳しくはそのうち調べる
  • Vec::with_capacity(): あらかじめ確保する領域の容量を指定してベクタを生成する
    • これをしないと空のベクタを作成したときに確保する領域が必要最小限になってしまい、新しい要素を追加するたびに再割り当て(コストが大きいらしい)が発生してしまってコストが高くなる
  • *map.get(&(a, b - 1)).unwrap_or(&0): 何やってるのかふんわりはわかってるけど厳密にはわからん。でも疲れたので調べるのはまた今度
  • ひとつひとつの処理をおっかけて何をやってるのかはなんとなくわかったけど、真ん中のパートが何をやっているのかがすごくふんわりとしかわかってない
  • あとまあgeminiに教えてもらってなるほどなーという感じだけどこれを自力で思いつけるかというと流石に無理ゲーだろという印象
    • たぶんあと数か月はこれを繰り返して自分の中の引き出しを増やしていくしかなさそう(=あと数か月はC問題を自力で解くことはできなさそう。うわー萎える)

参考にした記事

qiita.com

zenn.dev

rs.nkmk.me

最後に

 型の基礎とか基本的な配列操作とか文字列の操作とかイテレータとか、そういうところをもっと体系的に基礎固めしないといけなさそうだなあと感じている。問題でわからなかったところとかできなかったところを調べてというだけだとどうにも知識がネットワークになってくれない。  たぶんThe Bookをもっときちんと読むのが良いんだけど、できれば物理本が良いんだよなーという気持ち。本格的にやるならオライリーの『プログラミングRust 第2版』とか『コンセプトから理解するRust』とかが良いのかな?という気がするけど、かなりガチ本だから途中で挫折しそうだなあと尻込みしてしまって手が出せない。難しい文章を読むのが苦手なので。もうちょっとカジュアル寄りだと『実践Rustプログラミング入門 第2版』とかいろいろ新しくてよさげなのはあるけど、文法の体系的な知識というよりは実際にいろいろ作ってみよう的なコンセプトっぽいから微妙にニーズに合致してない。いまTSの勉強で読んでる『プロを目指す人のためのTypeScript入門 安全なコードの書き方から高度な型の使い方まで』くらいにほどほどに読みやすくて文法を幅広く体系的に学べるいい感じの本ないかなと探し中なのでおすすめとかがもしあったら是非教えてください。

www.oreilly.co.jp

gihyo.jp

www.shuwasystem.co.jp

gihyo.jp