多项式与快速傅里叶变换-学习笔记

本文介绍了快速傅里叶变换(FFT)在计算多项式乘法中的应用,详细阐述了从问题引入、单位复数根、基本思想到递归化迭代的过程,并通过实例展示了FFT如何构造及用于解决笛卡尔和、特定问题求解等。同时提到了拉格朗日插值法作为多项式系数表达的快速计算方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

问题引入:快速求多项式乘法

定义1.1 一个形如:

A(x)=i=0n1aixi

的函数被称为一个多项式的 系数表达 。系数表达多项式的加,乘和除我们在小学和初中已经学习过,这里不再赘述。

系数表达的好处有:

  1. 直观。我们所用的进位制就是使用多项式的系数表达。
  2. 方便求值。利用霍纳法则(秦九韶算法)可以方便的在O(n)求得多项式的值。

但缺点也有:

  • 难以计算两个多项式的乘积。

朴素的算法需要 O(n2) 时间才能计算多项式的乘积,一个简单的分治算法则可以在 O(nlg7) 内算出。为了提高效率,我们引入多项式的 点值表达 的概念:

定义1.2 对于多项式A(x),形如:

B={(x0,A(x0)),(x1,A(x1)),(x2,A(x2)),,(xn1,A(xn1))},ab,xaxb

的表达称为多项式A以 {x0,x1,...,xn1} 为基的点值表示。

由线性代数的知识可以知道,一个点值表示至多有一个多项式与之对应。但 xi 的选择是任意的,而且不会影响计算。

对于点值表示,计算多项式乘积就简单了许多:选取同一组 xi ,将对应的 yi 相乘即可。那么我们有了一个计算多项式值的初步思路:

  1. 将多项式A, B转化为点值表达
  2. 点值乘法
  3. 将点值表示转换回系数表示

单位复数根

定义2.1 复平面上 xn=1 的n个根均匀地分布在单位圆上,他们被称为单位复数根。令 ω0=1,ωm=cos2πn+isin2πn ,不难发现将 ω 每次乘上 ωm 就可以得到逆时针旋转的下一个单位复数根。

容易证明n次单位复数根和复数乘法构成的群 (ω,,1) 和模n整数加群 (Zn,+,0) 同构,所以性质就可以类比得知了。特别的,我们知道整数集*2(也就是加法的幂)后回变成偶数集,单位复数根也有类似的性质——n次单位负数根平方后会变成 n2 次单位复数根。正是这个重要的性质使我们选择用单位复数根作为点值表达的基(x)。

基本思想和递归代码

这里的思路有点类似整体二分。基本原理就是对于多项式:A(x),如果可以计算出这两个式子:

A[0](x)=a0+a2x+a4x2...A[1](x)=a1+a3x+a5x2...

x2 为基的点值表示,则A(x)以x为基的点值就可以在 O(n) 内求得:

A(x)=A[0](x2)+xA[1](x2)

然而二分的瓶颈是:只有当一个问题被完全转化为和原问题相同的子问题才可以使用递归,但这里的基由x变为了 x2 ,必须让 x2 构成的集合的规模为x构成集合规模的一半才能满足二分条件。我们惊奇的发现,如果选用n次单位复数根作为基,则恰好满足这个条件。将询问集 {Q(A,ωi)},Q(A,ωi) 询问多项式A在 ωi 时的点值二分到底层,再逐层向上回传答案并计算,不难得出这样的代码:

typedef complex<double> complex_num;

void fft(complex_num a[], int n)
{
    if (n == 1) return;
    complex_num a0[n/2+2], a1[n/2+2];
    complex_num dw = complex_num(cos(2 * M_PI/n), sin(2 * M_PI/n));
    complex_num w = complex_num(1, 0);
    for (int i = 0; i < n/2; i++) a0[i] = a[i<<1], a1[i] = a[(i<<1)+1];
    fft(a0, n>>1); fft(a1, n>>1);
    for (int i = 0; i < n/2; i++) {
        a[i] = a0[i] + w*a1[i];
        a[i+n/2] = a0[i] - w*a1[i];
        w = w*dw; // 转动w
    }
}

递归化迭代

由于这样写的常数非常之大,我们需要进一步优化。考虑递归树上询问集合的情况:

[0, 1, 2, 3, 4, 5, 6, 7]
[0, 2, 4, 6][1, 3, 5, 7]
[0, 4][2, 6][1, 5][3, 7]
[0][4][2][6][1][5][3][7]

如果将树根集合和叶子集合分别用二进制表示,就是:

