Yuulis.log

Yuulis.log

トンネルを抜けるとそこは参照エラーであった。

【AtCoder】ABC 408 D - Flip to Gather | 緑コーダーが解くAtCoder

atcoder.jp

配点: 400 点 / 実行時間制限: 2 sec / メモリ制限: 1024 MB / Difficulty: 950 / NoviSteps: 1Q

問題概要

長さ  N01のみからなる文字列  S が与えられる。これから、以下の操作を  0 回以上行うことで、  S に含まれる連続した1区間が高々  1 個となるようにするとき、必要な操作回数の最小値を求めよ。

  • 操作 :  1 \leq i \leq N を満たす整数  i を選び、 S_i0のときは1に、1のときは0に変更する。

 T 個のテストケースが与えられるので、それぞれについて答えを求めよ。

制約

  •  1 \leq T \leq 20000
  •  1 \leq N \leq 2 \times 10^5
  • 各入力ファイルについて、すべてのテストケースの  N の総和は  2 \times 10^5 以下である
  •  T,N は整数

考察

この問題は2通りの方針があるので、それぞれ紹介する。なお、添字は 0-indexed としている。

1の個数の累積和を用いる

 S に含まれる連続した1区間が高々  1 個となるためには、  S が以下の2通りのいずれかの状態になればよい。

  1.  S 内の1を全て0にする。
  2. ある区間  [l, r ] \: (0 \leq l \leq r \lt N) 内の  S_i \: (l \leq i \leq r) を全て1に、それ以外の文字を全て0にする。

与えられた  S をそれぞれの状態にするために必要な最小操作回数(特に後者)を求めるために、以下のように定義される累積和テーブルを用意する。

  •  c_i := 半開区間  [0, i) 内の  S_j \: (0 \leq j \leq i) に含まれる1の個数

これを用いると、上述の状態にするための最小操作回数は次のように計算される。

  1.  c_N
  2.  (2 c_l - l) - 2 c_{r+1} + c_N + r + 1
    • 区間  [0, l-1]0にする :  c_l
    • 区間  [l, r]1にする :  (r-l+1) - (c_{r+1} - c_l)
    • 区間  [r+1, N]0にする :  c_N - c_{r+1}
    • これらの和を取り、整理する。

したがって、あとは  c_N (1番目の状態)を初期値として、  l, r を動かしながら最小値を求めていけばよいことになる。

しかし、愚直に  l, r を動かしていたのでは計算量が  O(N^2) となって TLE してしまう。


ここで、  r を固定したとして、2番目の状態における  2 c_l - l について注目してみる。この部分を f(l) とすれば、累積minの要領でその時点の  r における  \underset{0 \leq l \leq r}{\min} f(l) を保持することができる。

具体的には、  m = \underset{0 \leq l \leq r}{\min} f(l) として、

  •  r = 0 \Rightarrow m = f(0)
  •  r = 1 \Rightarrow m = \min(m, f(1))  \cdots

とすればよい。


こうすることで、  r = 0, 1, \dots, N-1 まで増やしていくだけで操作回数の最小値を求めることができる。計算量は  O(N) だ。

状態DP(耳DP)を用いる

考察1では、最終的な  S の状態(目標)を2通りに分けたが、よく考えるとこれは1通りで表せる。

  •  0 個以上の0」と「 0 個以上の1」と「 0 個以上の0」をこの順に連結したもの。

この形には、前から順に3つの段階が存在する。

 S_i の各文字について、「その文字をどの段階に入れるか」を考えることで、以下のような DP テーブルを考えることができる。D問題の「D」はDPの「D」!

  •  \mathrm{dp}_{i, j} :=  S_i までを見て目標を達成する際、  S_i を段階  j に入れるとするときの flip すべき文字数の最小値

