以下の内容はhttps://yaox.hatenadiary.jp/より取得しました。


旅行その2 後編:タイ旅行の話 後半

この記事は 穏やかなぴょこりんクラスタ Advent Calendar 2025 のために書いたものです。

はじめに

前回タイ旅行の話の前半までで力尽きたので、 引き続き続きの話をします。

3日目:メークロン鉄道市場とダムヌンサドゥアック水上市場とルーフトップバー

ホテル~メークロン鉄道市場

例によって早寝早起きしたので、朝5時から活動開始です。

5:17 にホテルを出ると 5:25 頃にちょうど良いバスがあると聞いていたので、バス停にたってみます。

朝のバス停

しばらく待ってみたけれど、バスは来ず。

このルートはかなりの迂回路で効率が悪いとは知っていたし、
トイレにも行きたくなってきたので、予定を変更し、一度ホテルに帰ってトイレ休憩などを挟んで再出発。

バイクタクシー

Grab でバイクタクシーを呼んで最初の目的地、チャトゥチャックにある長距離バス乗り場へと向かいました。
ちなみに、バイクタクシーの選択肢には Standard バイクと Saver バイクの二種類があったのですが、違いは謎。
僕は後者を使いましたが、特別不満点はなかったです。

バイクタクシーは乗る人にもヘルメットの着用が本来義務付けられているのですが、
実態としては前のバイクの様子など御覧の通りです。

ちなみに僕は前のかごに入れていたヘルメットを使わせてもらったのですが、
ベルトが止まらずヘルメットを片手で抑えながら乗ることに。
こうなるならヘルメットない方が良いぞ…と思うなどしました。

とはいえ、こちらは象に乗ってきたばかりなので、象に比べたら不安はありませんでした(?)。

チャトゥチャックミニバスステーション

6:16 にミニバスステーションに到着し、メークロン行のチケットを購入します。

遅くとも7時発、可能ならそれより早いバスがあれば良いなと思ってこの時間に来たのですが、
ふたを開けてみたら、購入したチケットは 7:30 発でした。

購入したチケット

何度かカウンターのお姉さんに確認したりもしましたが、
どうあがいてもこれ以外に道はなさそうなので、あきらめてバスを待ちます。

サラダクリーム(?)と豚のサンドイッチ。味はでんぶみたいだった

なんだかんだで 7:30 になってもバスは出ず、バスが出発したのは 7:40 とかだったりします。

のんびりバスに揺られ、メークロンに到着したのは 9:25 頃

メークロン着

メークロン鉄道市場

市場をぶらぶらとしながら、今後の作戦を考えます。

というのも、メークロン鉄道市場は通常は上の写真のようにただの市場ですが、
電車が通る時間になると店を片付けて道を作り、店すれすれの間を電車が通る、というのが有名な場所なんですよね。

で、電車の時刻は最初が 8:30 着 9:00 発、次が 11:10 着 11:30 発です。

元々 8:30・9:00 の電車見ることを狙っていたのに間に合わず、次の電車にまでは結構待つことになってしまいます。

悩みながらも、とりあえず次の目的地に向かうまでの予習がてら、メークロンのバス乗り場を確認。

バス乗り場

メークロンからダムヌン・サドゥアックに行くソンテウって何時に出る?
というのを係りの人に聞こうとしたんですが、あまり意図が伝わらず時間はわからずじまい。

ただなんか「運転手には伝えとくよー」的なことを言っていたので、まぁいいか…という気持ちになり、
メークロン市場まで引き返して 11:10 到着の電車を待つことにしました。

市場で飲んだココナッツ

11:00 ぐらいからお店の人が店をバタバタと畳み始めます。

なんかお店のおばちゃんに呼び止められて、
屋台の屋根の垂れ下がってる布を傘でつついてひっくり返すお手伝いをすることになりました。

言葉は一ミリもわからなかったけれど、
何とかお手伝いができて満足いただけたようで、貴重な経験となりました。

市場にやってくる電車

こんなにすれすれ

通っている最中

想像以上に電車がすれすれで迫力がありました。
粘ってよかったです。

メークロン鉄道市場 ~ ダムヌン・サドゥアック水上市場

電車を見送った後、次の目的地、ダムヌン・サドゥアックに向かいます。
先ほどのバス停 (mae klong transport station) に戻り、おばさんにソンテウの場所を案内してもらいます。

バス停はここ↓

ソンテウ(2日ぶり2回目)

時刻表っぽいもの

この時刻表を見るに 11:40 発なのかな?と思っていたのですが、
なんか 11:25 くらいには出発してくれました。

事前におばさんと話をしておいた成果かもしれないし、たまたまかもしれない。

ソンテウ代は片道 20 バーツでした。

さて、このままソンテウに乗っているとダムヌン・サドゥアック水上市場につくわけですが。
事前の調査によれば、ダムヌン・サドゥアック水上市場のぼったくり会場の方につくはずです。

Google map で見ても(ぼったくり)の方とそうじゃない方の二種類がある。面白いですね。

ぼったくりの方

ぼったくりじゃない方

というわけで、何とかしてぼったくり市場をスルーしてぼったくりじゃない方に行こうと思っていたのですが、
地図とにらめっこしていると、ソンテウが市場のある通りで曲がらず北の方へ通り過ぎようとします。

たぶん後でUターンして市場に向かうのかもしれませんが、
ここぞとばかりに停止ボタンを押して降車。

「市場まで連れてくからまだ乗ってていいよ」的なことを
運転手のおじさんがめっちゃアピールしていましたが、
「手前のスーパーで飲み物買うからここがちょうどいいんで」という言い訳をひっさげ、
首尾よくぼったくり市場に連行回避に成功します。
ピンクガネーシャソンテウに乗っていたノウハウが生きましたね。

まぁ、手前で降りた分だけ炎天下を頑張って降りる必要こそあるわけですが。
そこは異国の街を歩くのは楽しいよね旅行テンションで乗り切っていきます。

ぼったくり市場の方(前を素通り)

道中の端から市場を上から眺めることもできます

求めていたボート乗り場の看板

ボートの受付

ボートの価格はモーターボートは最低4人から 400 バーツ/人、手漕ぎボートは1台 1000 バーツ/一時間、となっています。
こういう時に割り勘ができないのは一人旅のつらいところですね。

6年くらい前のブログでは手漕ぎボート一人 30 分 150 バーツだったことを知っており、
その後に値上がりしたことも把握はしていたのですが、このブログ記事の写真を見せながら少し交渉。

向こうも「日本人みんなそれ見せてくるんだよね…」とある意味慣れた様子でしたが、
とりあえず 30分 500 バーツで手打ちにすることにしました。

1,2 年前に 30 分 300 バーツで乗った人の話は知っていたので、
少し粘ればもうちょっとディスカウントできたかもしれませんが、
粘る時間も惜しいし、象と同額と思えばまぁ…という気持ちです。

ボートに揺られて市場を楽しむ

大して物欲もないしこの市場の売り物もかなりの観光客向け相場であることはしっていたので、
ココナッツアイス&スティッキーライスだけ買いました。 60 バーツでした。

ココナッツアイス&スティッキーライス

ボートを楽しんだ後はパッタイを食べて、水上市場は満足。

パッタイ&タイティー

バンコクへの帰ることとします。

実のところ、バンコクへの帰り方はちゃんとわかっていなくて

  • ダムヌン・サドゥアックから少し車で移動したところのバス停から帰る(6年前、ボート 150 バーツだったころの情報)

【体験記】メークロン線路市場と水上マーケットの自力での行き方 | タイ人ガイドのター

  • ミニバスステーション(どこだ?)から帰る(1年前、ボート 300 バーツの人の情報)

www.youtube.com

  • Grab 使ってメークロン市場に戻ってから長距離バスで戻る

バンコクからダムヌン・サドゥアック水上マーケットへの行き方・帰り方|りく

等のルートを見かけつつ、人によってまちまちというか、
鉄板ルートが見当たらないんですよね。

とりあえず、古い情報だけど一発で帰れたらうれしいと思い、
一番上のルートのミニバスステーションに向かってみることに。

ぼんやり向かいながら Grab の値段をチェックしつつ、
んー、これなら歩くか、と、数十分の徒歩移動をキメます。

バス停、廃業!

ストリートビューなどで見た時などからも若干いやな予感はしていたのですが、こちらのバス停は廃業してました。

どうしよっかなと思い、隣のレストランにいたお兄さんに Google 翻訳で帰り道を聞いてみることに。

僕「ミニバスステーションはどこにありますか?」
お兄さん「どこへ行くの?」
僕「バンコクへ帰りたいです」
お兄さん「まずはメークロンに行かなくてはなりません」
僕「メークロンに行くバスはどこから乗れますか?」
お兄さん(Google Map 指さし表示)

そうして得られた場所がこちらになります。

ストリートビューで見ても、ソンテウが止まってるのが分かりますね。

お兄さんにお礼を言いつつ教えてもらった場所に向かうと、ちょうどソンテウが止まっていたので乗車。

ソンテウ

すぐにソンテウが発車し、メークロンのバス停に戻ることに成功しました。

長距離バス乗り場。たくさんお世話になりました

メークロンのバス停で、今度はバンコク方面の長距離バスに乗り換えます。

昨日行かなかったルーフトップバーに行くことを決めていたので、 あえて帰る場所をルーフトップバー付近のエカマイ駅にしました。

エカマイ駅からは BTS 一駅となりのトンロー駅に移動し、ルーフトップバーに向かいます。

ルーフトップバー「ティチュカ」

トンロー駅付近にあるルーフトップバーのティチュカで夜景と酒を軽く楽しみました。 1杯 400 バーツとなかなかの価格帯なので、1杯だけ楽しむくらいでちょうどよいなと判断して撤退。

夕食はホテル近くの料理屋さんでスパイシー豚ひき肉揚げと、豚ひき肉とカリカリ麺のガパオ炒めを食べました。
ここはラープ シーロム3 というお店で、イサーン料理が特徴のお店なのですが、
何がイサーン料理か意識せずに頼んだので、イサーン料理を味わえていたのかは謎です。

夕食@ラープ シーロム3

あとはホテルでのんびりして3日目は終了です。

3日目の旅程まとめ

3日目まとめ

4日目:バンコク市内散策と帰りの飛行機

4日目、この日の夜の便で帰るので、この日の行動は軽めを予定していました。 バンコク3大寺院を巡ったらあとはロスタイム、くらいの気持ちでいます。

朝ごはん

なぜか4時45分ごろから起きてしまったので、
荷造りなどしつつ、朝ごはんとしてジョーク呼ばれるおかゆを食べに歩いて行きます。

ジョークのお店
Pork+Liver+Stomache+Intestines Congee egg なジョーク

スパイス聞いた肉団子とかレバーとかモツとかと一緒に食べるおかゆ、おいしかったです。
卵は温泉卵くらいの固さだったので、卵の黄身をほぐして味変することはできませんでした。

ワット・プラケオ

ホテルに戻り、チェックアウトをし、荷物を預けて最後の観光に向かいます。

まずは MRT シーロム駅から MRT サナーム・チャイ駅まで行き、そこから徒歩でワット・プラケオに向かいます。

ワット・プラケオ入口
開門前から入場待ち列ができていました。

ワット・プラケオは現在のタイの王朝の守護寺院ということもあり、警備もしっかりしてますね。
シリキット王太后が亡くなってから1か月もたたないくらいの時期だったこともあってか、
弔問をするタイ国民用とみられる特別列が別途作られていました。

本当は服装も気を遣わないといけないんだよな…と思いつつ、 大した服の持ち合わせがないので申し訳ない、という気持ちで観光をさせていただきました。

ワット・プラケオ本堂
プラサート・プラ・テープ・ビドン(ラーマ1世~9世の銅像が安置されているらしい)

おしゃれな壁
アンコールワットの模型(なぜここに…?)

ワット・ポー

ワット・プラケオから歩いて行けるところに、ワット・ポーがあります。

ワット・ポーの金の涅槃像

たぶんこの涅槃像が一番有名だし、タイと言えばこの像のイメージがわく人も多いんじゃないでしょうか。

涅槃像のほかにも仏像が色々ありますし、参拝に訪れている方も見かけましたし、
観光名所でもありながら寺院としても現役な風格があるのが良かったです。

廊下に並ぶ仏像たち

ワット・アルン

ワットアルンから少し歩き、ボートを渡った先にある寺院「ワット・アルン」に向かいます。
台風の影響があったらやだな…とも思いましたが、こちらはちゃんとボートが出ていました。

ボートと川の先に見えるのがワット・アルンです

ワット・ポー同様、こちらでも仏像がいたるところにり、参拝に来ている方がいたりという風景も見られましたが、 ワット・アルンで有名なのはこちらの登れる仏塔。

ワット・アルンの塔

塔の上の方(ここまでしか登れなかった)

