Official

E - グリッドの塗りつぶし / Grid Filling Editorial by admin

GPT 5.4 High

概要

行集合 \(R\) を固定すると、各列 \(j\) は「選ぶと何点増えるか」が独立に決まります。
この性質を使って、\(2^{H+W}\) 通りを全探索する代わりに、片方(小さい方の次元)の部分集合だけを全探索して最大値を求めます。

考察

まず、行と列を両方そのまま全探索すると、選び方は

\(2^H \times 2^W = 2^{H+W}\)

通りあります。
\(H, W \le 19\) なので、最大で \(2^{38}\) 通りとなり、これは到底間に合いません。


1. 行集合 \(R\) を固定して考える

\(R\) を固定したとき、塗られる値の合計 \(S\) は包除原理より

\[ S = \sum_{i \in R} \sum_{j=1}^{W} A_{i,j} + \sum_{j \in C} \sum_{i=1}^{H} A_{i,j} - \sum_{i \in R} \sum_{j \in C} A_{i,j} \]

です。これを整理すると、

\[ S = \sum_{i \in R} \sum_{j=1}^{W} A_{i,j} + \sum_{j \in C} \left( \sum_{i=1}^{H} A_{i,j} - \sum_{i \in R} A_{i,j} \right) \]

となります。

ここで、列 \(j\) を選んだときの「追加得点」を

\[ b_j = \sum_{i \notin R} A_{i,j} \]

とおくと、

\[ S = \left(\text{選んだ行の総和}\right) + \sum_{j \in C} b_j \]

と書けます。


2. 固定した \(R\) に対する最適な \(C\)

上の式では、列ごとの寄与が完全に独立です。
したがって各列 \(j\) について、

  • \(b_j > 0\) なら選ぶ
  • \(b_j \le 0\) なら選ばない

のが最適です。

よって、固定した \(R\) に対する最大値は

\[ f(R) = \sum_{i \in R} \sum_{j=1}^{W} A_{i,j} + \sum_{j=1}^{W} \max(0, b_j) \]

になります。

つまり、問題は

  • 行集合 \(R\) を全探索する
  • そのたびに各列の \(b_j\) を求める

だけで解けます。


3. ここで素朴にやると少し重い

\(R\) ごとに毎回 \(b_j = \sum_{i \notin R} A_{i,j}\) を最初から計算すると、

  • 部分集合の数:\(2^H\)
  • 各部分集合で全マスを見る:\(O(HW)\)

なので、全体で

\[ O(2^H \cdot H \cdot W) \]

になります。

制約内でも理論上はギリギリですが、Python では少し重めです。
そこで、「隣り合う部分集合の差が 1 行だけ」になるように列挙すると、更新を高速化できます。


4. Gray code を使うと 1 行だけ増減する

通常の部分集合列挙では、次の集合に移ると複数の行が同時に変わることがあります。
しかし Gray code 順に列挙すると、連続する 2 つの集合で変わるビットはちょうど 1 個です。

つまり、

  • 1 行追加した
  • または 1 行削除した

だけが毎回起こります。

すると、各列の \(b_j\) もその 1 行分だけ更新すればよくなります。


5. 列側の情報をどう持つか

各列について

\[ \text{resid}[j] = \sum_{i \notin R} A_{i,j} \]

を持っておきます。
これは「今の \(R\) に対して、列 \(j\) を選んだときに増える得点」です。

  • 最初は \(R = \emptyset\) なので
    \(\text{resid}[j] = \sum_i A_{i,j}\)

  • \(r\) を新たに \(R\) に入れると、その行はもう「列を選んだときの追加分」ではなくなるので

\[ \text{resid}[j] \mathrel{-}= A_{r,j} \]

  • 逆に行 \(r\)\(R\) から外すと

\[ \text{resid}[j] \mathrel{+}= A_{r,j} \]

です。

また、固定した \(R\) に対する列の最適値は

\[ \sum_j \max(0, \text{resid}[j]) \]

なので、これも毎回再計算するのではなく、
各列の値が変わるたびに「正の部分の総和」だけ更新すればよいです。


6. 小さい方の次元を全探索する

この問題は行と列が対称です。
したがって、もし \(H > W\) ならグリッドを転置して、常に

\[ H \le W \]

としておけば、列挙する部分集合数を \(2^{\min(H, W)}\) に抑えられます。

これにより計算量は

\[ O(2^{\min(H,W)} \cdot \max(H,W)) \]

になります。

アルゴリズム

  1. \(H > W\) ならグリッドを転置して、常に \(H \le W\) にする。
  2. 各行の総和 row_total[i]、各列の総和 total_col[j] を求める。
  3. 初期状態を \(R = \emptyset\) とする。
    • row_sum = 0
    • resid = total_col
    • pos_sum = \sum_j \max(0, resid[j])
    • このときの答え候補は row_sum + pos_sum
  4. 行集合 \(R\) を Gray code 順に列挙する。
  5. 1 つ前の集合との差分から、「どの行が追加/削除されたか」を求める。
  6. その 1 行に対して
    • row_sum を更新
    • 全列の resid[j] を更新
    • pos_sum も「古い値が正だったか」「新しい値が正か」を見て更新
  7. 各状態で row_sum + pos_sum を答え候補として最大値を取る。
  8. 最後に最大値を出力する。

計算量

  • 時間計算量: \(O(2^{\min(H,W)} \cdot \max(H,W))\)
  • 空間計算量: \(O(HW)\)

実装のポイント

  • 転置して小さい方を全探索するのが重要です。

  • Gray code は g = k ^ (k >> 1) で作れます。

  • 連続する Gray code の差分 diff = cur ^ g には、変化した 1 ビットだけが立ちます。 その位置を bit_length() - 1 で取り出しています。

  • pos_sum は毎回全列を走査して計算し直すのではなく、更新された列ごとに

    • 変更前に正なら引く
    • 変更後に正なら足す とすることで高速化しています。
  • 値の合計は大きくなるので、C++ などで書く場合は long long が必要です。

    ソースコード

import sys

data = list(map(int, sys.stdin.buffer.read().split()))
it = iter(data)
H = next(it)
W = next(it)
A = [[next(it) for _ in range(W)] for _ in range(H)]

if H > W:
    A = [list(row) for row in zip(*A)]
    H, W = W, H

row_total = [sum(row) for row in A]
total_col = [0] * W
for i in range(H):
    row = A[i]
    for j, v in enumerate(row):
        total_col[j] += v

resid = total_col[:]
pos_sum = sum(x for x in resid if x > 0)
row_sum = 0
ans = pos_sum
cur = 0

for k in range(1, 1 << H):
    g = k ^ (k >> 1)
    diff = cur ^ g
    bit = diff.bit_length() - 1
    row = A[bit]

    if (g >> bit) & 1:
        row_sum += row_total[bit]
        for j, v in enumerate(row):
            old = resid[j]
            if old > 0:
                pos_sum -= old
            new = old - v
            resid[j] = new
            if new > 0:
                pos_sum += new
    else:
        row_sum -= row_total[bit]
        for j, v in enumerate(row):
            old = resid[j]
            if old > 0:
                pos_sum -= old
            new = old + v
            resid[j] = new
            if new > 0:
                pos_sum += new

    total = row_sum + pos_sum
    if total > ans:
        ans = total
    cur = g

print(ans)

この解説は gpt-5.4-high によって生成されました。

posted:
last update: