C++
 Computer >> コンピューター >  >> プログラミング >> C++

C++で1だけで構成される部分行列の個数を数えるプログラム

0と1のみから構成される行列が与えられたとき、その中に存在する「すべての要素が1である部分行列」の個数を求める問題を考えてみましょう。求めた個数を出力として表示します。

入力例

例えば、次のような行列が入力として与えられたとします。

0010
0100
0101
1101

この場合、すべての要素が1である部分行列は全部で12個存在するため、出力は 12 となります。

解法のアプローチ

この問題を効率的に解くには、二次元累積和(2Dプレフィックスサム)二分探索 を組み合わせます。手順は以下の通りです。

  • n を行列の行数、m を行列の列数とします。
  • (n+1) × (m+1) のサイズを持つ配列 add(二次元累積和)を定義します。
  • 各マスに対して累積和を次のように計算します。
    • add[i+1][j+1] += matrix[i][j]
    • add[i+1][j+1] += add[i][j+1]
    • add[i+1][j+1] += add[i+1][j]
    • add[i+1][j+1] -= add[i][j](重複分を引く)
  • 答えを格納する変数 res を 0 で初期化します。
  • 各行 i、各列 j について以下を繰り返します。
    • matrix[i][j] が 0 であれば、このセルを左上とする部分行列は存在しないため、次の反復へスキップします。
    • 部分行列の高さ k を 1 から (n − i) まで変化させながら、二分探索で「すべてが1となる最大の幅」を求めます。
      • p := 0、q := m − j として初期化します。
      • p ≤ q の間、x := (p + q) / 2 とし、a := k * x(領域の面積)を計算します。
      • cur := add[i+k][j+x] − add[i][j+x] − add[i+k][j] + add[i][j] により、対象領域の合計値を O(1) で取得します。
      • cur == a であれば、その領域はすべて1なので、r := x として探索範囲を右側に広げます(p := x + 1)。そうでなければ q := x − 1 として範囲を狭めます。
    • r == 0 の場合は、これ以上幅を広げられないため内側のループを抜けます。
    • res := res + r として個数を加算していきます。
  • 最後に res を返します。

計算量

累積和により任意の矩形領域の合計が定数時間で求められるため、各セル・各高さについて二分探索 O(log m) を行う形になり、素朴な全探索よりも大幅に高速化できます。

C++での実装例

理解を深めるために、実際の実装を見てみましょう。

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

int solve(vector<vector<int>>& matrix) {
    int n = matrix.size();
    int m = matrix[0].size();
    int add[n + 1][m + 1];
    memset(add, 0, sizeof(add));

    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++) {
            add[i + 1][j + 1] += matrix[i][j];
            add[i + 1][j + 1] += add[i][j + 1];
            add[i + 1][j + 1] += add[i + 1][j];
            add[i + 1][j + 1] -= add[i][j];
        }
    }
    int res = 0;
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++) {
            if (!matrix[i][j])
                continue;
            for (int k = 1; k <= (n - i); k++) {
                int p = 0,
                    q = m - j;
                int r;
                while (p <= q) {
                    int x = (p + q) / 2;
                    int a = k * x;
                    int cur = add[i + k][j + x] - add[i][j + x] - add[i + k][j] + add[i][j];
                    if (cur == a) {
                        r = x;
                        p = x + 1;
                    } else
                        q = x - 1;
                }
                if (r == 0)
                    break;
                res += r;
            }
        }
    }
    return res;
}
int main() {
    vector<vector<int>> mat = {{0, 0, 1, 0}, {0, 1, 0, 0}, {0, 1, 0, 1}, {1, 1, 0, 1}};
    cout<< solve(mat) <<endl;
    return 0;
}

入力

{{0, 0, 1, 0}, {0, 1, 0, 0}, {0, 1, 0, 1}, {1, 1, 0, 1}}

出力

12

  1. グリッド内で照らされているセルの数を求めるC++プログラム

    問題の概要 ここでは、縦 h × 横 w のサイズを持つグリッドが与えられたとき、光で照らされているセルの数を求めるC++プログラムを紹介します。グリッドのセルには「電球」または「障害物」が置かれています。電球のあるセルは、そのセル自身と上下左右のセルを照らし、光は障害物に遮られない限りまっすぐ伝わっていきます。一方、障害物のあるセルは照らされることがなく、電球の光を遮って他のセルへ光が届かないようにします。電球の位置を配列 bulb、障害物の位置を配列 obstacles として受け取り、グリッド全体で照らされているセルの合計数を求めます。 たとえば、入力が h = 4、w = 4、bulb

  2. グリッド上に単一のパスを作るためにブロックすべきセル数を求めるC++プログラム

    問題の概要縦 h × 横 w のサイズを持つグリッドが与えられているとします。ロボットはセル (0, 0) の位置からスタートし、(h - 1, w - 1) の位置へ移動する必要があります。グリッドのセルには「ブロックされているセル」と「ブロックされていないセル」の2種類があり、ロボットはブロックされていないセルのみを通過できます。移動は上下左右の4方向が可能です。ロボットはあるセルから隣接するセルへ任意の方向に移動できるため、スタートからゴールまで複数の経路が存在する可能性があります。本問題では、(0, 0) から (h - 1, w - 1) までの経路を1本だけ残し、その経路に含まれな