あいにく工事?か何かで途中までしか登れませんでしたが、
塔の上からの景色も楽しみましたし、アユタヤの塔以上に迫力のある塔に上るのはアトラクション感があってよかったです。
あと、これはワット・アルンに限りませんが、装飾めっちゃ細かくてすごい。

昼ごはん

バンコク3大寺院巡りを楽しんだ後は、 Grab でバイクを読んで市街地までビュンと戻ります。
道路は割と混んでいましたが、バイクで車の間をすり抜けながら行けるのは便利ですね。怖いけど。

というわけで、ここまで食べる機会がなかったマッサマン・カレーを食べるために、目を付けていたお店に戻ってきました。

クルア・アロイ・アロイ

マッサマン・カレー

カレーは辛く、むしろ甘いくらいですが、それでも汗が出る感じで
「あ~、なんかよくわからないけどスパイス味わってる感じがする~」
というお味で良かったです。

まだ食べられそうだったので、カオソーイも注文。

カオソーイ

カレーの後にカレーうどんを食べてる人みたいになってしまいましたが、
カオソーイのスープはマッサマンカレーよりココナッツミルクが効いてる…みたいな、
食べ比べたからこその発見もありました。

食後の散歩(スネークファーム改めルンピニ公園)

本当はこの後スネークファームで蛇を見るつもりだったのですが、
行こうとしてみたら 13 時閉館の文字。

入れなかったスネークファーム

ワンチャン何かの間違いでと思って行ってみたけれど、
案の定だめだったため、予定を変更してすぐ近くのルンピニ公園に向かいます。

ルンピニ公園
ルンピニ公園のオオミズトカゲさん

公園にトカゲがいると聞いていたのできてみたんですが、
本当に日本で公園の池の亀がいるのと同じようなノリでトカゲさんがいました。

ヘビが見られなくてもトカゲが見られたのでヨシとしましょう。

マッサージ

元々スネークファームで時間を使うつもりでしたが、
ルンピニ公園で同じくらい時間をつかえるかと言えば無理(暑すぎる)なので、
急遽アソークに行ってマッサージをうけてみることに。

マッサージ屋さん「ヘルスランド」

メニュー表

タイ古式マッサージだと2時間必要で、さすがにそれは飛行機までの時間が心配だったので、
Therapeutic Massage なる全身マッサージ1時間を選択。 

内容は割と日本の整体マッサージに近いものかな、と感じました。
でも、全身の疲れがだいぶ取れたのでありがたかったです。

両替し、ホテルに戻り、空港へ

マッサージを終えたあと、少し時間があったのでターミナル 21 をぶらつこうかと思いましたが、
少し回ってあまりピンとくるものもなかったので、このまま撤退して空港に行くことを決意。
まず、近場の両替所で最小限のバーツを残して日本円に代えておきます。

ぱっと見レートがよさそうに思った両替所

416 バーツが 2000 円になりました。1バーツ = 4.807 円、1円 0.208 バーツ。
高いのか安いのかはわからないけど、過去2回の国内両替と大差ないレートだからまぁいいか、という気持ちです。
冷静に考えたら表示されてる数字は 0.208 なので、
この値が小さければ小さいほど良い…というところを混乱してやや微妙な判断をした気もする。

ホテルに戻ってからは、トランクを引きずり空港に向かいます。

BTS チョンノンシー駅 -> BTS サイアム駅 -> BTS パヤータイ駅、と電車を乗り継ぐのに 35 バーツ。 パヤータイ駅からエアポートリンクでスワンナプーム国際空港に行くのに 45 バーツ、トータル 80 バーツです。

両替時点では 200 バーツを残していたのですが、あまりに余裕だったのでココナッツスムージーとか飲んじゃいました。

ココナッツスムージー

スワンナプーム国際空港はANAのラウンジがないので、
タイ航空のラウンジで飛行機を待ちました。

タイ航空ラウンジのひと時

タイだからタイ航空のラウンジがいいな…とは思ったものの、ANAの飛行機のゲートとタイ航空のラウンジはかなり距離があるので、
早めに空港についた割には1時間程度しかラウンジを楽しめなかったです、無念。

4日目のまとめ

4日目まとめ

終わりに

というわけで、タイの旅行をしてきた話でした。

また行きたいくらい楽しかったんですが、
これに味を占めて他の東南アジアの国も行ってみたい気も。

金と時間が欲しくなりますね。

旅行その2 前編:タイ旅行の話 前半

この記事は 穏やかなぴょこりんクラスタ Advent Calendar 2025 のために書いたものです。

はじめに

なんやかんやで今年は世界遺産に2回行きました。

今回はそのうちの海外旅行編ということで、タイに旅行してきた話をします。

実のところ世界遺産はタイ旅行のついでなので、実質タイ旅行 Vlog かもしれない。
でもたぶん今日のブログの範囲で世界遺産も出てくるはずです。たぶん。

経緯

一昨年台湾に旅行をしたのは台湾旅行とTOCFLの話 - やおよろずの通りですが、 実は味を占めて去年も台湾に行ってました。

それはそれで楽しかったですし、台湾また行きたいなぁとは思いつつ。
そろそろ台湾以外の別の東南アジアも味わいたいな、と思い始めました。

ベトナムとかマレーシアとかインドネシアとかも選択肢にはありつつ、
一向に行き先が決まらないままずるずると夏まで何の計画も立てられなかったので、
サマーセールとにらめっこしてえいやっとタイ行きのフライトチケットと宿だけ確保しました。

あんまり深い理由はなくタイにしたものの、 冷静に考えてタイならアユタヤ(世界遺産)もあるな、と思ったので、
ついでに世界遺産も回ってきました。

話せば長くなるので、ひとまずは出国~アユタヤ巡りをした日までの話をこのブログではします。
その後の過ごし方についてはまた次回。

0日目:空港~ホテル

サマーセールで安かったから NRT 18:55 - 0:20 BKK という便をとっていたので、
午前中はのんびり過ごしつつ、時間に余裕をもって空港に向かいました。

両替については以下の記事を見る限り、
現地の良レートな両替所は僕が到着したころにはやっていなさそうです。

スワンナプーム空港で最も高レートの両替所。2〜3泊の短期旅行なら旅行費全てを両替するのもあり。 | タイ一択

この記事にある ATM キャッシングを試す手段も少し考えましたが、
空港に着いたらすぐにホテルに向かって寝たい気もしたので、2000バーツほど日本で両替していくことに。

空港でのレートは 10580円 = 2000バーツで、 1バーツ 5.29 円でした。

事前に見ていた VLog などではよく 1 バーツ 4.5 円換算をされている気がしますが、
このレートを見ると 1バーツ 5円と思った方がよさそうです。円の価値の低さが悲しいですね。

例のごとく SFC カードを振りかざしてラウンジでビールを楽しみ、出国。

スワンナプーム国際空港にはたいてい時間通りについたと記憶していますが、
飛行機降りてから入国審査・手荷物受取所に行くまで割と距離があったのと、
地味に入国審査が混んでいたので、自由に動けるようになったのは1時。
配車アプリの Grab で車を手配ながら Pickup Point にまで向かい、ホテルまで移動します。

Grab ピックアップポイント
この時間でも人がたくさんいました

タイに来るのは初めてなので Grab を使うのも初めてでしたが、トラブルなく利用できてよかったです。
この時間でも同じような人が Grab を手配するからか、 Pickup Point もだいぶ混んでいました。

ホテルはバンコク市街地付近のシーロムというエリアで、1:23-1:51 の乗車で 583 バーツでした。

ちなみに、帰国日に検証しましたが、ホテルから空港までは公共交通機関を使うと 80 バーツです。

深夜便は安いのが魅力ですが、
両替の不便さだったり交通の便の悪さでお金がかかるのは注意が必要そうですね。
購入時に何も考えていなかったのは少々反省しましたが、今回はこの差分混みでも安かったと思います。

ホテルについてから爆速就寝。

チェックイン時にパスポートを見せたら入国時のスタンプが見当たらないというハプニングもありましたが、
ページがへばりついているだけでした。その発想はなかった。

1日目:ピンクガネーシャ像とワット・パクナム

翌朝は起きれるかどうかで行動を変えるつもりでしたが、
旅行中特有のテンションの高さにより 5 時くらいに起きれたので朝から遠出することにしました。

ホテル~ピンクガネーシャ

まずは 6:30 にホテルを出て シーロム駅に向かい、
MRT ブルーラインを用いて、シーロム駅からペンチャブリー駅に向かいます。

ちなみに、 MRT の乗り方はコイン型トークンを使う方法と VISA タッチを使う方法があるのですが、
VISA のタッチが安定しなかったり厳禁の余力が時によって違ったりして、僕はその時々の気分で使い分けてました。

MRTの切符はコイン型トーク

MRT ペンチャブリー駅から徒歩数分で国鉄アソーク駅に着くので、 アソーク駅でチャチュンサオ駅に向かう電車を待ちます。

国鉄アソーク

改札などないし、ターミナルに行くにも線路を思いっきり横切っていく感じ。
ローカル感漂うシステム、地味にテンション上がりますね。

改札がないということはすなわち券売機もないということなので、
乗車してから車掌さんに言って切符を購入する方針。

車内の雰囲気
車掌さんから購入した切符

間違え防止にと思って Google Map に表示した Asoke - Chachoengsao の旅程を見せたのですが、
切符を見る限り明らかに行き先を間違っていて、なぜか 5 バーツでした。

チケット違いそうだけどいい? って改めて車掌さんに確認したのですが、
It's OK としか言われなかったので、ありがたく 5 バーツでチャチュンサオまで乗らせてもらうことに。

季節や天気によっては扇風機しかない車内は暑いとかもあるのかもしれませんが、
この日は曇天で気温も 30 度行かないくらいだったので、車内は快適。

スーパー帰りみたいなビニール袋を追ったおじさんおばさんの車内販売があって、
当初これで朝ごはんなり飲み物なり買うことも考えていたのですが、
あまり食べたいものがなかったので見送り。

車内販売おじさん

9時ごろ、チャチュンサオ駅に到着しました。

チャチュンサオ駅

駅に着いた後は、ソンテウ(乗り合いバス)でピンクガネーシャ像まで向かいます。

ソンテウ国鉄の駅よりも手前にある長距離バスの乗り場が始発地点のはずで、
当初は長距離バス乗り場まで歩いてソンテウに乗るつもりでしたが、
目的のソンテウ(青いラインが入ったもの)がちょうど待ち構えていたため、そのまま乗らせてもらいました。

ピンクガネーシャ行のソンテウ

ソンテウの乗り場は開放的なんですが、スピードは普通に車なので割と迫力がありました。
トラックの荷台に乗って移動するのと雰囲気似てるのかも。
ちなみにソンテウ代は片道 40 バーツです。

9:30ごろピンクガネーシャ付近のバス停に到着し、第一目的地であるピンクガネーシャを見てきました。

ピンクガネーシャ

ピンクガネーシャ像はワット・サマーン・ラッタナーラームとい寺院にある像で、
願いが3倍でかなうパワースポットとして知られているそうです。

誕生日の曜日に該当するネズミの片耳を抑えながらもう片耳にお願い事を言う…と、よく言われてる気がしますが、
曜日のタイミングに諸説ありだったり、他の説もあったりで、詳細はわかりません。

僕は一応、水曜日の早い時間の生まれっぽいので、緑色のネズミに願いごとをしました。
あっているかはわかりませんが、こういうのは気の持ちようでしょう。

お願いさせていただいた緑色のネズミ

すぐ近くに屋台通りっぽいところがあったので、朝ごはんを買うことに。

屋台

メニューは一ミリもわかりませんが、 Google レンズの翻訳を駆使し、
チキンヌードル(たしか写真一番上の右から2番目)を写真と指差しで注文。

チキンヌードル

あんまり味は覚えてないのですが、
醤油スープの格安ヌードルって感じのお味だった気がします。
鶏の血を固めたものっぽい茶色いプルプルしたやつとか、
日本では感じない味付けは東南アジアに来た感があってよかったです。

トッピングにもやしと生のゴーヤと謎の葉野菜あたりがあったのですが、
少しもやしを入れたものの、初手でおなか壊したらやだな…という気持ちが出てきてほとんど入れず。
後日調べた感じ、ゴーヤ入れるの割と定番みたいですし、いれて見ればよかったかもしれません。

あとは周辺の像とか景色を散策して、1時間程度でピンクガネーシャ付近は満足しました。

龍の像
蓮みたいな形の埠頭のような参拝所のような何か
菩薩像
色々と怒られそうな像の数々
ソンテウ乗り場付近の像

ピンクガネーシャからバンコクに戻り、ワットパクナムへ

10:50 発のソンテウに乗ってしまうかもう30分居座るか悩んでいたけれど、
運転手のおじちゃんと目があったので、 10:50 発のソンテウで駅に戻ります。

