蒟蒻桃酱的算法笔记——FFT从入门到使用!详解!(ACM/OI)

(首先膜一发高中生qwq亏我学了信号处理原理专业课了竟然是看高中生的博客才能看懂qwq。。。大概断断续续看了几十篇吧。。。

序言

可能是许久以来写的最认真的一篇了吧,加上今天感觉不太舒服,但是觉得要开始扎实的学点什么,所以,就从现在开始吧。感谢所有认真写博客的人的帮助!!!希望我也能贡献点什么qwq。。。

一直想学FFT,之前牛客的多小有一道组合数学就用FFT写的,而且当时还傻乎乎的用唯一分解定理,但是自己好久没静下心学什么了,而且自己的数学功底又不好,导致一直学不会。看了很多人的博客也没看明白,尤其是原根。但是看了一个OIer的博客,顿时明白了。。。然后赶紧记录下来。

另外,本文的代码实现全部参考匡斌(kuangbin)的模板(2018.7更新)

前言

你搜索这个关键词就已经知道这一是个数学的东西了。只想学会用很简单,但是这远远不够。所以在看这个博客之前应该先学一下复数的基本知识。

好了下面进入正文。

1.DFT IDFT FFT官方定义?

离散傅里叶变换(Discrete Fourier Transform,缩写为DFT),是傅里叶变换时域频域上都呈离散的形式,将信号的时域采样变换为其DTFT的频域采样。

FFT是一种DFT的高效算法,称为快速傅立叶变换(fast Fourier transform)。

                                              ——百度百科

在百度百科上能找到DFT和FFT这两个定义。正如定义,FFT和DFT实际上按照结果来看的话是一样的,但是FFT比较快的计算DFT和IDFT(离散反傅里叶变换·)。

快速数论变换(NTT)是快速傅里叶变换(FFT)在数论基础上的实现。

是不是有点迷QAQ?既然是官方定义那肯定不能让你看懂才对嘛~下面我们一一解释~

2.为什么要使用FFT?

我们在这里引入一个例子:求多项式乘积的朴素算法。

大家平时求    f(x) = a^{_{1}}f(x) = a_{1}x^2 + b_{1}x + c_{1}   与  g(x) = a_{2}x^2 + b_{2}x + c_{2}  的乘积时候,是怎么进行的呢?

我们令

K(x) = f(x)*g(x) = a_{1}x^2 *a_{2}x^2 + a_{1}x^2*b_{2}x + a_{1}x^2*c_{2} + b_{1}x *b_{2}x^2 + b_{1}x*b_{2}x + b_{1}x*c_{2} + c_{1} *a_{2}x^2 + c_{1}*b_{2}x + c_{1}*c_{2}

那么很显然我们进行了9次运算,复杂度是O(n^2).(具体代码实现不再展开)

但是如果数字足够大呢?比如100000?那朴素算法可太慢啦(;′⌒`),

3.学习FFT

3.1 在学习FFT之前...

3.1.1什么是FFT

FFT,即为快速傅氏变换,是离散傅氏变换的快速算法,它是根据离散傅氏变换的奇、偶、虚、实等特性,对离散傅立叶变换的算法进行改进获得的。它对傅氏变换的理论并没有新的发现,但是对于在计算机系统或者说数字系统中应用离散傅立叶变换,可以说是进了一大步。——360百科

如果上一个例子用朴素算法太慢啦!所以我们要用FFT进行优化,复杂度会降为O(nlog(n))

3.1.2项式的系数表示法与点值表示法

一个多项式,我们可以怎样来表示呢?

系数表示法就是用一个多项式的各个项系数来表达这个多项式。比如:

f(x) = a_{1}x^2 + b_{1}x + c_{1}\Leftrightarrow f(x) = \{a_{1},b_{1},c_{1}\}

点值表示法是把这个多项式看成一个函数,从上面选取n+1个点,从而利用这n+1个点来唯一的表示这个函数。为什么用(n+1)个点就能唯一的表示这个函数了呢?想一下高斯消元法,两点确定一条直线。再来一个点,能确定这个直线中的另一个参数,那么也就是说(n+1)个点能确定n个参数(不考虑倍数点之类的没用点)。如下:

\\f_{1}(x) = y_{1} = a_{0} + a_{1}x _{1}+ a_{2}x_{1}^2 + a_{3}x_{1}^3+...+a_{n}x_{1}^n\\ f_{2}(x) = y_{2} = a_{0} + a_{1}x_{2} + a_{2}x_{2}^2 + a_{3}x_{2}^3+...+a_{n}x_{2}^n\\ f_{3}(x) = y_{3} = a_{0} + a_{1}x_{3} + a_{2}x^2_{3} + a_{3}x_{3}^3+...+a_{n}x_{3}^n\\ f_{4}(x) = y_{4} = a_{0} + a_{1}x_{4} + a_{2}x_{4}^2 + a_{3}x_{4}^3+...+a_{n}x_{4}^n\\..\\ f_{n}(x) = y_{n} = a_{0} + a_{1}x_{n} + a_{2}x_{n}^2 + a_{3}x_{n}^3+...+a_{n}x_{n}^n\\

多项式由系数表示法转为点值表示法的过程,就成为DFT;

相对地,把一个多项式的点值表示法转化为系数表示法的过程,就是IDFT。

而FFT就是通过取某些特殊的x的点值来加速DFT和FFT的过程。

3.1.3复数的引入

复数分为实数和虚数。实数就是我们日常最常用的有理数和无理数。大家记得我们在开始学平方的时候,老师会说所有数的平方大于等于0对不对,那么虚数就引入了。虚数一般用i表示,对于虚数i,有i = \sqrt{-1}。另外,i对于虚数的意义,与1对于实数的意义是一样的。如果我说得不够明确,你可以看下面我引用的百科说明。

在数学中,虚数就是形如a+b*i的数,其中a,b是实数,且b≠0,i² = - 1。虚数这个名词是17世纪著名数学家笛卡尔创立,因为当时的观念认为这是真实不存在的数字。后来发现虚数a+b*i的实部a可对应平面上的横轴,虚部b与对应平面上的纵轴,这样虚数a+b*i可与平面内的点(a,b)对应。

可以将虚数bi添加到实数a以形成形式a + bi的复数,其中实数a和b分别被称为复数的实部和虚部。一些作者使用术语纯虚数来表示所谓的虚数,虚数表示具有非零虚部的任何复数

                                                                                                                                                                    ——百度百科

下面我们用一幅图来表示复数与复平面的关系(图源百度百科)

其中横坐标是实数轴,纵坐标是虚数轴,这样就可以把每个虚数看为一个向量了,对应的,虚数可以用普通坐标和极坐标(r,\theta )(其中r为虚数长度,theta为虚数和实数轴正半轴夹角)来表示。

接下来思考两个复数相乘是什么意义:

1.(a + bi)*(c+di) = (ac - bd) + (ad + bc)i

2.长度相乘,角度相加:(r_{1},\theta_{1})*(r_{2},\theta_{2}) = (r_{1} * r_{2},\theta_{1} + \theta_{2})这么一看的话,我们很容易想到如果两个长度为1的不同方向向量相乘,结果向量是不是一个长度依然为1的新向量呢?

3.1.4 单位复根的引入

我们回到之前的问题:多项式(点值表示法)的乘积。

考虑这样一个问题:

刚刚说到了DFT是把多项式从系数表示转到了点值表示(复杂度为O(n)),那么我们把点值相乘之后(选取相应位置,并且复杂度为O(n)),如果能够快速还原成系数表示,是不是就完美解决我们的问题了呢?上述过程如下:

假设我们DFT过程对于两个多项式选取的x序列相同,那么可以得到

\\f(x) = \{(x_{0},f(x_{0})),(x_{1},f(x_{1})),(x_{2},f(x_{2}))...(x_{n},f(x_{n}))\}\\ g(x) = \{(x_{0},g(x_{0})),(x_{1},g(x_{1})),(x_{2},g(x_{2}))...(x_{n},g(x_{n}))\}

如果我们设F(x) = f(x)*g(x)那么很容易得到F(x)的点值表达式:

F(x) = \{(x_{0},f(x_0)*g(x_{0})),(x_{1},f(x_1)*g(x_{1})),(x_{2},f(x_2)*g(x_{2}))...(x_{n},f(x_n)*g(x_{n}))\}

但是我们要的是系数表达式,接下来问题变成了从点值回到系数。如果我们带入到高斯消元法的方程组中去,会把复杂度变得非常高。光是计算x^i (0\leq i\leq n)就是n项,这就已经O(n^2)了,更别说还要把(n+1)个方程进行消元。。。。。。。。。

这里会不会觉得我们不去计算x^i比较好呢?1和-1的幂都很好算,但是也仅仅有两个不够啊,我们至少需要(n+1)个o(╥﹏╥)o那怎么办呢!想到我们刚刚学的长度为1的虚数了吗?不管怎么乘长度都是1!对就是它!我们需要的是\omega ^k = 1中的\omega,很容易想到-i和1是符合的。那其他的呢?

现在我们看上图的圆圈。容易发现这是一个单位圆(圆心为原点,半径为1),所有在圆上的复数的长度均为1,也就是说它不管做多少次方r永远为1,结果也仅仅角度的变化而已。但是!!!进过旋转总会让角度%360 == 0成立的,也就是结果为1.我们把符合以上条件的复数成为复根,用\omega表示。如果\omega ^k = 1,那么我们把\omega称为1的k次复根,记作\omega _k{}^n(因为符合这个k次之后等于1的复数有很多,比如i的4k次幂永远为1,所以,这个n是一个编号,表示这是角度从小到大的第几个(从x的正半轴开始逆时针))

是不是有点雾啊( ̄▽ ̄)/没事没事接下来我们举个栗子:

那么很容易发现当K = 4的时候,相当于把单位圆等分K= 4份。然后每一份按照极角编号。那么是不是(在K = 4的时候)我们只要知道\omega ^1_{4}(因为他的角度是相当于单位角度),就能知道\omega ^0_4\omega ^2_{4}\omega ^3_{4}了呢?当然是这样的。。。\omega ^0_4恒等于1,\omega ^2_{4} 的角度是\omega ^1_{4}的两倍,所以\omega ^2_{4} = (\omega ^1_{4})^2 = i^2 = -1,依次依次类推。

因此,我们只要知道\omega ^1_{k},就能求出\omega ^n_{k}。所以我们把\omega ^1_{k}称为单位复根,简写为\omega _{k}

3.2 FFT的流程

qwq终于写到核心部分了,也就是,FFT到底怎么来写呢?

3.2.1 FFT流程第一步之DFT(共两步)

FFT之所以快,是因为他采用了分治的思想。

就DFT(将系数表达转换成点值表达)来说,它分治的来求当当前的x = \omega ^k_{n}的时候整个式子的值。他的分治思想体现在将多项式分为奇次项和偶次项处理。

对于一共8项的多项式

\\f(x) = y_{1} = a_{0} + a_{1}x+ a_{2}x^2 + a_{3}x^3+a_{4}x^4 + a_{5}x^5 + a_{6}x^6 + a_{7}x^7

按照次数的奇偶来分成两组,然后右边提出来一个x

\\f(x) =( a_{0}+ a_{2}x^2 +a_{4}x^4+ a_{6}x^6 )+( a_{1}x+ a_{3}x^3+ a_{5}x^5+ a_{7}x^7)\\f(x)=( a_{0}+ a_{2}x^2 +a_{4}x^4+ a_{6}x^6 )+x( a_{1}+ a_{3}x^2+ a_{5}x^4+ a_{7}x^6)\\

分别用奇偶次次项数建立新的方程

\\G(x)= a_{0}+ a_{2}x +a_{4}x^2+ a_{6}x^3 \\ H(x) = a_{1}+ a_{3}x+ a_{5}x^2+ a_{7}x^3\\

那么原来的f(x)由新函数来表示(是不是我们二分了一个多项式呢~)

f(x) = G(x^2) + x*H(x^2)

给函数带个帽子表示此时在进行的是DFT过程,把x代进去,即有DFT(f(\omega ^n_{k})) = DFT(G((\omega ^n_{k})^2)) + \omega ^n_{k}DFT(H((\omega ^n_{k})^2))

!!!前方高能:

这个函数能处理的多项式长度只能是2^m(m\in N^*),否则在分治的时候左右不一样长,右边取不到系数了,程序没法进行。所以要在第一次DFT之前就把序列向上补成长度为2^m(m\in N^*)(高次系数补0)、最高项次数为(n-1)的多项式。

然后我在代入值的时候,因为要代入n个不同值,所以我们就代入\omega ^0_{n},\omega ^1_{n},\omega ^2_{n}...\omega _{n}^{n-1}(n = 2^m(m\in N^*))一共2^m个不同值。

/*
*做FFT 
*len必须是2^k形式 
*on == 1时是DFT,on == -1时是IDFT 
*/
void fft(Complex y[],int len){
	change(y,len);
	for(int h = 2;h <= len;h<<=1){
		Complex wn(cos(-1*2*PI/h),sin(-1*2*PI/h));
		for(int j = 0;j < len;j += h){
			Complex w(1,0);
			for(int k = j;k < j + h/2;k++){
				Complex u = y[k];
				Complex t = w*y[k + h/2];
				y[k] = u + t;
				y[k + h/2] = u - t;
				w = w*wn;
			}
		}
	}
} 

但是这个算法还需要从“分治”的角度继续优化。我们每一次都会把整个多项式的奇数次项和偶数次项系数分开,一只分到只剩下一个系数。但是,这个递归的过程需要更多的内存。因此,我们可以先“模仿递归”把这些系数在原数组中“拆分”,然后再“倍增”地去合并这些算出来的值。然而我们又要如何去拆分这些数呢?

设初始序列为 \{x_{0},x_{1},x_{2},x_{3},x_{4},x_{5},x_{6},x_{7}\}

一次二分之后 \{x_{0},x_{2},x_{4},x_{6}\},\{x_{1},x_{3},x_{5},x_{7}\}

两次二分之后 \{x_{0},x_{4}\}\{x_{2},x_{6}\}\{x_{1},x_{3}\}\{x_{5},x_{7}\}

三次二分之后 \{x_{0}\}\{x_{4}\}\{x_{2}\}\{x_{6}\}\{x_{1}\}\{x_{3}\}\{x_{5}\}\{x_{7}\}

有啥规律呢?其实就是原来的那个序列,每个数用二进制表示,然后把二进制翻转对称一下,就是最终那个位置的下标。比如x_{1}是001,翻转是100,也就是4,而且最后那个位置确实是4,是不是很神奇啊~~~

这里附上代码

/*
*进行FFT和IFFT前的反置变换 
*位置i和i的二进制反转后的位置互换 
*len必须为2的幂 
*/ 
void change(Complex y[],int len){
	int i,j,k;
	for(int i = 1,j = len/2;i<len-1;i++){
		if(i < j)	swap(y[i],y[j]);
		//交换互为小标反转的元素,i<j保证交换一次
		//i做正常的+1,j做反转类型的+1,始终保持i和j是反转的
		k = len/2;
		while(j >= k){
			j = j - k;
			k = k/2;
		} 
		if(j < k)	j+=k;
	}
} 

3.2.2FFT流程第二步之IDFT(共两步)

这一步IDFT(傅里叶反变换)的作用我说的已经很清楚啦,就是把上一步获得的目标多项式的点值形式转换成系数形式。但是似乎并不简单呢(雾)。。。但是,我们把单位复根代入多项式之后,就是下面这个样子(矩阵表示方程组)

而且现在我们已经得到最左边的结果了,中间的x值在目标多项式的点值表示中也是一一对应的,所以,根据矩阵的基础知识,我们只要在式子两边左乘中间那个大矩阵的逆矩阵就行了。由于这个矩阵的元素非常特殊,他的逆矩阵也有特殊的性质,就是每一项取倒数,再除以n,就能得到他的逆矩阵(这边根据的是单位原根的两个特殊性质推出来的,具体比较麻烦。如果想知道的话私我吧。)

如何改变我们的操作才能使计算的结果文原来的倒数呢?我们当然可以重新写一遍,但是这里有更简单的实现。这就要看我们求“单位复根的过程了”:根据“欧拉函数”e^iπ=-1,我么可以得到e^2πi=1。如果我要找到一个数,它的k次方=1,那么这个数ω[k]=e^(2πi/k)(因为(e^(2πi/k))^k=e^(2πi)=1)。而如果我要使这个数值变成“1/ω[k]”也就是“(ω[k])^-1”,我们可以尝试着把π取成-3.14159…,这样我们的计算结果就会变成原来的倒数,而其它的操作过程与DFT是完全相同的(这真是极好的)。我们可以定义一个函数,向里面掺一个参数“1”或者是“-1”,然后把它乘到“π”的身上。传入“1”就是DFT,传入“-1”就是IDFT,十分的智能。

所以我们fft函数可以集DFT和IDFT于一身。见下

/*
*做FFT 
*len必须是2^k形式 
*on == 1时是DFT,on == -1时是IDFT 
*/
void fft(Complex y[],int len,int on){
	change(y,len);
	for(int h = 2;h <= len;h<<=1){//模拟合并过程 
		Complex wn(cos(-on*2*PI/h),sin(-on*2*PI/h));//计算当前单位复根
		for(int j = 0;j < len;j += h){
			Complex w(1,0);//计算当前单位复根
			for(int k = j;k < j + h/2;k++){
				Complex u = y[k];
				Complex t = w*y[k + h/2];
				y[k] = u + t;//这就是吧两部分分治的结果加起来 
				y[k + h/2] = u - t;
				//后半个“step”中的ω一定和“前半个”中的成相反数
                //“红圈”上的点转一整圈“转回来”,转半圈正好转成相反数
                //一个数相反数的平方与这个数自身的平方相等
				w = w*wn;
			}
		}
	}
	if(on == 1){
		for(int i = 0;i < len;i++){
			y[i].x /= len;
		}
	}
} 

好了现在附上全部代码,序言说过代码来自kuangbin的模板~~~~~来大家和我一起Orz一发

#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>

using namespace std;

const double PI = acos(-1.0);
struct Complex{
	double x,y;
	Complex(double _x = 0.0,double _y = 0.0){
		x = _x;
		y = _y;
	}
	Complex operator-(const Complex &b)const{
		return Complex(x - b.x,y - b.y);
	}
	Complex operator+(const Complex &b)const{
		return Complex(x + b.x,y + b.y);
	}
	Complex operator*(const Complex &b)const{
		return Complex(x*b.x - y*b.y,x*b.y + y*b.x);
	}
};
/*
*进行FFT和IFFT前的反置变换 
*位置i和i的二进制反转后的位置互换 
*len必须为2的幂 
*/ 
void change(Complex y[],int len){
	int i,j,k;
	for(int i = 1,j = len/2;i<len-1;i++){
		if(i < j)	swap(y[i],y[j]);
		//交换互为小标反转的元素,i<j保证交换一次
		//i做正常的+1,j做反转类型的+1,始终保持i和j是反转的
		k = len/2;
		while(j >= k){
			j = j - k;
			k = k/2;
		} 
		if(j < k)	j+=k;
	}
} 
/*
*做FFT 
*len必须是2^k形式 
*on == 1时是DFT,on == -1时是IDFT 
*/
void fft(Complex y[],int len,int on){
	change(y,len);
	for(int h = 2;h <= len;h<<=1){
		Complex wn(cos(-on*2*PI/h),sin(-on*2*PI/h));
		for(int j = 0;j < len;j += h){
			Complex w(1,0);
			for(int k = j;k < j + h/2;k++){
				Complex u = y[k];
				Complex t = w*y[k + h/2];
				y[k] = u + t;
				y[k + h/2] = u - t;
				w = w*wn;
			}
		}
	}
	if(on == 1){
		for(int i = 0;i < len;i++){
			y[i].x /= len;
		}
	}
} 

const int MAXN = 200020;
Complex x1[MAXN],x2[MAXN];
char str1[MAXN/2],str2[MAXN/2];
int sum[MAXN];


int main(){
	while(scanf("%s%s",str1,str2) == 2){
		int len1 = strlen(str1);
		int len2 = strlen(str2);
		int len = 1;
		while(len < len1*2||len < len2*2)	len <<= 1;
		for(int i = 0;i < len1;i++)
			x1[i] = Complex(str1[len-1-i] - '0',0);
		for(int i = len1;i < len;i++)
			x1[i] = Complex(0,0);
		for(int i = 0;i < len2;i++)
			x2[i] = Complex(str2[len2-1-i] - '0',0);
		for(int i = len2;i < len;i++)
			x2[i] = Complex(0,0);
		fft(x1,len,1);
		fft(x2,len,1);
		for(int i = 0;i < len;i++)
			x1[i] = x1[i]*x2[i];
		fft(x1,len,-1);
		for(int i = 0;i < len;i++)
			sum[i] = int(x1[i].x + 0.5);
		while(sum[len] == 0&&len > 0)	len--;
		for(int i = len;i >= 0;i--)
			printf("%c",sum[i] + '0');
		printf("\n");
	}
	return 0;
} 

至此,FFT算是告一段落了。

但是,算竞选手可能像我一样有下面的疑问:

假如我要计算的多项式系数是别的具有特殊意义的整数,那么我通篇都在用浮点数运算,首先从时间上就会比整数运算慢,另外我最多只能用long double不能用long long类型,我能不能应用数论的变化从而避开浮点运算,达到“更高更快更强(*・ω< ) ”呢?

4.算竞选手看过来~NTT(数论优化的快速傅里叶变换)   

NTT解决的是多项式乘法带模数的情况,可以说有些受模数的限制,数也比较大,

但是它比较方便呀毕竟没有复数部分qwq


4.1 学习NTT之前...

4.1.1生成子群

子群:群(S,⊕), (S′,⊕), 满足S′⊂S,则(S′,⊕)是(S,⊕)的子群

拉格朗日定理:|S′|∣|S|证明需要用到陪集,得到陪集大小等于子群大小,每个陪集要么不想交要么相等,所有陪集的并是集合S,那么显然成立。

生成子群a\in S的生成子群<a>=\{a^{(k)},k\geq 1\},a是<a>的生成元

:群S中a的阶是满足a^r=e的最小的r,符号ord(a),有ord(a)=|<a>|,显然成立。

考虑群Z^*_{n}=\{[a],n\in Z_{n}:gcd(a,n)=1\}, |Z^*_{n}|=\phi (n)
阶就是满足a^r\equiv 1(mod \ n)的最小的r, ord(a)=r

4.1.2 原根

g满足ord_{n}(g)=|Z^*_{n}|=\phi(n),对于质数p,也就是说g^imod\ p,0\leq i<p结果互不相同.

模n有原根的充要条件 :n=2,4,p^e,2p^e

离散对数:g^t\equiv a(mod\ n), ind_{n,g}(a)=t因为g是原根,所以gt每ϕ(n)是一个周期,可以取到|Z∗n|的所有元素
对于n是质数时,就是得到[1,n−1]的所有数,就是[0,n−2]到[1,n−1]的映射
离散对数满足对数的相关性质,如ind(ab)\equiv ind(a)+ind(b)(mod\ n-1)

求原根可以证明满足g^r\equiv 1(mod\ p)的最小的r一定是p−1的约数
对于质数p,质因子分解p−1,若g^{(p-1)/pi}\neq 1(mod\ p)恒成立,g为p的原根

4.2 NTT

对于质数p = qn + 1,(n = 2^m),原根g满足g^{qn}\equiv 1(mod\ p),将g_{n} = g^p(mod\ q)看做\omega _{n}的等价,择其满足相似的性质,比如g_{n}^n \equiv 1(mod\ p),g_{n}^{n/2} \equiv -1(mod \ p).

然后因为这里涉及到数论变化,所以这里的N(为了区分FFT中的n,我们把这里的n称为N)可以比FFT中的n大,但是只要把\frac{qN}{n}看做这里的q就行了,能够避免大小问题。。。

常见的有p=1004535809=479*2^{21}+1, g=3,p=998244353=2*17*2^{23}+1, g=3

g^{qn}就是e^{2\pi n}的等价

迭代到长度l时g_{l} = g^{\frac{p-1}{l}},或者\omega _{n} = g_{l} = g^{\frac{N}{l}}_{N} = g^{\frac{p-1}{l}}_{N}

接下来放一个大数相乘的模板

参考网址如下https://blog.csdn.net/blackjack_/article/details/79346433

#include<cmath>
#include<ctime>
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<iostream>
#include<algorithm>
#include<iomanip>
#include<vector>
#include<string>
#include<bitset>
#include<queue>
#include<map>
#include<set>
using namespace std;
 
inline int read()
{
	int x=0,f=1;char ch=getchar();
	while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
	while(ch<='9'&&ch>='0'){x=10*x+ch-'0';ch=getchar();}
	return x*f;
}
void print(int x)
{if(x<0)putchar('-'),x=-x;if(x>=10)print(x/10);putchar(x%10+'0');}
 
const int N=300100,P=998244353;
 
inline int qpow(int x,int y)
{
	int res(1);
	while(y)
	{
		if(y&1) res=1ll*res*x%P;
		x=1ll*x*x%P;
		y>>=1;
	}
	return res;
}
 
int r[N];
 
void ntt(int *x,int lim,int opt)
{
	register int i,j,k,m,gn,g,tmp;
	for(i=0;i<lim;++i)
		if(r[i]<i)
			swap(x[i],x[r[i]]);
	for(m=2;m<=lim;m<<=1)
	{
		k=m>>1;
		gn=qpow(3,(P-1)/m);
		for(i=0;i<lim;i+=m)
		{
			g=1;
			for(j=0;j<k;++j,g=1ll*g*gn%P)
			{
				tmp=1ll*x[i+j+k]*g%P;
				x[i+j+k]=(x[i+j]-tmp+P)%P;
				x[i+j]=(x[i+j]+tmp)%P;
			}
		}
	}
	if(opt==-1)
	{
		reverse(x+1,x+lim);
		register int inv=qpow(lim,P-2);
		for(i=0;i<lim;++i)
			x[i]=1ll*x[i]*inv%P;
	}
}
 
int A[N],B[N],C[N];
 
char a[N],b[N];
 
int main()
{
	register int i,lim(1),n;
	scanf("%s",&a);
	n=strlen(a);
	for(i=0;i<n;++i) A[i]=a[n-i-1]-'0';
	while(lim<(n<<1)) lim<<=1;
	scanf("%s",&b);
	n=strlen(b);
	for(i=0;i<n;++i) B[i]=b[n-i-1]-'0';
	while(lim<(n<<1)) lim<<=1;
	for(i=0;i<lim;++i)
		r[i]=(i&1)*(lim>>1)+(r[i>>1]>>1);
	ntt(A,lim,1);ntt(B,lim,1);
	for(i=0;i<lim;++i)
		C[i]=1ll*A[i]*B[i]%P;
	ntt(C,lim,-1);
	int len(0);
	for(i=0;i<lim;++i)
	{
		if(C[i]>=10)
			len=i+1,
			C[i+1]+=C[i]/10,C[i]%=10;
		if(C[i]) len=max(len,i);
	}
	while(C[len]>=10)
		C[len+1]+=C[len]/10,C[len]%=10,len++;
	for(i=len;~i;--i)
		putchar(C[i]+'0');
	puts("");
	return 0;
}

emmmm...现在去做题啦,如果那里写的不对或者有什么问题第一时间联系我啊!!!

qwq好菜啊我

猜你喜欢

转载自blog.csdn.net/qq_37136305/article/details/81184873