13.8 平移與子矩陣

平移就是每一格都加上同一個位移:把小圖 A 疊到大圖 B 上、左上角對齊 (top, left),那麼 A(i, j) 對到的是 B(top + i,\ left + j)。要試遍所有位置,就是枚舉左上角——這也是「在大圖裡找子矩陣」的標準寫法。

例題:矩陣總和(APCS 2020 年 1 月中級)

題目:兩個同大小矩陣的「矩陣距離」=對應位置數值不同的元素個數。給 s \times t 的小矩陣 An \times m 的大矩陣 B1 \le s \le n \le 101 \le t \le m \le 100、元素 0 \sim 9),B 裡每個 s \times t 的子矩陣若與 A 的距離不超過 r1 \le r \le 100)就符合條件。輸出符合條件的子矩陣個數;再對它們比較「子矩陣元素總和」與「A 元素總和」,輸出絕對差的最小值,沒有符合的輸出 -1

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

int main() {
    int s, t, n, m, r;
    cin >> s >> t >> n >> m >> r;
    vector<vector<int>> A(s, vector<int>(t));
    vector<vector<int>> B(n, vector<int>(m));
    int sumA = 0;
    for (int i = 0; i < s; i++) {
        for (int j = 0; j < t; j++) {
            cin >> A[i][j];
            sumA += A[i][j];
        }
    }
    for (int i = 0; i < n; i++)
        for (int j = 0; j < m; j++) cin >> B[i][j];

    int eligibleCount = 0;
    int minimumSumDifference = INT_MAX;
    for (int top = 0; top + s <= n; top++) {           // 左上角的橫排:要放得下 s 列
        for (int left = 0; left + t <= m; left++) {    // 左上角的直排:要放得下 t 行
            int distance = 0;
            int sumB = 0;
            for (int i = 0; i < s; i++) {              // A 的 (i, j) 對應 B 的 (top+i, left+j)
                for (int j = 0; j < t; j++) {
                    const int value = B[top + i][left + j];
                    sumB += value;
                    if (value != A[i][j]) distance++;
                }
            }
            if (distance <= r) {
                eligibleCount++;
                minimumSumDifference = min(minimumSumDifference, abs(sumA - sumB));
            }
        }
    }

    cout << eligibleCount << '\n';
    if (eligibleCount == 0)
        cout << -1 << '\n';
    else
        cout << minimumSumDifference << '\n';
    return 0;
}

迴圈上界寫 top + s <= nleft + t <= m,子矩陣就永遠不會伸出 B 外,連 inside 都不用;四層迴圈的總量是 10 \times 100 \times 10 \times 100 = 10^6,一秒內輕鬆跑完。

延伸:形狀打表與碰撞

平移的另一個長相是「把一個形狀放進格子裡、往某個方向推」。形狀不規則怎麼辦?把形狀打成表——列出它每一個方格相對於某個基準點的位移,之後「放在某處會不會出界或撞到東西」就是把表跑一遍逐格檢查。

運貨站(APCS 2022 年 10 月中級):R \times C 的倉庫(R \le 30C \le 50)依序收到 n 件貨物(n \le 200),形狀是 A~E 五種之一、方向固定,最上方位於第 y_i 列。貨物先試「剛好完全進入倉庫」的最右位置——放不下就丟棄;放得下就一格一格往左推,直到再往左會出界或碰到別的貨物為止。輸出最後倉庫的空格數與被丟棄的貨物數。

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

// 五種形狀打成表:每個方格相對於「最上列、最右行」的位移 (dr, dc)
vector<pair<int, int>> shape(char type) {
    if (type == 'A') return {{0, 0}, {1, 0}, {2, 0}, {3, 0}};
    if (type == 'B') return {{0, -2}, {0, -1}, {0, 0}};
    if (type == 'C') return {{0, -1}, {0, 0}, {1, -1}, {1, 0}};
    if (type == 'D') return {{0, 0}, {1, -2}, {1, -1}, {1, 0}};
    return {{0, 0}, {1, -1}, {1, 0}, {2, -1}, {2, 0}};        // E
}

int R, C;
vector<vector<bool>> occupied;

// 把形狀的最右行放在第 right 行、最上列放在第 top 列,放得下嗎?
bool canPlace(int top, int right, const vector<pair<int, int>>& cells) {
    for (auto [dr, dc] : cells) {
        int r = top + dr, c = right + dc;
        if (r < 0 || r >= R || c < 0 || c >= C) return false;   // 出界
        if (occupied[r][c]) return false;                       // 撞到別的貨物
    }
    return true;
}

int main() {
    int n;
    cin >> R >> C >> n;
    occupied.assign(R, vector<bool>(C, false));
    int used = 0, discarded = 0;
    while (n--) {
        char type;
        int top;
        cin >> type >> top;
        vector<pair<int, int>> cells = shape(type);
        int right = C - 1;                          // 先試「剛好完全進入倉庫」的最右位置
        if (!canPlace(top, right, cells)) { discarded++; continue; }
        while (canPlace(top, right - 1, cells)) right--;   // 還能往左就繼續推
        for (auto [dr, dc] : cells) occupied[top + dr][right + dc] = true;
        used += cells.size();
    }
    cout << R * C - used << ' ' << discarded << '\n';
    return 0;
}

shape() 回傳的就是那張表:把題目附圖裡每個方格的位置照抄(dr, dc)。之後平移形狀=改變基準點 (top, right),檢查碰撞=把表跑一遍——和 13.3 的方向表是同一種思路:會重複用到的一組座標,就打成表