事前の調査の中でソンテウの止め方を把握してなかったので様子をうかがっていたのですが、
他に乗っていた人の様子を見て、押しボタンがあることが分かったので、それに倣います。

ソンテウ止めるボタン

日本のバスのノリで停留所みたいな概念があるのかな…と思い、
googlemap とにらめっこしながら「そろそろ次の停留所は駅だろう」と思ったところでボタンを押したんですが、
即座に停止して降りる空気になってしまったので、1.2 km 程手前から駅までは歩くことに。
眠気覚ましのコーヒーを買いながら駅に向かいました。
コーヒーはデフォルトで甘いやつでした。

帰りのチケットは 13バーツ。 昼ごはんおよび両替をどうしようと考えながら無計画にバンコク行きでチケットを買ってしまいましたが、
ちゃんと Asoke と言った方が良かったかもしれません。

帰りの切符

まぁでも、この価格帯なら正直誤差ですね。

アソーク駅付近での両替と、 Terminal 21 のフードコートでの昼ご飯

元々はチャチュンサオ駅付近でお昼ご飯を食べることも考えていたのですが、
朝ごはんが遅め&思ったより早めの電車で帰ることになったことから、
事前に調べたメモや検索結果とにらめっこして、計画を練り直しました。

まず、バンコクのおすすめ両替所まとめ!市内&空港で高レート両替できる場所を徹底紹介 | タイ一択 を見て、
アソーク駅付近にある superrich exchange という場所で両替をすることにします。

BTS アソーク駅の両替所は露骨に混んでいそうだしどうしよう…と継続して調べていると、
BTS アソーク駅のすぐそばにある MRT スクンビット駅にも superrich があるということで、そちらに向かうことに。

参考:【バンコク】おすすめ両替所の場所はどこ?スクンビット・アソーク編! - Thai Blog

superrich exchange

ここまでほとんど現金をお金を使っていないこともあり、二万円を両替。
1円 = 0.206 バーツということで、2万円が 4120バーツになりました。出国時と同じ書き方に直すと 1バーツが 4.854 円です。
4.5円 より全然高いですが、 5円を下回ってくれるのは助かりますね。

両替をすませ、 BTS アソーク駅ほぼ直結の「Terminal 21」という施設の5回にあるフードコート「Pier21」で食事をすることに。

Pier 21

タイオムレツ使ったオムライスと、トムヤムヌードルと、ソムタム(青パパイヤサラダ)
ドリアンとスティッキーライス(もち米)とココナッツミルク
ライチティー

なんとこれだけ食べて 148 バーツ。安くて助かりますね。
後にわかりますが、他のところで食事をすると1品で普通に 120 バーツとかするので、破格です。

アソークから移動し、ワット・パクナム観光

昼ごはんと休憩を済ませ、時刻は 15:50 ごろ。 元々はバンコク市内の三大寺院を回ることもワンチャン考えていたのですが、
バンコク三大寺院は閉まるのが早めだし、
そろそろだいぶ疲れてきたということもあり、次の目的地をワット・パクナムに決定し、向かいます。

MRT スクンビット駅から MRT バンパイ駅に行き、そこから徒歩です。

Google Map を見ていれば道にはそこまで迷いませんでしたし、割と移動はすんなりいきました。 謎のショートカットがあったり、川を橋で渡ったり、道中も異国情緒あふれていて楽しかったです。

謎のショートカット
道中の川

周囲でずっと流れている仏教感のある音楽を耳にしながら、靴を脱ぎ、ワット・パクナムの中に入ります。

ワット・パクナム

ワットパクナムは、一番上の階にあるエメラルド色の天井画と仏塔が有名です。

ワット・パクナムの内部

実際とてもきれいでした。
写真だとわからないんですけれど、エメラルド色の天井絵、ずーっとキラキラしているのが良い。

しばらく風景を楽しんで、隣にある大仏像も少し眺めて、ワット・パクナムは満足しました。
だいぶ眠気も来ていたのでもしかしたら何かしら見るところを逃しているかもしれない。

金色の大仏様

ホテルへの帰路と夕食

Google先生の言われるがまま、ちょっと遠い BTS の駅まで歩いてホテルに向かうことになりました。 BTS Wutthakat 駅まで歩き、 BTS に乗り、 BTS Chong Nonsi 駅に行き、ホテルに戻ります。

この時ようやく知りましたが、実はホテルは MRT シーロム 駅 より BTS Chong Nonsi 駅の方が近かったみたいです。

ホテルに戻りながら夕食どうしよう…と考えていたのですけれど、
帰りがけに「ここどっかの youtube 動画で見たな…」と思った店を見つけていたので、食べに行ってみることに。

プーパッポンカレーとピータンのガパオ

カレーよりもピータンのガパオの方が辛かったです。

どちらもおいしかったけれど、プーパッポンカリーのおいしさの神髄はわからなかったかも。
有名店に行くと値段がすごいのでリーズナブル路線を選んだんですが、
本当に良いところで食べた方が印象は変わったかもしれない。

「century egg だけど大丈夫?」という店員さんの質問に首をかしげてしまったんですが、
century egg でピータンの意味だという学びもありました。

あとはかえってコンビニで買ったビール飲んだりもしてましたが、
早起きしたこともあってか気づいたら眠ってました。

0-1日目の旅程まとめ

1日目の旅程まとめ

記事中に書かなかった参考文献

この日の旅程はおおむね以下の動画や記事を参考にさせていただきました。

  • ピンクガネーシャ&ワットパクナムという流れはおおむね以下動画と一緒です www.youtube.com

  • 当初は考えていた、駅からバスターミナルに徒歩で向かってからソンテウに乗る計画は以下を参考にしていました www.youtube.com

  • バスターミナルでのソンテウ乗り場の位置などは以下も参考にしていました

【ピンクガネーシャ 行き方と帰り方】ロットゥーや電車での快適アクセスがおすすめ - Amazing TRIP

2日目:アユタヤ巡りとジョッドフェアーズナイトマーケット

ここまでが世界遺産の話をするまでの前振り。長かったですね。
というわけで2日目はようやくお待ちかねの世界遺産、アユタヤ巡りです。

アユタヤという世界遺産について

世界遺産検定2級のテキストを改めて見返すと

  • アユタヤ朝が 14-17 世紀に栄えたよ
  • 山田長政が活躍した日本人街跡があるよ
  • プラ・プラーン様式の寺院があるよ

ということが書かれていました。 世界遺産検定、たくさんの世界遺産を勉強するけれど1つの世界遺産の知識はどうしても表層上の者になりがちですね。

事前に調べていたアユタヤの見どころとしては、

  • ビルマミャンマー)からの攻撃により、仏像が頭だけ切り落とされまくっている
  • 切り落とされた仏像の頭が樹木の根の中に埋まった状態になってるものがある寺院「ワット・マハタート」が有名

あたりは事前に調べました。
他にも細かな話はありますが、個別の寺院の話をするところで話します。

ホテル~アユタヤ

6:40 くらいにホテルを出て、付近の屋台で朝ごはんを買います。

朝ごはんを買った屋台

コミュニケーションが難しかったので、並べられてるものの中から雰囲気で買いました。

続いて、MRT シーロム駅からブルーラインに乗り、 MRT バンスー駅に向かいます。

MRTバンスー駅は国鉄クルンテープアピワット中央駅とつながっていて、
GoogleMap や案内掲示板を見ながら歩いたらなんかいい感じにつきました。

写真は撮り忘れていたようですが、
アユタヤへの行き方。バンコクからの鉄道、ロットゥー、タクシー、ツアーの詳細を比較して紹介 | タイ一択 あたりが参考になるかと思います。

カウンターで切符を購入し、屋台で購入したお弁当を食べながら電車を待ちます。

アユタヤまで、20バーツ

屋台で買ったお弁当

お弁当は唐辛子を食べなければ他は玉ねぎが辛かったのぞ除きさほど辛くなかったです。
(たぶん) 青パパイヤがさわやかテイストでさっぱり感を出してくれるのが良い。
青パパイヤすきかもしれない。

ちなみに、ここの改札は自動改札ではあるものの、改札は電車の出発する20分前までは空かないみたいです。

アユタヤ行電車

電車では隣と左斜め前にヨーロッパ(一人はオランダ出身だけど、もう一人は自信なし)のエンジニア二人組で、軽く話しました。
COBOL 使ってるが golang は知らない、rubyは日本人が作った言語として知られてる…みたいなギャップだったり、
休暇中に slack や outlook メールみたら上司から怒りの連絡が来ちゃうよ~みたいな話が新鮮でした。

…が、旅程中ずっと話しまくれるほど話の引き出しも多くなく、 眠気も来てしまったので、途中からはうとうとモードだった。すまない。

アユタヤの寺院巡り

アユタヤ駅

アユタヤ駅についたらトゥクトゥク*1の運転手のおじさんが呼び込みをスルーし、川に向かいます。

アユタヤの主要な寺院は駅の向かいにある川を渡った先にあるので、
川をボートで渡ってから自転車を借りる、が当初の計画だったのですが…

ボート乗り場終了のお知らせ

数日前に台風があった影響か、乗り場がぶっ壊れてるのでボートでないよ、とのことでした。

ここぞとばかりにトゥクトゥクのおじさんが営業を仕掛けてきますが、
ここまで行動を共にしていたヨーロッパ人と「いやぁ…さすがに…ねぇ?」とスルーし、
駅近くのレンタサイクル屋さんで自転車をレンタル。

自転車レンタルは 60 バーツですが、トゥクトゥクを使うと数千バーツのオーダーになる上、
油断するとぼったくり会場まで連れていかれるので、よい判断だったと思います。

自転車を借り、ヨーロッパ人の二人と別れ、移動開始。 まず初めに、自転車で川を渡るのは結構大変でした。

川を渡るまでのルート

レンタサイクルで上述の地図をもらってはいたのですが、
実際に自転車をこいでみると写真の場所がどこなのかがわからないし、
Step1 でいきなり川と逆方向に進むようだが、付近の道でその後Uターンでるそうな道もないように見える。

同じような観光客が団体でさまよいつつ、
何とか金魚の糞ムーブをして何とか川を渡ることに正解しました。

結論としては、一回川と逆方向に結構がっつり進む必要があり、 そこからUターンしてでっかい車道の端を車に紛れて川を渡るしかないみたいです。

google map で見る川の渡り方

川と逆方向に移動しようとするタイミングであたかも川を渡れそうな方向にも道が続いてるのが罠ですね。

ともあれ、何とか川を渡ることができたら、待望のアユタヤ寺院群観光です。

ちなみに、寺院全部で使えるチケットが 300 バーツでした。
事前調査では優良な寺院は各 50 バーツくらいと聞いていたので、
元とれるのか…と不安でしたが、後程調べた感じ、 1か所 80 バーツに値上がりしてたみたいなので、元は取れたみたいです。

ワット・マハタート

ワット・マハタート

アユタヤと言えばこれと言われるほど有名な、仏像の顔が木の根っこにくるまれてるやつです。

予想通り大量の観光客ツアーであふれていましたし、
実はこの寺院は御覧の観光スポットが有名ではありつつ、
寺院としては何が目的で建てられたかも謎だったりするので、軽く回りを見て次に行きます。

プラ・プラーン様式の特徴とされる砲弾状の塔、たぶんこういうの

こういう景色いいよね

ワット・ラーチャーブーラナ

ワット・ラーチャーブーラナ

ワット・マハタートのすぐ北にある寺院。
王位継承の争いで死んだ2人の兄を弔うため、1424年にアユタヤ王朝8代目の国王であるサームプラヤー王によって建立されたもの。

仏塔内部に入ることもできて、中の静謐な雰囲気がまた味わい深かったです。

仏塔の中の雰囲気

写真だとなかなか伝わりにくい感じですが…パワースポット感がある。良かったです。

綺麗に首だけ破壊されてる仏像
仏像の首が生首みたく置かれてるところ。えぐい

ワット・プラシーサンペット

ワット・プラシーサンペット

アユタヤにおける王室の守護寺院…要するに一番偉い寺院です。
セイロン様式の3基の仏塔(チェディ)に3人の王の遺骨が納められています。

世界遺産2級第五版テキストにはプラ・プラーン様式としか書いてなかったので、
一番の寺院がプラ・プラーン様式でないのは面白いですね。

いい景色
自転車と仏塔

隣にはウィーハン・プラ・モンコン・ボピットという、
タイ最大のブロンズ仏像を収めた寺院があるのですが、近場まで行くだけでにしました。

ウィーハン・プラ・モンコン・ボピット

ワット・プララーム

ワット・プララーム

2代目アユタヤ国王であるラメスワン王が、初代国王のウートーン王を弔うために1369年に建立した菩提寺です。

入口からワット・ラーチャーブーラナと似た感じの映え写真が取れますが、
ワット・ラーチャーブーラナよりも割とコンパクトでした。

仏塔と破壊された仏像

ワット・ロカヤスタラーム

