数论函数变换

本文解析了几道典型的数论竞赛题,包括求特定数学函数的和、计算互质数对的数量以及涉及最小公倍数的问题,展示了如何通过数学推导简化问题并降低计算复杂度。

对于各种积性函数,都可以通过两种方法进行计算:

  1. 分解质因数
  2. 线性筛

那么,我们就可以得到一些常用的积性函数值。

typedef pair<int, int> P;
int prime[MAXN], cnt, phi[MAXN], mu[MAXN], mnsum[MAXN], a[MAXN];
bool isp[MAXN];
P f[MAXN];//约数和
void Euler(int n) {
    mu[1] = 1;
    phi[1] = 1;
    f[1] = P(1, 1);
    for(int i = 2; i <= n; i++) {
        f[i].second = i;
        if(!isp[i]) {
            prime[++cnt] = a[i] = i;
            phi[i] = i - 1;
            mu[i] = -1;
            f[i].first = mnsum[i] = i + 1;
        }
        for(int j = 1; j <= cnt && i * prime[j] <= n; j++) {
            isp[i * prime[j]] = 1;
            if(i % prime[j] == 0) {
                phi[i * prime[j]] = phi[i] * prime[j];
                mu[i * prime[j]] = 0;
                mnsum[i * prime[j]] = mnsum[i] + a[i] * prime[j];
                f[i * prime[j]].first = f[i].first / mnsum[i] * mnsum[i *
                    prime[j]]; a[i * prime[j]] = a[i] * prime[j];
                break;
            }
            phi[i * prime[j]] = phi[i] * (prime[j] - 1);
            mu[i * prime[j]] = -mu[i];
            f[i * prime[j]].first = f[i].first * (prime[j] + 1);
            a[i * prime[j]] = prime[j];
            mnsum[i * prime[j]] = prime[j] + 1;
        }
    }
}

先来一道简单题:bzoj2005
题目大意:求 ΣniΣmjgcd(i,j)21
然后推一推式子
ΣniΣmjgcd(i,j)21
= nm+2ΣniΣmjgcd(i,j)
= nm+2ΣniΣmjΣd|gcd(i,j)ϕ(d)
= nm+2Σmax(n,m)dϕ(d)ndmd
然后枚举d这道题就可以A掉了,复杂度为O(n)。

#include <cstdio>
#include <algorithm>
#include <cmath>
using namespace std;

const int MAXN = 100003;
int phi[MAXN], prime[MAXN], cnt;
bool isp[MAXN];
void Euler(int n) {
    phi[1] = 1;
    for(int i = 2; i <= n; i++) {
        if(!isp[i]) {
            prime[++cnt] = i;
            phi[i] = i - 1;
        }
        for(int j = 1; j <= cnt && i * prime[j] <= n; j++) {
            isp[i * prime[j]] = 1;
            if(i % prime[j] == 0) {
                phi[i * prime[j]] = phi[i] * prime[j];
                break;
            }
            else phi[i * prime[j]] = phi[i] * (prime[j] - 1);
        }
    }
}
long long ans;
int main() {
    int n, m, k;
    scanf("%d%d", &n, &m);
    k = max(n, m);
    Euler(k);
    ans -= (long long)n * m;
    for(int d = 1; d <= k; d++) {
        ans += 2LL * phi[d] * (n / d) * (m / d);
    }
    printf("%lld\n", ans);
    return 0;
}

来看另一道题:bzoj1101
题目大意:对于给定的整数N,M和d,有多少正整数对x,y,满足x<=N,y<=M,并且gcd(x,y)=d。
等价于x<=N/d,y<=M/d,互质的x,y的对数。
那么我们另n=N/d, m = M/d
于是原题就变成了求 ΣniΣmje(gcd(i,j))
其中 e(x)=(x==1)=Σd|xμ(d)
然后根据套路我们再来推一波式子
ΣniΣmje(gcd(i,j))
= ΣniΣmjΣd|gcd(i,j)μ(d)
= Σmin(n,m)dμ(d)ndmd
根据上一题的做法,每次询问复杂度O(n),显然不能胜任。
但是我们观察式子,显然 ndmd 的取值只有 n+m 个,那么我们可以预处理 μ 的前缀和,并分块完成。

