密码破解者

GitHub跳转原题关系图返回列表

把所有加密操作按相反顺序依次撤销,其中分别实现栅栏密码、维吉尼亚密码和 QWE 键盘码的逆变换即可还原原文。

OJ: luogu

题目 ID: P2636

难度:普及/提高-

标签:字符串模拟推导

日期: 2026-06-20 23:48

题意

题目给出一个最终密文,以及它曾经经历过的 N 层加密方式。

加密方式只有三种:

  1. 栅栏密码
  2. 维吉尼亚密码
  3. QWE 键盘码

输入中的操作顺序就是真实加密顺序,要求输出最后解密得到的原文。

思路

先看一个可以直接验证想法的朴素解:

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

struct Operation {
    int type;
    int rail;
    string key;
};

int n;
string s;
vector<Operation> ops;

char qwe_inv_upper[26];

void build_qwe_inverse() {
    string keyboard = "QWERTYUIOPASDFGHJKLZXCVBNM";
    for (int i = 0; i < 26; i++) {
        qwe_inv_upper[keyboard[i] - 'A'] = char('A' + i);
    }
}

int letter_value(char ch) {
    if ('A' <= ch && ch <= 'Z') {
        return ch - 'A';
    }
    if ('a' <= ch && ch <= 'z') {
        return ch - 'a';
    }
    return 0;
}

char decode_vigenere_char(char ch, int shift) {
    if ('A' <= ch && ch <= 'Z') {
        int x = ch - 'A';
        x = (x - shift + 26) % 26;
        return char('A' + x);
    }
    if ('a' <= ch && ch <= 'z') {
        int x = ch - 'a';
        x = (x - shift + 26) % 26;
        return char('a' + x);
    }
    return ch;
}

char decode_qwe_char(char ch) {
    if ('A' <= ch && ch <= 'Z') {
        return qwe_inv_upper[ch - 'A'];
    }
    if ('a' <= ch && ch <= 'z') {
        char up = char(ch - 'a' + 'A');
        char plain = qwe_inv_upper[up - 'A'];
        return char(plain - 'A' + 'a');
    }
    return ch;
}

string decode_vigenere(const string &cipher, const string &key) {
    string plain = cipher;
    int key_len = (int) key.size();
    for (int i = 0; i < (int) plain.size(); i++) {
        int shift = letter_value(key[i % key_len]);
        plain[i] = decode_vigenere_char(plain[i], shift);
    }
    return plain;
}

string decode_qwe(const string &cipher) {
    string plain = cipher;
    for (int i = 0; i < (int) plain.size(); i++) {
        plain[i] = decode_qwe_char(plain[i]);
    }
    return plain;
}

// brute.cpp:更直观的栅栏密码逆变换。
// 直接把每一列的字符回填到每一组对应的位置,再顺序读出所有分组。
string decode_rail_bruteforce(const string &cipher, int width) {
    int len = (int) cipher.size();
    int groups = (len + width - 1) / width;

    vector<int> row_len(groups, 0);
    for (int i = 0; i < groups; i++) {
        row_len[i] = min(width, len - i * width);
    }

    vector<string> rows(groups);
    for (int i = 0; i < groups; i++) {
        rows[i] = string(row_len[i], '?');
    }

    int pos = 0;
    for (int col = 0; col < width; col++) {
        for (int row = 0; row < groups; row++) {
            if (col < row_len[row]) {
                rows[row][col] = cipher[pos];
                pos++;
            }
        }
    }

    string plain;
    plain.reserve(len);
    for (int row = 0; row < groups; row++) {
        plain += rows[row];
    }
    return plain;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    build_qwe_inverse();

    cin >> n;
    cin >> s;

    ops.resize(n);
    for (int i = 0; i < n; i++) {
        cin >> ops[i].type;
        ops[i].rail = 0;
        ops[i].key.clear();
        if (ops[i].type == 1) {
            cin >> ops[i].rail;
        } else if (ops[i].type == 2) {
            cin >> ops[i].key;
        }
    }

    for (int i = n - 1; i >= 0; i--) {
        if (ops[i].type == 1) {
            s = decode_rail_bruteforce(s, ops[i].rail);
        } else if (ops[i].type == 2) {
            s = decode_vigenere(s, ops[i].key);
        } else {
            s = decode_qwe(s);
        }
    }

    cout << s << '\n';
    return 0;
}

这题的主框架其实很直接:

  • 正向是按 1 -> 2 -> ... -> N 层加密
  • 逆向解密就必须按 N -> N-1 -> ... -> 1 的顺序逐层撤销

所以关键只剩三种密码的逆操作怎么写。

QWE 键盘码最简单,本质只是 26 个字母的一一映射。预处理一个“密文字母到原文字母”的逆表,逐字符替换即可。

维吉尼亚密码也不难。加密相当于把当前字母按密钥字母的值向后平移,解密时就向前平移同样的位数,并且密钥长度不足时循环使用。

