问题描述
将M进制的数X转换为N进制的数输出。
输入
输入的第一行包括两个整数:M和N(2<=M,N<=36)。
下面的一行输入一个数X,X是M进制的数,现在要求你将M进制的数X转换成N进制的数输出。
输出
输出X的N进制表示的数。
hint
注意输入时如有字母,则字母为大写,输出时如有字母,则字母为小写。
输入输出样例
思路分析
本题是属于大整数运算章节的题目,这提示了我们题目的数据可能是long与long long都无法表示的大整数,要用结构体专门存储,并为其设计相应的运算函数。意识到这一点是破题第一步。
进制转换的基本思想都已经十分熟稔了:对于m进制表示的数x,从低到高取其数位上的数,乘以m对应的幂次值,累加,可以得到x的十进制数据。当需要相互转换的进制m和n不存在同为某数幂次值(如8=2^3 4=2^2)时,必须先将x转换为十进制数,再逐次对新的基底n取模,得到新数的每一位。
当数据是大整数时,以上常规步骤中,累乘m求各项的基底幂次值这一步存在难点,极有可能出现累乘结果不能用基本数据类型表示的情况,因而涉及到大整数的乘法运算。此外,数据x本身也需要用大整数结构体存储。稍加思索可以分析出程序有以下重要模块:
- m进制数据x转十进制大整数
- 十进制大整数转换为n进制数据输出
- 对于步骤1需要设计计算基底累乘结果的大整数乘法函数,和计算各项运算结果的大整数加法函数
- 对于步骤2,需要设计能够实现大整数对新基底n进行取模和除法运算功能的函数。
本题还有其他值得关注的细节,如对于输入数据含有字符的情况,可以建立字符与整数的映射关系表来实现对应与转化。用到C++标准库中的map库。更多的感想不在这块内容里赘述,参见本文最后的思考总结。
参考代码
代码又磨了一下午,值得注意的是为了节省资源,我自创了一些函数,尽量少开大整数结构体,能自接收就自接收。
#include <cstdio>
#include <cstring>
#include <cmath>
#include <map>
#include <vector>
#include <algorithm>
using namespace std;
//全局变量
const int maxn = 100010;//保证数据足够大
char num[maxn];//接收数据x
vector<char> result;//存储进制转换结果
map<char,int> scale;//字符与数的映射表
//数与字符的映射
char reScale[40] = "abcdefghijklmnopqrstuvwxyz";
char scaleMark[40] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
//定义bign结构体
struct bign{
int d[maxn];
int len;
int sign;
bign(){//不含参量的初始化函数
memset(d,0,sizeof(d));
len = 0;
sign = 1;
}
bign(int _d){//含参量的初始化函数
memset(d,0,sizeof(d));
sign = 1;
if(_d>0) d[0] = _d;
else{
d[0] = _d*(-1);
sign = -1;
}
len = 1;
}
};
//关于bign的函数
void calSum(bign &a,bign b,int d){
//相当于执行a += b*d的常规步骤
//先得到b*d的结果并用b存储
int carry = 0;
for(int i=0;i<b.len;i++){
int temp = carry + b.d[i] * d;
b.d[i] = temp % 10;
carry = temp / 10;
}
while(carry){
b.d[b.len++] = carry % 10;
carry /= 10;
}
//再计算与a相加的结果并赋值给a
//整个过程没有新开bign,节省程序运行资源
for(int i=0;i<a.len||i<b.len;i++){
int temp = carry + a.d[i] + b.d[i];
if(i<a.len) a.d[i] = temp % 10;
else a.d[a.len++] = temp % 10;
carry = temp / 10;
}
if(carry) a.d[a.len++] = carry;
}
void POW(bign &a,int b){
//相当于执行a *= b的常规操作,没有新开bign
int carry = 0;
for(int i=0;i<a.len;i++){
int temp = carry + a.d[i] * b;
a.d[i] = temp % 10;
carry = temp / 10;
}
while(carry){
a.d[a.len++] = carry % 10;
carry /= 10;
}
}
void smash(bign &a,int b,int &r){
//相当于执行a/=b,r=a%b ,也没有新开bign
for(int i=a.len-1;i>=0;i--){
r = r*10 + a.d[i];
if(r<b){
a.d[i] = 0;
}else{
a.d[i] = r / b;
r %= b;
}
}
//要记得去除冗余0
//减法和除法都有去除冗余0的操作
while(a.d[a.len-1]==0&&a.len>1) a.len--;
}
//进制转换函数
bign mToDecimal(int m,char x[]){
//将m进制数转换为十进制大整数并返回
bign product(1);//基底幂值,初始化为1
bign ret;//保存转换为十进制的结果
int j = 0;
bool flag = false;
if(x[0]=='-') {
j++;
flag = true;
}
for(int i=strlen(x)-1;i>=j;i--){
int d;
if(x[i]>='A'&&x[i]<='z'){
d = scale[x[i]];
}else d = x[i] - '0';
//乘以基底幂值累加
calSum(ret,product,d);
POW(product,m);//累乘基底
}
if(flag) ret.sign = -1;
return ret;
}
void decimalToM(int m,bign x){
//将十进制大整数转换为新进制数表示并输出
if(x.len==1&&x.d[0]==0){
//0需要单独处理,因为我用vector存储结果
printf("0\n");
return;
}
int r = 0;
result.clear();//每次都要初始化
if(x.sign==-1) putchar('-');
while(x.len!=1||x.d[0]!=0){
//当x没被除尽(为0),继续smash
smash(x,m,r);
char c;
if(r>=10){
c = reScale[r - 10];
}else c = r + '0';
result.push_back(c);
r = 0;//r也要重新赋值为0
}
//结果要倒着输出哦~
for(int i=result.size()-1;i>=0;i--) putchar(result[i]);
putchar('\n');
}
int main(){
int m,n;
for(int i=0;i<26;i++) {
//先要把映射表初始化一下
scale[scaleMark[i]] = scale[reScale[i]] = 10+i;
}
while(scanf("%d%d",&m,&n)!=EOF){
scanf("%s",num);
bign c = mToDecimal(m,num);
decimalToM(n,c);
}
}
思考总结
这个题目一开始拿到的时候,我居然完全没有意识到大整数的事情,还按照之前进制转换的常规操作去写(而且常规的也写错了:P)。而当我第一次尝试用大整数运算的各类函数去解决这个问题的时候,各种各样的毛病都出现了,比如:对新基底取模的运算r没有重新赋值为0造成了一些难以形容的bug。随着不断的尝试,这些问题逐个得到解决,但是当我以为程序已经功能完备时,codeup再次对我说:你再想想。
不想了,先吃饭
吃完饭我再次来到电脑前盯了一会屏幕上杂乱无章的程序,我从头开始梳理这个问题,忽然意识到麻烦的不仅是数据本身的长度,还有累乘基底的长度!一个是面子,一个是里子,表层的东西总是显然的,但是深一层的却没那么轻易察觉到,但如果没有意识到里子的事,我们就不能说真正懂这个题,那么失败也不是偶然的结果。
所以,要处理product累乘超限的问题,考虑用到大整数乘法,再次堆砌了一个大整数运算函数。这个时候我在DevC++上运行,却发现出现了之前不曾有过的with return value 3221225725这个错误,上网搜索得知是内存空间发生了数据溢出,通常是因为开了许多大容量数组导致的。
显然,是因为我在大整数运算函数中总是新开bign,每次都会对其超大容量数组成员进行初始化,空间消耗超出了计算机的承受范围。有两种解决方案:1.减少函数;2.函数内减少空间开销。我两个方向上都做出了优化,最后亮起绿灯的那一刻,只能说我的大整数运算PTSD终于被治好了!