#include <cstdio>
#include <algorithm>
using namespace std;

const int MAXN = 50003;
int mu[MAXN], prime[MAXN], cnt;
bool isp[MAXN];
void Euler(int n) {
    mu[1] = 1;
    for(int i = 2; i <= n; i++) {
        if(!isp[i]) {
            prime[++cnt] = i;
            mu[i] = -1;
        }
        for(int j = 1; j <= cnt && i * prime[j] <= n; j++) {
            isp[i * prime[j]] = 1;
            if(i % prime[j] == 0) {
                mu[i * prime[j]] = 0;
                break;
            }
            mu[i * prime[j]] = -mu[i];
        }
    }
}
int cal(int n, int m) {
    if(n > m) swap(n, m);
    int res = 0, last;
    for(int i = 1; i <= n; i = last + 1) {
        last = min(n / (n / i), m / (m / i));
        res += (n / i) * (m / i) * (mu[last] - mu[i - 1]);
    }
    return res;
}
int main() {
    Euler(50000);
    for(int i = 2; i <= 50000; i++) mu[i] += mu[i - 1];
    int T, a, b, d;
    scanf("%d", &T);
    while(T--) {
        scanf("%d%d%d", &a, &b, &d); a /= d; b /= d;
        printf("%d\n", cal(a, b));
    }
    return 0;
}

还有一题:bzoj3529
题目大意:另F(i)表示i的约数和,q次给定n,m,a,求 ΣniΣmjandF(gcd(i,j))<=aF(gcd(i,j))mod231
有个a的限制,好像式子并不能推了啊。所以先把它去掉。
g(i)=ΣnxΣmye(gcd(x,y)==i)
那么可以得到 g(i)=Σi|dμ(di)ndmd
于是 ans=Σmin(n,m)iF(i)g(i)
展开化简得到 Σmin(n,m)dndmdΣi|dF(i)μ(di)
然后对 Σi|dF(i)μ(di) 求个前缀和
枚举每个i更新i的倍数即可
现在考虑将a离线处理,询问按a排序
用树状数组维护前缀和即可。

#include <cstdio>
#include <algorithm>
using namespace std;

typedef pair<int, int> P;
const int MAXN = 100003;
int prime[MAXN], cnt, mu[MAXN], mnsum[MAXN], a[MAXN];
bool isp[MAXN];
P f[MAXN];
void Euler(int n) {
    mu[1] = 1;
    f[1] = P(1, 1);
    for(int i = 2; i <= n; i++) {
        f[i].second = i;
        if(!isp[i]) {
            prime[++cnt] = a[i] = i;
            mu[i] = -1;
            f[i].first = mnsum[i] = i + 1;
        }
        for(int j = 1; j <= cnt && i * prime[j] <= n; j++) {
            isp[i * prime[j]] = 1;
            if(i % prime[j] == 0) {
                mu[i * prime[j]] = 0;
                mnsum[i * prime[j]] = mnsum[i] + a[i] * prime[j];
                f[i * prime[j]].first = f[i].first / mnsum[i] * mnsum[i * prime[j]];
                a[i * prime[j]] = a[i] * prime[j];
                break;
            }
            mu[i * prime[j]] = -mu[i];
            f[i * prime[j]].first = f[i].first * (prime[j] + 1);
            a[i * prime[j]] = prime[j];
            mnsum[i * prime[j]] = prime[j] + 1;
        }
    }
}
int bit[MAXN];
void add(int x, int v) {
    for(; x <= 100000; x += x & -x) bit[x] += v;
}
int sum(int x) {
    int res = 0;
    for(; x; x -= x & -x) res += bit[x];
    return res;
}
struct Node {
    int n, m, a, id;
    bool operator < (const Node &x) const {
        return a < x.a;
    }
    inline void read(int i) {
        id = i; scanf("%d%d%d", &n, &m, &a);
    }
}q[MAXN];
int ans[MAXN];
int cal(int n, int m) {
    if(n > m) swap(n, m);
    int res = 0, last;
    for(int i = 1; i <= n; i = last + 1) {
        last = min(n / (n / i), m / (m / i));
        res += (n / i) * (m / i) * (sum(last) - sum(i - 1));
    }
    return res & 0x7fffffff;
}
int main() {
    Euler(100000);
    sort(f + 1, f + 100001);
    int T;
    scanf("%d", &T);
    for(int i = 1; i <= T; i++) q[i].read(i);
    sort(q + 1, q + T + 1);
    int cur = 1, N, M, A;
    for(int Q = 1; Q <= T; Q++) {
        N = q[Q].n, M = q[Q].m, A = q[Q].a;
        while(cur <= 100000 && f[cur].first <= A) {
            for(int i = f[cur].second; i <= 100000; i += f[cur].second)
                add(i, f[cur].first * mu[i / f[cur].second]);
            cur++;
        }
        ans[q[Q].id] = cal(N, M);
    }
    for(int i = 1; i <= T; i++) printf("%d\n", ans[i]);
    return 0;
}