真正稍微绕一点的是栅栏密码。

它的加密方式是:

  • 先把原文按长度 L 分组
  • 再按列依次把字符取出来

因此解密时要反过来:

  1. 先根据字符串长度和 L,算出每一列应该有多少字符
  2. 按这些长度把密文切成若干列
  3. 再按“每组从左到右”的顺序重新拼回原文

把这三个逆函数都写好后,从后往前套一遍就行。

代码

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

struct Operation {
    int type;     // 1=栅栏密码,2=维吉尼亚密码,3=QWE 键盘码
    int rail;     // 栅栏密码分组长度
    string key;   // 维吉尼亚密码密钥
};

int n;
string s;
vector<Operation> ops;

char qwe_inv_upper[26];

// 建立 QWE 键盘码的逆映射:密文字母 -> 原文字母。
void build_qwe_inverse() {
    string keyboard = "QWERTYUIOPASDFGHJKLZXCVBNM";
    for (int i = 0; i < 26; i++) {
        qwe_inv_upper[keyboard[i] - 'A'] = char('A' + i);
    }
}

int letter_value(char ch) {
    if ('A' <= ch && ch <= 'Z') {
        return ch - 'A';
    }
    if ('a' <= ch && ch <= 'z') {
        return ch - 'a';
    }
    return 0;
}

char decode_vigenere_char(char ch, int shift) {
    if ('A' <= ch && ch <= 'Z') {
        int x = ch - 'A';
        x = (x - shift + 26) % 26;
        return char('A' + x);
    }
    if ('a' <= ch && ch <= 'z') {
        int x = ch - 'a';
        x = (x - shift + 26) % 26;
        return char('a' + x);
    }
    return ch;
}

char decode_qwe_char(char ch) {
    if ('A' <= ch && ch <= 'Z') {
        return qwe_inv_upper[ch - 'A'];
    }
    if ('a' <= ch && ch <= 'z') {
        char up = char(ch - 'a' + 'A');
        char plain = qwe_inv_upper[up - 'A'];
        return char(plain - 'A' + 'a');
    }
    return ch;
}

// 逆向还原维吉尼亚密码。
string decode_vigenere(const string &cipher, const string &key) {
    string plain = cipher;
    int key_len = (int) key.size();
    for (int i = 0; i < (int) plain.size(); i++) {
        int shift = letter_value(key[i % key_len]);
        plain[i] = decode_vigenere_char(plain[i], shift);
    }
    return plain;
}

// 逆向还原 QWE 键盘码。
string decode_qwe(const string &cipher) {
    string plain = cipher;
    for (int i = 0; i < (int) plain.size(); i++) {
        plain[i] = decode_qwe_char(plain[i]);
    }
    return plain;
}

// 栅栏密码逆变换:
// 已知加密结果是“按列依次取出”,这里按每一列的长度把字符串切开,再按行拼回去。
string decode_rail(const string &cipher, int width) {
    int len = (int) cipher.size();
    int groups = (len + width - 1) / width;
    int rem = len % width;

    vector<int> column_len(width, groups);
    if (rem != 0) {
        for (int col = rem; col < width; col++) {
            column_len[col]--;
        }
    }

    vector<int> start(width, 0);
    int pos = 0;
    for (int col = 0; col < width; col++) {
        start[col] = pos;
        pos += column_len[col];
    }

    string plain;
    plain.reserve(len);

    for (int row = 0; row < groups; row++) {
        for (int col = 0; col < width; col++) {
            if (row < column_len[col]) {
                plain.push_back(cipher[start[col] + row]);
            }
        }
    }

    return plain;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    build_qwe_inverse();

    cin >> n;
    cin >> s;

    ops.resize(n);
    for (int i = 0; i < n; i++) {
        cin >> ops[i].type;
        ops[i].rail = 0;
        ops[i].key.clear();
        if (ops[i].type == 1) {
            cin >> ops[i].rail;
        } else if (ops[i].type == 2) {
            cin >> ops[i].key;
        }
    }

    // 输入给出的是正向加密顺序,所以解密时要倒着撤销。
    for (int i = n - 1; i >= 0; i--) {
        if (ops[i].type == 1) {
            s = decode_rail(s, ops[i].rail);
        } else if (ops[i].type == 2) {
            s = decode_vigenere(s, ops[i].key);
        } else {
            s = decode_qwe(s);
        }
    }

    cout << s << '\n';
    return 0;
}

复杂度

设字符串长度为 m,加密层数为 N

每次逆变换都只线性扫描字符串一次,所以总时间复杂度是:

O(Nm)O(N * m)

空间复杂度是:

O(m+N)O(m + N)

总结

这题没有难在复杂算法,而是难在“把题目描述准确翻译成逆操作”。

做法的核心就一句话:

  • 所有操作倒着做
  • 分别把三种密码的逆变换写正确