3 条题解

  • 1
    @ 2026-8-3 8:44:59

    更好的阅读体验:https://blog.csdn.net/tenkuo/article/details/150500190

    诱导公式: https://blog.csdn.net/tenkuo/article/details/153632690

    
    // a 数组一开始存的是系数,后面通过 a1 和 a2 计算得出每一个 k 对应的 y 值 
    // limit:数组的大小,*a 当前数组
    void FFT(int limit, complex *a) { 
        if(limit == 1) return;  // 递归终止条件:只有一个常数项
        //最后 n = 1的情况只有一个常数 a0,本来 a数组里面就存着,啥也不干直接返回就好 
        
        complex a1[limit>>1], a2[limit>>1];
        // 按下标奇偶性分类
        for(int i = 0; i < limit; i += 2) {
            a1[i>>1] = a[i];    // 偶数下标
            a2[i>>1] = a[i+1];  // 奇数下标
        }
        
        // 递归处理两个子问题
        FFT(limit >> 1, a1);
        FFT(limit >> 1, a2);
        
        // 计算单位根
        complex Wn = complex( cos(2.0 * Pi / limit), sin(2.0 * Pi / limit) );
        //pi 就是圆周率 π,在完整代码中会讲怎么定义,Wn 是一开始先计算的 w_n^1 
        complex w = complex( 1, 0 );  // 当前的 w_n^k 
        // 此时 w 还只是一个乘积基底,也就是 1,所以等于 1 + 0*i
        
        // 合并结果,计算每一个 w_n^k的 y 
        for(int i = 0; i < (limit>>1); i++, w = w * Wn) {  //蝴蝶操作(代码后面有解释为啥叫这个)
            complex t = w * a2[i]; //因为 w * a2[i]计算了两次,设个变量能省点时间 
            a[i] = a1[i] + t;
            a[i + (limit>>1)] = a1[i] - t;
        }
    }
    

    https://www.luogu.com.cn/problem/P3803

    #include<bits/stdc++.h> 
    using namespace std;
     
    const int N = 3e6 + 10;  //这里得开大点,最好是 2 * (maxn + maxm)
    const double Pi = acos(-1.0); // acos(-1.0)是 π 的精确值(自行百度) 
     
    struct Complex {   // 我这里自定义复数结构体啦,c++库里也有 
        double x, y;  // x为实部,y为虚部
    } a[N], b[N];  // 两个多项式的数组
     
    Complex operator+(Complex a, Complex b) {  //重载复数加法 
        return {a.x + b.x, a.y + b.y}; 
    }
     
    Complex operator-(Complex a, Complex b) {   //重载复数减法 
        return {a.x - b.x, a.y - b.y}; 
    }
     
    Complex operator*(Complex a, Complex b) { 
        return {a.x * b.x - a.y * b.y , a.x * b.y + a.y * b.x}; 
    		// 重载复数乘法 (a+bi) * (c+di) = (ac-bd) + (ad+bc)i
    }
     
    int n, m, l, r[N];  // r是位逆序
    int limit;  // FFT变换长度
     
    void FFT(Complex *A, int type) {   //type:1表示正变换,-1表示逆变换,后面有解释 
        for (int i = 0; i < limit; i++) if(i < r[i]){
            swap(A[i], A[r[i]]);  //相互是位逆序的交换,只交换一次,避免重复交换
        }
        
        for (int mid = 1; mid < limit; mid <<= 1) {   // mid是当前子问题的半长
            //Wn = cos( 2 * Pi / limit ) + i * type * sin( 2 * Pi / limit ) 
            //关于逆变换的 w_n^{-k},就等于 w_n^{n - k},sin值要变成负的,直接乘是 -1的 type就好 
            Complex Wn = { cos(Pi / mid), type * sin(Pi / mid) };
            
            // R是当前子问题的完整长度,j表示当前处理到哪个位置
            for(int R = mid << 1, j = 0; j < limit; j += R) {
                Complex w = {1, 0};
                
                // 枚举子问题的左半部分(0到mid-1)
                for(int k = 0; k < mid; k++, w = w * Wn) {  // 蝴蝶操作
                // 这里可以把 j + k 当作 i,x 当作 a1[i],y 当作 a2[i] 
                    Complex x = A[j + k];
                    Complex y = w * A[j + mid + k]; 
                    
                    A[j + k] = x + y;   
                    A[j + mid + k] = x - y; 
                }
            }
        }
    }
     
    int main() {
    	ios::sync_with_stdio(false);
    	cin.tie(0); 
    	
        int n, m;
        cin >> n;
        cin >> m;
        for (int i = 0; i <= n; i++) {
            cin >> a[i].x;  // 实部为系数,虚部默认为 0
            a[i].y = 0;
        }
        
        for(int i = 0; i <= m; i++) {
            cin >> b[i].x;
            b[i].y = 0;
        }
        
        limit = 1;
        l = 0;
        // 是这个意思 limit < n + m + 1
        while (limit <= n + m) {   // 实际运用中并不严格要求 n 和 m 都是形如 2^k 的数
            // 我们会计算出 >= n + m + 1 最小的二进制数 limit,用 limit 来 FFT
            // 相当于将多出来的位补 0
            limit <<= 1; 
            l++; 
        }
        
        for (int i = 0; i < limit; i++) {  // 计算二进制位逆序
            r[i] = (r[i >> 1] >> 1) | ((i & 1) << (l - 1));
            //i>>1:去掉 i 的最低位,r[i>>1]:得到 i>>1 的位逆序
            //r[i>>1]>>1:将位逆序右移一位(为新位腾出空间)
            //i&1:获取 i 的最低位,(i&1)<<(l-1):将最低位移到最高位
            //最后合并两部分
            //自己试一下验证正确性 
            // 因为 i >> 1 后,前面肯定会空出一个该有的位
            // 所以 r[i >> 1] 的最后一个位是 0
            // 这个 0 本来是属于 i 的最低位的,现在移走让最低位到最前面
        }
        
        // FFT正变换
        FFT(a, 1);
        FFT(b, 1);
        
        for (int i = 0; i < limit; i++) {
            a[i] = a[i] * b[i];  //两两点值相乘
        }
        
        // FFT逆变换
        FFT(a, -1);
        
        for(int i = 0; i <= n + m; i++) {
            cout << (int)(a[i].x / limit + 0.5) << " ";   //逆变换后需要除以 limit
            // 为什么要 + 0.5 四舍五入,理论上应该得到整数 42
            // 但由于 fft 浮点误差,实际可能是:
            // 42.0000000001  或  41.9999999999
    	}
    	cout << "\n";
        return 0;
    }
    
    

    #include <bits/stdc++.h> 
    using namespace std;
     
    typedef long long LL;
    const int M = 3e6 + 10;  //这里得开大点,最好是 2 * (maxn + maxm)
    const LL P = 998244353;  
     
    LL qpow(LL a, LL b) {
        LL res = 1;
        a %= P;
        while (b) {
        	if (b & 1) {
        		res = res * a %P;
    		}
    		a = a * a %P;
    		b /= 2;
    	}
        return res;
    }
     
    LL a[M], b[M], r[M];
    int limit, l;
     
    void NTT(LL *A, LL type) {  //type:原根的特定幂次(正变换用原根,逆变换用原根的逆元)
        for (int i = 0; i < limit; i++) if(i < r[i]){
            swap(A[i], A[r[i]]);  
        }
        
        for (int mid = 1; mid < limit; mid <<= 1) {   //mid 是当前半长 
            // 计算当前长度 2 * mid 对应的单位根:x ^ {limit / (2 * mid)}
            // 为什么是 limit / (2 * mid)?就相当于原来的 g^{(P - 1) / limit} 上面的幂次 *当前长度 /limit
    		//就等于 g^{(P - 1) / 当前长度} 
            LL Wn = qpow( type, limit / (2 * mid) );
            
            // R是当前子问题的完整长度,j表示当前处理到哪个位置
            for(int R = mid << 1, j = 0; j < limit; j += R) {
                LL w = 1;  // 初始化当前单位根为 1(即 w_n^0)
                
                for(int k = 0; k < mid; k++, w = w * Wn %P) {  // 蝴蝶操作
                    LL x = A[j + k];
                    LL y = w * A[j + mid + k] %P; 
                    
                    A[j + k] = (x + y)%P;   
                    A[j + mid + k] = (x - y + P)%P;   //这里一定要 + P!!不然会输出负数!! 
                }
            }
        }
    }
     
    int main() {
    	ios::sync_with_stdio(false);
    	cin.tie(0); 
    	
        int n, m;
        cin >> n;
        cin >> m;
        
        for (int i = 0; i <= n; i++) {
            cin >> a[i]; 
        }
        
        for(int i = 0; i <= m; i++) {
            cin >> b[i];
        }
        
        limit = 1;
        l = 0;
        while (limit <= n + m) {
            limit <<= 1; 
            l++; 
        }
        
        for (int i = 0; i < limit; i++) {  // 计算二进制位逆序
            r[i] = (r[i >> 1] >> 1) | ((i & 1) << (l - 1));
        }
        
        //(P-1)/N 是N次单位根
        LL t = qpow(3ll, (P - 1) / limit);
        
        // 执行 NTT正变换
        NTT(a, t);
        NTT(b, t);
        
        for (int i = 0; i < limit; i++) {
            a[i] = a[i] * b[i] % P;  
        }
        
        // 计算原根的逆元(用于逆变换)
        LL inv_t = qpow(t, P - 2);
        
        // 执行 NTT逆变换
        NTT(a, inv_t);
        
        LL invl = qpow(limit, P - 2); //计算 N在模 P下的逆元
        
        for (int i = 0; i < n + m + 1; i++) {
            cout << a[i] * invl % P << " ";   // 逆变换后需要除以 limit(乘以 limit的逆元)
    	}
    	cout << endl;
        return 0;
    }
    
    

    update 2025.12.7:修正了原文表达模糊的部分,添加了更细致的公式推导。

    update 2025.12.14:重写了 6.FFT 逆变换 和 7.FFT 的正确性证明。

    update 2026.8.2:修正了定义域错误,添加代码注释。

    • 0
      @ 2026-8-4 8:58:21
      #include<bits/stdc++.h>
      using namespace std;
      #define int long long
      const int N=2e6+10,P=998244353;
      int qpow(int a,int b){int ans=1;for(;b;b>>=1,a=a*a%P)if(b&1)ans=ans*a%P;return ans;}
      int a[N],b[N],c[N];
      void ntt(int s[],int n,int x)
      {
      	if(n==1)return;
      	int s1[n/2],s2[n/2];
      	for(int i=0;i<n/2;i++)s1[i]=s[2*i],s2[i]=s[2*i+1];
      	ntt(s1,n/2,x*x%P);ntt(s2,n/2,x*x%P);
      	for(int i=0,xi=1;i<n/2;i++,xi=xi*x%P)
      	{
      		s[i]=(s1[i]+s2[i]*xi)%P;
      		s[i+n/2]=((s1[i]-s2[i]*xi)%P+P)%P;
      	}
      }
      signed main()
      {
      	ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
      	int n,m;cin>>n>>m;
      	for(int i=0;i<n;i++)cin>>a[i];
      	for(int i=0;i<m;i++)cin>>b[i];
      	int D=1;while(D<n+m-1)D<<=1;int inv=qpow(D,P-2);
      	int x=qpow(3,(P-1)/D);
      	ntt(a,D,x);ntt(b,D,x);
      	for(int i=0;i<D;i++)c[i]=a[i]*b[i]%P;
      	int inv1=qpow(x,P-2);
      	ntt(c,D,inv1);
      	for(int i=0;i<n+m-1;i++)cout<<c[i]*inv%P<<' ';
      	return 0;
      }
      • 0
        @ 2025-12-7 9:00:41

        卷积定义题目里已经说的很明确了,先打个暴力试试水:

        #include<bits/stdc++.h>
        using namespace std;
        
        typedef long long LL;
        const int N = 1 << 20;
        const LL P = 998244353;
        
        LL a[N], b[N], ans[N];
        
        template<typename T> void qr(T &x) {
        	int f = 1; x = 0; char c = getchar();
        	for (; !isdigit(c); c = getchar()) if (c == '-') {
        		f = -1;
        	}
        	for (; isdigit(c); c = getchar()) {
        		x = x * 10 + (c - '0');
        	}
        	x *= f;
        }
        
        template<typename T> void qw(T x) {
        	if (x < 0) {
        		putchar('-');
        		x *= -1;
        	}
        	if (x >= 10) {
        		qw(x / 10);
        	}
        	putchar(x % 10 + '0');
        } 
        
        int main () {
        	ios::sync_with_stdio(false);
        	cin.tie(0);
        	
        	int n, m;
        	qr(n); qr(m);
        	for (int i = 0; i < n; i ++) {
        		qr(a[i]);
        	}
        	for (int i = 0; i < m; i ++) {
        		qr(b[i]);
        	}
        	
        	for (int i = 0; i < n; i ++) {
        		for (int j = 0; j < m; j ++) {
        			ans[i + j]= (ans[i + j] + a[i] * b[j] % P) % P;
        		}
        	}
        	
        	for (int i = 0; i <= n - 1 + m - 1; i ++) {
        		qw(ans[i]);
        		putchar(' ');
        	}
        	putchar('\n');
        	
        	return 0;
        }
        
        

        因为时间复杂度的大头是 N^2 枚举,所以快读不能起什么作用,加与不加都是 64 分。

        考虑到数据范围是 10^6,又是大整数计算,可以使用 NTT。 没学过指路:https://blog.csdn.net/tenkuo/article/details/150500190

        直接套板子即可:

        
        #include <bits/stdc++.h> 
        using namespace std;
         
        typedef long long LL;
        const int M = 3e6 + 10;   //这里得开大点,2e6 不够 
        const LL P = 998244353;  
         
        LL qpow(LL a, LL b) {
            LL res = 1;
            a %= P;
            while (b) {
            	if (b & 1) {
            		res = res * a %P;
        		}
        		a = a * a %P;
        		b /= 2;
        	}
            return res;
        }
         
        LL a[M], b[M], r[M];
        int limit, l;
         
        void NTT(LL *A, LL type) {  //type:原根的特定幂次(正变换用原根,逆变换用原根的逆元)
            for (int i = 0; i < limit; i++) if(i < r[i]){
                swap(A[i], A[r[i]]);  
            }
            
            for (int mid = 1; mid < limit; mid <<= 1) {   //mid 是当前半长 
                // 计算当前长度 2 * mid 对应的单位根:x ^ {limit / (2 * mid)}
                // 为什么是 limit / (2 * mid)?就相当于原来的 g^{(P - 1) / limit} 上面的幂次 *当前长度 /limit
        		//就等于 g^{(P - 1) / 当前长度} 
                LL Wn = qpow( type, limit / (2 * mid) );
                
                // R是当前子问题的完整长度,j表示当前处理到哪个位置
                for(int R = mid << 1, j = 0; j < limit; j += R) {
                    LL w = 1;  // 初始化当前单位根为 1(即 w_n^0)
                    
                    for(int k = 0; k < mid; k++, w = w * Wn %P) {  // 蝴蝶操作
                        LL x = A[j + k];
                        LL y = w * A[j + mid + k] %P; 
                        
                        A[j + k] = (x + y)%P;   
                        A[j + mid + k] = (x - y + P)%P;   //这里一定要 + P!!不然会输出负数!! 
                    }
                }
            }
        }
         
        int main() {
        	ios::sync_with_stdio(false);
        	cin.tie(0); 
        	
            int n, m;
            cin >> n;
            cin >> m;
            
            for (int i = 0; i < n; i++) {
                cin >> a[i]; 
            }
            
            for(int i = 0; i < m; i++) {
                cin >> b[i];
            }
            
            limit = 1;
            l = 0;
            while (limit <= n + m) {
                limit <<= 1; 
                l++; 
            }
            
            LL invl = qpow(limit, P - 2); //计算 N在模 P下的逆元
            
            for (int i = 0; i < limit; i++) {  // 计算二进制位逆序
                r[i] = (r[i >> 1] >> 1) | ((i & 1) << (l - 1));
            }
            
            //(P-1)/N 是N次单位根
            LL t = qpow(3ll, (P - 1) / limit);
            
            // 执行 NTT正变换
            NTT(a, t);
            NTT(b, t);
            
            for (int i = 0; i <= limit; i++) {
                a[i] = a[i] * b[i] % P;  
            }
            
            // 计算原根的逆元(用于逆变换)
            LL inv_t = qpow(t, P - 2);
            
            // 执行 NTT逆变换
            NTT(a, inv_t);
            
            for (int i = 0; i <= n - 1 + m - 1; i++) {
                cout << a[i] * invl % P << " ";   // 逆变换后需要除以 limit(乘以 limit的逆元)
        	}
        	cout << "\n";
            return 0;
        }
        
        

        另:本题是卡 FFT 的,甚至只能拿到 4 分(大数据下 FFT 会有精度问题)。 椰树神了喵。

        • 1

        信息

        ID
        3117
        时间
        300ms
        内存
        1024MiB
        难度
        9
        标签
        递交数
        14
        已通过
        5
        上传者