輪郭をなぞるだけのブログ

浅学菲才のためにおそらく嘘も多い

Codeforces Round #632 (Div. 2) - C

問題へのリンク

C - Eugene and an array
codeforces.com

問題概要

長さ n の数列 a が与えられる。区間の和が 0 となる連続部分列を含まない連続部分列の個数を求めよ。

制約

1\leq n\leq 2\times 10^5
-10^9\leq a_{i}\leq 10^9

考察

ある条件を満たす数え上げの問題である。
本番では、さっと順序立てて書けずに、こんがらがって時間を溶かしまくったので、反省として記事を書く。


まず累積和を取る。
例のごとく、
S_{i} := 左から i 個目まで取った時の累積和
としておく。
このとき、区間 [l, r) の和は、S_{r} - S_{l - 1} で求められる。
すると、ある区間 [l, r) の和が 0 になるということは、S を用いて以下のように表現できる。
S_{r} - S_{l - 1} = 0
つまり
S_{l - 1} = S_{r}

(この段落の説明は、後述で図でも示している。)
出力する答えを ans 、今見ている連続部分列を sub としておく。
基本的な方針として、sub を伸ばしていくのだが、新たに追加する要素 a_{r-1} までの累積和 S_{r} の値が、任意の sub の要素までの累積和と異なるならば、そのまま伸ばすことができる。
問題は S_{r} がある場合である。
このときはまず、sub の中で取れる連続部分列の数を ans に加算しておく。
このときに加算される値は、sub の長さを x とすると、 x \times (x+1) \ /\  2 と計算できる。
sub の左端を l としておき、S_{r} がなくなるまで、l を右に動かしていく。
これは、sub に含まれる要素までの累積和を set などで持っておくと、尺取り法の要領でできる。
ここまでの処理をしてから、連続部分列 sub を伸ばす。
右端が n までいったときにも、最後に x \times (x+1) \ /\ 2ans に加算する。

ただ、これだけでは不十分である。
というのも、重複が起こり得るからで、その分を取り除いておく必要がある。
例えば、a = \{1, 3, -4, 1, 2\} であるような場合を考えてみる。
上述の手順で実験する。
f:id:OutLine:20200409195401p:plain
f:id:OutLine:20200409195414p:plain
f:id:OutLine:20200409195429p:plain
ここまでは難なく進むが、問題は次の場合 (r=3) である。
f:id:OutLine:20200409195606p:plain
連続部分列に a_{r-1} を入れようとしているが、S_{r} と等しい累積和が含まれているので、まだ含めることができない。
f:id:OutLine:20200409195947p:plain
まず、上図の矢印で囲まれた [0, 2) で取れる連続部分列の個数を ans に加算しておく。
f:id:OutLine:20200409201532p:plain
次に、上図のように累積和の等しい値がなくなるまで左端を動かす。
f:id:OutLine:20200409201540p:plain
ここまできてようやく連続部分列に a_{r-1} を含めることができる。
f:id:OutLine:20200409201547p:plain
ただ、注意深く見ると分かるが、上図の矢印部分が重複して数え上げられる。
この部分は、後の処理で再び加算されるので、a_{r-1} を含める前に、この連続部分列で取れる連続部分列の個数を ans から差し引いておく。

これで問題が解決できる。


本番では、どのデータ構造が適切かと考えているときに、重複をどう処理すればいいのかという考えに至り、ものすごくこんがらがっていた。
考えを纏めて素早くコードを書けるようになりたい。

コード

本番では尺取り部分を while 文で書いていたが、for 文で書いておく。

#include <iostream>
#include <vector>
#include <set>
using namespace std;

int main(){
    int n;
    cin >> n;
    vector<int> a(n);
    vector<int64_t> S(n + 1);
    S[0] = 0;
    for(int i = 0; i < n; ++i){
        cin >> a[i];
        S[i + 1] = S[i] + a[i];
    }
    
    int64_t ans = 0;
    int left = 0;       // 今見ている連続部分列の左端
    int len = 0;        // 今見ている連続部分列の長さ
    set<int64_t> st;    // 累積和に関する集合
    st.insert(0);
    for(int right = 1; right <= n; ++right){
        // S[r] が st に含まれていなければ、そのまま連続部分列を伸ばす
        if(st.find(S[right]) == st.end()){
            st.insert(S[right]);
            ++len;
        }
        // S[r] が st に含まれているとき
        else{
            // 直前までの連続部分列で取れる連続部分列の個数を加算
            ans += (int64_t)len * (len + 1) / 2;
            // S[r] がなくなるまで左端を動かす
            while(st.find(S[right]) != st.end()){
                st.erase(S[left++]);
                --len;
            }
            // 後にも数え上げられる重複分を取り除く
            ans -= (int64_t)len * (len + 1) / 2;
            // 右端を伸ばす
            st.insert(S[right]);
            ++len;
        }
    }
    // 残る連続部分列の分を加算
    ans += (int64_t)len * (len + 1) / 2;
    cout << ans << endl;
}
雑記

記事を書くのも久々になってしまった。
相変わらず冗長すぎる。

KMP なり学んだアルゴリズムを書こうと思っていたが、めんどくさくなって下書きに溜まっていく一方である。
ブログを備忘録として用いるために、ちゃんと書こうね…

Atcoder では緑から水 (現在 : 緑)、Codeforces では緑から青 (現在 : 青) をさまよっている。
今回で再び青になった。
全く安定しなくて笑うほかない。

追記

この操作を使える問題には、例えばこれがある。
D - Xor Sum 2