【AtCoder】ABC 442 F - Diagonal Separation 2

atcoder.jp favicon

実行時間制限: 2 sec / メモリ制限: 1024 MiB / Difficulty: 1295 / NoviSteps: 1D / 配点: 500 点

問題概要

N×NN \times N のグリッドがあり、グリッドの上から ii 行目、左から jj 列目のマスをマス (i,j)(i, j) と表す。

グリッドの各マスは白または黒に塗られていて、グリッドの情報は NN 個の文字列 S1,S2,,SNS_1, S_2, \ldots, S_N によって与えられ、Si,j=S_{i, j} = . のときマス (i,j)(i, j) は白く塗られており、Si,j=S_{i, j} = # のときマス (i,j)(i, j) は黒く塗られている。

あなたは、いくつかのマスの色を塗り替えて以下の条件をともに満たすようにしたい。

  • すべての行に対して以下の条件が成り立つ。
    • 0kN0 \leq k \leq N を満たす整数 kk が存在して、その行の左から kk 個のマスは白く塗られており、その他のマスは黒く塗られている。
  • すべての列に対して以下の条件が成り立つ。
    • 0kN0 \leq k \leq N を満たす整数 kk が存在して、その列の上から kk 個のマスは白く塗られており、その他のマスは黒く塗られている。

条件を満たすために塗り替える必要のあるマスの数として考えられる最小値を求めよ。

制約

  • 1N50001 \leq N \leq 5000
  • NN は整数
  • SiS_i., #からなる長さ NN の文字列

考察

最終的なグリッドの状態を「左上が白、右下が黒」という階段状の境界を持つ形に変形することが本問の目標である。

具体的には、行ごとの白のマス kik_i が単調非増加 (k1k2kN)(k_1 \geq k_2 \geq \cdots \geq k_N) となるように各行の塗り替えを行っていくことになる。

これは、上の行から順に白マスの数を決定していく動的計画法で解くことができる。

ナイーブなDP

まずは、DPテーブルを以下のように定義する :

  • dpi,k:=\mathrm{dp}_{i, k} := 上から ii 行目までを条件を満たすように塗り替え、かつ ii 行目の白マスの数を kk にしたときの、必要な最小のマスの塗り替え個数

初期値は k=0,1,,Nk = 0, 1, \cdots, N に対して dp0,k=0,dpi,k=(i1)\mathrm{dp}_{0, k} = 0, \quad \mathrm{dp}_{i, k} = \infty \: (i \geq 1) とする。

また、各行において、直前の行の白マスの数 jj が現在の行の白マスの数 kk 以上であれば条件を満たすことに注意すると、遷移は以下のようになる :

dpi,k=cost(i,k)+minkjNdpi1,j(1)\mathrm{dp}_{i, k} = \mathrm{cost}(i, k) + \underset{k \leq j \leq N}{\min} \mathrm{dp}_{i-1, j} \tag{1}

ただし、cost(i,k)\mathrm{cost}(i, k)ii 行目を左から kk 個のマスを白、その他のマスを黒にするために必要な最小のマスの塗り替え個数を表す。実装については後述する。

さて、このDPテーブルの状態数は N×(N+1)N \times (N+1) であり、式(1)(1) の第二項の計算に O(N)O(N) 時間かかるため、全体の計算量は少なくとも O(N3)O(N^3) となり、今回の制約下では TLE してしまう。

累積minによる高速化

よく考えると、式(1)(1) の第二項では重複した計算が行われている。


例 (クリックで開く)

例えば、N=5N = 5 のとき、ii 行目の dpi,3\mathrm{dp}_{i, 3} を計算する際には

min3j5dpi1,j=min(dpi1,3,dpi1,4,dpi1,5)\underset{3 \leq j \leq 5}{\min} \mathrm{dp}_{i-1, j} = \min(\mathrm{dp}_{i-1, 3}, \mathrm{dp}_{i-1, 4}, \mathrm{dp}_{i-1, 5})

を計算するが、同じく dpi,2\mathrm{dp}_{i, 2} を計算する際には

min2j5dpi1,j=min(dpi1,2,dpi1,3,dpi1,4,dpi1,5)\underset{2 \leq j \leq 5}{\min} \mathrm{dp}_{i-1, j} = \min(\mathrm{dp}_{i-1, 2}, \mathrm{dp}_{i-1, 3}, \mathrm{dp}_{i-1, 4}, \mathrm{dp}_{i-1, 5})

を計算する。