初期値は  \mathrm{dp}_{0, 0} = 0 、それ以外の場所が  \infty で、遷移は以下のようになる  (i = 1, 2, \dots N-1)。ただし、

 
f(c, x)
= \left\{ \begin{array}{ll}
0 & (c = x) \\
1 & (c \neq x)
\end{array} \right.

であるとする。

  •  \mathrm{dp}_{i+1, 0} \leftarrow \min(\mathrm{dp}_{i+1, 0}, \mathrm{dp}_{i, 0}) + f(S_i, 0)
  •  \mathrm{dp}_{i+1, 1} \leftarrow \min(\mathrm{dp}_{i+1, 1}, \mathrm{dp}_{i, 0}) + f(S_i, 1)
  •  \mathrm{dp}_{i+1, 1} \leftarrow \min(\mathrm{dp}_{i+1, 1}, \mathrm{dp}_{i, 1}) + f(S_i, 1)
  •  \mathrm{dp}_{i+1, 2} \leftarrow \min(\mathrm{dp}_{i+1, 2}, \mathrm{dp}_{i, 1}) + f(S_i, 0)
  •  \mathrm{dp}_{i+1, 2} \leftarrow \min(\mathrm{dp}_{i+1, 2}, \mathrm{dp}_{i, 2}) + f(S_i, 0)


具体的な遷移は下図のようになる。

サンプルケース1のテストケース1


最終的な答えは  \min(\mathrm{dp}_{N, 0}, \mathrm{dp}_{N, 1}, \mathrm{dp}_{N, 2}) である。

計算量は  O(N) となる。

実装例

考察1

累積和テーブルはpsとしている。

#include <bits/stdc++.h>
using namespace std;

#define rep(i, start, end) for (auto i = (start); (i) < (end); (i)++)

template <typename T>
inline bool chmin(T &a, T b) { return ((a > b) ? (a = b, true) : (false)); }

// ======================================== //

int solve()
{
    int N;
    string S;
    cin >> N >> S;

    vector<int> ps(N + 1, 0);
    rep(i, 0, N) ps[i + 1] = ps[i] + (S[i] - '0');

    int res = ps[N];
    int l_part = 2 * ps[0] - 0;
    rep(r, 0, N)
    {
        chmin(l_part, 2 * ps[r] - r);

        int r_part = -2 * ps[r + 1] + ps[N] + r + 1;
        chmin(res, l_part + r_part);
    }

    return res;
}

int main()
{
    int T;
    cin >> T;

    while (T--)
    {
        cout << solve() << endl;
    }

    return 0;
}

atcoder.jp

考察2

#include <bits/stdc++.h>
using namespace std;

#define rep(i, start, end) for (auto i = (start); (i) < (end); (i)++)

constexpr int INF = 1e+9;

template <typename T>
inline bool chmin(T &a, T b) { return ((a > b) ? (a = b, true) : (false)); }

// ======================================== //

int solve()
{
    int N;
    string S;
    cin >> N >> S;

    vector<vector<int>> dp(N + 1, vector<int>(3, INF));
    dp[0][0] = 0;

    rep(i, 0, N)
    {
        if (S[i] == '0')
        {
            chmin(dp[i + 1][0], dp[i][0] + 0);
            chmin(dp[i + 1][1], dp[i][0] + 1);
            chmin(dp[i + 1][1], dp[i][1] + 1);
            chmin(dp[i + 1][2], dp[i][1] + 0);
            chmin(dp[i + 1][2], dp[i][2] + 0);
        }
        else
        {
            chmin(dp[i + 1][0], dp[i][0] + 1);
            chmin(dp[i + 1][1], dp[i][0] + 0);
            chmin(dp[i + 1][1], dp[i][1] + 0);
            chmin(dp[i + 1][2], dp[i][1] + 1);
            chmin(dp[i + 1][2], dp[i][2] + 1);
        }
    }

    return min({dp[N][0], dp[N][1], dp[N][2]});
}

int main()
{
    int T;
    cin >> T;

    while (T--)
    {
        cout << solve() << endl;
    }

    return 0;
}

atcoder.jp

実装時間: 50分

コメント

考察2のように、「操作の何段階目かという情報を状態に持つDP」のことを状態DP / 耳DPというらしい。

www.mathenachia.blog

DP解法の方が考えることは少なそう。