NoiminのNoise

競技プログラミング (多め) とWeb (たまに) ,自然言語処理 (ブログではまだ)。数式の書き方を一気に KaTeX に変えようとして記事を全削除してインポートし直すなどしたので,過去にブックマークされた記事は URL が変わってしまっている可能性があります…….

Educational DP Contest W - Intervals

W - Intervals

問題概要

長さ $ N (\leq 2 \times 10^5) $ の '0' と '1' のみからなる文字列を考える.

各 $ i (1 \leq i \leq M) $ について,$ l_i $ 文字目から $ r_i $ 文字目までに '1' が ひとつでも含まれるならば,スコアに $ a_i $ を加算するとき,取りうるスコアの最大値を求めよ.

解法概要

$ dp_{i} = $ 最右の '1' が i 文字目である場合のスコアの最大値

という DP を考える (まず私の場合この発想がなかなか出てこなかったが,思い返してみれば一番端の状態を固定して DP を考える問題は時々見かける気がする).

すると,$ dp_{i} $ を考えるとき, 1文字目から (i-1) 文字目まではスコアが最大になるように決めて良いことになる (i 文字目は定義の通り固定で, (i+1) 文字目以降は同じく定義よりすべて '0') .最右の '1' が i 文字目である場合のスコアの最大値

という DP を考える (まず私の場合この発想がなかなか出てこなかったが,思い返してみれば一番端の状態を固定して DP を考える問題は時々見かける気がする).つまり,$ j \lt i $ という制約のもとで最大の $ dp_{j} $ に,i 文字目を '1' にしたときに加算されるスコアを足したものが $ dp_{i} $ の値となる.

なので,$ dp_{i} $ を考える番になったら,最初に $ dp_{j} (1 \leq j \lt i) $ の最大値を $ dp_{i} $ に入れておく.次に,区間の右端が i であるようなスコア加算クエリを適用していく. j 番目のクエリを適用するとき $ dp_{l_j} $ から $ dp_{r_j} $ までに $ a_i $ を加算すればよい.

最終的に,DP テーブル全体での最大値がこの問題の解となる.

なお,N の制約的に時間計算量が $ O(N^2) $ だと厳しそうなので,遅延評価セグメントツリーを使うことで $ O(N\log N) $ に抑える.遅延評価セグメントツリーを使うことで高速化ができる部分は上記の太字の部分である.

(以下画像 + 2 つの段落は自分がしそうになった勘違いについてメモっているだけなので読み飛ばしていただいて構いません)

f:id:noimin:20190507223618p:plain

例えば上図の状況では, $ dp_{4} $ を考えようとしている.$ dp_{3} $ 考えたところで,点線より上のクエリはすでに DP テーブルに適用されている.これまでの最大値は 4 なので, $ dp_{4} = 4$ としておく.次に,$ dp_{5} $ を考える前に右端のインデックスが 4 のクエリを適用しておく.

ひょっとしたら,「最終的な $ dp_{4} $ の値には点線より下のクエリも関係するのだから,点線より下のクエリも考慮すべきでは?」と私と同じことを思った人がいるかもしれない.$ dp_{4} $ に対応する 01 文字列は 4 文字目が '1',5 文字目以降が '0' と決まっているので,4文字目を区間に含むようなクエリはすべて適用しなければならないし,そうでないクエリは 1 つも適用してはならない.つまり $ dp_{4} $ の値を考えるにあたって,点線より下のクエリを適用するかしないかについては選択の余地がない.そのため,ここでは3文字目までを最適に決めるため $ dp_{3} $ までの最大値をとるだけにし,今後クエリを適用する際には問答無用で $ dp_{l_j} $ から $ dp_{r_j} $ までに $ a_i $ を加算していく.

ソースコード

#include <iostream>
#include <climits>
#include <vector>

using namespace std;

typedef long long ll;

const int MAX_N = 200000;

template<typename T> class LazyRMQ {
    public:
    int n;
    T inf = INT_MAX;
    vector<T> data, lazy;
    
    LazyRMQ(int m, T init_value=INT_MAX){
        // 2のべき乗にする
        n = 1;
        while(n < m) n <<= 1;
        data.assign(2*n-1, init_value);
        lazy.assign(2*n-1, 0);
        inf = init_value;
    }

    void eval(int k, int kl, int kr){
        if(data[k] == inf) data[k] = 0;
        if(lazy[k] == 0) return;
        data[k] += lazy[k];
        if(kr - kl > 1){
            lazy[2*k+1] += lazy[k];
            lazy[2*k+2] += lazy[k];
        }
        lazy[k] = 0;
    }

    // [s,t)
    void add(int s, int t, T x, int k, int kl, int kr){
        eval(k, kl, kr);
        if(kr <= s || t <= kl) return;
        if(s <= kl && kr <= t){
            lazy[k] += x;
            eval(k, kl, kr);
            return;
        }
        int kc = (kl+kr)/2;
        add(s, t, x, 2*k+1, kl, kc);
        add(s, t, x, 2*k+2, kc, kr);
        data[k] = max(data[2*k+1], data[2*k+2]);
    }

    void add(int s, int t, T x) {
        add(s, t, x, 0, 0, n);
    }

    // [s,t)
    T find(int s, int t, int k, int kl, int kr){
        eval(k, kl, kr);
        if(kr <= s || t <= kl) return inf;
        if(s <= kl && kr <= t) return data[k];
        int kc = (kl+kr)/2;
        T vl = find(s, t, 2*k+1, kl, kc);
        T vr = find(s, t, 2*k+2, kc, kr);
        return max(vl, vr);
    }

    T find(int s, int t) {
        return find(s, t, 0, 0, n);
    }
};

int main() {
    int n,m,l,r; ll a;
    cin >> n >> m;
    vector<pair<int, ll>> ranges[n+1];
    for(int i=0;i<m;++i) {
        cin >> l >> r >> a;
        ranges[r].emplace_back(l, a);
    }

    LazyRMQ<ll> tree(n+2, -(1<<30));
    for(int i=1;i<=n;++i) {
        ll opt = tree.find(0, i);
        tree.add(i, i+1, opt);
        for(pair<int, ll> p: ranges[i]) {
            tree.add(p.first, i+1, p.second);
        }
    }

    cout << tree.find(0, n+1) << endl;
}

所感

やっと Educational DP Contest 全完できた!

ただ,解法概要がまともな日本語になっている自信がない.