ワット・ロカヤスタラーム

涅槃像が有名、というか僕も涅槃像を見に来ました。
一説によれば某スト2サガットのステージにある涅槃像のモデルとされています。

改修されて綺麗になったのか、サガットのステージっぽさは少し減って見えるかもしれないですね。
とはいえ世代を通ってきている者なので、見れてよかったです。

ワット・チャイワッタラーナーム

比較的距離はあるけれど、頭部破壊された像の保存状態が一番良い寺院とのことです。

ワット・チャイワッタラーナーム

正面の仏塔は改装中でした。

破壊された仏像たち
破壊された仏像たち part 2

確かに仏像の破壊されかたが味わい深かったです。

余談ですが、写真撮ってーとお願いしたおじさんに「好的」って言われたんですが、
咄嗟に中国語をぱっと聞いて理解できたのがうれしかったです。

遅めの昼食とエレファントライド

一通り予定していた寺院をめぐって満足したので、
自転車をこいで昼食の場所に向かいます。

アユタヤは川エビが有名とのことなので、事前に調べていた有名店で奮発します。

レストラン「バーン・マイ・リム・ナーム」

タイティーと、川エビを揚げた料理

川エビ

大きくてぷりっぷりなエビがおいしかったです。 川エビ、あっさり味だけれど臭みもない感じで、海エビより好きかもしれない。 川エビを揚げた料理の方は割と酢豚の豚が川エビになった感じがしました。おいしかったです。

たくさん食べて腹が膨れつつも、アユタヤでもう一つ名物を買いに別店舗へ。

「ロティ・サイマイ・アビーディン」お店

アユタヤではロティ・サイマイというお菓子が有名とのことなので、お土産に買いました。
ホテルに帰ってから食べましたが、綿菓子をクレープで包んでくるんで食べるお菓子です。

こんな感じのものが買えます
のせて
くるみます

綿菓子と言いつつ結構ザクザク触感で、クレープ部分がもちもちで、おいしかったです。
自分で作って食べる感じも楽しくていいですね。

アユタヤの食べ物ノルマも達成したので、最後の閉めに象に乗りました。

事前調査では 200バーツ, 400バーツ, 500バーツなど複数コースがあると聞いていましたが、
500 バーツでワット・プラ・ラームまで行って帰ってくるコース一択になっていました。

とはいえ他に象に乗る機会はそうあるわけじゃないので、乗らせていただきます。

象の皆様

想像以上に揺れるというか、一歩進むごとに背もたれがドンッと背中を押してくるので、
落ちるんじゃないかとかなりビビりました。 

このため象の上からの写真とかは大したものが撮れなかったですが、
おとしたペットボトルを鼻で拾ってくれるところとか、
チップを受け渡すのを鼻で受け取ってくれるとか見どころもあり、そういった体験も含めてよかったです。

アユタヤ~バンコクに戻り、ジョッドフェアーズナイトマーケットへ

象乗りを満喫した後は再び電車でバンコクに戻ります。

帰りのチケット
帰りの電車

帰りのチケットは立ち乗りっぽくありましたが、
車内を少し歩いて空いているスペースにしれっともぐりこむことで座りながら移動させてもらいました。

向かいに座ってたおじさんがミカン食べながら皮を窓から投げ捨てていて風情(?)がありました。

2日目の予定はアユタヤが中心で、アユタヤ以降の予定はその時になってから考えることにしていたので、
ルーフトップバーに行くかナイトマーケットに行くかあたりで悩んだ末、
ルーフトップバーに行っても日没の時間もすぎちゃうし、おなかもすいてないからナイトマーケットで買い食いする夕食くらいがちょうどよいと考え、
いったんホテルに戻って小休憩を挟んでのち、ナイトマーケットに向かいます。

初日は割と出費が少なかったものの、アユタヤでは割と出費があったことをふまえて、
ナイトマーケット付近の BigC という施設に Superrich Exchange で追加両替をしました。

BigCという施設

1万円が2055 バーツ、1円 0.2055バーツ、1バーツ4.866円というレートでした。前日よりちょっと下がっちゃいましたね。

ジョッドフェアーズ・ナイトマーケット

ナイトマーケットをぐるっと一蹴した感じ、火山排骨なるものが有名な様子。

店頭の火山排骨のレプリカ

明らかにおひとり様には優しくない量だったので、
一番小さなサイズで出してくれそうな場所を探して食べてきました。

火山排骨と空心菜炒め

火山排骨は排骨が酸味のあるスープに漬かっている感じでした。
あわせるスープとして酸味のあるものをチョイスするのは日本ではあんま見ない気がして、面白かったです。
空心菜炒めの方(というか、一緒に入ってる唐辛子)が辛くてライスつけといてよかったです。

デザートにマンゴーシェイクとロティを食べてこの日はお開きとなりました。

マンゴーシェイク
ロティ

2日目の旅程まとめ

アユタヤ以外
アユタヤ

記事内で書かなかった参考資料

  • 自転車を使ったアユタヤ観光の計画は以下を参考にさせていただきました。
    youtu.be

  • アユタヤ寺院の細かな詳細や回る個所の選択には以下を参考にさせていただきました www.youtube.com

  • 昼ごはんはこちらを参考にしました
    www.youtube.com

終わりに

以上、タイ旅行の記録前半でした。

世界遺産ネタと絡めるために2日目までの話を書いたんですが、思った以上に長くなってしまった、すまない。
次回はタイ旅行の後半戦で、3日目と4日目の話を予定です。
4日目は帰国日でコンテンツ少なめなので、たぶん今回よりは短めだと思います。たぶん。

*1:現地の乗り物の一つだけれど、ぼったくりで有名なので僕は乗らないと最初から決めていた

旅行の話その1:百舌鳥古墳を見に行ってみた

この記事は 穏やかなぴょこりんクラスタ Advent Calendar 2025 のために書いたものです。

はじめに

先日世界遺産検定2級取得の話とともに、
勉強したとこで旅行をするわけじゃないんだよな、って話をしましたが、
なんやかんやで今年は世界遺産に2回行きました。

今回はそのうちの国内旅行編ということで、百舌鳥の古墳を見てきた話をします。

経緯

健康診断を終えたタイミングでたくさん食べたいもの食べたいな~と思い、
食い倒れなら大阪だよな、と思って大阪に行くことにしました。

大阪と言えば万博世界遺産あったな。ということで、
帰り日、新幹線に乗る前までの時間を使って古墳を見ることにしました。

世界遺産検定における百舌鳥・古市古墳群の知識

世界遺産検定2級を勉強すると以下のことを覚えます

  • 百舌鳥・古市の古墳がまとめて世界遺産に登録されてる
  • 取り壊されかけたけど生き延びた「いたすけ古墳」という古墳が古墳保存のシンボルになってる
  • 古墳の回りに陪冢(ばいちょう)という古墳の子供みたいなのがある

百舌鳥いってみた

駅到着

Google 先生の言われるがままにとりあえず百舌鳥駅に行きました。

ようこそ

以下が周辺地図です。右下(百舌鳥駅周辺)が本当にがっつり古墳だらけになっていて面白いですね。

周辺地図

というか、実際に行ってみて知りましたが、百舌鳥って堺市だったんですね。
検定の勉強上は覚える必要がなかったのでノーチェックでしたが、
こういった気づきがあるのも実際に行ってみるなどしてみる醍醐味ですね。

初手古墳

駅を降りて少し歩くと、さっそく古墳がありました。

これ、古墳です

…ぶっちゃけ言われないと「これが古墳??」という感じですが、
これこそが世界遺産検定でやったやつ「陪冢(ばいちょう)」です。

あなた収塚古墳っていうのね

見比べていただけるとわかりますが、この古墳は駅到着時点の地図には乗っていません。
このように、想像していた以上にこの付近には古墳がわらわらとあります。

ビジターセンターにある古墳の地図。多すぎ

実際に行かないとこの多さは体感することがなかったので、いってみてよかったです。

レンタサイクルと仁徳天皇陵

駅のすぐ近くにビジターセンターがあり、

ビジターセンター

センターで自転車を借りられます。

レンタサイクル

ビジターセンターのすぐ後ろは仁徳天皇陵なのですが

でっっっっか

正面の拝殿近くででこの広さです。陪冢との広さの差が半端ないですね。

自転車乗り入れ禁止

看板を見てふと気づきましたが、陵墓の管理って宮内庁が行うんですね…

陵墓 - 宮内庁

言われてみたらそれはそう、って感じですが、目の当たりにするまであんまり認識してなかった。
こういった気づきがあるのも実際に行ってみて面白かったポイントでした。

仁徳天皇陵を回りつつ陪冢をいろいろ見る

はい
はい
はい
はい
はい
はい

…いや、多っ

想像以上に多くてビビりました。
多くの古墳を陪冢に持っている仁徳天皇陵のすごさを改めて実感しますね。

履中天皇陵古墳とかいたすけ古墳とか

さて、ここまでは仁徳天皇陵と陪冢を見てきたわけですが、 開幕の地図でもっと目立っていたでかめの古墳はほかにもあります。

履中天皇陵古墳の展望台

仁徳天皇陵は冒頭にも書いた通りでかすぎてまともな写真を撮ることすら難しいのですが、
履中天皇陵古墳は、展望台があったりして割といい感じに写真が撮れるほどよさがありがたかっです。

いたすけ古墳

世界遺産検定で何度も出てきたいたすけ古墳もしっかり回れました。
他の古墳と比べ木々がなくなっており黄色い表面になってるんだな…みたいな違いも
行ってみないとなかなか知れない気づきです。

ちなみに、写真の右側に崩落した橋みたいなのが映っていますが、
これが古墳を取り崩すときに利用されていた橋とのこと。
エモいですね。

今回自転車で回った展望台から見るとこんな感じ。

左側の山仁徳天皇陵で右側の山が履中天皇

想像以上にスケールが大きく、自転車で周囲をぐるぐる回りながらいろんな角度から古墳を見て回るの、楽しかったです。

おまけ:さかい利晶の里

駅周辺地図の左上にしれっと載っているのですが、
なんか古墳の近くに千利休与謝野晶子ゆかりの地があり、二人をまとめた文化館があります。

さかい利晶の里

千利休与謝野晶子のゆかりの地が近いのも、
百舌鳥古墳が近いのも全然想像していなかったし、情報が渋滞してる感がすごいですが、
せっかくなので行ってきました。

千利休屋敷跡
与謝野晶子生家跡

さかい利晶の里と言いつつ、特別展枠で山崎豊子展もやっていたし、
千利休屋敷跡で説明ボランティアさん(?)と話していたら
行基ゆかりの地もあって…」みたいな話も飛び出してきて面白かった。
堺にゆかりにある人多すぎる。全く知らんかった。

終わりに

というわけで、百舌鳥の古墳+堺市周辺を観光してきた話でした。
食い倒れ旅行のついでくらいの気持ちで行ったのですが、
行ってみないとわからない発見もたくさんあり、想像以上にコンテンツ盛沢山で楽しかったです。

マスコットなのか何なのか、随所で見かけたハニワ部長

今年とった資格について

この記事は 穏やかなぴょこりんクラスタ Advent Calendar 2025 のために書いたものです。

はじめに

過去のアドベントカレンダーでたまに資格をネタに絡めたりする*1僕ですが、
結論から申しますと、今年はほとんど資格を取りませんでした。

唯一とった資格は世界遺産検定2級です *2

というわけで今回は、世界遺産2級をとった周辺で印象に残った話を書きます。

経緯

世界遺産と沖縄旅行の話 - やおよろず世界遺産3級取得後、
テキストは購入しながら塩漬けにしてたんですが、
いつの間にか新しい版のテキストが出ていて手持ちのテキストが試験範囲なのは今年の3月だったので、受けました。

勉強したこと

テキスト2回ほど読んで、
目次の地図にある位置情報から世界遺産の名前と特徴をふんわりいえるように詰め込んで、
過去問題集解いて、 youtube で模擬テスト的なものをいくつかピックアップしてお勉強しました

結果

受かりました。対戦ありがとうございました。

基礎知識は手薄な自覚がある*3のですが、文化遺産系の正答率が割と低かったちょっと悔しいですね。

2級とった感想とか小話とか

試験範囲の決め方が謎

2級は3級の上位互換かと思ったら、
3級では覚えるけど2級では覚えない世界遺産とかもあったため、
2級300個+3級のうち2級で出てこなかったやついくつか、を覚えることになりました。
特に不満はないものの、いったいどういう基準で3級2級とか決めてるんだろうか。

3級テキストに比べて2級テキストは勉強に使いづらい

3級テキストは赤字で強調された文字が索引にあるから索引ベースで説明できるか確かめたり、
テキスト冒頭の地図に位置が記された世界遺産が試験範囲内と対応していたので、
地図ベースで記憶するなどしやすかったんですが…