来一道lcm的题:bzoj2154
题目大意:求 ΣniΣmjlcm(i,j)mod20101009
这里跟之前的区别在于,gcd(i,j)在分母上,那么考虑反演
f(x)=1x
F(x)=f X μ 这里X是Dirichlet积
F(x)=Σd|xf(d)μ(xd)
那么由莫比乌斯反演我们有 f=F X 1
那么ΣniΣmjlcm(i,j)
= ΣniΣmjijf(gcd(i,j))
= ΣniΣmjijΣd|gcd(i,j)F(d)
sum(x,y)=ΣxiΣyjij
于是可以进一步化简 ans=ΣndF(d)ddsum(nd,md)

#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;

typedef long long ll;
const int mod = 20101009, MAXN = 10000003;
int mu[MAXN], prime[MAXN], sum[MAXN];
int cnt;
bool isp[MAXN];
void getmu(int n) {
    mu[1] = 1;
    for(int i = 2; i <= n; i++) {
        if(!isp[i]) {
            mu[i] = -1;
            prime[++cnt] = i;
        }
        for(int j = 1; j <= cnt && i * prime[j] <= n; j++) {
            isp[i * prime[j]] = 1;
            if(i % prime[j] == 0) {
                mu[i * prime[j]] = 0;
                break;
            }
            mu[i * prime[j]] = -mu[i];
        }
    }
}

ll n, m, ans;

ll query(ll x, ll y) { return (x * (x + 1) / 2 % mod) * (y * (y + 1) / 2 % mod) % mod; }

ll F(ll x, ll y) {
    ll res = 0, last;
    for(ll i = 1; i <= min(x, y); i = last + 1) {
        last = min(x / (x / i), y / (y / i));
        res = (res + (sum[last] - sum[i - 1]) * query(x / i, y / i) % mod) % mod;
    }
    return res;
}

int main() {
    cin>>n>>m;
    getmu(min(n, m));
    for(ll i = 1; i <= min(n, m); i++) sum[i] = (sum[i - 1] + (i * i * mu[i]) % mod) % mod;
    ll last;
    for(ll d = 1; d <= min(n, m); d = last + 1) {
        last = min(n / (n / d), m / (m / d));
        ans = (ans + (last - d + 1) * (d + last) / 2 % mod * F(n / d, m / d) % mod) % mod;
    }
    ans = (ans + mod) % mod;
    cout<<ans<<endl;
    return 0;
}

P.S.:推式子治好了我多年的公式恐惧症啊
总结一下:当推式子陷入僵局的时候,尝试将一些东西处理出来,通过分块、反演等方法降低复杂度。对于式子本身的特性进行观察,有时也可以发现优化的地方。
在推式子过程中,将有关变量放在一块处理是化简的常用技巧。

