把所有加密操作按相反顺序依次撤销,其中分别实现栅栏密码、维吉尼亚密码和 QWE 键盘码的逆变换即可还原原文。
OJ: luogu
题目 ID: P2636
难度:普及/提高-
标签:字符串模拟推导
日期: 2026-06-20 23:48
题意
题目给出一个最终密文,以及它曾经经历过的 N 层加密方式。
加密方式只有三种:
- 栅栏密码
- 维吉尼亚密码
- 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分组 - 再按列依次把字符取出来
因此解密时要反过来:
- 先根据字符串长度和
L,算出每一列应该有多少字符 - 按这些长度把密文切成若干列
- 再按“每组从左到右”的顺序重新拼回原文
把这三个逆函数都写好后,从后往前套一遍就行。
代码
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。
每次逆变换都只线性扫描字符串一次,所以总时间复杂度是:
空间复杂度是:
总结
这题没有难在复杂算法,而是难在“把题目描述准确翻译成逆操作”。
做法的核心就一句话:
- 所有操作倒着做
- 分别把三种密码的逆变换写正确