C语言程序设计100例之(75):Vigenère 密码
例75 Vigenère 密码
问题描述
16 世纪法国外交家 Blaise de Vigenère 设计了一种多表密码加密算法 Vigenère 密码。Vigenère 密码的加密解密算法简单易用,且破译难度比较高,曾在美国南北战争中为南军所广泛使用。
在密码学中,我们称需要加密的信息为明文,用M表示;称加密后的信息为密文,用C表示;而密钥是一种参数,是将明文转换为密文或将密文转换为明文的算法中输入的数据,记为k。在Vigenère 密码中,密钥k 是一个字母串,k=k1,k2,…,kn。当明文 M=m1,m2,…,mn时,得到的密文 C=c1,c2,…,cn,其中ci =mi®ki,运算®的规则如下表所示:
Vigenère 加密在操作时需要注意:
®运算忽略参与运算的字母的大小写,并保持字母在明文M中的大小写形式;
当明文M 的长度大于密钥 k的长度时,将密钥 k重复使用。
例如,明文 M=Helloworld,密钥 k=abc 时,密文 C=Hfnlpyosnd。
输入
共 2 行。
第一行为一个字符串,表示密钥 k,长度不超过 100,其中仅包含大小写字母。
第二行为一个字符串,表示经加密后的密文,长度不超过 1000,其中仅包含大小写字母。
输出
一个字符串,表示输入密钥和密文所对应的明文。
输入样例
CompleteVictory
Yvqgpxaimmklongnzfwpvxmniytm
输出样例
Wherethereisawillthereisaway
(1)编程思路1。
定义数组char table[26][26];保存解密码表,其中table[i][j]表示密文字符(i+’A’)与密钥字符(j+’A’)解密所应对应的明文字符。
由运算®规则表可知,字符A解密时,对应解密表应为:
table[0][0]=’A’,
table[0][1]=’Z’(由于加密时,Z®B=A,所以解密时,table[0][1]=’Z’),
同理,table[0][2]=’Y’, table[0][3]=’X’,…,table[0][25]=’B’。
由运算®规则表进一步可知,将字符A的解密表循环右移1位(由于加密运算表中字符是循环左移1位的),可得字符B的解密表,即
table[1][0]=’B’,table[1][1]=’A’,table[1][2]=’Z’, table[1][3]=’Y’,…,table[0][25]=’C’。
同理,将字符B的解密表循环右移1位,可得字符C的解密表,即
table[2][0]=’C’,table[2][1]=’B’,table[2][2]=’A’, table[2][3]=’Z’,…,table[2][25]=’D’。
……
得到了解密码表后,对于密文中的每个字符,按密钥和解密码表直接得到对应的明文字符即可。
为了方便处理,输入密钥后,将密钥中的所有字母全部变成对应的小写字母。
(2)源程序1。
#include <stdio.h>
#include <string.h>
int main()
{
char table[26][26];
int i,j;
table[0][0]='A';
for (i=1;i<26;i++)
table[0][i]='Z'+1-i;
for (i=1;i<26;i++)
{
for (j=1;j<26;j++)
table[i][j]=table[i-1][j-1];
table[i][0]=table[i-1][25];
}
char keyword[101];
char mess[1001];
scanf("%s%s",keyword,mess);
int len=strlen(keyword);
for (i=0;i<len;i++)
if (keyword[i]>='A' && keyword[i]<='Z')
keyword[i]+=32;
for (i=0;mess[i]!='\0';i++)
{
if (mess[i]>='A' && mess[i]<='Z')
printf("%c",table[mess[i]-'A'][keyword[i%len]-'a']);
else
printf("%c",table[mess[i]-'a'][keyword[i%len]-'a']+32);
}
printf("\n");
return 0;
}
(3)编程思路2。
上面的思路是先按加密运算规则构造好解密码表,这样对于每个待解密的字符,直接查表即可。但实际上,仔细分析加密运算规则,可以发现,Vigenère 密码的加密本质上还是字符移位替换,只不过对于不同的密钥字符,移位的距离有所不同而已。
例如,对于密钥字符A,明文字符A~Z加密时,字符保持不变;
对于密钥字符B,明文字符A~Z加密时,每个字母用其直接后继的字母(即后面的第1个字母)替换,即字母A用B替换,B用C替换,…,Y用Z替换,Z用A替换;
对于密钥字符C,明文字符A~Z加密时,每个字母用其后面的第2个字母替换,即字母A用C替换,B用D替换,…,Y用A替换,Z用B替换;
……
也就是,加密时,对于任意的密钥字符key(设为小写字母),任意的明文字符mess(为字母A~Z,设也用小写字母表示)的加密替换表达式为
mess=(mess-‘a’+key-‘a’)%26+’a’
由于解密运算是加密运算的逆运算,因此,对于任意的密钥字符key(设为小写字母),任意的密文字符mess(为字母A~Z,设也用小写字母表示)的解密替换表达式为
mess=(mess-‘a’-(key-‘a’)+26)%26+’a’
=(mess-key+26)%26
这样,解密时直接按解密替换表达式进行字符替换即可。
(4)源程序2。
#include <stdio.h>
#include <string.h>
int main()
{
char keyword[101];
char mess[1001];
scanf("%s%s",keyword,mess);
int len=strlen(keyword);
int i,j;
for (i=0;i<len;i++) // 将密钥全部变为小写
if (keyword[i]>='A' && keyword[i]<='Z')
keyword[i]+=32;
for (i=0,j=0;mess[i]!='\0';i++,j++)
{
if (mess[i]>='a' && mess[i]<='z')
mess[i] = 'a' + (mess[i] - keyword[j] + 26) % 26;
else
mess[i] = 'A' + (mess[i]+32-keyword[j] + 26) % 26;
if (j == len-1) j=-1;
}
printf("%s\n",mess);
return 0;
}
习题75
75-1 密码破解者
本题选自洛谷题库 (https://www.luogu.org/problem/P2636)
问题描述
有如下3种加密方式:
一、栅栏密码:
所谓栅栏密码,就是把要加密的明文分成L个一组,然后把每组的第1个字连起来,形成一段无规律的话。一般比较常见的是2栏的棚栏密码。
比如明文:THERE IS A CIPHER
去掉空格后变为:THEREISACIPHER
两个一组,得到:TH ER EI SA CI PH ER
先取出第一个字母:TEESCPE
再取出第二个字母:HRIAIHR
连在一起就是:TEESCPEHRIAIHR
这样就得到我们需要的密文了。
但也可能有更多的栏数。
注:若明文长度不能整除栏数,则分组后剩下的单独为一组,如:
THERE IS A CIPHER 用3栏加密分组为:THE REI SAC IPH ER
先后取出第一二三个字母(最后一组只能取前两个),加密后为:
TRSIE HEAPR EICH(去掉空格)。
二、维吉尼亚(Vigenère)密码:
维吉尼亚密码首先应用了“密钥”的思想,其在密码届具有十分重要的意义。
在密码学中,我们称需要加密的信息为明文,用M表示;称加密后的信息为密文,用C表示;而密钥是一种参数,是将明文转换为密文或将密文转换为明文的算法中输入的数据,记为k。在Vigenère 密码中,密钥k 是一个字母串,k=k1,k2,…,kn。当明文 M=m1,m2,…,mn时,得到的密文 C=c1,c2,…,cn,其中ci =mi®ki,运算®的规则如下表所示:
当明文 M 的长度大于密钥 k 的长度时,将密钥 k 重复使用。
例如,明文 M=Helloworld,密钥 k=abc 时,密文 C=Hfnlpyosnd。
三、QWE键盘码:
随着键盘普及,也出现了相应的键盘码。
这是一个常见的键盘,在左边字母区有三行字母分别为:
QWERTYUIOP
ASDFGHJKL
ZXCVBNM
从第一排第一列开始分别用Q替代A,W替代B……,M替代Z以此类推。
如 CODING 加密后即为 EGROFU.
输入
输入第一行为一个正整数N 表示截获的密文共用了N重密码加密。
第二行为一个字符串S表示加密后的密文。
以下3-N+2行共N行每行开头一个正整数K(1<=K<=3)表示对应的加密方式。
给出的加密方式按顺序给出,即给出的第i重加密为实际加密过程的第i重(1<=i<=N)。
若K=1则表示用栅栏密码加密,之后一个正整数L表示加密所用栏数。
若K=2则表示用维吉尼亚码加密,之后一个字符串T表示密钥。
若K=3则表示用QWE键盘码加密。
输出
共一行。一个字符串,表示破解N重加密后的明文。
输入样例
2
YSLTRIQXSHTQTR
1 2
3
输出样例
FULLSPEEDAHEAD
(1)编程思路。
一共就三种加密方式,编写三个对应的函数进行解密。其中:
函数void Fence(int L)对加密时明文分成L个一组所生成的密文进行解密。
函数void Vigenere(char *keyword)对用密钥keyword进行加密后的密文进行解密,采用例75中给出的解密替换表达式进行解密。
函数void QWE()实现对采用QWE键盘码加密的密文进行解密。把QWE键盘码的顺序和字母ABCD相对应,得到解密码表为char qwe[] = "kxvmcnophqrszyijadlegwbuft";。
在进行解密时,要按照加密顺序进行反向解密。例如,加密顺序是1、3,解密顺序应为3、1。
(2)源程序。
#include <stdio.h> #include <string.h> char mess[30005]; char key[1001][1001]; void Fence(int L) { int i,j,k; char temp[30005]; int len=strlen(mess); for (i=0,j=0,k=0;i<len; i++,j+=L) { if (j>=len) j=++k; temp[j] = mess[i]; } strcpy(mess,temp); } void Vigenere(char *keyword) { int len=strlen(keyword); int i,j; for (i=0;i<len;i++) // 将密钥全部变为小写 if (keyword[i]>='A' && keyword[i]<='Z') keyword[i]+=32; for (i=0,j=0;mess[i]!='\0';i++,j++) { if (mess[i]>='a' && mess[i]<='z') mess[i] = 'a' + (mess[i] - keyword[j] + 26) % 26; else mess[i] = 'A' + (mess[i]+32-keyword[j] + 26) % 26; if (j == len-1) j=-1; } } void QWE() { char qwe[] = "kxvmcnophqrszyijadlegwbuft"; int i; for (i=0; mess[i]!='\0';i++) { if (mess[i]>='a' && mess[i]<='z') mess[i] = qwe[mess[i] - 'a']; else mess[i] = qwe[mess[i]+32-'a']-32; } } int main() { int n; scanf("%d",&n); scanf("%s",mess); int op[1001],L[1001]; int i; for (i=1;i<=n;i++) { scanf("%d",&op[i]); if (op[i]==1) scanf("%d",&L[i]); if (op[i]==2) scanf("%s",key[i]); } for (i=n;i>=1;i--) { if (op[i]==1) Fence(L[i]); if (op[i]==2) Vigenere(key[i]); if (op[i]==3) QWE(); } printf("%s\n",mess); return 0; }
75-2 SRL加密
问题描述
设有SRL(Shake,Rattle and rolL)加密方法如下:
待加密的文本信息按行主顺序放入一个n阶方阵中,每个字符位于方阵唯一的单元格中。如果信息没有完全填满方阵,空单元格将填充字母表的大写字母,从字母A开始,一直到字母Z(根据需要重复)。例如,6阶方阵中的信息“Meet me at the pizza parlor”将如下图所示。注意,所有字符都存储为大写字母。
要加密此信息,将执行3个单独的操作,如下所示:
1)S(Shake)操作:方阵中每个奇数列向上移动一个字符,最上面的字符移动到该列的底部。每个偶数列向下移动,最底部的字符移动到列的顶部。列从1开始编号,如下图所示。
2)R(Rattle)操作:方阵中每个奇数行向右移动一个字符,最右边的字符移动到同一行最左边的列。每个偶数行向左移动一个字符,最左边的字符移动到同一行中最右边的列。行从顶部1开始编号,如下图所示。
3)L(roll)操作:方阵周围的每个奇数“圈”顺时针旋转一个字符,而每个偶数“圈”逆时针旋转一个字符,如下图所示。“圈”号标记为“圈”的最上面一行的行号(最上面一行是第1行),即最外一“圈”为第1圈。
方阵大小在加密密钥中指定,大小从3x3到100x100不等。
输入
输入包括多个测试用例。每个测试用例占两行。其中第一行是加密密钥。第二行是要加密的文本信息。加密密钥总是以两位数的方阵大小开始,“00”的大小解释为100;然后是一系列任意顺序的“S”、“R”或“L”字符。对于每个字符,将执行相应的操作,例如,对于每个“R”字符,将执行“R(Rattle)”操作。
加密密钥限制为80个字符。待加密的文本信息限制为10000个字符。假设信息始终适合指定的方阵。
输出
每个测试用例输出加密后的文本信息。加密后文本信息的长度是方阵阶数的平方(例如,3阶方阵产生长度为9的加密字符串)。
输入样例
04RSRR
I love ice cream
06SRL
Meet me at the Pizza Parlor
输出样例
IREAELCIMVE OC
EIEEAGTTIMT E P ZHRZB PAORDAFLEA CMH
(1)编程思路。
编写void shake(char (*a)[110], int n)和void rattle(char (*a)[110], int n)分别模拟S操作和R操作,这两个操作的实现比较简单。若将一个二维数组的一行或一列看成是一个一维数组,则这个两个操作的实现本质上就是将一个一维数组的各元素循环左移(或右移)1位。
将有n个元素的一维数组a循环左移1位,可以写成如下的一重循环
T=a[0];
for (i=1;i<n;i++) a[i-1]=a[i];
a[n-1]=t;
而将有n个元素的一维数组a循环右移1位,可以写成如下的一重循环
T=a[n-1];
for (i=n-1;i>0;i--) a[i]=a[i-1];
a[0]=t;
编写函数void roll(char (*a)[110], int n)实现L(roll)操作,这个操作的实现比前2个操作的实现稍微麻烦一些。
一个n阶方阵可以看成由n/2圈组成,例如,4阶方阵由2圈组成,5阶方阵实际上由3圈组成,但最内圈只有1个元素,忽略掉最内圈的1个数的话,5阶方阵也可以看成由2圈组成。
进行L操作时,方阵周围的每个奇数“圈”顺时针旋转一个字符,而每个偶数“圈”逆时针旋转一个字符。一圈顺时针旋转可以看成是两行和两列的移位操作。
例如,最外圈顺时针旋转一个字符,可以看成顶行字符右移1位,最右列字符向下移1位,最底行字符左移1位,最左列字符向上移1位。具体实现时,先保存最外圈最左上角的字符(t=a[0][0]),然后,最左列字符向上移1位,最底行字符左移1位,最右列字符向下移1位,顶行字符右移1位。最后将保存的t赋给a[0][1]即可。
逆时针操作的实现与顺时针类似,参见源程序。
(2)源程序。
#include <stdio.h> #include <string.h> char mat[110][110]; void shake(char (*a)[110], int n) { int i, j; char temp; for (i=0; i<n; i++) { if (i%2==0) { temp = a[0][i]; for (j=1; j<n; j++) a[j-1][i] = a[j][i]; a[n-1][i] = temp; } else { temp = a[n-1][i]; for (j = n-1; j > 0; j--) a[j][i] = a[j-1][i]; a[0][i] = temp; } } } void rattle(char (*a)[110], int n) { int i, j; char temp; for (i = 0; i < n; i++) { if (i%2 == 0) { temp = a[i][n-1]; for (j = n-1; j > 0; j--) a[i][j] = a[i][j-1]; a[i][0] = temp; } else { temp = a[i][0]; for (j = 1; j < n; j++) a[i][j-1] = a[i][j]; a[i][n-1] = temp; } } } void roll(char (*a)[110], int n) { int i,j,num,k; char temp; num = n/ 2; for (k = 0; k < num; k++,n--) { if (k%2==0) { j = i = k; temp = a[i][j]; i++; while (i < n) { a[i-1][j] = a[i][j]; i++; } i--; j++; while (j < n) { a[i][j-1] = a[i][j]; j++; } j--; while (i > k) { a[i][j] = a[i-1][j]; i--; } while (j > k+1) { a[i][j] = a[i][j-1]; j--; } a[i][j] = temp; } else { j = i = k; temp = a[i][j]; j++; while (j < n) { a[i][j-1] = a[i][j]; j++; } j--; i++; while (i < n) { a[i-1][j] = a[i][j]; i++; } i--; while (j > k) { a[i][j] = a[i][j-1]; j--; } while (i > k+1) { a[i][j] = a[i-1][j]; i--; } a[i][j] = temp; } } } int main() { char key[81],mess[10005]; while (scanf("%s",key)!=EOF) { getchar(); gets(mess); int len1 = strlen(key); int len2 = strlen(mess); // 求出矩阵大小,注意00代表100! int size = (key[0]-'0')*10 + key[1]-'0'; if (size == 0) size = 100; int i,j; for (i=len2, j=0; i<size*size; i++, j++) mess[i]='A'+j%26; mess[i]='\0'; int k = 0; for (i = 0; i < size; i++) for (j = 0; j < size; j++) { mat[i][j] = mess[k]; k++; } for (i=2; i<len1; i++) { if (key[i]=='S') shake(mat,size); else if (key[i] == 'R') rattle(mat,size); else if (key[i] == 'L') roll(mat, size); } for (i = 0; i < size; i++) { for (j = 0; j < size; j++) { if (mat[i][j]>='a' && mat[i][j]<='z') mat[i][j]= mat[i][j]-'a'+'A'; printf("%c",mat[i][j]); } } printf("\n"); } return 0; }
75-3 不可靠消息
问题描述
某王国的国王选了六名仆人作为信使,他们的名字是J先生、C小姐、E先生、A先生、P先生和M先生。某位信使收到给国王的消息后,他们会向下一位信使传递消息,直到消息传到国王手中。
给国王的消息由数字('0'-'9')和字母('A'-Z','a'-'z')组成。大写字母和小写字母在消息中是有区别的。例如,“ke3E9Aa”是一条消息。
因为每个信使在将消息传递给下一个信使(或国王)之前都会稍微更改消息,这样国王总是收到错误的消息。这激怒了国王,他要求王宫主管想办法来纠正它。为了完成国王的任务,主管仔细分析了信使的错误,并获得了每个信使错误的特征。令人惊讶的是,每位信使在转发消息时都只会犯同样的错误。观察到以下事实。
J先生将消息的所有字符向左旋转一位。例如,他将“aB23d”转换为“B23da”。
C小姐将消息的所有字符向右旋转一个。例如,她将“aB23d”转换为“daB23”。
E先生将消息的左半部与右半部交换。如果消息的字符数为奇数,则中间的字符不会移动。例如,他将“e3ac”转换为“ace3”,将“aB23d”转换为“3d2aB”。
A先生颠倒了消息。例如,他将“aB23d”转换为“d32Ba”。
P先生将消息中的所有数字递增1。如果一个数字是“9”,它就变成了“0”。字母不变。例如,他将“aB23d”转换为“aB34d”,将“e9ac”转换为“e0ac”。
M先生将消息中的所有数字递减一。如果一个数字是“0”,它就变成了“9”。字母不变。例如,他将“aB23d”转换为“aB12d”,将“e0ac”转换为“e9ac”。
请编写程序,根据信使的顺序,从最后的消息中推断出原始消息。例如,如果送信人依次是A->J->M->P,而给国王的消息是“aB23d”,那么最初的信息是什么?根据信使错误的特点,导致最终消息的顺序是“32Bad”->“daB23”->“aB23d”->“aB12d”->“aB23d”:因此,原始消息应该是“32Bad”。
输入
输入的第一行包含一个正整数n,表示测试用例的数量。每个测试用例包括两行,第1行是传递消息的信使顺序,第2行是最后给国王的消息。转发消息的信使数在1到6之间(含1到6)。同一个信使不会出现多次。消息的长度介于1和25之间(含1和25)。
输出
对于每个测试用例,输出原始消息。
输入样例
5
AJMP
aB23d
E
86AE
AM
6
JPEM
WaEaETC302Q
CP
rTurnAGundam1isdefferentf
输出样例
32Bad
AE86
7
EC302QTWaEa
TurnAGundam0isdefferentfr
(1)编程思路。
分别编写6个函数void Jfun(char str[])、void Cfun(char str[])、void Efun(char str[])、void Afun(char str[])、void Pfun(char str[])和void Mfun(char str[])来模拟处理J先生、C小姐、E先生、A先生、P先生和M先生的变换操作。每个函数的实现都是一个简单的一重循环,具体参见源程序。
(2)源程序。
#include <stdio.h> #include <string.h> void Jfun(char str[]) { int i; char t; t=str[0]; for (i=1;str[i]!='\0';i++) { str[i-1]=str[i]; } str[i-1]=t; } void Cfun(char str[]) { int i,len; char t; len=strlen(str); t=str[len-1]; for (i=len-2;i>=0;i--) { str[i+1]=str[i]; } str[0]=t; } void Efun(char str[]) { int i,j,t; i=0; j=(strlen(str)+1)/2; while (str[j]!='\0') { t=str[i]; str[i]=str[j]; str[j]=t; i++; j++; } } void Afun(char str[]) { int i,j,t; for (i=0,j=strlen(str)-1;i<j;i++,j--) { t=str[i]; str[i]=str[j]; str[j]=t; } } void Pfun(char str[]) { int i; for (i=0; str[i]!='\0';i++) if (str[i]>='0'&& str[i]<='8') str[i]++; else if (str[i]=='9') str[i]='0'; } void Mfun(char str[]) { int i; for (i=0; str[i]!='\0';i++) if (str[i]>='1'&& str[i]<='9') str[i]--; else if (str[i]=='0') str[i]='9'; } int main() { char str[30],s[8]; int n,i; scanf("%d",&n); while (n--) { scanf("%s%s",s,str); for (i=strlen(s)-1;i>=0;i--) { switch(s[i]) { case 'J':Cfun(str);break; case 'C':Jfun(str);break; case 'E':Efun(str);break; case 'A':Afun(str);break; case 'P':Mfun(str);break; case 'M':Pfun(str);break; } } printf("%s\n",str); } return 0; }