[000, 001, 010, 011, 100, 101, 110, 111]
[000, 100, 010, 110, 001, 101, 011, 111]

发现叶子的位置恰好时树根对应二进制反转,即:

inline int rev(int i, int n)
{
    int v0 = 0;
    while(n--) v0 = (v0+(i&1))<<1, i>>=1;
    return v0>>1;
}

n表示二进制的长度,即问题规模以2为底的对数。这里用的是一个简单的霍纳法则(秦九韶算法)完成这一操作。

那么接下来就可以用循环改写FFT递归版本了。一个小技巧是在递归程序子问题合并的代码中:

for (int i = 0; i < n/2; i++) {
    a[i] = a0[i] + w*a1[i];
    a[i+n/2] = a0[i] - w*a1[i];
    w = w*dw; // 转动w
}

我们用一个临时变量t代替w*a1[i],可以减少一半的乘法。这个操作也叫做“蝴蝶操作”。

求解逆FFT

在《算法导论》上用矩阵手段证明了使用单位复数根的共轭复数做FFT可以实现逆FFT。然而没有看懂。此处留坑待补。

完整代码

struct Complex {
    double x, y;
    Complex(){x = y = 0;}
    Complex(double a, double b):x(a), y(b){}
    Complex operator + (const Complex &c) const { return Complex(x+c.x, y+c.y); }
    Complex operator - (const Complex &c) const { return Complex(x-c.x, y-c.y); }
    Complex operator * (const Complex &c) const { return Complex(x*c.x-y*c.y, x*c.y+y*c.x); }
};

inline int lg(int i)
{
    int k, t;
    for (k = 1, t = 0; k < i; k <<= 1, t++);
    return t;
}

inline int rev(int i, int n)
{
    int v0 = 0;
    while(n--) v0 = (v0+(i&1))<<1, i>>=1;
    return v0>>1;
}
int p = 0;
void fft(Complex a[], int n, int flag)
{
    Complex A[n+1], t, u;
    for (int i = 0, l = lg(n); i < n; i++) A[rev(i, l)] = a[i];
    for (int i = 2; i <= n; i<<=1) {
        Complex dw = Complex(cos(flag*2*M_PI/i), sin(flag*2*M_PI/i));
        for (int j = 0; j < n; j += i) {
            Complex w = Complex(1, 0);
            for (int k = 0; k < i>>1; k++) {
                t = w*A[k+j+(i>>1)];
                u = A[k+j];
                A[k+j] = u+t;
                A[k+j+(i>>1)] = u-t;
                w = w*dw;
            }
        }
    }
    for (int i = 0; i < n; i++) a[i] = A[i];
}

构造FFT

FFT的用处远不止快速计算大整数乘法。我们用一些例子来解释构造FFT快速求解其他问题的手段。

例一:求两个有界整数的集合的笛卡尔和1

对于集合A, B,包含0-10n范围内的n个整数,我们希望计算A与B的笛卡尔和,定义如下:

C={x+y|xA,yB}

分析与解:用T(A, x)表示集合A中x出现的次数,构造两个多项式:

A(x)=i=010nT(A,i)xiB(x)=i=010nT(B,i)xi

不难证明 A×B 对应的 k 次幂系数,就是C中k出现的次数。注意到这个构造的思路是:数字求和对应相乘时幂次的求和,数字个数(由于乘法原理)对应系数相乘。

例二:ZJOI2014 力

给定 q1,q2,,qn,计算

Ej=i<jqi(ij)2i>jqi(ij)2

分析与解:由于有求和,我们可以考虑构造多项式。考虑乘积中n次项是如何生成的,不难想到生成过程中走了一个“蝴蝶形”的路线。

图示

我们可以构造:

A(x)=q1+q2x+q3x2++qnxn1+0xn+0xn+1++0x2n1B(x)=1(n1)21(n2)21xn10xn+1xn+1+122xn+2++1(n1)2x2n1

不难证明 A×B 对应的 n..2n1 次系数为所求。

拉格朗日插值法

给定一个多项式的任一点值表达,如何快速求出多项式的系数表达?如果采用朴素的高斯消元法,复杂度为 O(n3) 。而拉格朗日公式允许我们在 O(n2) 的复杂度内计算多项式的系数表达或值。

{(xi,yi)|i=0,1,,n1} 所对应的多项式为:

L(x)=j=0n1(yji=0,ijkxxixjxi)

参考资料

《算法导论》多项式与快速傅里叶变换
https://ruanx.pw/post/FFT.html
http://blog.csdn.net/iamzky/article/details/22712347


  1. 出处:《算法导论》P531,30.1-7
<think>嗯,用户想了解Excel中的VLOOKUP函数的用法和参数含义。首先,我需要回忆一下VLOOKUP的基本结构。VLOOKUP是垂直查找函数,用于在表格的首列查找指定的值,然后返回该行中指定列的数据。它的四个参数分别是查找值、表格范围、列索引号和匹配模式。 接下来,我得确认每个参数的具体作用。第一个参数是lookup_value,也就是用户要查找的值,比如产品ID或者姓名。第二个参数是table_array,即查找的范围,这里要注意的是,查找值必须位于这个范围的第一列,否则函数无法正确工作。第三个参数是col_index_num,也就是用户想要返回的数据在表格范围中的第几列,这个列数是从表格范围的第一列开始算起的,不是整个工作表的列。第四个参数是range_lookup,决定是精确匹配还是近似匹配,通常用FALSE或0来指定精确匹配,避免错误。 然后,用户可能需要一个具体的例子来理解如何使用。比如,假设有一个产品价格表,用户想根据产品ID查找对应的价格。这时候,VLOOKUP就可以在ID列查找,然后返回价格列的数据。同时,要提醒用户注意表格范围最好使用绝对引用,比如$A$2:$C$100,这样在拖动公式时范围不会变化。 可能遇到的问题也需要考虑。比如,如果查找值不在表格的第一列,或者列索引号超过了表格的列数,函数会返回错误。此外,数据格式不一致也可能导致问题,比如查找值是数字,而表格中是文本格式的数字,这时候需要统一格式。还有,当出现#N/A错误时,可能是查找值不存在,或者匹配模式设置不正确,需要检查参数是否正确。 用户可能还会问到如何避免常见的错误,比如部分数据匹配出错的情况。这时候需要检查是否有多余的空格,或者数据格式不一致,或者表格范围没有锁定导致拖动时偏移。此外,XLOOKUP作为VLOOKUP的升级版,可能在某些情况下更方便,比如可以向左查找或者返回多列数据,这也是可以提及的内容。 最后,整理步骤,从基础语法到参数说明,再到具体案例,逐步引导用户理解如何使用VLOOKUP函数。同时,提供常见错误和解决方法,帮助用户在实际应用中避免问题。如果有必要,还可以比较VLOOKUP和其他查找函数的区别,比如HLOOKUP或XLOOKUP,但根据用户的问题,主要集中在VLOOKUP本身即可。</think>### Excel VLOOKUP函数详解 #### 一、基础语法与参数说明 VLOOKUP函数的语法为: $$=VLOOKUP(lookup\_value, table\_array, col\_index\_num, [range\_lookup])$$ 包含4个参数: 1. **lookup_value**(必填):要查找的值(如单元格引用或具体值) 2. **table_array**(必填):包含数据的表格范围(必须包含查找列和返回列) 3. **col_index_num**(必填):返回值所在列的序号(从table_array第一列开始计数) 4. **range_lookup**(可选):匹配类型 - `TRUE`/`1`:近似匹配(默认值,需数据升序排列) - `FALSE`/`0`:精确匹配(常用选项) [^1][^2] #### 二、使用步骤演示(工资表查询案例) 假设需要根据员工编号查询工资: 1. 建立查询单元格(如`B12`) 2. 输入公式: ```excel =VLOOKUP(A12, $A$2:$D$100, 4, 0) ``` - `A12`:待查询的员工编号 - `$A$2:$D$100`:锁定数据区域(绝对引用) - `4`:返回第4列(工资列) - `0`:精确匹配 [^2][^3] #### 三、常见错误与解决方法 | 错误现象 | 原因 | 解决方案 | |---------|------|---------| | #N/A | 查找值不存在 | 检查数据源或改用`IFERROR`容错 | | #REF! | 列序号超出范围 | 确认col_index_num ≤ 表格列数 | | 部分匹配失败 | 数据格式不一致 | 统一数值/文本格式 | | 结果错位 | 表格未锁定 | 使用`$`符号固定区域引用 | [^3][^4] #### 四、进阶技巧 1. **多条件查询**: 使用辅助列合并多个条件字段 ```excel =VLOOKUP(A2&B2, $D$2:$F$100, 3, 0) ``` 2. **通配符匹配**: `"*"`匹配任意字符,`"?"`匹配单个字符 ```excel =VLOOKUP("张*", $A$2:$C$100, 3, 0) ``` 3. **跨表查询**: 引用其他工作表数据 ```excel =VLOOKUP(A2, Sheet2!$A$2:$D$100, 4, 0) ``` [^1][^4]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值