<think>我们使用FFT(快速傅里叶变换)来实现两个数的乘法,这是一种高效的大数乘法方法,时间复杂度为O(n log n)。但是,这里数的表示是负二进制(base -2),这与传统的二进制(base 2)不同。
负二进制表示中,每一位的权重是(-2)^k。因此,一个数字可以表示为:
$$ n = \sum_{k=0}^{m-1} d_k \times (-2)^k $$
其中,d_k是0或1。
我们的目标是计算两个负二进制数A和B的乘积C=A*B,结果也用负二进制表示。
步骤:
1. 将两个负二进制数转换为标准的多项式形式(系数为0或1),但注意基是-2。
2. 使用FFT进行多项式乘法。由于负二进制的多项式乘法在点值表达下也是线性的,我们可以用FFT加速。
3. 将乘积结果转换回负二进制表示。
但是,由于基是负数,直接进行多项式乘法后得到的系数可能不是0或1,而是一个整数(可能为负),因此我们需要将乘积结果转换回负二进制表示。
具体步骤:
第一步:输入两个负二进制字符串(或数组),将其转换为多项式系数向量。注意,最低位对应多项式的0次项系数。
例如:负二进制数"1101"(即1*(-2)^0 + 0*(-2)^1 +1*(-2)^2 +1*(-2)^3 = 1 + 0 + 4 -8 = -3)在向量中表示为[1,0,1,1](从低到高:索引0对应最低位,即权重(-2)^0)。
第二步:使用FFT进行多项式乘法。注意,多项式乘法的结果系数可能很大,且可能为负数。
第三步:将多项式乘法的结果(一个整数系数数组)转换回负二进制表示。
如何将整数转换为负二进制?
转换整数为负二进制的方法:不断用-2去除整数,取余数和商。余数只能是0或1(因为要保证每一位是0或1),所以当余数为负数时,调整余数为0或1,同时调整商。
具体转换算法(整数n转负二进制):
1. 初始化一个空数组。
2. 当n不为0时:
a. 计算余数:r = n mod (-2)。注意,在C++中,负数的模运算可能得到负数,因此我们需要调整。
b. 计算商:n = n / (-2)(整数除法)。
c. 如果余数r是负数,则调整:r = r + 2(这样余数就变为0或1),同时n = n + 1(因为r为负时,相当于多减了2,所以商加1来补偿)。
d. 将余数r(0或1)加入结果数组(作为当前最低位)。
3. 反转结果数组(使得最高位在最后)?实际上,我们每次加入的是最低位,所以最后不需要反转,但要注意存储顺序。
但是,我们这里得到的是多项式乘法的结果,这个结果是一个大整数(可能为正或负),我们可以先把这个大整数算出来,然后用上述方法转换?但是这样可能会很慢(因为大整数除法效率低),而且我们有多项式系数数组,这个数组的每一位实际上表示的是在基-2下的权重,但注意,由于多项式乘法的结果系数可能大于1或为负数,我们需要将其“进位”转换。
实际上,在得到多项式乘积的系数数组后,我们需要将其标准化(即调整每一位,使得每一位只能是0或1)。但是,由于基是-2,进位规则和正进制不同。
进位规则(从低到高处理):
设系数数组为c[0..m-1](c[0]对应最低位,权重(-2)^0),我们遍历每一位,如果当前位c[i]不是0或1,则需要向高位进位。
具体规则:
设当前位为i,值为v = c[i]。
1. 当v不在{0,1}时,我们需要调整:
a. 计算商和余数:r = v mod 2,但注意,我们希望余数非负(0或1),所以如果v是负数,调整方法同上。
b. 实际上,我们可以这样:将v分解为:v = a * 2 + r,其中r为0或1,但这样分解并不直接满足基-2的进位。
实际上,更直接的方法是利用以下关系:
(-2)^i = (-2)^{i+1} * (-1/2) => 所以每两个连续的位可以相互调整。
一个经典的方法是:从最低位开始,如果当前位c[i]是负数,则将其加2(这样变成非负),然后向高位借1(因为基是负数,借位相当于加上基的相反数,所以高位要加1?)。但是,由于基是-2,借位规则不同。
另一种方法:从低位到高位处理,每次将当前位调整到0或1,并将多余的进位(或借位)传递到高位。
具体步骤(参考转换整数为负二进制的思想,但这里我们处理的是系数数组,每个系数对应基-2的幂):
1. 初始化进位carry=0(这个进位不是简单的进位,而是因为基是负数而需要调整的进位)。
2. 从最低位(i=0)到最高位(i=m-1)处理,同时注意最高位之后可能还有进位,所以循环直到没有进位且处理完所有位。
3. 对于每一位i:
a. 令当前值v = c[i] + carry(注意,进位carry是从低位传递过来的,但进位传递规则不同)
b. 我们想要将v表示为:v = (-2) * x + r,其中r为0或1。但是,实际上我们想要调整当前位为r,然后将x传递给下一位(作为进位)?注意,这里下一位是i+1,权重是(-2)^{i+1},而当前位权重是(-2)^i。
c. 实际上,我们可以这样:将v分解为:r = v mod 2(取非负余数,即0或1),然后计算进位到高位的值:carry = (r - v) / 2。但是,这样分解是否满足?因为:
v = (-2)*carry + r => carry = (v - r) / (-2) [注意:这里除以-2]
d. 但是,由于r的取值必须使得carry是整数,所以我们可以取r满足v-r能被2整除(即r=v mod 2,且0<=r<2,所以r=0或1)。但是,这样分解后,carry=(v-r)/(-2) = (r-v)/2,这个值可能是负数。
例如:v=2,则r=0(因为2 mod 2=0),carry=(0-2)/2 = -1,即:2 = (-2)*(-1) + 0。
v=3,则r=1(3 mod 2=1),carry=(1-3)/2 = (-2)/2=-1,即:3=(-2)*(-1)+1。
v=-1,则r=1(因为-1 mod 2=1,因为-1=2*(-1)+1,余数1),carry=(1-(-1))/2=2/2=1,即:-1=(-2)*1+1。
所以,规则是:
r = v mod 2 (取非负余数,即0或1,如果v是负数,则模2的结果可能是1,如-1 mod 2=1)
carry = (v - r) / (-2) (因为v = (-2)*carry + r)
注意:这个carry将加到下一位(i+1)上。
4. 将当前位c[i]设为r。
5. 进位carry传递给下一位(即i+1位),然后处理下一位。
6. 当处理完所有位后,如果进位carry不为0,则需要继续向更高位处理(在数组后面添加新的位)。
注意:这个进位规则可以保证最终每一位都是0或1。
因此,整个算法流程:
1. 将两个负二进制数转换为系数数组(注意:数组下标0对应最低位,即权重(-2)^0)。
2. 对两个系数数组进行补零,使它们的长度变为2的幂,且长度至少为len1+len2-1(以便进行FFT乘法)。
3. 使用FFT计算两个多项式(系数为0或1)的乘积(注意:这里多项式乘法的系数是卷积结果,但注意基是-2,而FFT是复数域运算,所以我们需要用复数FFT?但整数乘法可以用整数,为了避免精度问题,我们使用足够大的浮点数,或者用NTT?但这里基数是0或1,所以卷积结果是整数,但可能很大,所以用FFT在浮点数上计算,然后四舍五入取整,注意精度问题)。
然而,由于负二进制表示的多项式乘法,我们实际上是在整数环上,但FFT通常用于复数。我们可以用FFT计算卷积,然后取整。但注意,负二进制数的乘法结果是一个整数,而多项式乘积的系数和这个整数在基-2下的表示是不同的。实际上,我们计算的是多项式乘积,这个乘积的多项式在x=-2处的值就是两个数的乘积,但注意,我们并不需要先求出整个整数,而是需要得到基-2表示。所以,我们实际上是在做多项式乘法,然后调整系数到基-2表示(即每位0或1)。
但是,这里有一个关键:多项式乘积的系数数组表示的是什么?在基-2下,这个多项式在x=-2处的值就是乘积结果。但是,我们得到的系数数组c[0..n-1]满足:
P(x) = c[0] + c[1]*x + c[2]*x^2 + ... + c[n-1]*x^{n-1}
当x=-2时,P(-2)就是乘积。然而,这个系数数组c[i]并不一定是0或1,所以我们需要将其调整成0或1(通过上述进位规则),调整后的系数数组就是负二进制表示。
因此,步骤:
a. 计算两个数的多项式乘积(系数卷积),得到系数数组C(长度为n,每个元素是整数,可能很大,也可能为负)。
b. 对这个系数数组C,使用上述进位规则(从低到高)调整,使得每一位变为0或1,并得到新的系数数组(即负二进制表示)。
注意:在调整过程中,可能会在数组后面添加新的位(进位到更高位)。
所以,我们不需要先将整个乘积算出来(即不需要计算P(-2)的值),而是直接调整系数数组。
但是,我们如何得到多项式乘积的系数数组?注意,两个多项式(系数为0或1)的乘积,其系数可以通过FFT计算卷积得到。
具体步骤:
步骤1:将两个负二进制字符串转换为系数数组(从低位到高位存储)。
例如:A = "1101"(负二进制,表示-3) -> 从低位开始:第一位(权重(-2)^0)是1,第二位是0,第三位是1,第四位是1 -> 数组A_arr = [1,0,1,1](索引0对应最低位)。
步骤2:对两个数组进行补零,使长度变为2的幂,且长度至少为lenA+lenB-1(为了循环卷积等于线性卷积)。
步骤3:使用FFT计算两个数组的线性卷积(即多项式乘积的系数):
a. 对A_arr做FFT(长度为N,N>=lenA+lenB-1,且为2的幂)
b. 对B_arr做FFT
c. 逐点相乘得到C_fft = A_fft * B_fft
d. 对C_fft做IFFT(逆变换),得到卷积结果C,这个C是复数数组,但虚部应该接近0,实部是整数,所以取实部并四舍五入取整。
步骤4:将得到的卷积结果C(浮点数组)转换为整数数组(注意:可能会有精度误差,所以需要取整,并确保是整数)。
步骤5:对整数数组C(从低到高,即C[0]对应乘积多项式常数项)使用进位规则调整(基-2进位):
初始化进位carry=0,然后从i=0到当前最高位(包括新进位产生的位):
v = C[i] + carry (注意:这里C[i]是卷积结果数组的第i个元素,即x^i的系数)
计算:r = v mod 2 (非负余数,即0或1)
carry = (v - r) / (-2) (注意:这里除法是整数除法,向0取整?)
将C[i]设置为r
然后i++,如果i达到当前数组长度且carry不为0,则在数组后面添加0,继续处理。
步骤6:去掉结果数组的高位0(即最高位开始连续的0,但注意最低位不能去,如果全0则保留一个0)。
步骤7:将调整后的数组(每个元素0或1)转换为字符串(从高位到低位输出,注意数组索引0是最低位,所以输出时要反转,或者从后往前输出)。
注意:步骤5的进位规则是核心,可以处理负数和正数。
但是,步骤5中,v = C[i] + carry,然后调整。注意,carry是前一位传递过来的进位值(整数,可能为正或负)。
例子:测试一个简单的乘法,例如1*1=1,负二进制表示:1*1=1,即[1] * [1] -> 卷积结果为[1](即只有常数项1)。调整:v=1,r=1 mod 2=1,carry=(1-1)/(-2)=0,所以结果数组为[1]。输出:从高位到低位?实际上,我们数组只有一位,所以输出"1"(注意,负二进制表示中,1就是1)。
再测试:2*2=4,但是2在负二进制中如何表示?2的负二进制:2 = 1*(-2)^1 + 1*(-2)^0? 不对,因为1*(-2)^1 + 1*(-2)^0 = -2+1=-1。实际上,2的负二进制表示是"110"(从低位到高位:0,1,1)?验证:0*(-2)^0 + 1*(-2)^1 + 1*(-2)^2 = 0 -2 +4=2。所以2的负二进制是"110"(高位在数组后面,即数组为[0,1,1])。
所以,2*2=4,4的负二进制表示:4 = 1*(-2)^2 + 1*(-2)^3? 验证:4=4,但1*(-2)^2=4,所以可以表示为"100"(即4=4,没有低位,所以数组为[0,0,1]?但这样权重是(-2)^0=1, (-2)^1=-2, (-2)^2=4,所以4应该表示为:0*(1)+0*(-2)+1*(4)=4,所以是"100"(从高位到低位:1,0,0,但数组存储从低位到高位是[0,0,1])。所以最后输出时,数组从低到高是[0,0,1],输出字符串从高到低是"100"。
现在计算2*2:数组A=[0,1,1](2),数组B=[0,1,1](2)。多项式乘法:A(x)=0+1*x+1*x^2, B(x)=0+1*x+1*x^2,则乘积C(x)=A(x)*B(x)= (x+x^2)*(x+x^2)=x^2+2x^3+x^4。所以系数数组:0,0,1,2,1(对应x^0到x^4)。即C=[0,0,1,2,1](索引0对应x^0)。
现在调整这个数组(基-2进位):
初始化carry=0
i=0: v = C[0]+carry=0+0=0 -> r=0 mod2=0, carry=(0-0)/(-2)=0 -> C[0]=0, carry=0
i=1: v=C[1]=0 -> r=0, carry=0 -> C[1]=0
i=2: v=1 -> r=1 mod2=1, carry=(1-1)/(-2)=0 -> C[2]=1, carry=0
i=3: v=2 -> r=2 mod2=0, carry=(2-0)/(-2)=2/(-2)=-1 -> C[3]=0, carry=-1
i=4: v=C[4]+carry=1+(-1)=0 -> r=0, carry=0 -> C[4]=0
然后carry=0,结束。
调整后的数组:[0,0,1,0,0](索引0到4)。去掉高位的0:最后两位是0,去掉后为[0,0,1](因为最高位是1,所以保留)。所以数组为[0,0,1](索引0对应最低位),输出字符串:从高位到低位:1,0,0 -> "100",即4,正确。
再测试:(-1)*(-1)=1。负二进制表示:-1的负二进制是"11"(因为1*(-2)^0+1*(-2)^1=1-2=-1),所以数组A=[1,1](低位在前),B=[1,1]。
卷积:A(x)=1+x, B(x)=1+x,乘积C(x)=1+2x+x^2。系数数组:[1,2,1](索引0对应常数项1)。
调整:
i0: v=1 -> r=1, carry=(1-1)/(-2)=0 -> [1,2,1] -> 变为[1,2,1](carry=0)
i1: v=2 -> r=0, carry=(2-0)/(-2)= -1 -> 设置C[1]=0, carry=-1
i2: v=1+(-1)=0 -> r=0, carry=0 -> C[2]=0
然后carry=0,结束。数组变为[1,0,0](索引0:1, 索引1:0, 索引2:0)。注意,此时数组表示:最低位1,然后两个0。所以输出字符串:从高位到低位:0,0,1 -> 但这样是"001",实际上我们去掉高位的0(从最高位开始去掉连续的0直到遇到非0),所以变成"1"。正确。
但是,注意在调整后,数组[1,0,0]的最高位(索引2)是0,所以去掉后变成[1](即只有最低位1)。所以输出"1"。
因此,算法可行。
现在,我们实现FFT部分。由于FFT是标准算法,我们可以使用Cooley-Tukey算法。注意,这里我们使用复数FFT,因此需要包含头文件<complex>,并注意精度问题(因为卷积结果可能很大,所以需要足够大的长度和双精度浮点)。
步骤:
1. 将两个数组补零到长度N(N是2的幂,且N>=lenA+lenB-1)。
2. 对每个数组进行FFT(使用复数数组)。
3. 逐点相乘。
4. 逆FFT。
5. 取实部,四舍五入取整(注意精度问题,应该加0.5再取整)。
然后进行进位调整。
注意:进位调整时,由于卷积结果可能有负数,所以我们的进位规则可以处理负数。
代码结构:
1. 负二进制字符串转数组(从低位到高位):
string -> vector<int>,例如:字符串"1101"(表示-3)-> [1,0,1,1](注意:字符串的第一个字符是最高位?还是最低位?)
用户输入的负二进制字符串,通常是从高位到低位,例如"1101"表示1*(-2)^3 + 1*(-2)^2 + 0*(-2)^1 + 1*(-2)^0 = -8+4+0+1=-3。
所以,字符串的第0个字符是最高位(权重(-2)^{len-1}),最后一个字符是最低位(权重(-2)^0)。因此,我们需要将字符串反转,使得数组索引0对应最低位。
或者,我们可以不反转字符串,直接按从高位到低位读取,然后存到数组时,数组索引0存最低位(即字符串的最后一个字符),索引1存次低位(字符串的倒数第二个字符)... 这样,数组下标0对应最低位。
例如:字符串s="1101"(长度为4),则:
arr[0] = s[3] - '0' (即'1')
arr[1] = s[2] - '0' (即'0')
arr[2] = s[1] - '0' (即'1')
arr[3] = s[0] - '0' (即'1')
这样,数组arr=[1,0,1,1](索引0是最低位)。
所以,我们可以这样转换:
vector<int> arr;
for (int i = s.size()-1; i>=0; i--) {
arr.push_back(s[i]-'0');
}
2. 实现FFT和IFFT(使用迭代FFT,避免递归开销)。
3. 卷积计算(注意:FFT计算的是循环卷积,但我们需要线性卷积,所以补零长度>=lenA+lenB-1,且为2的幂)。
4. 进位调整(如上所述)。
5. 将调整后的数组(从低位到高位)转换为负二进制字符串(从高位到低位输出):
去掉高位连续的0(注意:数组的最后一个元素是最高位,所以从数组尾部向前扫描,直到第一个非0,然后从那里开始,从后往前(即从最高位到最低位)输出)。
例如:数组[0,0,1](索引0:0, 索引1:0, 索引2:1)表示:最低位0,次低位0,最高位1。所以对应的负二进制数:最高位1,次高位0,最低位0 -> 字符串"100"。
具体:调整后的数组为result_arr(长度n),则:
string res = "";
// 从最高位(最后一个元素)开始,去掉前导0(注意:数组最后一个元素是最高位,第一个元素是最低位)
int i = n-1;
while (i>=0 && result_arr[i]==0) i--; // 跳过最高位的0
if (i<0) { // 全0
return "0";
}
for (; i>=0; i--) { // 从最高位(非0)到最低位
res += ('0'+result_arr[i]);
}
但是,注意:我们的数组result_arr的存储顺序是:索引0是最低位,索引1是次低位,...,索引n-1是最高位。所以,我们输出时,从索引n-1(最高位)开始,一直到索引0(最低位)。
但是,在进位调整过程中,数组可能已经扩展了(因为进位到更高位),所以数组长度可能大于n,但存储顺序不变。
注意:进位调整后的数组可能有前导0,所以输出前要去掉。
代码实现:
由于FFT的迭代实现比较复杂,我们使用递归实现?但题目要求C++,且效率要求高,所以使用迭代FFT(非递归)。
这里,我们实现一个复数FFT类(使用std::complex<double>)。
注意:FFT需要补零到2的幂。
步骤:
1. 计算总长度N=1; while (N < lenA+lenB-1) N<<=1;
2. 创建复数数组xa和xb,长度N,初始化为0。
3. 将数组A_arr和B_arr的值复制到xa和xb(实部,虚部为0)。
4. FFT(xa, N, 1); // 1表示正变换
5. FFT(xb, N, 1);
6. for i=0 to N-1: xa[i] *= xb[i];
7. FFT(xa, N, -1); // -1表示逆变换
8. 逆变换后,需要除以N(在逆变换函数中处理)。
注意:FFT的迭代实现(非递归)需要位逆序置换。
由于代码较长,我们分函数实现。
注意:精度问题,卷积结果最大可能达到min(lenA, lenB) * 1(因为系数只有0和1,所以卷积结果最大为min(lenA, lenB)),所以长度不会太大,双精度浮点可以处理。
但是,如果数字很大(位数很多),则卷积结果可能很大,但双精度浮点可以表示精确整数到2^53,所以如果总位数(lenA+lenB)不超过大约50万位(即卷积结果最大50万,小于2^19),那么双精度浮点可以精确表示。但为了安全,我们假设输入位数不是特别大(比如几千位),所以双精度足够。
如果位数很大,则需要使用NTT(数论变换),但这里我们使用FFT。
下面,我们开始写代码:
1. 辅助函数:位逆序置换(用于FFT的迭代实现)
2. FFT函数(参数:复数数组,长度,变换方向(正变换1,逆变换-1))
注意:我们使用Cooley-Tukey算法,蝶形运算。
由于时间关系,我们直接实现迭代FFT。
代码结构:
```cpp
#include <iostream>
#include <vector>
#include <cmath>
#include <complex>
#include <string>
#include <algorithm>
using namespace std;
const double PI = acos(-1.0);
typedef complex<double> Complex;
// 位逆序置换,将数组元素按位逆序排列
void bitReverse(vector<Complex> &a) {
int n = a.size();
for (int i = 1, j = 0; i < n; i++) {
int bit = n >> 1;
for (; j & bit; bit >>= 1)
j ^= bit;
j ^= bit;
if (i < j)
swap(a[i], a[j]);
}
}
// FFT函数,如果inv=1则为正变换,inv=-1为逆变换
void fft(vector<Complex> &a, int inv) {
int n = a.size();
bitReverse(a);
for (int len = 2; len <= n; len <<= 1) {
double ang = 2 * PI / len * inv;
Complex wn(cos(ang), sin(ang));
for (int i = 0; i < n; i += len) {
Complex w(1, 0);
for (int j = 0; j < len/2; j++) {
Complex u = a[i+j], v = w * a[i+j+len/2];
a[i+j] = u + v;
a[i+j+len/2] = u - v;
w *= wn;
}
}
}
if (inv == -1) {
for (int i = 0; i < n; i++)
a[i] /= n;
}
}
// 负二进制字符串转系数数组(从低位到高位存储)
vector<int> negBaseStringToArray(const string& s) {
vector<int> arr;
// 从字符串末尾(最低位)开始,逆序存储
for (int i = s.size()-1; i>=0; i--) {
arr.push_back(s[i]-'0');
}
return arr;
}
// 系数数组(从低位到高位)转负二进制字符串(从高位到低位输出)
string arrayToNegBaseString(const vector<int>& arr) {
if (arr.empty()) return "0";
vector<int> tmp = arr;
// 去掉高位的0(从最高位开始,即数组最后一个元素)
int n = tmp.size();
int i = n-1;
while (i>=0 && tmp[i]==0) i--;
if (i<0) return "0";
string res = "";
for (; i>=0; i--) {
res += ('0' + tmp[i]);
}
return res;
}
// 计算卷积,并调整进位
vector<int> multiply(vector<int> a, vector<int> b) {
// 计算卷积长度
int n = 1;
int lenA = a.size(), lenB = b.size();
int totalSize = lenA + lenB - 1;
while (n < totalSize) n <<= 1;
vector<Complex> ca(n, 0), cb(n, 0);
for (int i=0; i<lenA; i++) ca[i] = Complex(a[i], 0);
for (int i=0; i<lenB; i++) cb[i] = Complex(b[i], 0);
// FFT
fft(ca, 1);
fft(cb, 1);
// 点乘
for (int i=0; i<n; i++) ca[i] *= cb[i];
// IFFT
fft(ca, -1);
// 取实部,四舍五入
vector<int> c(n);
for (int i=0; i<n; i++) {
// 四舍五入,注意可能为负
double real = ca[i].real();
if (real >= 0)
c[i] = int(real + 0.5);
else
c[i] = int(real - 0.5);
}
// 现在c是卷积结果(系数数组),长度n,索引0对应常数项(即最低位)
// 进位调整(基-2进位)
vector<int> res; // 结果数组(从低位到高位)
int carry = 0;
for (int i=0; i<n || carry; i++) {
int v = carry;
if (i < n) v += c[i];
// 计算当前位和进位
int r = v % 2;
if (r < 0) r += 2; // 确保余数非负(但v%2在C++中,如果v为负,结果可能是-1或0,所以调整)
// 实际上,我们取非负余数
r = r % 2;
if (r < 0) r += 2; // 确保0<=r<2
r %= 2; // 确保0或1
carry = (v - r) / (-2); // 进位
if (i < n)
res.push_back(r);
else
res.push_back(r);
}
// 注意:进位调整后,可能有高位的0,但我们在转换为字符串时会去掉
return res;
}
int main() {
string s1, s2;
cout << "Enter first number in base -2: ";
cin >> s1;
cout << "Enter second number in base -2: ";
cin >> s2;
// 转换为数组(从低位到高位)
vector<int> a = negBaseStringToArray(s1);
vector<int> b = negBaseStringToArray(s2);
// 乘法
vector<int> c = multiply(a, b);
// 转换为字符串
string res = arrayToNegBaseString(c);
cout << "Result: " << res << endl;
return 0;
}
```
注意:上面的进位调整部分,我们使用了:
r = v % 2;
if (r<0) r+=2; // 这样确保r在[0,1]区间
r %= 2; // 再次取模确保
然后carry = (v - r) / (-2);
另外,在调整进位时,如果i超过原数组长度,则继续向高位进位(在res数组中添加新位)。
测试:输入"1"和"1",输出"1";输入"110"(表示2)和"110"(表示2),输出"100"(表示4)。
注意:负二进制表示中,0的表示是"0",所以如果输入0(比如"0"),则数组为空?我们在转换函数中,对于空数组返回"0"。
但是,在negBaseStringToArray函数中,如果输入字符串是"0",则我们转换后的数组是[0](因为从最后一个字符开始,只有一个字符'0',所以push_back(0))。所以数组为[0],然后在乘法中,0乘以任何数都是0,进位调整后数组为[0],然后转换为字符串时,去掉高位0(数组只有一个0,所以i从0开始,然后i=0时,tmp[0]=0,然后i--后i=-1,所以返回"0")。
所以,可以处理0。
但是,有一个潜在问题:在进位调整时,如果v为0,则r=0,carry=0,没问题。如果v为1,则r=1,carry=0。如果v为2,则r=0,carry=(2-0)/(-2)=-1。然后下一位加上-1。
测试:v=-1:r = (-1)%2,在C++中,负数的模运算:(-1)%2的结果可能是-1(取决于编译器),所以我们需要调整。
在代码中,我们做了:r = v%2; if (r<0) r+=2; 然后r%=2; 这样,如果v=-1,则r=-1 -> 调整:r=1(因为-1+2=1),然后carry=(-1-1)/(-2)= (-2)/(-2)=1。所以正确。
因此,进位调整部分可以处理负数。
但是,注意:在卷积结果中,系数可能是负数,所以需要这样处理。
另外,在取整时,如果卷积结果为负,则四舍五入取整(加0.5或减0.5)可能会引入误差?但因为我们卷积结果是整数,所以理论上实部应该是整数,但由于浮点误差,可能接近整数,所以四舍五入应该正确。
测试:卷积结果应该为整数,所以四舍五入到最近整数。
如果浮点误差导致离两个整数一样近?但卷积结果应该是整数,所以理论上实部就是整数,但由于计算误差,可能不是整数。所以,我们四舍五入到最近整数。
例如:实部为2.4999,则取整为2;实部为2.5,则取整为3。但卷积结果应该是整数,所以2.5可能是由于浮点误差,但实际应该是3?所以四舍五入是合理的。
注意:FFT的精度问题可能导致误差,所以如果位数很大(比如几万位),则需要用NTT。但这里我们假设位数较小(几百位)。
最后,我们输出结果。
注意:我们只实现了乘法,没有考虑负数?因为负二进制表示已经包含了符号。
例如:-1的负二进制是"11",所以输入负数也是可以的。
因此,代码完成。
但是,我们测试一个负数乘法:(-1)*2 = -2。
-1: "11" -> 数组[1,1](低位1,高位1)
2: "110" -> 数组[0,1,1](低位0,1,1)
卷积:a=[1,1](补零到长度4:变成[1,1,0,0]),b=[0,1,1](补零到[0,1,1,0])
FFT后点乘再逆变换,得到卷积系数数组(整数):
a(x)=1+x, b(x)=0+1*x+1*x^2
c(x)= (1+x)*(x+x^2) = x + 2x^2 + x^3
系数数组:[0,1,2,1](常数项0,x项1,x^2项2,x^3项1)
进位调整:
i0: v=0 -> r=0, carry=0 -> [0,1,2,1] -> 0
i1: v=1 -> r=1, carry=0 -> 1
i2: v=2 -> r=0, carry=(2-0)/(-2)=-1 -> 0, carry=-1
i3: v=1+(-1)=0 -> r=0, carry=0 -> 0
调整后数组:[0,1,0,0](注意,进位调整后,数组长度不变,但这里i3后没有进位,所以数组为[0,1,0,0](索引0:0,1:1,2:0,3:0))
转换为字符串:从最高位(索引3)开始,为0;索引2为0;索引1为1(非0),然后索引0为0 -> 所以输出"10"(即索引1:1和索引0:0,但注意:数组索引0是最低位,索引1是次低位,所以字符串从高到低:索引3,2,1,0 -> 0,0,1,0 -> 去掉前导0后为"10")。
但是,-2的负二进制表示:-2 = 0*(-2)^0 + 1*(-2)^1 + 1*(-2)^2? 验证:0+(-2)+4=2,不对。
实际上,-2的负二进制表示:-2 = 1*(-2)^1 + 1*(-2)^0? 1*(-2)^1 + 1*(-2)^0 = -2+1=-1,不对。
正确表示:-2 = 10(负二进制)?验证:1*(-2)^1 + 0*(-2)^0 = -2+0=-2,所以"10"(字符串)表示-2。所以输出"10"正确。
所以,测试通过。
注意:负二进制表示中,-2的表示是"10",即从高位到低位:1,0。
因此,代码正确。
最后,注意在进位调整时,我们创建了一个新的res数组,而不是在原来的c数组上修改。因为进位可能增加位数,所以动态扩展。
完整代码如上。
注意:在FFT函数中,我们使用了位逆序置换,然后蝶形运算。蝶形运算中,len从2开始,每次翻倍。
由于时间关系,我们不再优化。
运行示例:
输入:s1="110", s2="110" (2乘2)
输出:100(4)
输入:s1="11", s2="110" (-1乘2)
输出:10(-2)
输入:s1="1", s2="1" (1乘1)
输出:1(1)
输入:s1="0", s2="110" (0乘2)
输出:0
输入:s1="111", s2="1" (3乘1=3)-> 3的负二进制:3 = 1*(-2)^0 + 1*(-2)^1 + 1*(-2)^2? 1-2+4=3,所以"111"表示3。乘积3,负二进制表示还是"111"?不对,因为3的负二进制表示是"111"?验证:1+(-2)+4=3,正确。所以"111"*"1" -> 输出"111"。
但是,我们计算:a=[1,1,1](3),b=[1](1),卷积:1+1*x+1*x^2 乘以 1 -> 1+1*x+1*x^2,系数数组[1,1,1]。
进位调整:i0:1->r=1,carry=0; i1:1->r=1,carry=0; i2:1->r=1,carry=0; 所以数组[1,1,1] -> 字符串"111"(从高位到低位:1,1,1 -> 输出"111")。
但是,3的负二进制表示是"111"吗?注意,字符串"111":第一个1是最高位(权重(-2)^2=4),第二个1是(-2)^1=-2,第三个1是(-2)^0=1,所以4-2+1=3,正确。
所以输出正确。
因此,代码实现完成。
注意:如果输入字符串包含非'0'和'1'的字符,需要检查,但题目假设输入合法。
最后,我们输出代码。
注意:在逆变换后,我们除以了n(在逆变换函数中),所以点乘后逆变换得到的是卷积结果除以n?不,我们在逆变换函数中已经除以了n(在inv=-1时),所以最后取整时就是卷积结果。
完整代码:</think>以下是使用FFT算法实现负二进制(base -2)表示的两个数乘法的C++代码。该算法通过将数字转换为多项式形式,利用FFT加速多项式乘法,最后将结果转换回负二进制表示。
```cpp
#include <iostream>
#include <vector>
#include <cmath>
#include <complex>
#include <string>
#include <algorithm>
using namespace std;
const double PI = acos(-1.0);
typedef complex<double> Complex;
// 位逆序置换
void bitReverse(vector<Complex>& a) {
int n = a.size();
for (int i = 1, j = 0; i < n; i++) {
int bit = n >> 1;
for (; j & bit; bit >>= 1)
j ^= bit;
j ^= bit;
if (i < j)
swap(a[i], a[j]);
}
}
// FFT变换,inv=1为正变换,inv=-1为逆变换
void fft(vector<Complex>& a, int inv) {
int n = a.size();
bitReverse(a);
for (int len = 2; len <= n; len <<= 1) {
double ang = 2 * PI / len * inv;
Complex wn(cos(ang), sin(ang));
for (int i = 0; i < n; i += len) {
Complex w(1, 0);
for (int j = 0; j < len / 2; j++) {
Complex u = a[i + j];
Complex v = w * a[i + j + len / 2];
a[i + j] = u + v;
a[i + j + len / 2] = u - v;
w *= wn;
}
}
}
if (inv == -1) {
for (int i = 0; i < n; i++)
a[i] /= n;
}
}
// 负二进制字符串转系数数组(低位在前)
vector<int> negBaseStringToArray(const string& s) {
vector<int> arr;
for (int i = s.size() - 1; i >= 0; i--) {
arr.push_back(s[i] - '0');
}
return arr;
}
// 系数数组转负二进制字符串(高位在前)
string arrayToNegBaseString(const vector<int>& arr) {
if (arr.empty()) return "0";
int n = arr.size();
int i = n - 1;
while (i >= 0 && arr[i] == 0) i--;
if (i < 0) return "0";
string res;
for (; i >= 0; i--) {
res += ('0' + arr[i]);
}
return res;
}
// 负二进制乘法核心:FFT卷积 + 基-2进位调整
vector<int> multiply(vector<int> a, vector<int> b) {
int lenA = a.size(), lenB = b.size();
int totalSize = lenA + lenB - 1;
int n = 1;
while (n < totalSize) n <<= 1;
vector<Complex> ca(n, 0), cb(n, 0);
for (int i = 0; i < lenA; i++) ca[i] = Complex(a[i], 0);
for (int i = 0; i < lenB; i++) cb[i] = Complex(b[i], 0);
fft(ca, 1);
fft(cb, 1);
for (int i = 0; i < n; i++) ca[i] *= cb[i];
fft(ca, -1);
vector<int> c(n);
for (int i = 0; i < n; i++) {
c[i] = (int)round(ca[i].real());
}
vector<int> res;
int carry = 0;
for (int i = 0; i < n || carry != 0; i++) {
int v = carry;
if (i < n) v += c[i];
int r = v % 2;
if (r < 0) r += 2;
r %= 2;
carry = (v - r) / (-2);
res.push_back(r);
}
return res;
}
int main() {
string num1, num2;
cout << "Enter first number in base -2: ";
cin >> num1;
cout << "Enter second number in base -2: ";
cin >> num2;
vector<int> a = negBaseStringToArray(num1);
vector<int> b = negBaseStringToArray(num2);
vector<int> result = multiply(a, b);
string res_str = arrayToNegBaseString(result);
cout << "Multiplication result: " << res_str << endl;
return 0;
}
```
### 算法说明
1. **负二进制表示**:
- 数字表示为 $n = \sum_{k=0}^{m-1} d_k \times (-2)^k$,其中 $d_k \in \{0,1\}$
- 示例:`"110"` → $0×(-2)^0 + 1×(-2)^1 + 1×(-2)^2 = -2 + 4 = 2$
2. **算法步骤**:
- **步骤1**:将负二进制字符串转换为系数数组(低位在前)
- **步骤2**:通过FFT计算多项式卷积(时间复杂度 $O(n \log n)$)
- **步骤3**:基-2进位调整(处理负权重进位)
- **步骤4**:将结果数组转换回负二进制字符串
3. **关键优化**:
- 使用迭代FFT避免递归开销
- 动态进位调整处理负权重
- 高位零压缩优化输出
### 示例测试
| 输入 (base -2) | 解释 | 结果 |
|----------------|------|------|
| `"1" * "1"` | $1 × 1 = 1$ | `"1"` |
| `"110" * "110"`| $2 × 2 = 4$ | `"100"` |
| `"11" * "110"` | $-1 × 2 = -2$ | `"10"` |
| `"111" * "1"` | $3 × 1 = 3$ | `"111"` |
### 复杂度分析
- **时间复杂度**:$O(n \log n)$,主导步骤为FFT
- **空间复杂度**:$O(n)$,存储系数数组和复数向量
### 相关问题
1. FFT算法如何处理大整数乘法中的精度问题?
[当数字极大时,浮点精度不足会导致错误结果,此时需改用数论变换(NTT)或拆解卷积][^1]
2. 负二进制表示相比标准二进制有哪些应用优势?
[负二进制允许无符号表示负数,简化某些数学运算][^2]
3. 如何优化FFT在嵌入式系统中的内存占用?
[可采用迭代FFT减少栈开销,或使用实数FFT优化存储][^3]
4. 进位调整步骤是否适用于其他负基数系统?
[是,类似算法可推广到任意负基数系统,只需修改进位规则][^4]
[^1]: 引用[1]涉及预测步骤,但FFT精度问题需结合数值分析处理
[^2]: 引用[2]讨论结构化编程,负二进制表示简化了符号处理
[^3]: 引用[3]分治思想与FFT的分治特性一致
[^4]: 引用[4]中参数构造类似多项式转换的预处理