AI 代码审查Review工具 是一个旨在自动化代码审查流程的工具。它通过集成版本控制系统(如 GitHub 和 GitLab)的 Webhook,利用大型语言模型(LLM)对代码变更进行分析,并将审查意见反馈到相应的 Pull Request 或 Merge Request 中。此外,它还支持将审查结果通知到企业微信等通讯工具。 一个基于 LLM 的自动化代码审查助手。通过 GitHub/GitLab Webhook 监听 PR/MR 变更,调用 AI 分析代码,并将审查意见自动评论到 PR/MR,同时支持多种通知渠道。 主要功能 多平台支持: 集成 GitHub 和 GitLab Webhook,监听 Pull Request / Merge Request 事件。 智能审查模式: 详细审查 (/github_webhook, /gitlab_webhook): AI 对每个变更文件进行分析,旨在找出具体问题。审查意见会以结构化的形式(例如,定位到特定代码行、问题分类、严重程度、分析和建议)逐条评论到 PR/MR。AI 模型会输出 JSON 格式的分析结果,系统再将其转换为多条独立的评论。 通用审查 (/github_webhook_general, /gitlab_webhook_general): AI 对每个变更文件进行整体性分析,并为每个文件生成一个 Markdown 格式的总结性评论。 自动化流程: 自动将 AI 审查意见(详细模式下为多条,通用模式下为每个文件一条)发布到 PR/MR。 在所有文件审查完毕后,自动在 PR/MR 中发布一条总结性评论。 即便 AI 未发现任何值得报告的问题,也会发布相应的友好提示和总结评论。 异步处理审查任务,快速响应 Webhook。 通过 Redis 防止对同一 Commit 的重复审查。 灵活配置: 通过环境变量设置基
【直流微电网】径向直流微电网的状态空间建模与线性化:一种耦合DC-DC变换器状态空间平均模型的方法 (Matlab代码实现)内容概要:本文介绍了径向直流微电网的状态空间建模与线性化方法,重点提出了一种基于耦合DC-DC变换器的状态空间平均模型的建模策略。该方法通过数学建模手段对直流微电网系统进行精确的状态空间描述,并对其进行线性化处理,以便于系统稳定性分析与控制器设计。文中结合Matlab代码实现,展示了建模与仿真过程,有助于研究人员理解和复现相关技术,推动直流微电网系统的动态性能研究与工程应用。; 适合人群:具备电力电子、电力系统或自动化等相关背景,熟悉Matlab/Simulink仿真工具,从事新能源、微电网或智能电网研究的研究生、科研人员及工程技术人员。; 使用场景及目标:①掌握直流微电网的动态建模方法;②学习DC-DC变换器在耦合条件下的状态空间平均建模技巧;③实现系统的线性化分析并支持后续控制器设计(如电压稳定控制、功率分配等);④为科研论文撰写、项目仿真验证提供技术支持与代码参考。; 阅读建议:建议读者结合Matlab代码逐步实践建模流程,重点关注状态变量选取、平均化处理和线性化推导过程,同时可扩展应用于更复杂的直流微电网拓扑结构中,提升系统分析与设计能力。
<think>我们正在讨论快速数论变换(NTT)及其逆变换(INTT)。NTT是FFT在有限域(模运算)上的类比,使用原根代替单位根。逆变换(INTT)用于将点值表示转换回系数表示。根据引用[2],在FFT中,逆变换(IDFT)可以通过将DFT中的单位根替换为其共轭复数,并在最后除以n来实现。类似地,在NTT中,逆变换可以通过将正变换中的原根替换为原根的逆元(模逆元),并在最后乘以n的模逆元来实现。步骤:1.在NTT正变换中,我们使用模p(通常是质数)下的n次主单位根g_n=g^{(p-1)/n},其中g是模p的一个原根。2.在逆变换中,我们使用g_n的逆元,即g_n^{-1}modp,因为g_n^n≡1(modp),所以g_n^{-1}=g_n^{n-1}modp。3.执行与FFT类似的蝴蝶操作,但使用模运算。4.最后,将结果乘以n^{-1}modp(n的模逆元)。注意:NTT要求n是2的幂,且模数p满足p=c*2^k+1(即p-1必须包含足够的2的因子)。常见模数:998244353,其原根g=3。因为998244353-1=998244352=2^23×7×17,所以对于n是2的幂且n<=2^23,可以使用。逆变换算法步骤(INTT):输入:点值表示A=(a0,a1,...,an-1),其中n是2的幂,模数p,原根g。输出:系数表示(多项式系数)。1.构造逆变换用的单位根:wn=pow(g,(p-1)/n,p)的正变换中使用的单位根,然后其逆元为wn_inv=pow(wn,p-2,p)(根据费马小定理,因为p是质数)。2.将数组A作为输入,进行类似FFT的迭代过程,但使用wn_inv代替wn。3.迭代结束后,将每个元素乘以n^{-1}modp。代码实现(Python):我们可以先实现NTT(正变换),然后通过修改单位根和最后乘以逆元来实现INTT。注意:在迭代过程中,蝴蝶操作与FFT相同,但使用模运算。下面给出NTT和INTT的代码,以及一个示例应用。示例:多项式乘法使用NTT将两个多项式从系数表示转换为点值表示,然后点值相乘,再用INTT转换回系数表示。注意:两个n次多项式相乘,结果最高为2n-1次,所以需要将多项式扩展到2的幂(长度至少为2n)。步骤:1.将两个多项式A和B补0到长度N(N是大于等于2n的最小2的幂)。2.对A和B分别进行NTT得到点值表示A'和B'。3.点乘:C'[i]=A'[i]*B'[i]modp。4.对C'进行INTT得到多项式C的系数。代码实现:首先,实现NTT正变换(使用迭代版本,避免递归开销):迭代版本的NTT(快速傅里叶变换的迭代版本)称为蝴蝶操作。迭代NTT步骤:1.将数组按二进制反转重新排列(位逆序置换)。2.枚举区间长度len从2到n,每次合并相邻的区间。使用单位根:对于每个区间,使用单位根乘以奇数项(旋转因子)。同样,INTT的迭代版本类似,但使用单位根的逆元,并在最后乘以n的逆元。我们定义:mod=998244353原根g=3函数:ntt(a,inverse=False)当inverse为True时,执行逆变换。注意:在逆变换时,我们使用单位根的逆元,并在最后乘以n^{-1}。代码中需要预先计算位逆序置换数组,以及单位根(或单位根逆元)的幂。由于时间限制,我们给出一个简化的实现(未做优化,但清晰)。步骤:1.位逆序置换:可以预先计算rev数组。2.迭代:从区间长度2开始,每次倍增。3.对于每个区间,计算单位根的幂(从0到len/2-1),然后对每个区间的每个位置进行蝴蝶操作。蝴蝶操作(正变换):forjinrange(0,n,len):forkinrange(0,len//2):u=a[j+k]v=a[j+k+len//2]*w%moda[j+k]=(u+v)%moda[j+k+len//2]=(u-v)%mod在逆变换中,我们使用w_inv,并且最后要乘以n_inv。但是注意:逆变换的迭代部分和正变换的结构相同,只是单位根不同。所以我们可以用相同的迭代结构。实现细节:在迭代时,正变换和逆变换的单位根选择:如果inverse为True,则使用单位根的逆元(即wn_inv=pow(wn,mod-2,mod))作为基础单位根,并且最后乘以n_inv。但是,实际上我们可以统一使用正变换的单位根,然后在逆变换时使用单位根的逆元(注意:在迭代过程中,单位根要取逆元,且最后乘以n的逆元)。另外,在迭代过程中,逆变换也可以使用正变换的单位根,但需要将单位根数组反转?引用[2]中提到在FFT中IDFT使用共轭复数,在NTT中就是使用逆元。因此,在迭代过程中,当进行逆变换时,我们使用单位根的逆元(即w=pow(wn,mod-2,mod))作为基础,然后每次迭代乘以w(实际上是乘以w_inv,因为逆变换的单位根是正变换单位根的逆元)。但是,更常见的做法是:在逆变换时,我们使用正变换的单位根(即同一个wn),但在蝴蝶操作中,将单位根替换为它的逆元(即w=pow(wn,(mod-2)*(n//len)?)不对。实际上,我们预先计算整个变换中会用到的单位根。在正变换中,我们使用单位根数组,在逆变换中,我们使用逆元数组。然而,我们可以这样:在逆变换时,将单位根设置为正变换单位根的逆元(注意:这里我们指的是每一层使用的单位根都要取逆元),然后最后乘以n^{-1}。具体代码步骤:1.如果inverse为True,则调用ntt(a,inverse)时,首先将数组进行位逆序置换(与正变换相同),然后在迭代过程中使用单位根的逆元(即w=pow(wn,mod-2,mod)),但注意:这里每一层的单位根是基础单位根的若干次方,所以我们可以直接对每一层的基础单位根取逆元。2.或者,我们可以这样:在逆变换时,将单位根数组替换为对应的逆元数组,然后执行和正变换完全相同的迭代过程,最后再乘以n^{-1}。实际上,更高效的做法是:在逆变换中,我们使用正变换单位根的逆元作为基础单位根,然后迭代过程与正变换相同,最后乘以n^{-1}。下面给出代码:注意:我们预先计算单位根数组,但这里为了简化,我们在每一层计算基础单位根:wn=pow(g,(mod-1)//n,mod)用于正变换,而逆变换时,基础单位根为pow(wn,mod-2,mod)或者直接使用pow(g,(mod-1)//n*(mod-2),mod)但这样计算量大。在迭代过程中,每一层需要的单位根是基础单位根的len/2次幂?不对,每一层需要的单位根是基础单位根的n/len次幂(因为每一层的区间长度是len,那么单位根应该是n//len次单位根,即wn^(n/len)=w_{len},但通常我们取wn=pow(g,(mod-1)//n,mod),那么w_{len}=wn^(n/len)=g^{(mod-1)/len},即len次单位根)。因此,在每一层,区间长度为len,我们需要单位根w=pow(wn,n//len,mod)(正变换)?不对,实际上,我们通常这样计算:每一层的基础单位根是w=pow(g,(mod-1)//len,mod)(正变换),或者w=pow(g,(mod-1)//len*(mod-2),mod)(逆变换)。为了避免重复计算,我们可以在每一层计算基础单位根:如果正变换,w=pow(g,(mod-1)//len,mod);如果逆变换,w=pow(g,(mod-1)//len*(mod-2),mod)即w=pow(w,mod-2,mod)。但是这样计算量较大。另一种方法是:我们预先知道模数和原根,对于给定的长度len(len是2的幂),我们可以计算单位根w=pow(g,(mod-1)//len,mod)用于正变换,而逆变换则用w_inv=pow(w,mod-2,mod)。因此,代码可以这样写:我们用一个函数同时支持正变换和逆变换。步骤:1.位逆序置换(rev数组)2.从len=2开始,直到len<=n(n是数组长度),每次len*=2w=pow(g,(mod-1)//len,mod)#正变换的单位根ifinverse:w=pow(w,mod-2,mod)#逆变换时使用单位根的逆元forjinrange(0,n,len):w_k=1#当前单位根的幂forkinrange(0,len//2):#蝴蝶操作u=a[j+k]v=a[j+k+len//2]*w_k%moda[j+k]=(u+v)%moda[j+k+len//2]=(u-v)%modw_k=w_k*w%mod3.如果是逆变换,则最后将每个元素乘以n^{-1}modmod。但是,上面的代码在逆变换时,我们在每一层使用了单位根的逆元,并且最后乘以n^{-1}。然而,上面的迭代过程在逆变换时与正变换的迭代过程完全相同(除了单位根取逆元),这符合引用[2]的描述。注意:位逆序置换在正变换和逆变换中都需要做。但是,我们也可以将位逆序置换放在最后(在逆变换时,有些实现将位逆序置换放在最后一步)。但通常,迭代NTT的实现都是先做位逆序置换(或者后做位逆序置换,但可以通过设置避免)。我们这里采用先做位逆序置换。另一种常见实现:不做显式的位逆序置换,而是通过迭代方式直接得到顺序的数组(即最后一步做位逆序置换)。但这里我们采用先做位逆序置换,然后迭代合并。位逆序置换函数可以预先计算。下面,我们给出完整的NTT函数(包括正变换和逆变换)。注意:当n不是2的幂时,我们需要将数组长度扩展到2的幂。为了方便,我们假设输入数组a的长度已经是2的幂(由调用者保证)。代码:定义:mod=998244353g=3函数:ntt(a,inverse=False)参数a:整数列表,元素在模mod下inverse:False表示正变换,True表示逆变换步骤:1.计算n=len(a)2.计算位逆序置换数组rev(长度为n)3.将a按照rev重新排列4.迭代:len从2到n(每次乘以2)w_n=pow(g,(mod-1)//len,mod)#正变换中长度为len的单位根ifinverse:w_n=pow(w_n,mod-2,mod)#逆变换则取逆元forjinrange(0,n,len):#处理每个区间w=1forkinrange(j,j+len//2):#每个区间的前半部分#蝴蝶操作u=a[k]v=a[k+len//2]*w%moda[k]=(u+v)%moda[k+len//2]=(u-v)%modw=w*w_n%mod#更新单位根幂次5.如果是逆变换,则乘以n^{-1}modmodinv_n=pow(n,mod-2,mod)foriinrange(n):a[i]=a[i]*inv_n%mod但是,上述代码中,位逆序置换数组rev如何计算?rev[i]表示i的二进制位反转(在log2(n)位内)后的值。例如:n=8,则:i:0(000)->0(000)->01(001)->4(100)->42(010)->2(010)->2?不对,应该是反转后为(010)还是2?不对,反转后是(010)变成(010)?不对,反转应该是(010)->(010)即2,但通常反转后是(010)变成(010)吗?注意,反转是在log2(n)位内反转。正确:n=8,位宽为3:0(000)->000->01(001)->100->42(010)->010->23(011)->110->64(100)->001->15(101)->101->56(110)->011->37(111)->111->7计算rev数组的代码:rev[0]=0foriinrange(1,n):rev[i]=rev[i//2]//2ifi&1:rev[i]|=n//2或者另一种方法(更直观):rev=[0]*nbit_length=n.bit_length()-1foriinrange(n):rev[i]=rev[i//2]//2|((i&1)<<(bit_length-1))但这样计算比较复杂,我们使用:rev=[0]*nforiinrange(1,n):rev[i]=rev[i>>1]>>1ifi&1:rev[i]|=n>>1注意:n是2的幂,所以n>>1就是最高位为1其他为0。例如:n=8,rev[1]=rev[0]>>1|(4)->0|4=4rev[2]=rev[1]>>1|(0)->4>>1=2->2rev[3]=rev[1]>>1|(4)->2|4=6rev[4]=rev[2]>>1|(0)->2>>1=1->1rev[5]=rev[2]>>1|(4)->1|4=5rev[6]=rev[3]>>1|(0)->6>>1=3->3rev[7]=rev[3]>>1|(4)->3|4=7这个递推公式正确。因此,我们可以这样写:注意:rev[0]=0。代码实现rev数组:rev=[0]*nforiinrange(1,n):rev[i]=rev[i>>1]>>1ifi&1:rev[i]|=n>>1但是,这里rev[i]的值可能大于等于n?不会,因为n>>1是最高位,而后面移位不会超过n。然后,我们交换a[i]和a[rev[i]],注意只需要交换一次(i<rev[i]时交换,避免重复交换又换回来)。或者,我们可以直接创建一个新数组:temp=[0]*nforiinrange(n):temp[i]=a[rev[i]]a[:]=temp现在,我们可以写出完整的NTT函数。但是,注意:在迭代过程中,我们使用了每一层的基础单位根w_n,然后在该层内,我们使用w从1开始,乘以w_n得到下一个单位根。另外,在每一层中,单位根的幂次是递增的,所以内层循环中,w=w*w_n%mod。我们写代码:注意:在迭代过程中,len从2开始,然后4,8,...,n。代码:下面给出一个示例:多项式乘法。假设有两个多项式A和B,我们计算C=A*B。步骤:1.将A和B补0到长度L(L>=len(A)+len(B)-1,且L是2的幂)2.对A进行NTT(正变换)3.对B进行NTT(正变换)4.点乘:C[i]=A[i]*B[i]modmod5.对C进行NTT(逆变换)然后C就是乘积多项式的系数。注意:由于NTT是在循环卷积下进行的,如果多项式乘积次数>=L,则会发生循环卷积。所以L必须大于等于结果多项式的次数(即len(A)+len(B)-1)。下面我们写代码示例。由于用户要求实现NTT逆变换,我们重点给出NTT函数(包括逆变换)和多项式乘法的例子。注意:模数使用998244353,原根g=3。代码:</think>### 快速数论变换(INTT)原理与实现 快速数论变换(INTT)是快速数论变换(NTT)的逆运算,用于将点值表示转换回系数表示。在模数 $p$(通常为 NTT 友好质数如 998244353)下,INTT 的数学基础是原根和模逆元。 #### INTT 数学原理 设多项式 $A(x) = \sum_{k=0}^{n-1} a_k x^k$ 在点 $x_i = g^{\frac{p-1}{n}i}$($g$ 为 $p$ 的原根)处的点值为 $A_i$,则: - **正变换 (NTT)**:$A_j = \sum_{k=0}^{n-1} a_k \cdot (g_n)^k \mod p$ - **逆变换 (INTT)**:$a_k = \frac{1}{n} \sum_{j=0}^{n-1} A_j \cdot (g_n^{-kj}) \mod p$ 其中 $g_n = g^{\frac{p-1}{n}}$ 是 $n$ 次主单位根,$g_n^{-1}$ 是其模逆元,$\frac{1}{n}$ 是 $n$ 的模逆元[^1][^2]。 #### INTT 算法步骤 1. **输入**:点值序列 $[A_0, A_1, \dots, A_{n-1}]$(长度 $n=2^m$) 2. **位逆序置换**:重排输入序列 3. **迭代计算**: - 从区间长度 $len=2$ 开始,倍增至 $n$ - 每层使用单位根逆元 $ \omega = g_n^{-1} \mod p $ - 执行蝴蝶操作:$\begin{cases} u' = u + \omega \cdot v \\ v' = u - \omega \cdot v \end{cases}$ 4. **缩放**:结果乘以 $n^{-1} \mod p$ #### Python 实现(模 $p=998244353$) ```python MOD = 998244353 G = 3 # 原根 def ntt(a, inverse=False): n = len(a) # 位逆序置换 rev = [0] * n for i in range(1, n): rev[i] = rev[i >> 1] >> 1 if i & 1: rev[i] |= n >> 1 for i in range(n): if i < rev[i]: a[i], a[rev[i]] = a[rev[i]], a[i] # 迭代计算 len_block = 2 while len_block <= n: wn = pow(G, (MOD-1)//len_block, MOD) if inverse: # INTT 使用单位根逆元 wn = pow(wn, MOD-2, MOD) for i in range(0, n, len_block): w = 1 for j in range(i, i + len_block//2): u = a[j] v = w * a[j + len_block//2] % MOD a[j] = (u + v) % MOD a[j + len_block//2] = (u - v) % MOD w = w * wn % MOD len_block <<= 1 # INTT 缩放 if inverse: inv_n = pow(n, MOD-2, MOD) a[:] = [x * inv_n % MOD for x in a] return a # INTT 直接调用 def intt(a): return ntt(a, inverse=True) # 示例:多项式乘法 def poly_multiply(a, b): n = 1 while n < len(a) + len(b): n <<= 1 a_pad = a + [0] * (n - len(a)) b_pad = b + [0] * (n - len(b)) # NTT -> 点乘 -> INTT a_ntt = ntt(a_pad) b_ntt = ntt(b_pad) c_ntt = [a_ntt[i] * b_ntt[i] % MOD for i in range(n)] return intt(c_ntt) # 测试 if __name__ == "__main__": A = [1, 2] # 多项式 1 + 2x B = [3, 4] # 多项式 3 + 4x C = poly_multiply(A, B) # 结果应为 [3, 10, 8, 0] -> 3 + 10x + 8x^2 print("多项式乘积系数:", C[:3]) # 截取有效部分 [3, 10, 8] ``` #### 关键说明 1. **单位根逆元**:INTT 使用 $g_n^{-1} \equiv g_n^{p-2} \mod p$(费马小定理) 2. **缩放因子**:INTT 结果需乘 $n^{-1} \mod p$ 3. **时间复杂度**:$O(n \log n)$,与 FFT 相同 4. **应用场景**: - 多项式乘法(如加密算法、编码理论) - 大整数乘法(将整数视为多项式系数) - 卷积计算(信号处理、图像处理)[^3] #### 优化方向 1. **预处理单位根**:预先计算所有 $\omega$ 值减少模幂运算 2. **非递归实现**:避免位逆序置换开销 3. **SIMD 指令**:利用 CPU 并行指令加速蝴蝶操作 4. **蒙哥马利模约减**:优化模运算性能
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值