Editorial for 矩陣轉換 (APCS 2016-03 中級)


簡潔題意

矩陣有兩種操作:翻轉=第一列與最後一列交換、第二列與倒數第二列交換……(上下顛倒);旋轉=順時針轉 \(90^\circ\)。矩陣 \(A\) 依序做了 \(M\) 個操作變成 \(B\)。給你 \(B\)(\(R\) 列 \(C\) 行)和這 \(M\) 個操作(\(0\) 旋轉、\(1\) 翻轉,照施作順序列出),請還原 \(A\):先輸出 \(A\) 的列數和行數,再輸出內容,每行最後一個數字後沒有空白(\(1 \le R, C, M \le 10\))。

依正確通過的測試資料筆數給分,其中:

  • 子題組 1(\(30\) 分):每個操作都是翻轉。
  • 子題組 2(\(70\) 分):無額外限制。

先拿下子題組 1(30 分):只有翻轉,翻回去就是了

翻轉做兩次等於沒做——所以「翻轉的反操作」還是翻轉。只有翻轉的時候,\(B\) 再翻 \(M\) 次就是 \(A\)(\(M\) 是偶數等於沒動、奇數等於翻一次;\(M \le 10\),照翻 \(M\) 次就好,不必先算奇偶)。

翻轉怎麼寫:第 \(i\) 列和倒數第 \(i\) 列(第 \(R - 1 - i\) 列)整列交換,i 只要跑到 \(R / 2\) 之前——再往下跑會把交換過的換回來。用二維陣列裝矩陣(上冊 6.9),交換用 swap(上冊 7.6,住在 <algorithm>)。輸出的格式照題目:數字之間一個空白、行末沒有空白——if (j > 0) cout << ' '; 就是「除了每行第一個數字,前面都先印一個空白」。

#include <iostream>
#include <algorithm>
using namespace std;

const int MAX_N = 10;

int main() {
    int R, C, M;
    cin >> R >> C >> M;
    int a[MAX_N][MAX_N];
    for (int i = 0; i < R; i++)
        for (int j = 0; j < C; j++) cin >> a[i][j];

    for (int k = 0; k < M; k++) {
        int op;
        cin >> op;                 // 子題組 1 保證每個操作都是翻轉(1)
        for (int i = 0; i < R / 2; i++) {            // 第 i 列和倒數第 i 列整列交換
            for (int j = 0; j < C; j++) {
                swap(a[i][j], a[R - 1 - i][j]);
            }
        }
    }

    cout << R << ' ' << C << '\n';
    for (int i = 0; i < R; i++) {
        for (int j = 0; j < C; j++) {
            if (j > 0) cout << ' ';
            cout << a[i][j];
        }
        cout << '\n';
    }
    return 0;
}

這份程式交上去,兩個範例和子題組 2 都會 WA(範例都有旋轉)——這是正常的,逐筆給分之下子題組 1 的 \(30\) 分穩穩到手。想自己驗,用下面自測表的第 3 列。

從 30 分到 100 分:從最後一個操作倒著做反操作

題目給的是做完之後的 \(B\),要回到 \(A\) 就得把每個操作倒著解開——先穿襪子再穿鞋,脫的時候要先脫鞋再脫襪子。所以迴圈從第 \(M\) 個操作往回跑到第 \(1\) 個,每一個都做它的反操作

  • 翻轉的反操作還是翻轉:reverse(matrix.begin(), matrix.end()) 把整個 vector 的列順序顛倒,就是上下翻轉。
  • 順時針旋轉的反操作是逆時針旋轉13.9 的表:逆時針轉 \(90^\circ\) 後,新圖的 \((i, j)\) 來自舊圖的 \((j,\ cols - 1 - i)\),而且大小從 \(rows \times cols\) 變成 \(cols \times rows\)——所以每次都要新開一張表 restored、搬過去、再 swap 回來(10.6),不能在原地轉。

把矩陣裝進巢狀 vector13.1),using Matrix = vector<vector<int>>; 幫這個長型態取個短名字(12.7)。操作要先全部讀進 vector 再倒著跑——邊讀邊做是順著做,不是倒著做。

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

using Matrix = vector<vector<int>>;

