Atcoder Regular Contest_008_D_タコヤキオイシクナール
橙色の問題.箱に入れるという動作がどういうものか解釈してみましょう.
問題の説明
箱が個存在し,箱それぞれには2つの実数が記載されている. ある実数を入力するととなって出力される.
最初箱に記載されている数字はすべてであり,入力される数字はである.我々はから個の箱を通した後のの値を求めたい.
M個のクエリが存在し,クエリには番目の箱をに書き換える指示が出ている.
クエリの指示を1個ずつ行い,それに応じて個の箱にを通した後のの最大値と最小値を求めよ.
制約
- は実数であり,
考えた事
まず,この問題のに注目したい.これは明らかに大きすぎる.この制約からわかるようにの計算量で問題を扱う事はできない.ここで注目したいのがの存在である.
をの箱に入力した場合,出力されるのはである.したがって,の箱は問題の本質にはかかわらず不要である,これをまず除去しよう.こういった不要なものを除去するテクニックとして座標圧縮があげられる.
座標圧縮については上のサイトを参照して頂きたい.ざっくりいえばどれくらいの差や,具体的な位置情報を保持せず,大小関係や前後関係だけにして情報量を減らすというものである.今回の問題だとはいらない情報なのでそこを詰める事とする.こうする事で処理しなければならない情報が高々個となり,の上限がである事からも分かるように扱いやすくなる.
座標圧縮のやり方も色々あるが,今回はを先に読み込んで
vll pp = p; sort(ALL(pp)); pp.erase(unique(ALL(pp)), pp.end()); N = pp.size(); rep(i, M) { p[i] = lower_bound(ALL(pp), p[i]) - pp.begin(); }
といった形で行った.
https://qiita.com/ysk24ok/items/30ae72f4f1060b088588:qiita.com
を参考にしてもらいたい.要するにsort
をかけ,重複要素のポインタをunique
で発見し,erase
をかけている.
後はlower_bound
を行い,何番目に大きいかを受け取る事で大小関係を変えずに座標圧縮を達成してる.難しければsort
をかけた後,map
を用いて行ってもよい.
さて,とりあえずこれで箱を高々個とする事ができた.しかし,これだけではこの問題を解く事は出来ない.ひとつのクエリを書き換える度に毎回毎回個の箱を通していては,計算量がとなり間に合わない.1 どうすればいいだろうか.ここでポイントとなるのは書き換えればならないデータ量の削減である.たとえばデータを1つ1つ線形構造で繋いでいる場合,1つを書き換えてしまうと他のすべてに影響を及ぼすため,すべてやり直さなければならなくなり,計算量が増えてしまう.
直感的な話題になるが,たとえばとあって,この配列を用いて何か操作を行うとする.2 この時にをに書き換えた時,前半部分は書き換えが必要だが,後半に関してはあんまり書き換えがいらなく見えるだろう.この直感を活かして計算量を減らしたデータ構造がある.セグメント木だ.
セグメント木とは(完全)二分木であり,区間毎にある演算を行った時にどうなるかの結果を高速に管理するデータ構造である. zenn.dev
セグメント木は親の区間を2分割して子が持つようになっているものである.区間の最小として自分自身のみが含まれているものである事に注目すると,この木の深さはであるから,1つのデータが更新された時に書き換える箇所も個となるため,計算量としてはで更新を行える点がポイントである.
セグメント木はその性質上,演算に対して単位元が存在するものと,結合律が成り立っている必要がある. このような集合と演算を代数学の用語でモノイドという. たとえば和の演算や積の演算は,それと実数集合上とでモノイドをなす.3 本題に戻ろう.今回であればどの集合と演算とでモノイドとなるだろうか.集合は簡単であり実数全体である.演算はなんであろうか. ここで箱に入れるという操作に注目したい.これは実数を実数に変換するもので,操作はであった.この操作をとする.
これに対して函数の合成を演算としてとすると二項演算の単位元としてを取る事ができ,また合成関数は結合則が成り立つため,実数と演算からなる集合はモノイドを形成する.
という事でとしたモノイドを構築する事に成功した.4
後はこれを実装するだけである.今回はAtcoder Libraryにあるsegtree
を活用した.自前でセグ木を実装する技術も非常に大切であり,これを読んでいる皆様におかれましても是非とも取り組んで頂きたい.
実装する際には演算を行った場合に何が返ってくるかを指定する必要がある.実はこれはそう難しくなく,,となるとした時, となるから,として返り値を出力すればいい.
単位元はさきほど説明した通りで表現できる.以上をプログラムで表すと以下のようになる.
using P = pair<ld, ld>; P op(P p, P q) { return {p.first * q.first, p.second * q.first + q.second}; } P e() { return {1, 0}; }
これでセグメント木の実装は終わった.最後に個をセグメント木で処理し,各々最大値と最小値を比較するように組み込めばACが得られる.ここまでお読みいただきありがとうございました.
プログラム
#include <bits/stdc++.h> using namespace std; #include <atcoder/all> using namespace atcoder; using ll = long long; using ld = long double; using P = pair<ld, ld>; using vll = vector<long long>; #define rep(i, n) for (ll i = (0); i < (ll)(n); i++) #define ALL(n) begin(n), end(n) template <class T> inline T CHMAX(T& a, const T b) { return a = (a < b) ? b : a; } template <class T> inline T CHMIN(T& a, const T b) { return a = (a > b) ? b : a; } P op(P p, P q) { return {p.first * q.first, p.second * q.first + q.second}; } P e() { return {1, 0}; } ll target; bool f(ll v) { return target <= v; } int main() { ll N, M; cin >> N >> M; vll p(M); vector<ld> a(M); vector<ld> b(M); rep(i, M) { cin >> p[i] >> a[i] >> b[i]; } vll pp = p; sort(ALL(pp)); pp.erase(unique(ALL(pp)), pp.end()); N = pp.size(); rep(i, M) { p[i] = lower_bound(ALL(pp), p[i]) - pp.begin(); } segtree<P, op, e> seg(N); ld maxtako = 1.0; ld mintako = 1.0; rep(i, M) { seg.set(p[i], {a[i], b[i]}); P takoyaki = seg.all_prod(); ld tako = takoyaki.first + takoyaki.second; CHMAX(maxtako, tako); CHMIN(mintako, tako); } cout << setprecision(18) << mintako << endl; cout << setprecision(18) << maxtako << endl; }