なんか2級は冒頭地図に範囲内外関わらない多数の世界遺産が載っていたので「勉強すべきやつどこ?」がわからなかったり、
用語は公式ページにある言われてる割に、公式ページの用語集が整理されてなかったりで、
勉強するための下地を自分で作るところがちょいと手間でした。
結局用語はあんまり本腰入れて覚えておらず、すべて雰囲気で乗り切った感がある。

世界遺産を知れば旅行をしたくなる…という目論見と、現実

世界遺産を勉強するモチベの一つに旅行をより楽しむため、というものがあったんですが、
勉強しながら「わー、きれいな景色だなー、いってみたいなー」と思ったとて、
そこから旅行を計画するには僕の行動力と思い切りがまだ足らない、という学びがありました。

例えば、勉強する中で一番興味そそられたのはアルゼンチンのロス・グラシアレス国立公園なんですけれど、
アルゼンチンに行く時間と値段を見てそっ閉じしてしまう…みたいな、微妙な手の届かなさがあるんですよね。

イタリアのヴェネチアとかスペインのアントニ・ガウディ作品群(サクラダ・ファミリア含む)とかならワンチャンあるかなぁ…
とか、現実ラインと興味のバランスははまだ探り探りです。

とはいえ、世界遺産目的じゃない旅行を計画しながら「ついでに世界遺産も見るか」みたいなことは今年2回ありましたし、
去年「世界遺産らしい」と聞いて写真だけ撮った場所が出てきたりもしたので、総じて勉強してよかったと思います。

なんとなく語りたい世界遺産紹介

ヨセミテ

言わずと知れた(?)カリフォルニアの世界遺産の一つ。
去年がっつり有給をとれるタイミングでここぞとばかりに行きました。綺麗でした(小学生並みの感想)

ハーフドームという半分にぱっくり割れた岩山@ヨセミテ

ちなみに、ヨセミテは(第五版テキストにおいては)2級でも3級でもでてきますが、
自由の女神は3級にしか出てきません。謎ですね。

フランク・ロイド・ライトの20世紀建築作品群

去年、ヨセミテ行ったついでにシカゴに寄り道してシカゴ大学に行った時
「ここ世界遺産だよ~」って教えてもらって写真だけ撮ってきた建物が、2級の試験範囲な世界遺産でした。

フランク・ロイド・ライトの構成資産、ロビー邸

上野の国立西洋美術館を含む「ル・コルビジェの作品群」が世界遺産なのは国内の世界遺産として比較的有名(?)ですが、
フランク・ロイド・ライトル・コルビジェに並ぶ近代建築の三大巨匠らしいです。

ざっくりとした二人の違いは、
ル・コルビジェが機能性優先なのに対してフランク・ロイド・ライトは自然との調和を重視してたっぽい。

