phyllo’s algorithm note

レッドコーダーへの道のりは遠い。休んでる場合じゃない!

ABC177 F. I hate Shortest Path Problem

概要

縦H+1マス、横Wマスのマス目がある。
最初、一番上のいずれかのマスからスタートし、右または下に1マスずつ移動していくことを繰り返す。
ただし、上から1以上H以下の縦iマスの左からA_i~B_iマスについては、下に移動することができない。

上からi (i=1~H)のマスのいずれかに移動するための最小移動回数を求めよ。
ただし、移動できない場合は-1を出力せよ。

制約

  • 1 <= H, W <= 2 * 10^5
  • 1 <= A_i <= B_i <= W

解法

(遅延セグメント木解)

indexが0~Wまである配列を考える。
問題文は、A_i, B_iが与えられたら、A_i-1の値がxだとしたら、A_iマスはx+1、A_i +1マスはx+2、・・・のようにindexに対して等差(公差は正)になるように範囲更新できればよい。
例としては、

最初:0 0 0 0 0 0
↓
(A_i, B_i)=(1,3)、x=1が与えられる
↓
0 1 2 0 0 0

のように更新できれば、1段下に下がったときの各マスへの横方向への移動距離がわかる。
求めたい答えは、全体での最小値で、範囲最小値が取得できれば、それに縦方向の移動距離iを足したものが答えになる。

したがって、

  • 範囲等差(公差は正)更新
  • 範囲min

ができればよい。
これを遅延セグメント木で考える。

遅延セグメント木において、モノイドSに対して作用素Fが適用される場合を考える。

f:id:phyllo_algo:20200930184543p:plain
「範囲等差(公差は正)更新」は、位置が依存しているので、単純にSが「範囲の最小値」だけを保持しているだけでは、FがSに作用したときのSの更新ができない。
(足りない情報としては、「Fの左端」と「Sの左端」の情報がないので、Fの範囲で左からx、x+1、x+2、・・・とした場合、Sの左端の位置でのx+?がわからないので、Sの範囲での最小値がわからない。)

そこで、SとFに「範囲(左端indexと右端index)」を情報ともたせると、位置に依存した計算を行うことができる。

具体的には、

  • Sは「範囲min」「そのSが扱う範囲(左端index, 右端index)」(この問題では右端は不要)
  • Fは「Fの左端での値x」「そのFが適用される範囲(左端index, 右端index)」(この問題では右端は不要)

をもたせる。
(図ではそれぞれの「l」と「r-1」の情報。半開区間で[l, r)のほうがよさそう)

FがSに作用した場合、Sの範囲の一番左の値は「F.x + (S.l-F.l)」で更新できる。
(Sの範囲の一番右の値も「F.x + (S.r-1-F.l)」でわかる)
Sの範囲で一番小さい値は、(公差が正なら)一番左の値になるので、結局Sのminは「F.x + (S.l-F.l)」で更新すれば良い。

f:id:phyllo_algo:20200930184600p:plain
また、SとS'からS''を計算する場合は、

  • 範囲minは、SとS'の範囲minの値で小さい方
  • S''の扱う範囲は、S.lからS'.r-1まで

とわかるので、そのように計算すれば良い。

static const ll INF = (ll)(1e15);

struct S {
  ll val;
  ll range_l, range_r;
};
struct F {
  ll x;
  ll range_l, range_r;
};

S op(S l, S r){
  S ret;
  ret.val = min(l.val, r.val); //範囲min

  ret.range_l = min(l.range_l, r.range_l); //左端
  ret.range_r = max(l.range_r, r.range_r); //右端
  return ret;
}
S e(){
  return S{INF,-INF,INF};
}
S mapping(F l, S r){
  if(l.x == INF) return r;
  return S{l.x+(r.range_l-l.range_l), r.range_l, r.range_r}; //範囲minをSの左端の値にする
}
F composition(F l, F r){
  if(l.x == INF) return r;
  return l;
}
F id(){
  return F{INF,-INF,INF};
}

コード的には、

  int H, W;
  cin >> H >> W;
  lazy_segtree<S, op, e, F, mapping, composition, id> seg(W+1);
  rep(i,W+1){ seg.set(i, S{0,i,i+1}); } //最初は0で初期化
  seg.set(0, S{W+1,0,1}); //index=0だけはW+1(移動できない)にしておく

  rep(i,H){
    int A, B;
    cin >> A >> B;
    {
      S s = seg.get(A-1);
      seg.apply(A-1, B+1, F{s.val,A-1,B+1}); //A-1~Bで範囲等差更新(左端の値を指定)
    }
    {
      S s = seg.all_prod(); //全範囲での範囲min
      if(s.val != W+1) cout << s.val+(i+1) << endl; //移動できる場合は、「横方向移動距離+縦方向移動距離」を出力
      else cout << -1 << endl;
    }
  }

反省

「SとFに範囲情報を持たせる」は汎用的そうで、「範囲f(x,i)更新」とかも同様に扱える。