Editorial for 造字程式 (APCS 2023-01 中級)


簡潔題意

長度 \(K\) 的小寫字串 \(S\),做 \(Q\) 次操作。每次操作給一個 \(1 \sim K\) 的排列 \(P\):舊字串第 \(i\) 個字元搬到新字串第 \(P_i\) 個位置,新字串就是下一次的舊字串。對前 \(R\) 個位置各輸出一行:第 \(j\) 行是第 \(1\) 次到第 \(Q\) 次操作後,新字串第 \(j\) 個位置的字元依序接起來(\(1 \le R \le K \le 20\)、\(1 \le Q \le 20\))。

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

  • 子題組 1(\(60\) 分):\(R = 1\)。
  • 子題組 2(\(40\) 分):無額外限制。

先拿下子題組 1(60 分):照排列搬字元,每次只記第一格

一次操作就是「搬家」:舊字串的第 \(i\) 格搬到新字串的第 \(P_i\) 格。兩件事要注意:

  1. 要開一條新字串,不能在原字串上直接搬。搬的時候目的地的舊字元可能還沒被搬走就被蓋掉了——跟 \(s\) 同長度、先填滿空白的 string next(K, ' ')10.7 的「\(n\) 個同一字元」初始化)就是那條新字串。
  2. 題目的位置從 \(1\) 數、C++ 的索引從 \(0\) 數(上冊 6.3):第 \(P_i\) 個位置是 next[p - 1]。排列的數字不用先存成陣列,讀一個搬一個就好。

搬完 s = next; 整條複製過去(10.6),新字串就變成下一次操作的舊字串。\(R = 1\) 時只要記第一格:每次操作完把 s[0] 接到答案 line 後面(10.8+=),\(Q\) 次做完印出來,剛好長度 \(Q\)。

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

int main() {
    int K, Q, R;
    string s;
    cin >> K >> Q >> R >> s;        // 子題組 1 保證 R = 1:只要記第 1 個位置

    string line = "";               // 每次操作後第 1 個位置的字元,依序接起來
    for (int q = 0; q < Q; q++) {
        string next(K, ' ');        // 新字串:先開好 K 格
        for (int i = 0; i < K; i++) {
            int p;
            cin >> p;
            next[p - 1] = s[i];     // 舊字串第 i 格(0-base)搬到新字串第 p 個(1-base)
        }
        s = next;                   // 新字串變成下一次操作的舊字串
        line += s[0];
    }
    cout << line << '\n';
    return 0;
}

拿範例 1 對一次:abcde 經過四次操作依序是 bacedacdebcdeabdbcea,第一格依序 bacd。這份程式交上去,範例 2 和子題組 2 會 WA(只印了一行)——這是正常的,逐筆給分之下子題組 1 的 \(60\) 分穩穩到手。

從 60 分到 100 分:每個位置各一條字串

題目要的輸出是「直的」:每次操作產生一條新字串(橫的),但第 \(j\) 行要的是第 \(j\) 個位置跨越 \(Q\) 次操作的字元。所以不能每次操作印一行,要先收集:開 \(R\) 條字串 rows[0] ~ rows[R-1]vector<string>10.7 的 string 陣列),每次操作完把 s[j] 接到 rows[j] 後面,最後一行一條印出來。子題組 1 的 line 就是 rows[0]

#include <iostream>
#include <string>
#include <vector>
using namespace std;

int main() {
    int K, Q, R;
    string s;
    cin >> K >> Q >> R >> s;

    vector<string> rows(R);         // rows[j]=第 j 個位置每次操作後的字元,依序接起來
    for (int q = 0; q < Q; q++) {
        string next(K, ' ');        // 新字串:先開好 K 格
        for (int i = 0; i < K; i++) {
            int p;
            cin >> p;
            next[p - 1] = s[i];     // 舊字串第 i 格(0-base)搬到新字串第 p 個(1-base)
        }
        s = next;                   // 新字串變成下一次操作的舊字串
        for (int j = 0; j < R; j++) rows[j] += s[j];
    }
    for (int j = 0; j < R; j++) cout << rows[j] << '\n';
    return 0;
}

運算量:\(Q \le 20\) 次操作、每次搬 \(K \le 20\) 格,小到不用算。

測過再交:範例 2 剛好 R = K

範例 1 只看第一格、範例 2 是 \(R = K = 4\)(每一格都要印)——「只印前 \(R\) 行、\(R\) 比 \(K\) 小」的二維情況兩個範例都沒有。補幾筆手算得動的:

輸入 正確輸出 這一筆在測什麼
3 2 2abc2 3 12 3 1 cbac \(R < K\) 的二維;同一個排列做兩次,第二次要從第一次的結果繼續(新字串沒存回去的程式印 ccaa;搬的方向反了的程式印 bcca
4 2 3abcd1 2 3 44 3 2 1 adbccb 恆等排列不動、再整串反過來;\(K = 4\) 但只印 \(R = 3\) 行
2 1 2ab2 1 ba \(Q = 1\):每行只有一個字元
1 3 1z111 zzz \(K = 1\):怎麼搬都是自己

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

常犯錯誤
  • 搬的方向反了(寫成 next[i] = s[p - 1]):題目是「舊的第 \(i\) 個搬到新的第 \(P_i\) 個」——範例 1 印 bdeb(該印 bacd)。
  • 在原字串上直接搬:還沒搬走的字元先被蓋掉——範例 1 印 aaca
  • 忘了 \(1\)-base 轉 \(0\)-basenext[p] = s[i]):第 \(K\) 個位置寫到字串外面、第 \(0\) 格永遠是空白——範例 1 印出一串空白。
  • 新字串沒存回 s:每次都拿原始字串在搬——範例 1 印 bbbb
  • 每次操作印一行(橫的):題目要每個位置一行(直的)——範例 1 印成四行 bacd
  • 只記最後一次操作的結果:範例 1 印 d