ここで、 min(dpi1,3,dpi1,4,dpi1,5)\min(\mathrm{dp}_{i-1, 3}, \mathrm{dp}_{i-1, 4}, \mathrm{dp}_{i-1, 5}) は両方の計算で共通しているため、重複して計算されてしまっている。 この部分の最小値が事前に分かっていれば、dpi,2\mathrm{dp}_{i, 2} の計算時にはその値と dpi1,2\mathrm{dp}_{i-1, 2} とを新たに比較すればよく、計算量を削減できるはずだ。


したがって、直前の行のDPテーブルに対して、あらかじめ後ろからの累積minを計算しておくことで、式 (1)(1) の第二項を O(1)O(1) 時間で計算できるようになる。

具体的には、累積minテーブルを

psi1,k=min(dpi1,k,psi1,k+1)(0kN)\mathrm{ps}_{i-1, k} = \min(\mathrm{dp}_{i-1, k}, \mathrm{ps}_{i-1, k+1}) \quad (0 \leq k \leq N)

のように定義し、DPの漸化式を以下のように書き換える :

dpi,k=cost(i,k)+psi1,k(2)\mathrm{dp}_{i, k} = \mathrm{cost}(i, k) + \mathrm{ps}_{i-1, k} \tag{2}

これにより、 cost(i,k)\mathrm{cost}(i, k) の適切な実装によりDP遷移は O(1)O(1) 時間となり、全体で O(N2)O(N^2) の計算量で解くことができるようになった。

cost(i,k)\mathrm{cost}(i, k) の計算

最後に、 cost(i,k)\mathrm{cost}(i, k) の計算方法について考える。

ii 行目を「左から kk 個を白、残りを黒」にするためには、

  1. 左側 kk 個のマスの中の黒マスを白に塗り替える。
  2. 右側 NkN-k 個のマスの中の白マスを黒に塗り替える。

とすればよく、このときに塗り替えるマスの個数の和が cost(i,k)\mathrm{cost}(i, k) となる。


グリッドの各行について、黒マスの個数の累積和を前計算しておけば、この値は O(1)O(1) で求められる。

具体的には、bi,xb_{i, x}ii 行目の左から xx 個のマスの中の黒マスの個数とすると、

  • 右側 NkN-k 個にある黒マスの数: bi,Nbi,kb_{i, N} - b_{i, k}
  • 右側 NkN-k 個にある白マスの数: (Nk)(bi,Nbi,k)(N-k) - (b_{i, N} - b_{i, k})

と表せるので、

cost(i,k)=bi,k+(Nk)(bi,Nbi,k) \mathrm{cost}(i, k) = b_{i, k} + (N-k) - (b_{i, N} - b_{i, k})

と計算することができる。

実装例

実装では、式 (2)(2) による遷移でDPテーブルを参照していないことを利用して、in-place にDPテーブルを更新していく形にしている。

CPP
1.#include <bits/stdc++.h>
2.using namespace std;
3.
4.#define all(x) (x).begin(), (x).end()
5.#define rep(i, start, end) for (auto i = (start); (i) < (end); (i)++)
6.#define rrep(i, start, end) for (auto i = (start); (i) >= (end); (i)--)
7.
8.constexpr int INF = 1e+9;
9.
10.// ======================================== //
11.
12.int main()
13.{
14. int N;
15. cin >> N;
16. vector<string> S(N);
17. rep(i, 0, N) cin >> S[i];
18.
19. vector<int> dp(N + 1, 0);
20. rep(i, 0, N)
21. {
22. vector<int> blacks(N + 1, 0);
23. rep(j, 0, N) blacks[j + 1] = blacks[j] + (S[i][j] == '#');
24.
25. auto calc_cost = [&](int k) -> int
26. {
27. int to_white = blacks[k];
28. int to_black = (N - blacks[N]) - (k - blacks[k]);
29. return to_white + to_black;
30. };
31.
32. vector<int> ps_min(N + 2, INF);
33. rrep(k, N, 0) ps_min[k] = min(ps_min[k + 1], dp[k]);
34.
35. rep(k, 0, N + 1) dp[k] = calc_cost(k) + ps_min[k];
36. }
37.
38. cout << *min_element(all(dp)) << endl;
39.
40. return 0;
41.}
Submission #72831725 - JPRS Programming Contest 2026#1 (AtCoder Beginner Contest 442)atcoder.jp favicon

実装時間: 40分

コメント

累積和を用いたDPの高速化、慣れていきたい。