ペルシア湾の真珠産業関連遺跡(バーレーン

バーレーンに真珠産業関連遺跡があるんですよ。
ここまでの話だと「ふぅん」なんですが、日本の真珠養殖技術の発達によって廃れたという歴史的背景があったのが面白かった。
これは世界遺産勉強しないと知る機会がなかったと思うのでしれてよかったです。

イヴレーア(イタリア)

イタリアで世界遺産って言ったら大体イメージするのはヴェネチアとか教会とかだと思うんですが、
北の方にオリベッティ社(タイプライターとかオフィス・コンピュータが有名らしい)の企業都市として栄えた、
イヴレーアという地域があるそうな。
イタリアに企業都市ができるくらい有名なコンピュータ系企業あったとは知りませんでした。

イヴレーアに限らず、産業栄えた跡の遺産系は
「そもそもこの国でそういう工業が栄えてたんだ…」という知識も併せて興味を引くことが多かった気がします。
スイスに時計製造特化型の都市がある(ラ・ショー・ド・フォン)とか、
ノルウェーで合成肥料の製造に使う目的で水力発電が発達した場所がある(リューカン・ノトッデン)とか。

ジャイアンツ・コーズウェイ(イギリス)

イギリスに正六角形の石柱がひたすら並ぶ景観があるそうな。
テキストに写真も載っていたけど、いったいどういう原理なんだ…という不思議さも相まって割と面白く感じました。

そのほか

薄々お気づきかもしれませんが、こののあとの旅行ネタで2個ほど世界遺産がまた出てきます。

終わりに

世界遺産検定2級しかとってないのになぜ資格一本記事書こうと思ったかというと、今後の旅行ネタの布石でした。

国内の世界遺産と国外の世界遺産を一つずつ行ってきたので、それらの話はまた後日。

*1:世界遺産と沖縄旅行の話 - やおよろず とか 台湾旅行とTOCFLの話 - やおよろず とか 今年受けた資格試験を振り返る - やおよろず

*2:本当は英検1級とかAWS資格の失効するやつとりなおしとかもやれればよかったんですが、なんやかんやこれらはやらぬまま時が過ぎてしまった。反省。

*3:勉強のモチベが世界にどういう名物があるかを知りたい的なものだったので、興味の向き先を優先した

今年読んだ小説について

この記事は 穏やかなぴょこりんクラスタ Advent Calendar 2025 のために書いたものです。

1年ぶりで予約投稿の仕方を忘れて一瞬フライング投稿しましたが、もし見かけた人がいても見なかったことにしてください。

はじめに

今年もアドベントカレンダーの季節ですね。早いものです。
昨今の AI ブームだったり私生活の変化なりで今年はプログラミング系の話題が全くないぞ…と思ったのですが、
去年のアドベントカレンダーを見たらひたすら Facebook Hackercup について語っていたので、
今年は柔らかい話題ばかりでも許されるだろう。去年の僕ぐっじょぶ。

競技プログラミングから距離が遠ざかってきて以降、僕は休日の時間の使い方が迷子になりがちなんですが、 今年は年始の休みやGW休みにガッと小説を読む機会が多くありました。

というわけで、今年読んだ小説について振り返ろうと思います。 1月と5月あたりの話を振り返りながら書くので割と雑だったりシンプルだったりするのは許してほしい。

ハサミ男

概要

死体の首筋にハサミを突き立てる類の連続殺人鬼がいます。 ハサミ男が殺そうとした人がいました。 が、なぜかその人が首筋にハサミを突き立てられて死んでいます。 ハサミ男が殺してないのに。なぜにホワイ、となる話。

感想

どんでん返しミステリと言えばこれ、とよく言われる作品。 前評判通りというか、長らく語り継がれるだけのクオリティだった。 最後が味のある終わり方なのもよかった。

十角館の殺人

概要

孤島にある十角形をベースとした建物「十角館」に 推理小説研究会のメンバーが合宿をするが、そこで連続殺人が起こる。 一方、島の外では、合宿不参加の先輩の元に、連続殺人の動機とも思えるような手紙が届く。 島の中の話と島の外の話を行ったり来たりしながら進んでいく物語。

感想

ミステリの大御所とよく言われる作品。ようやく読めた。
種を明かしてしまえば「あーね」とはなるものの、
読んでいる最中には真相にはたどり着けなかったし、
記憶を消してもう一度読みたいと言ってた人がいたのもわかるなぁ、って感じ。

動物城2333

概要

人間並みの知能を持つ動物たち(たぶん二足歩行とかする)が、動物の国を持っている。
人間国とは冷戦状態。
そんな中、動物の国に、人間の大使がやってくる。
が、人間の大使が死体で発見される。
このままだと国際問題になりそうで困った…という話。

感想

作者がそれ系の創作も手掛けてる方っぽくて、
割とマダミスのような、いろんなキャラのいろんな目的・思惑が交錯する類の話。
そもそも世界観がファンタジーということもあるので、
普通のミステリとは一風変わったテイストなのが良かった。

地雷グリコ

概要

地雷を3か所だけ設置できるグリコ(地雷グリコ)で文化祭の露店のポジション争いをする… という冒頭から始まり、
グーチョキパー以外の技を一つずつ追加したじゃんけん(自由律じゃんけん)など、
多様な面白ゲームでいろんな対戦・決め事が行われる話。
しまいには学校同士の抗争にまで話が発展していく。

感想

今年読んだ中で一番かも、ってくらい面白かった。
ダウナー女子な主人公がひょうひょうとゲームで対戦相手を出し抜くのが見ていて爽快。 なじみ深いゲームに一味加えただけで深みが増す感じのゲーム設計も面白いし、 少しずつ勝負事の規模がでかくなるのに対して、見劣りしない展開・対戦相手のキャラ付け・結末までの流れも良かった。

Q eND A

概要

早押しクイズをします。クイズに負けたら頭がパーンして死にます。 ちなみに個々人に特殊能力があります。 特殊能力がばれても頭がパーンして死にます。

感想

ぶっ飛んでるな~~~w って草をはやしながら一気読みするタイプの話だと思う。 とは言いつつ、終盤にはただクイズをするだけ以上の展開もあり、 一気に読み切ってしまったものの、読んでて楽しかった。

謎の香りはパン屋から

概要

このミステリーがすごい受賞作。 パン屋でバイトしてる主人公が中心の日常ミステリ。 「バイト仲間で親友とライブビューイングを一緒に行こうと約束してたのに、ドタキャンされた。でもなんか親友の様子に違和感がある…」 みたいな謎を解決するところから始まる短編集。

感想

このミス選評でも色々語られていたけれど、
ほんわか優しいミステリで、うまくまとまっている、という授賞理由がとてもしっくりくる。
ミステリとしてすごくとがってるとかじゃないけれど、
パン屋に入った時にいい香りがしてつい顔が緩んじゃうような、おなかのすく、優しいお話だった。

この手の日常ミステリ結構好きなんだけど、北村薫さんの空飛ぶ馬くらいしかほかに知らない。おすすめは緩く募集しています。

恋とか愛とかやさしさなら

概要

婚約した恋人がいました。
翌日、恋人が盗撮によって逮捕されました。
なんで???ってなるし、その上で恋人を許せるかどうかで揺れ動く話。

感想

あぁ~、こういった論理とかだけじゃ説明つかない行動、人間してるな~~~! という感想になるというか、人間を感じることができる作品。

とはいえ、僕はバリバリの理系脳なので、首をかしげてしまったり「そうはならんやろ」って言いたくなるところもあるけれど。
人間って理解できないものだしな、うん……って思う。

カフネ

概要

弟をなくした姉と、弟の元カノがいます。
なんやかんやでこの二人で一緒に家事代行サービスをすることになります。

家事代行サービスでいろんな家庭に触れつつ、
弟をなくした苦しみから立ち直ったり、 人と人のつながりや絆を深めていく話。

感想

メインとなるキャラクター二人がそれぞれ弱さと強さを持っている上、 家事代行というテーマでいろいろな人間を感じることができる作品なため、 読んでいて力づけられるような、心温まる話だった。

この後に今年の本屋大賞ノミネート作品が結構続くんですが、
本屋大賞受賞作なのもめちゃくちゃ納得。

読み終わった後に勢いでなんとなく母に勧めた本(その1)。

小説

概要

主人公は、本の虫でした。
本の虫は、読書仲間を見つけ、学校付近にある屋敷の書庫で読書三昧の日々を送ります。
しかし、二人にはちょっとした違いがありました。
本の虫は読む専でしたが、読書仲間は物語を書く方の人でした。

感想

「なぜ小説を読むだけではだめなのか」に一定の解を見出す小説、という理解。
作者が提示した解にも一定の納得感はあるけれど、
自分個人にはそこまで腹落ちしなかったというか、
たぶん大元の疑問についてそこまで深く思い悩んだり困ったりしてなかったのかな、と思った。

後半いきなり「流れ変わったな」展開な節があるので、若干そこで振り落とされてしまった感じも無きにしも非ず。

禁忌の子

概要

お医者さんが水死体の検死をしようとしたら自分とうり二つな死体で超びっくり。
一人っ子のはずなのに… と思いつつ、自身の生まれや両親について少しずつ探っていく、という話。

感想

一応ミステリというカテゴリではあるけれど、
おそらく作者が書きたいのはミステリというより、
タイトル通りの「禁忌の子」があらわしている社会問題とかその辺の話な気がする。

ただ、そのテーマや落ちも含めて全体としては楽しめてよかった。

死んだ山田と教室

概要

クラスメイトの人気者である山田が交通事故で死にました。
お通夜モードの教室の中、なぜかスピーカーから山田の声が聞こえてきます。
どうやら山田とスピーカー越しに会話できるようです。

感想

序盤の会話劇は本当に男子高校生のノリなので若さを取り戻したいときにとても良かったし、読んでいて元気になる。
一方で、卒業の日を迎えてもまだ話が続く…ってところにも一工夫があり、
そこからはまた物語の雰囲気が変わって味わい深い。

結末はなんとも言えない余韻がするタイプの話だった。

アルプス席の母

概要

野球推薦で大阪の高校で両暮らしをする息子…を応援する、シングルマザーお母ちゃんの話。

感想

名門校だと父母会っていう概念があって…とか、
父母会も学年の違いで軋轢があって…とか、
身の回りに知り合いもいない大阪で奮闘する母と、
野球部の中でのポジション争いで思い悩んだりしながら、あっという間に成長していく息子…という、
高校生の頃のフレッシュさ、成長の早さ、パワフル母ちゃんの逞しさを感じる小説だった。

読み終わった後に勢いでなんとなく母に勧めた本(その2)。

全員犯人、だけど被害者、しかも探偵

概要

社長室で社長が殺されました。
関係者が廃墟に集められ、閉じ込められました。 廃墟の中のスピーカーから、「社長を殺した犯人だけ生きて帰してやる」と言われました。

みんながこぞって犯人としてトリックを主張し、
他の人が片っ端から推理の穴を指摘し…と、
みんなが犯人や探偵や被害者をやりまくる小説。

感想

コンセプトは面白いけれど、
タイトルで広げすぎちゃった風呂敷を畳むのにちょっと無理やり感がなくもない。
ただ、そういった粗削りな部分も含めてハチャメチャ感を楽しむには良いのかな、ってタイプの小説。

方舟

概要

10人くらいの男女がが地下建造物に生き埋めになりました。
一応、誰かひとりが犠牲になれば、入口を開けることができそうではあります。

そんな中、地下建造物の中で殺人が起こりました。

…ほな、犯人に犠牲になってもらおうか…しかし誰が犯人なんだろ…という話

感想

最後のどんでん返しにはしびれましたね~
個人的にはどんでん返しっぷりではハサミ男よりも方舟が好きだったかも。

根本的な舞台設定や、生き埋めになるまでの流れなどにはやや違和感もあるけれど、
そこさえ飲み込んでしまえば楽しめると思う。

生殖記

概要

物語の語り手が生殖器です。 語り手の宿主(?)は性的マイノリティな独身男性です。

宿主の日常やら性の悩みやらを生殖器の目線でコミカルに描いていく話。

感想

うーん、設定の勝利w
設定が面白いという観点では「全員犯人~」とかも同様なのだけれど、
ミステリじゃないという土俵の違いもあってか、話の違和感やらも感じずにただただ面白く読めた。

宿主の悩みや葛藤などいろいろと聞かされた上でこの結末は…ハッピーエンドなんだろうか…?
という疑問はちょっと感じつつ、最終的には本人がなんか吹っ切れて幸せそうならそれでいいのかもな、という、
人間を感じる感じの読後感だった。

おわりに

今年読んだ小説の雑紹介でした。
たぶんちゃんと感想とか記録するならそれ用のサービスとか使って書いた方がいいんですが、
そうすると義務化感じちゃって逆に面倒になるタイプなので、
こういうタイミングで放出するのが一番気楽ですね。

MetaHackerCup 2024 PracticeRound D 問題を語る その4:Treapを使った解法

この記事は レジリエントでウェルビーイングなぴょこりんクラスタ Advent Calendar 2024 の為に書いたものです。

はじめに

この記事は前回の続きです。

続きものとなりますので、前回の内容はこちらから参照してください。
Treap と Implicit Treap というものがあり、今回使うのは後者。 Implicit Treap は以下の操作を  O(log N) で行えるデータ構造、という話をしました。。

  • ある位置 p に要素 x を挿入する
  • ある位置 p にある要素を削除する
  • ある範囲の要素の最大値を計算する
  • ある範囲の要素の最小値を計算する
  • ある範囲の要素すべての総和を計算する
  • ある範囲の要素すべてに値 a を加算する
  • ある範囲の要素を左右反転させる

今回は、このデータ構造を使って改めて MetaHackerCup 2024 PracticeRound D2 問題を解いていきましょう。

Treap をつかって問題を解いてみる

期間が空いて忘れてしまったかもしれませんが、
今回解きたかった問題の概要は1日目の記事を参照してください。

Treap を使って解けるよ、と言っていた公式 (原文) が言っていることを改めておさらいすると、

 E でストーンを投げた場合、
ストーンが止まるまでには他のストーンに衝突が起こったりもするけれど、
最終的な石が止まる位置というのは、
ストーンがない座標のみを左から数えて  E 番目の位置である。
また、それ以前に存在していたストーンは、すべて座標が -1 される。

ストーンが一つ発射されたあとのストーンの位置

つまりは、特定の P に値を入れることと、P以前の値を1引くという操作を高速に(データ量に触れていませんが、この処理を  O(log N) で)計算できれば良い。 こういうのに便利なデータ構造として Treap ってのがあるので、それをうまく使えば計算できるよ。

というものでした。

解説をもとに Treap の解法を書いてみようとする、が…

P 以前の値を1引くことも P に値を入れることも Treap でできるから楽勝…って思うかもしれませんが、実はここに一山あります。

特定のPに値を入れることと、P以前の値から1を引くという操作を Treap で行うことを踏まえてコードを書いてみると、以下のようになるかと思います。

    int Case;
    cin >> Case;
    int ans = 0;
    for (int ci = 0; ci < Case; ci++) {
        int N, G;
        cin >> N >> G;
        ImplicitTreap t;
        rep(i, N) {
            int E;
            cin >> E;

            // determine E-th empty space as p, and index of p in ImplicitTreap
            // 
            //         ??? 
            //
            t.insert(p_index, p);
            t.add(0, p_index, -1);    
        }

        // find the position nearest to G as Q, and its index 
        // 
        //         ??? 
        //
        cout << "Case #" << ci + 1 << ": " << N - q_index << " " << abs(Q - G)
             << endl;
    }

Implicit Treap に値を入れたり範囲加算・減算をしたりするときにはその index が必要だし、
そもそも E 番目のストーンのない場所である p を計算する方法はいまいちピンときませんね。

また、その山場を突破したとしても、Gに最も近いストーンの位置を求めることについても、
Treap が提供する操作では直に実現できなさそうです。

僕はこれが分からずまたしばらく思い悩んだのですが、
しばらく考えた結果(これが最適化はわかりませんが)一応の答えにはたどり着くことができました。

結論としては、どちらも二分探索をつかいました。

p(ストーンがない E 番目の座標)や p_index (Implicit Treap 上の p の位置)の求め方

ある力Eで発射したストーンが最終的に到達する位置 p は、
ストーンがない E 番目の座標として計算できるわけですが、
これはすなわち、p_index が計算できるのであれば、 p = E+p_index と計算することができるため、
p_index を計算することができれば十分です。

Implicit Treap t にはストーンの現在の座標が昇順に入っているわけなので、
ストーン t[i] の位置エネルギーを Energy(t[i]) と表記すると、
Energy(t[left]) <= E < Energy(t[right]) となるように left, right を二分探索で更新すれば、
最終的にPが挿入されるべき個所(p_index) を決めることができます。

            // determine E-th empty space as p, and index of p in ImplicitTreap
            int left = -1;
            int right = i;
            while(left+1<right){
                int mid = (left+right)/2;
                // get t[mid] 
                int mid_elem = t.getMax(0,mid+1);
                // calculate energy of t[mid] stone
                int mid_energy = mid_elem - mid;
                if( mid_energy <= E) {
                    left = mid;
                } 
                else{
                    right = mid;
                }
            }
            int p_index = right;
            int p = E+p_index;

ちなみに上記のコードでも //get t[mid] 付近で使っていますが、
t[i] の座標は t.getMax(0, i+1) で計算することが可能です。

一般的な Implicit Treap では要素が昇順に並んでいるとは限らないのですが、
今回は要素を昇順に並べているため、特別な関数を増やす必要もないんですね。

もっとも、 i 番目の要素参照なんて昇順でなくても使いたいものなので、 Treap の実装に追加していてもよい気はしますけど。

Q(Gに最も近いストーンの座標)の求め方

こちらについてはもっと単純で、 t[left] <= G < t[right] が満たされるように left, right を二分探索で更新すれば、 Gの左右にあるストーンが求まるので、それぞれ検証してみて値の近い方を選択すればOKとなります。

        // find the position nearest to G as Q, and its index 
        int left = -1;
        int right = N;
        while(left+1 < right){
            int mid = (left+right)/2;
            // get t[mid]
            int mid_elem = t.getMax(0,mid+1);
            if(mid_elem <= G){
                left = mid;   
            }
            else{
                right = mid;
            }
        }

        int cand1 = left;
        int stone1 = t.getMax(0,cand1+1); // t[cand1];
        int cand2 = right;
        int stone2 = t.getMax(0,cand2+1); // t[cand2];

        int q_index = cand1;
        int Q = stone1;
        if(abs(stone1-G) >= abs(stone2-G) && cand2 != N){ // t[cand2] = t.getMax(...) を使っていると cand2 =N のケースがコーナーケースになるため、最後の条件ではじく
            q_index = cand2;
            Q = stone2;
        }

最終的なコードと動作確認

ここまでの話をまとめたコード

typedef struct Node {
    int val;
    int priority;
    int cnt;
    int min;
    int max;
    int acc;
    ll sum;
    Node* left;
    Node* right;

    Node(int val, int priority)
        : val(val),
          priority(priority),
          cnt(1),
          sum(val),
          min(val),
          max(val),
          acc(0),
          left(nullptr),
          right(nullptr) {}
} Node;

class ImplicitTreap {
   private:
    Node* root;

    random_device seed_gen;
    mt19937 engine{seed_gen()};
    uniform_int_distribution<int> distribution{INT_MIN, INT_MAX};

    int count(Node* t) { return !t ? 0 : t->cnt; }
    ll sum(Node* t) { return !t ? 0 : t->sum; }
    int getMax(Node* t) { return !t ? INT_MIN : t->max; }
    int getMin(Node* t) { return !t ? INT_MAX : t->min; }

    // update information of t using information of its children
    // this function assumes all the information of the child are updated
    Node* update(Node* t) {
        if (t) {
            t->cnt = count(t->left) + count(t->right) + 1;
            t->sum = sum(t->left) + sum(t->right) + t->val;
            t->min = min(t->val, min(getMin(t->left), getMin(t->right)));
            t->max = max(t->val, max(getMax(t->left), getMax(t->right)));
        }
        return t;
    }

    // push acc, lazy info of node t into its child
    void push(Node* t) {
        if (t) {
            if (t->left) {
                t->left->acc += t->acc;
                t->left->max = t->left->max + t->acc;
                t->left->min = t->left->min + t->acc;
                t->left->sum = t->left->sum + (t->left->cnt) * t->acc;
            }
            if (t->right) {
                t->right->acc += t->acc;
                t->right->max = t->right->max + t->acc;
                t->right->min = t->right->min + t->acc;
                t->right->sum = t->right->sum + (t->right->cnt) * t->acc;
            }
            t->val = t->val + t->acc;
            t->acc = 0;
        }
    }

    // merge Node* l and Node *r into unique tree
    Node* merge(Node* l, Node* r) {
        if (l) push(l);
        if (r) push(r);
        if (!l || !r) return l ? l : r;
        if (l->priority > r->priority) {
            l->right = merge(l->right, r);
            return update(l);
        } else {
            r->left = merge(l, r->left);
            return update(r);
        }
    }

    // split Node* into l=[0,k), r=[k,n)
    pair<Node*, Node*> split(Node* t, int k) {
        if (!t) return make_pair(nullptr, nullptr);
        push(t);
        if (count(t->left) >= k) {
            pair<Node*, Node*> s = split(t->left, k);
            t->left = s.second;
            return make_pair(s.first, update(t));
        } else {
            pair<Node*, Node*> s = split(t->right, k - count(t->left) - 1);
            t->right = s.first;
            return make_pair(update(t), s.second);
        }
    }

    // insert item into pos of t
    void insert(Node*& t, int pos, Node* item) {
        if (!t) {
            t = item;
            return;
        }
        pair<Node*, Node*> s = split(t, pos);
        t = merge(merge(s.first, item), s.second);
    }

    // erase t[pos]
    void erase(Node*& t, int pos) {
        if (!t) return;
        pair<Node*, Node*> p = split(t, pos + 1);
        pair<Node*, Node*> p2 = split(p.first, pos);
        if (p2.second) delete p2.second;
        t = merge(p2.first, p.second);
    }

    void print(Node* t, int depth) {
        if (!t) {
            for (int i = 0; i < depth; i++) cout << "  ";
            cout << "null" << endl;
            return;
        } else {
            print(t->right, depth + 1);
            for (int i = 0; i < depth; i++) cout << "  ";
            cout << "val: " << t->val << ", " << "priority: " << t->priority
                 << ", " << "count: " << t->cnt << ", " << "sum: " << t->sum
                 << ", " << "max: " << t->max << ", " << "min: " << t->min
                 << ", " << "acc: " << t->acc << endl;
            print(t->left, depth + 1);
        }
    }

    // return the sum of [left,right)
    ll sum(Node*& t, int left, int right) {
        pair<Node*, Node*> p = split(t, right);
        pair<Node*, Node*> p2 = split(p.first, left);
        ll ans = sum(p2.second);
        t = merge(merge(p2.first, p2.second), p.second);
        return ans;
    }

    // return the min of [left,right)
    ll getMin(Node*& t, int left, int right) {
        pair<Node*, Node*> p = split(t, right);
        pair<Node*, Node*> p2 = split(p.first, left);
        ll ans = getMin(p2.second);
        t = merge(merge(p2.first, p2.second), p.second);
        return ans;
    }

    // return the max of [left,right)
    ll getMax(Node*& t, int left, int right) {
        pair<Node*, Node*> p = split(t, right);
        pair<Node*, Node*> p2 = split(p.first, left);
        ll ans = getMax(p2.second);
        t = merge(merge(p2.first, p2.second), p.second);
        return ans;
    }

    // add value to the all elements in [left,right)
    void add(Node*& t, int left, int right, int value) {
        pair<Node*, Node*> p = split(t, right);
        pair<Node*, Node*> p2 = split(p.first, left);
        if(p2.second) p2.second->acc += value;
        t = merge(merge(p2.first, p2.second), p.second);
    }

   public:
    ImplicitTreap() { root = nullptr; }

    // insert val into ImplicitTreap[pos]
    void insert(int pos, int val) {
        insert(root, pos, new Node(val, distribution(engine)));
    }
    // erase ImplicitTreap[pos]
    void erase(int pos) { erase(root, pos); };

    // sum ImplicitTreap[left,right)
    ll sum(int left, int right) { return sum(root, left, right); }

    // max ImplicitTreap[left,right)
    int getMax(int left, int right) { return getMax(root, left, right); }

    // min ImplicitTreap[left,right)
    int getMin(int left, int right) { return getMin(root, left, right); }

    // add value to all of ImplicitTreap[left,right)
    void add(int left, int right, int value) { add(root, left, right, value); }

    // print
    void print() { print(root, 0); }
};

int main() {
    int Case;
    cin >> Case;
    int ans = 0;
    for (int ci = 0; ci < Case; ci++) {
        int N, G;
        cin >> N >> G;
        ImplicitTreap t;
        rep(i, N) {
            int E;
            cin >> E;

            // determine E-th empty space as p, and index of p in ImplicitTreap
            int left = -1;
            int right = i;
            while(left+1<right){
                int mid = (left+right)/2;
                // get t[mid] 
                int mid_elem = t.getMax(0,mid+1);
                // calculate energy of t[mid] stone
                int mid_energy = mid_elem - mid;
                if( mid_energy <= E) {
                    left = mid;
                } 
                else{
                    right = mid;
                }
            }
            int p_index = right;
            int p = E+p_index;
            t.insert(p_index, p);
            t.add(0, p_index, -1);    
        }

        // find the position nearest to G as Q, and its index 
        int left = -1;
        int right = N;
        while(left+1 < right){
            int mid = (left+right)/2;
            // get t[mid]
            int mid_elem = t.getMax(0,mid+1);
            if(mid_elem <= G){
                left = mid;   
            }
            else{
                right = mid;
            }
        }

        int cand1 = left;
        int stone1 = t.getMax(0,cand1+1); // t[cand1];
        int cand2 = right;
        int stone2 = t.getMax(0,cand2+1); // t[cand2];

        int q_index = cand1;
        int Q = stone1;
        if(abs(stone1-G) >= abs(stone2-G) && cand2 != N){
            q_index = cand2;
            Q = stone2;
        }

        cout << "Case #" << ci + 1 << ": " << N - q_index << " " << abs(Q - G)
             << endl;
    }
    return 0;
}

使って公式のテスト入力を実行し、出力が以前に解いた解法の出力と一致することが確認できました。

ちなみに計算量は Treap 操作と二分探索を要素数だけ繰り返すので O(N (log(N))^{2}) とかだと思います。
なんとなく実行時間は Implicit Treap の方が遅かった気がする。
まぁでもコンテストの実行制限には(たぶん)引っかからない速度感だとおもいます。

おわりに

というわけで、今回は四回にわたって、Treap がつかえるといわれる問題の Treap 不要解と、 Treap 利用解を説明してきました。
Treap 解の実装までひたすらかかった挙句に不要解より遅い気がするし実装も複雑だし大変じゃね?
... なんて思った人も多いと思うのですが、
実のところ、個人の体感として Treap 関係の問題はこういった現象が起きてることがほとんどなんですよね。

だからこそ、10年以上競技プログラミングをやっている自分も Treap の実装を持ってなかったし、 Implicit Treap と Treap の違いを把握もしていませんでした。
(というか、調べても良くわからないで挫折…を繰り返していた)

そんな背景があったので、かなり読者置いてけぼりの記事だった自覚はあれど、
アドベントカレンダーという体で重い腰を上げてちゃんと向き合うことができて個人的にはよかったです。満足。

おまけ

Q ところで Meta Hacker Cup の Practice Round についてばかり話してたけど、肝心の本戦はどうだったの?
A 寝落ちにより参加できませんでした。

MetaHackerCup 2024 PracticeRound D 問題を語る その3:Treap って色々あんねん

この記事は レジリエントでウェルビーイングなぴょこりんクラスタ Advent Calendar 2024 の為に書いたものです。

はじめに

この記事は前回の続きです。 続きものとなりますので、前回の内容は(ここ)から参照してください。
Treap でとけるよって言われた問題、 Treap なしで解けちゃったんだけど、結局 Treap 使ってどう解くの? という話でした。

今回は、この方針を決めるために、 Treap って結局何なのか、を深堀していこうと思います。

Treap とは何なのか

前回でも少し話題にしましたが、 Treap は乱数を用いた平衡二分木です。

二分木・二分探索木のおさらい

Treap について話す前に二分木についておさらいすると、子が二つしかない木構造のことです。

二分木

二分木の中でもよく用いられている二分探索木は、
データの大小に応じて小さい値を左、大きい値を右に格納していくことで、
 N 個のデータをおよそ  O(log N) の深さの二分木に格納することができて、
結果的に挿入・検索・削除等の操作を  O(log N) で行うことができる、というものでした。

上のようなイメージ通りの動作をしてくれれば基本的に二分探索木は優秀ですが、
素朴な実装においては
データの挿入順を恣意的な順番にすると、意図せず操作に  O(N) の時間がかかってしまうことがあることが問題でした。

二分探索木が O(N) かかってしまうようなケース

平衡二分木と Treap

平衡二分木といった時には、 何らかの方法でこういった木の偏りをならすことで、
データの順などによらず、挿入・検索・削除を  O(log N) で行えるようにするデータ構造のことを指します。

Treap において木の偏りをならすのはどのように行われているかというと、
データが挿入されるごとに裏データとして乱数を割り当て、乱数の小さい順にデータが挿入されたときと同じような状態に木の形を作っていく、というものです。

Treap のイメージ

データが格納されるごとに乱数の順でデータが挿入されたものと結果が同じようになる、というのは

  • Step 1 はデータ列 1 が格納された形と一緒
  • Step 2 はデータ列 [2, 1] が格納された形と一緒
  • Step 3 はデータ列 [3, 2, 1] が格納された形と一緒
  • Step 4 はデータ列 [3, 2, 1, 4] が格納された形と一緒
  • Step 5 はデータ列 [3, 2, 1, 6, 4] が格納された形と一緒
  • Step 6 はデータ列 [3, 2, 1, 6, 4, 7] が格納された形と一緒

ってことです。 Step 1 では根にあった1が Step 2では葉になってたり、 Step 4では 3の子であった 4 が Step 5 では 6 の子になってる、とかがミソです。

もちろん、乱数の付き方次第ではバランスの悪い木が作れる可能性は存在しますが、少なくとも恣意的なデータの挿入などでバランスを悪くすることは不可能、という点で二分探索木より優れています。

原理はわかったけど、こんなデータ構造って実装できるの? という疑問は生じるかもしれませんが、できます。
実装の仕方は insert-erase ベースと merge-split ベースの二種類があり、お好みの方針で実装すればよいです。
この部分についても細かく話してもよいのですが、今回したい話としては脱線するため、思い切って割愛します。

参考文献と実装例を書いておいたので、余力がある人は見てみてください。

プログラミングコンテストでのデータ構造 2 ~平衡二分探索木編~ | PPT
Treap - Algorithms for Competitive Programming

using namespace std;

typedef struct Node {
    int val;
    int priority;
    int cnt;
    int sum;
    Node* left;
    Node* right;

    Node(int val, int priority)
        : val(val),
          priority(priority),
          cnt(0),
          sum(0),
          left(nullptr),
          right(nullptr) {}
} Node;

class Treap {
   private:
    Node* root;

    random_device seed_gen;
    mt19937 engine{seed_gen()};
    uniform_int_distribution<int> distribution{INT_MIN, INT_MAX};

    int count(Node* t) { return !t ? 0 : t->cnt; }
    int sum(Node* t) { return !t ? 0 : t->sum; }
    Node* update(Node* t) {
        if (t) {
            t->cnt = count(t->left) + count(t->right) + 1;
            t->sum = sum(t->left) + sum(t->right) + t->val;
        }
        return t;
    }

    // merge Node* l and Node *r into unique tree
    // this function is under assumption that
    // any node value of l <= any node value of r
    Node* merge(Node* l, Node* r) {
        if (!l || !r) return l ? l : r;
        if (l->priority > r->priority) {
            l->right = merge(l->right, r);
            return update(l);
        } else {
            r->left = merge(l, r->left);
            return update(r);
        }
    }

    // split Node* into two trees (l,r), l has val <=v, r tree has val >k
    void split(Node* t, int v, Node*& l, Node*& r) {
        if (!t) {
            l = r = nullptr;
            return;
        } else if (t->val <= v) {
            split(t->right, v, t->right, r);
            l = update(t);
            return;
        } else {
            split(t->left, v, l, t->left);
            r = update(t);
            return;
        }
    }

    // insert item into t
    void insert(Node*& t, Node* item) {
        if (!t) {
            t = item;
            return;
        }
        Node *t1, *t2;
        split(t, item->val, t1, t2);
        t = merge(merge(t1, item), t2);
    }

    // erase value v from t
    void erase(Node*& t, int v) {
        if (!t) return;
        if (t->val == v) {
            Node* old_t = t;
            t = merge(t->left, t->right);
            delete old_t;
            return;
        } else if (t->val > v) {
            erase(t->left, v);
            return;
        } else {
            erase(t->right, v);
            return;
        }
    }

    void print(Node* t, int depth) {
        if (!t) {
            for (int i = 0; i < depth; i++) cout << "  ";
            cout << "null" << endl;
            return;
        } else {
            print(t->right, depth + 1);
            for (int i = 0; i < depth; i++) cout << "  ";
            cout << "val: " << t->val << ", " << "priority: " << t->priority
                 << endl;
            print(t->left, depth + 1);
        }
    }

   public:
    Treap() { root = nullptr; }
    void insert(int val) { insert(root, new Node(val, distribution(engine))); }
    void erase(int val) { erase(root, val); };

    void print() { print(root, 0); }
};

いったんは、こんな感じで挿入・削除・検索などの操作を  O(log N) で実現できるらしい、と思ってください。

あらためて MetaHackerCup の問題を考える…が…

さてここまでで、 Treap がどういったものかを説明しました。
Treap は、乱数を使うことで挿入・削除・検索などの操作を  O(log N) で実現できるデータ構造でしたね。
それでは改めて、 Meta Hacker Cup の解説が言っていたことを思い出してみましょう。

原文 を意訳した前回の記事から抜粋すると。

つまりは、特定の P に値を入れることと、P以前の値を1引くという操作を高速に(データ量に触れていませんが、この処理を  O(log N) で)計算できれば良い。 こういうのに便利なデータ構造として Treap ってのがあるので、それをうまく使えば計算できるよ。

とのことでした。

つまり、今回の問題を解くために求められていることは、

  • 特定の P に値を入れること
  • P 以前のすべての値を1引くこと

を、  O(log N) で計算することです。

…何か話がかみ合っていないような、変な感じがしますよね?

Treap は挿入・削除・検索などの操作を  O(log N) で実現できるデータ構造、とお勉強してきたわけですが、
上述の操作を  O(log N) で操作できるって話は出てこなかったし、工夫してどうにかなる気も一切しません。

一体なにがどうなっているんだ…となると思いますし、実際僕もここで思い悩みました。

さんざん悩んだ挙句に理解したことの結論を言うと
「Treap は Treap でも、 Meta Hacker Cup でつかうのは Implicit Treap の方」
ということだったようでした。

実は、ここまで学んできた Treap の他に Treap の亜種みたいなのがあるんですね。

参考文献 平衡二分探索木とTreap Implicit Treap - joeの日記

次はそれを見ていきましょう。

Implicit Treap とは何か

Implicit Treap 基礎

Implicit Treap は Treap とは打って変わって、データの大小関係は考えないデータ構造です。
どっちかというと二分探索木というより vector なイメージに近く、以下のような操作が行えます。

  • (0以上配列長未満の) 位置 p にデータ xを格納する
  • (0以上配列長未満の) 位置 p のデータを削除する

なんだこれは、ただ配列とか vector とか使えばいいだけじゃないか、と思うかもしれませんが、いったんその気持ちは飲み込んでください。
これと Treap と何の関係があるんだ、という話をまずしていきます。

二分探索木って vector に対応付けられるよね、という話

突然ですが、二分探索木は木のノードを要素の大小をもとに左右に振り分ける特性上、
しかるべき順序でみたら、データが昇順に並んでいるとみることができます。
直観的に図示するなら、以下のような感じです。

この意味で、二分探索木は vector と対応付けられる、ということができます。 これは一対一対応ではなくて、二分探索木が定まれば vector は定まるが、 vecotr に対応する二分探索木は何通りか考えられます。 例えば、同じ vecotr でも異なる二分探索木として以下のようなものも作れますね。

あらためて、 Implicit Treap とは何なのか?

先ほどの議論を踏まえて Implicit Treap について一言で説明するなら、 Implicit Treap は、

  • データそのものでなく、データの位置情報 p を使ってデータを二分探索木状に格納する。
  • 二分木の構築の仕方として、 Treap の概念を使う

といったものです。

  • Step 1 は vector [100] に対応する木を構築したい。ノード追加順は乱数に従い [100] とする。
  • Step 2 は vector [100, 200] に対応する木を構築したい。ノード追加順は乱数に従い [200, 100] とする
  • Step 3 は vector [100, 300, 200] に対応する木を構築したい。ノード構築順は乱数に従い [300, 200, 100] とする。
  • Step 4 は vector [100, 400, 300, 200] に対応する木を構築したい。ノード構築順は乱数に従い [300, 200, 100, 400] とする。
  • Step 5 は vector [100, 400, 500, 300, 200] に対応する木を構築したい。ノード構築順は乱数に従い [300, 200, 100, 500, 400] とする。
  • Step 6 は vector [100, 400, 600, 500, 300, 200] に対応する木を構築したい。ノード追加順は乱数に従い [300, 200, 100, 500, 400, 600] とする。

といった具合。 青丸で追加したのが各要素の index で、
この index の配置が通常の Treap のようになっている、というのが今回のミソとなります。

実装例

using namespace std;

typedef struct Node {
    int val;
    int priority;
    int cnt;
    Node* left;
    Node* right;

    Node(int val, int priority)
        : val(val), priority(priority), cnt(1), left(nullptr), right(nullptr) {}
} Node;

class ImplicitTreap {
   private:
    Node* root;

    random_device seed_gen;
    mt19937 engine{seed_gen()};
    uniform_int_distribution<int> distribution{INT_MIN, INT_MAX};

    int count(Node* t) { return !t ? 0 : t->cnt; }

    Node* update(Node* t) {
        if (t) {
            t->cnt = count(t->left) + count(t->right) + 1;
        }
        return t;
    }

    // merge Node* l and Node *r into unique tree
    Node* merge(Node* l, Node* r) {
        if (!l || !r) return l ? l : r;
        if (l->priority > r->priority) {
            l->right = merge(l->right, r);
            return update(l);
        } else {
            r->left = merge(l, r->left);
            return update(r);
        }
    }

    // split Node* into l=[0,k), r=[k,n)
    pair<Node*, Node*> split(Node* t, int k) {
        if (!t) return make_pair(nullptr, nullptr);
        if (count(t->left) >= k) {
            pair<Node*, Node*> s = split(t->left, k);
            t->left = s.second;
            return make_pair(s.first, update(t));
        } else {
            pair<Node*, Node*> s = split(t->right, k - count(t->left) - 1);
            t->right = s.first;
            return make_pair(update(t), s.second);
        }
    }

    // insert item into pos of t
    void insert(Node*& t, int pos, Node* item) {
        if (!t) {
            t = item;
            return;
        }
        pair<Node*, Node*> s = split(t, pos);
        t = merge(merge(s.first, item), s.second);
    }

    // erase t[pos]
    void erase(Node*& t, int pos) {
        if (!t) return;
        pair<Node*, Node*> p = split(t, pos + 1);
        pair<Node*, Node*> p2 = split(p.first, pos);
        if (p2.second) delete p2.second;
        t = merge(p2.first, p.second);
    }

    void print(Node* t, int depth) {
        if (!t) {
            for (int i = 0; i < depth; i++) cout << "  ";
            cout << "null" << endl;
            return;
        } else {
            print(t->right, depth + 1);
            for (int i = 0; i < depth; i++) cout << "  ";
            cout << "val: " << t->val << ", " << "priority: " << t->priority
                 << ", " << "count: " << t->cnt << endl;
            print(t->left, depth + 1);
        }
    }

   public:
    ImplicitTreap() { root = nullptr; }
    void insert(int pos, int val) {
        insert(root, pos, new Node(val, distribution(engine)));
    }
    void erase(int val) { erase(root, val); };

    void print() { print(root, 0); }
};

Implicit Treap は何がうれしいか。

さて、ぐにゃぐにゃと説明を続けてきましたが、ここまでを改めてまとめると、

Implicit Treap は以下の操作を  O(logN) でできる、というものでした

  • 位置 p にデータ x を追加する
  • 位置 p にあるデータを削除する。

これだけだと、何がうれしいねん、ですよね。
複雑なことをしただけで実現できる操作も大したことがないですし。

Implicit Treap のうれしいところというのは、この形でデータを格納することによって、
任意の部分木が特定の vector の範囲に対応付けられることにあります。

これによって範囲を取り扱う計算とかがかなりしやすくなっており、
補助データを追加するなどの実装追加を行うことによって、 以下のような操作も高速で計算することが可能になっています。

  • 特定範囲の中での最小値の検出
  • 特定範囲の中での最大値の検出
  • 特定範囲の値の和の検出
  • 特定範囲に対する加算
  • 特定範囲の reverse (ただし、下記実装例には含まれません)

min, max, sum, add を実装した Treap の実装例

using namespace std;
typedef struct Node {
    int val;
    int priority;
    int cnt;
    int min;
    int max;
    int acc;
    ll sum;
    Node* left;
    Node* right;

    Node(int val, int priority)
        : val(val),
          priority(priority),
          cnt(1),
          sum(val),
          min(val),
          max(val),
          acc(0),
          left(nullptr),
          right(nullptr) {}
} Node;

class ImplicitTreap {
   private:
    Node* root;

    random_device seed_gen;
    mt19937 engine{seed_gen()};
    uniform_int_distribution<int> distribution{INT_MIN, INT_MAX};

    int count(Node* t) { return !t ? 0 : t->cnt; }
    ll sum(Node* t) { return !t ? 0 : t->sum; }
    int getMax(Node* t) { return !t ? INT_MIN : t->max; }
    int getMin(Node* t) { return !t ? INT_MAX : t->min; }

    // update information of t using information of its children
    // this function assumes all the information of the child are updated
    Node* update(Node* t) {
        if (t) {
            t->cnt = count(t->left) + count(t->right) + 1;
            t->sum = sum(t->left) + sum(t->right) + t->val;
            t->min = min(t->val, min(getMin(t->left), getMin(t->right)));
            t->max = max(t->val, max(getMax(t->left), getMax(t->right)));
        }
        return t;
    }

    // push acc, lazy info of node t into its child
    void push(Node* t) {
        if (t) {
            if (t->left) {
                t->left->acc += t->acc;
                t->left->max = t->left->max + t->acc;
                t->left->min = t->left->min + t->acc;
                t->left->sum = t->left->sum + (t->left->cnt) * t->acc;
            }
            if (t->right) {
                t->right->acc += t->acc;
                t->right->max = t->right->max + t->acc;
                t->right->min = t->right->min + t->acc;
                t->right->sum = t->right->sum + (t->right->cnt) * t->acc;
            }
            t->val = t->val + t->acc;
            t->acc = 0;
        }
    }

    // merge Node* l and Node *r into unique tree
    Node* merge(Node* l, Node* r) {
        if (l) push(l);
        if (r) push(r);
        if (!l || !r) return l ? l : r;
        if (l->priority > r->priority) {
            l->right = merge(l->right, r);
            return update(l);
        } else {
            r->left = merge(l, r->left);
            return update(r);
        }
    }

    // split Node* into l=[0,k), r=[k,n)
    pair<Node*, Node*> split(Node* t, int k) {
        if (!t) return make_pair(nullptr, nullptr);
        push(t);
        if (count(t->left) >= k) {
            pair<Node*, Node*> s = split(t->left, k);
            t->left = s.second;
            return make_pair(s.first, update(t));
        } else {
            pair<Node*, Node*> s = split(t->right, k - count(t->left) - 1);
            t->right = s.first;
            return make_pair(update(t), s.second);
        }
    }

    // insert item into pos of t
    void insert(Node*& t, int pos, Node* item) {
        if (!t) {
            t = item;
            return;
        }
        pair<Node*, Node*> s = split(t, pos);
        t = merge(merge(s.first, item), s.second);
    }

    // erase t[pos]
    void erase(Node*& t, int pos) {
        if (!t) return;
        pair<Node*, Node*> p = split(t, pos + 1);
        pair<Node*, Node*> p2 = split(p.first, pos);
        if (p2.second) delete p2.second;
        t = merge(p2.first, p.second);
    }

    void print(Node* t, int depth) {
        if (!t) {
            for (int i = 0; i < depth; i++) cout << "  ";
            cout << "null" << endl;
            return;
        } else {
            print(t->right, depth + 1);
            for (int i = 0; i < depth; i++) cout << "  ";
            cout << "val: " << t->val << ", " << "priority: " << t->priority
                 << ", " << "count: " << t->cnt << ", " << "sum: " << t->sum
                 << ", " << "max: " << t->max << ", " << "min: " << t->min
                 << ", " << "acc: " << t->acc << endl;
            print(t->left, depth + 1);
        }
    }

    // return the sum of [left,right)
    ll sum(Node*& t, int left, int right) {
        pair<Node*, Node*> p = split(t, right);
        pair<Node*, Node*> p2 = split(p.first, left);
        ll ans = sum(p2.second);
        t = merge(merge(p2.first, p2.second), p.second);
        return ans;
    }

    // return the min of [left,right)
    ll getMin(Node*& t, int left, int right) {
        pair<Node*, Node*> p = split(t, right);
        pair<Node*, Node*> p2 = split(p.first, left);
        ll ans = getMin(p2.second);
        t = merge(merge(p2.first, p2.second), p.second);
        return ans;
    }

    // return the max of [left,right)
    ll getMax(Node*& t, int left, int right) {
        pair<Node*, Node*> p = split(t, right);
        pair<Node*, Node*> p2 = split(p.first, left);
        ll ans = getMax(p2.second);
        t = merge(merge(p2.first, p2.second), p.second);
        return ans;
    }

    // add value to the all elements in [left,right]
    void add(Node*& t, int left, int right, int value) {
        pair<Node*, Node*> p = split(t, right);
        pair<Node*, Node*> p2 = split(p.first, left);
        p2.second->acc += value;
        t = merge(merge(p2.first, p2.second), p.second);
    }

   public:
    ImplicitTreap() { root = nullptr; }

    // insert val into ImplicitTreap[pos]
    void insert(int pos, int val) {
        insert(root, pos, new Node(val, distribution(engine)));
    }
    // erase ImplicitTreap[pos]
    void erase(int pos) { erase(root, pos); };

    // sum ImplicitTreap[left,right)
    ll sum(int left, int right) { return sum(root, left, right); }

    // max ImplicitTreap[left,right)
    int getMax(int left, int right) { return getMax(root, left, right); }

    // min ImplicitTreap[left,right)
    int getMin(int left, int right) { return getMin(root, left, right); }

    // add value to all of ImplicitTreap[left,right)
    void add(int left, int right, int value) { add(root, left, right, value); }

    // print
    void print() { print(root, 0); }
};

ここまでくると少しうまみを感じれてきますね。
競技プログラミングにおいて範囲加算・減算などをする際によく用いられるデータ構造には LazySegmentTree とかもありますが、 範囲 reverse などが Implicit Treap に優位性があるっぽいです。

次回予告

さて、ここまでの話を改めてまとめると

Implicit Treap は以下の操作を  O(log N) で行えるデータ構造でした。

  • ある位置 p に要素 x を挿入する
  • ある位置 p にある要素を削除する
  • ある範囲の要素の最大値を計算する
  • ある範囲の要素の最小値を計算する
  • ある範囲の要素すべての総和を計算する
  • ある範囲の要素すべてに値 a を加算する
  • ある範囲の要素を左右反転させる

次回は、このデータ構造を使って改めて MetaHackerCup 2024 PracticeRound D2 問題を解いていきましょう。




以上の内容はhttps://yaox.hatenadiary.jp/より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14