int main() {
    int rows, cols, operationCount;
    cin >> rows >> cols >> operationCount;

    Matrix matrix(rows, vector<int>(cols));
    for (int i = 0; i < rows; i++)
        for (int j = 0; j < cols; j++) cin >> matrix[i][j];

    vector<int> operations(operationCount);
    for (int k = 0; k < operationCount; k++) cin >> operations[k];

    for (int k = operationCount - 1; k >= 0; k--) {          // 從最後一個操作倒著還原
        if (operations[k] == 1) {
            reverse(matrix.begin(), matrix.end());           // 翻轉:整排上下對調
        } else {
            rows = (int)matrix.size();
            cols = (int)matrix[0].size();
            Matrix restored(cols, vector<int>(rows));        // 逆時針旋轉 90 度:大小變成 cols x rows
            for (int i = 0; i < cols; i++)
                for (int j = 0; j < rows; j++)
                    restored[i][j] = matrix[j][cols - 1 - i];
            matrix.swap(restored);
        }
    }

    rows = (int)matrix.size();
    cols = (int)matrix[0].size();
    cout << rows << ' ' << cols << '\n';
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            if (j) cout << ' ';
            cout << matrix[i][j];
        }
        cout << '\n';
    }
    return 0;
}

輸出前的 rowscols 要重新從 matrix 拿——轉了奇數次之後長寬是對調的。運算量小到不用算:矩陣最多 \(100\) 格、最多 \(10\) 個操作。

逆時針還是順時針,別在腦中硬記

還原用的是逆時針 restored[i][j] = matrix[j][cols - 1 - i];如果題目要你正著做順時針,是 B[j][R - 1 - i] = A[i][j]。兩條長得像、意思相反,最保險的做法是拿一個 \(2 \times 3\) 的小矩陣代進去,看左上角的元素跑去哪裡——順時針轉完它應該在右上角。

測過再交:範例 1 有兩次旋轉,轉錯方向也會對

範例 1 的操作是「翻、轉、轉」——兩次旋轉加起來是 \(180^\circ\),順時針逆時針都一樣,所以旋轉方向寫反的程式範例 1 照樣全對;它也是「翻轉和兩次旋轉」這種剛好可以交換順序的組合,順著做不倒著做也會對。範例 2 能抓到這兩種錯,但範例沒有「只有翻轉」和「正方形」的情況:

輸入 正確輸出 這一筆在測什麼
2 2 11 23 40 2 22 41 3 只有一次旋轉、而且是正方形(長寬不會提醒你轉錯):反操作寫成順時針的程式印 3 14 2
1 3 21 2 30 0 1 33 2 1 一列轉兩次=整個倒過來,中間經過 \(3 \times 1\):大小變了又變回來
2 3 31 2 34 5 61 1 1 2 34 5 61 2 3 只有翻轉、奇數次:子題組 1 版就能驗。把翻轉做成左右翻的程式印 3 2 16 5 4
1 1 350 1 0 1 15 \(1 \times 1\) 怎麼轉都是自己

四筆都親眼看過正確,這題就穩了。

常犯錯誤
  • 順著操作順序做(從第 \(1\) 個做到第 \(M\) 個):範例 1 剛好過;範例 2 印 3 2 13 1 2(該印 2 1 31 2 3)。
  • 旋轉的反操作寫成順時針:範例 1 的兩次旋轉抵銷、照樣過;範例 2 印 3 2 13 1 2;自測表第 1 列印 3 14 2
  • 輸出的列數行數用一開始讀進來的 \(R\)、\(C\):轉了奇數次長寬會對調——範例 1 印出 2 3 開頭、後面跟著一堆 \(0\)(拿錯的大小去讀一張 \(3 \times 2\) 的表);範例 2 直接當掉。
  • 把旋轉寫成轉置restored[i][j] = matrix[j][i]):範例 1 印 1 23 11 1
  • 翻轉做成左右翻:範例 1 印 1 23 11 1;範例 2 剛好過;自測表第 3 列印 3 2 16 5 4
  • 行末多印一個空白:本站的評測其實對行末空白寬容,但題目明寫「最後一個數字後並無空白」——用 if (j) cout << ' '; 的寫法做零風險。