FFT & NTT & FWT

 


只是学习笔记,真心推荐 cmd ,他讲的真的细到把所有的前置知识都讲了一遍。

FBI WARNINGNTT

FFT & NTT & FWT 大杂烩

首先我们引入卷积的概念,对于两个多项式进行卷积,形如:

F(x)=f(x)g(x)

Fn=ij=nfigj

其中 Fn,fi,gj 均表示多项式 F,f,g 的某一项系数。

一般来说两个多项式进行朴素实现是 O(mn)m,n 分别是两个多项式的次数。

那么就下来介绍几种对于特殊的卷积可以加快速度的算法。


一、FFT

FFT 是针对于加法卷积(也就是多项式相乘)的快速计算方法。

大体思路

首先我们知道对于一个 n 次多项式可以由 n+1 个不同的点来表示,而对于两个多项式 f(x),g(x) 相乘,我们可以通过 n+1 个点在 O(n) 时间复杂度内完成,然后在通过插值将得到的 n+1 个点还原成一个多项式。这就是 FFT 的大体思路。

image

但是朴素求值和插值都是 O(n2) 的,所以我们现在要优化求值和插值的过程。

加速

前置:单位根

首先大家应该学过复数,接下来介绍单位根

有一个单位圆,我们通过 n 条线将它均分成了 n 份,我们依次编号 0,1,2,,n1 ,他们对应的复数就是单位根 wni

image

如图是 n=4 的情况,他恰好对应着坐标轴。

有一天FFT的发明者 傅里叶 突然想把 wni 代入到多项式中,然后发现这玩意有很好的性质以至于可以快速求值和插值。

首先由几个性质在几何角度看来非常容易证明:

1、 wn0=1

2、 wnk=wnkmodn

3、 (wnk)j=wnjk

4、 wnk×wnj=wnk+j

5、 w2n2k=wnk

6、 n 是偶数, wnk+n2=wnk


主体

那么接下来讲解 FFT 的主要过程:

求值:

对一个长度为 n 的序列,它对应着一个 n1 次多项式 F(x)

对于 F(x) ,我们按奇偶把他一分为二,下标为偶数的在前面,称为 Fl(x) ,下标为奇数的在后面,称为 Fr(x)

image

那么我们尝试写出 F(x)Fl(x),Fr(x) 的关系式:

F(x)=Fl(x2)+xFr(x2)

接下来就是单位根出场的时候了,我们将 wnk 代入 F(x) 和上述式子:

对于 k<n2

F(wnk)=Fl(wn2k)+wnkFr(wn2k)=Fl(wn2k)+wnkFr(wn2k)

对于 k<n2 ,我们代入 wnk+n2

F(wnk+n2)=Fl(wn2k+n)+wnk+n2Fr(wn2k+n)=Fl(wn2k)wnkFr(wn2k)

上面的式子意味着什么,它意味着我们可以通过分治去 O(nlogn) 计算 F(wnk) 的值。

好,我们现在已经可以 O(nlogn) 的将一个多项式转化为 (wnk,F(wnk)) 的点值表达式了,然后可以 O(n) 点乘出最终的多项式 , 然后我们现在需要快速将这个多项式插回去。

插值:

设我们刚才把 F(x) 转成了 G(x) 。那么我们直接不加证明的给出:

nF(k)=i=0n1(wnk)iG(i)

算了还是证明一下:

我们将 G(i) 代入

=i=0n1wnikj=0n1wnijF(j)=j=0n1F(j)wniji=0n1wnik=j=0n1F(j)i=0n1wni(jk)

分类讨论,对于 j=k 的情况:

贡献为 F(k)i=0n1wn0=nF(k)

对于 jk 的情况:

贡献为 F(j)i=0n1wnip , 等比数列求和, i=0n1wnip=wnnp1wnp1=0 ,也就是没有贡献!

所以最终就有

=nF(k)=

所以我们将插值也转成了求值,同样可以 O(nlogn) 求。

实现

朴素实现

现在可以再来看一开始的流程图

image

整个过程就比较清晰了吧。

给出模板题: P3803 【模板】多项式乘法(FFT)

码(未卡常版本)
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> pii;
typedef unsigned long long ull;
#define mk make_pair
#define ps push_back
#define fi first
#define se second
const int N=5e6+10,inf=0x3f3f3f3f;
const ll mod=1e9+7,linf=0x3f3f3f3f3f3f3f3f;
inline ll read(){
char c=getchar();ll x=0;bool f=0;
while(!isdigit(c))f=c=='-'?1:0,c=getchar();
while(isdigit(c))x=(x<<1)+(x<<3)+(c^48),c=getchar();
return f?-x:x;
}
const double PI=acos(-1);
int n,m;
struct jj{
// 复数结构体
double x,y;
inline jj operator +(const jj&k){return {x+k.x,y+k.y};}
inline jj operator -(const jj&k){return {x-k.x,y-k.y};}
inline jj operator *(const jj&k){return {x*k.x-y*k.y,x*k.y+y*k.x};}
}f[N],g[N],tp[N];
inline void fft(jj *f,int l,int r,bool fl){
if(l==r)return;
int mid=l+r>>1,len=r-l+1;
for(int i=l;i<=r;++i)
tp[i]=f[i];
for(int i=l;i<=mid;++i){
f[i]=tp[(i-l)*2+l],f[mid+1+(i-l)]=tp[(i-l)*2+l+1];// 按照奇偶分裂
}
fft(f,l,mid,fl),fft(f,mid+1,r,fl);// 分治计算
jj op={cos(2*PI/len),sin(2*PI/len)},now={1,0};// op=w_n^1
if(fl)op.y*=-1;// 如果 fl 为 1 表示是插值,此时 op=w_n^-1
for(int i=l;i<=mid;++i){
tp[i]=f[i]+now*f[mid+1+(i-l)];
tp[mid+1+(i-l)]=f[i]-now*f[mid+1+(i-l)];
now=now*op;
}
for(int i=l;i<=r;++i)
f[i]=tp[i];
}
signed main(){
#ifndef ONLINE_JUDGE
freopen("in.in","r",stdin);
freopen("out.out","w",stdout);
#endif
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
n=read(),m=read();
for(int i=0;i<=n;++i)
f[i]={read(),0};
for(int i=0;i<=m;++i)
g[i]={read(),0};
m+=n;n=1;
while(n<=m)n<<=1;
--n;
fft(f,0,n,0),fft(g,0,n,0);
for(int i=0;i<=n;++i)
f[i]=f[i]*g[i];
fft(f,0,n,1);
for(int i=0;i<=m;++i){
cout<<(int)(f[i].x/(n+1)+0.499)<<' ';//有精度问题,四舍五入
}
}

常数非常大,luogu最大点跑了 800 ms ,考虑一些优化。

优化

首先可以把递归改为非递归版的迭代。其次一个重要优化是 蝴蝶变换 。他说的是我们递归的过程会把 F(x) 按奇偶分裂,我们最终分裂好的整个序列原来在 i (下标从 0 开始)位置的数,现在在 i 按照二进制翻转后的数为下标的地方,比如说 10101112(8710) ,在翻转后是 11101012(11710)

首先证明这个事:

我们脑动模拟一下递归过程,每次按照偶在左半边,奇在右半边去分,相当于看二进制下最后一位,按照大小确定了最终二进制下第一位的大小。

有点抽象,但是有图:

以下将经过 i 次分裂的序列称为 Fi

这是一开始的 F0(x)

image

然后我们按照最后一位的大小,分开

image

这时可以看到 F1 的下标的最后一位和 F0 下标的第一位相同,因为一开始我们是按第一位大小排的,后来我们按最后一位大小排的。同时前一半和后一半没有关系了,他们各自内部都是按大小排好的,而且此时所有数的最后一位是啥不重要了,相当于所有数最后一位没了,倒数第二位顺延为最后一位,继续递归处理。

那么下一层也就根据 F1 的最后一位,也就是倒数第二位确定了第二位的是啥,然后 ,也就相当于二进制翻转。

image

而事实也确实如此。

好,设 posi 表示 i 翻转后的答案,那么我们怎么求 posi ? 可以考虑先翻转最后一位之前的,由 posi/2 得到,同时翻转最后最后一位,于是有 pos[i]=(pos[i>>1]>>1)|(i&1?n>>1:0)n 是序列长度。

所以我们就获得了一个常数较小的 FFT:

码(卡常版)
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> pii;
typedef unsigned long long ull;
#define mk make_pair
#define ps push_back
#define fi first
#define se second
const int N=1e6+10,inf=0x3f3f3f3f;
const ll mod=1e9+7,linf=0x3f3f3f3f3f3f3f3f;
inline ll read(){
char c=getchar();ll x=0;bool f=0;
while(!isdigit(c))f=c=='-'?1:0,c=getchar();
while(isdigit(c))x=(x<<1)+(x<<3)+(c^48),c=getchar();
return f?-x:x;
}
const double PI=acos(-1);
struct jj{
double x,y;
inline jj operator +(const jj&k){return {x+k.x,y+k.y};}
inline jj operator -(const jj&k){return {x-k.x,y-k.y};}
inline jj operator *(const jj&k){return {x*k.x-y*k.y,x*k.y+y*k.x};}
}f[N<<2],g[N<<2],ji[N<<2];
int pos[N<<2];
inline void fft(jj *f,int n,bool fl){
for(int i=0;i<n;++i)
if(i<pos[i])swap(f[i],f[pos[i]]);//pos[i] 表示预处理二进制翻转后的位置
for(int len=2,mid;len<=n;len<<=1){
mid=len>>1;
jj op=ji[len],lp;//ji[len]表示预处理 w_n^1 ,因为 sin,cos 太慢了,所以预处理会少调用几次
if(fl)op.y*=-1;
for(int i=0;i<n;i+=len){
jj now={1,0};
for(int j=i;j<i+mid;++j){
lp=now*f[j+mid];
f[j+mid]=f[j]-lp;f[j]=f[j]+lp;
now=now*op;
}
}
}
}
int n,m;
signed main(){
#ifndef ONLINE_JUDGE
freopen("in.in","r",stdin);
freopen("out.out","w",stdout);
#endif
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
n=read(),m=read();
for(int i=0;i<=n;++i)
f[i]={read(),0};
for(int i=0;i<=m;++i)
g[i]={read(),0};
m+=n;n=1;ji[1]={1,0};
while(n<=m)n<<=1,ji[n]={cos(2*PI/n),sin(2*PI/n)};
for(int i=0;i<n;++i)
pos[i]=(pos[i>>1]>>1)|(i&1?n>>1:0);
fft(f,n,0),fft(g,n,0);
for(int i=0;i<=n;++i)
f[i]=f[i]*g[i];
fft(f,n,1);
for(int i=0;i<=m;++i){
cout<<(int)(f[i].x/n+0.499)<<' ';
}
}

luogu最大点 469 ms ,而且出奇的好写。


upd:经 Qyun 和 CuFeO4 推荐更新“三次变两次优化”

三次变两次

因为是复数运算,设一个复数多项式 F(x)=f(x)+g(x)i ,那么有

F2(x)=f2(x)g2(x)+2f(x)g(x)i

所以我们要求的就是 F2(x) 的虚部除以2,那么再插值的时候只需要插一次就行,从三次变成了两次,快了约 14

码(最终版)
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> pii;
typedef unsigned long long ull;
#define mk make_pair
#define ps push_back
#define fi first
#define se second
const int N=1e6+10,inf=0x3f3f3f3f;
const ll mod=1e9+7,linf=0x3f3f3f3f3f3f3f3f;
inline ll read(){
char c=getchar();ll x=0;bool f=0;
while(!isdigit(c))f=c=='-'?1:0,c=getchar();
while(isdigit(c))x=(x<<1)+(x<<3)+(c^48),c=getchar();
return f?-x:x;
}
const double PI=acos(-1);
struct jj{
double x,y;
inline jj operator +(const jj&k){return {x+k.x,y+k.y};}
inline jj operator -(const jj&k){return {x-k.x,y-k.y};}
inline jj operator *(const jj&k){return {x*k.x-y*k.y,x*k.y+y*k.x};}
}f[N<<2],ji[N<<2];
int pos[N<<2];
inline void fft(jj *f,int n,bool fl){
for(int i=0;i<n;++i)
if(i<pos[i])swap(f[i],f[pos[i]]);
for(int len=2,mid;len<=n;len<<=1){
mid=len>>1;
jj op=ji[len],lp;
if(fl)op.y*=-1;
for(int i=0;i<n;i+=len){
jj now={1,0};
for(int j=i;j<i+mid;++j){
lp=now*f[j+mid];
f[j+mid]=f[j]-lp;f[j]=f[j]+lp;
now=now*op;
}
}
}
}
int n,m;
signed main(){
#ifndef ONLINE_JUDGE
freopen("in.in","r",stdin);
freopen("out.out","w",stdout);
#endif
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
n=read(),m=read();
for(int i=0;i<=n;++i)
f[i].x=read();
for(int i=0;i<=m;++i)
f[i].y=read();
m+=n;n=1;ji[1]={1,0};
while(n<=m)n<<=1,ji[n]={cos(2*PI/n),sin(2*PI/n)};
for(int i=0;i<n;++i)
pos[i]=(pos[i>>1]>>1)|(i&1?n>>1:0);
fft(f,n,0);
for(int i=0;i<=n;++i)
f[i]=f[i]*f[i];
fft(f,n,1);
for(int i=0;i<=m;++i){
cout<<(int)(f[i].y/n/2+0.499)<<' ';
}
}

luogu最大点 348 ms。

例题

P1919 【模板】高精度乘法 | A*B Problem 升级版

另一个板子题,FFT 加速即可。

P3338 [ZJOI2014] 力

给出序列 q ,有

Ei=i=1j1qi(ij)2i=j+1nqi(ij)2

求所有的 Ei

我们考虑如何把 Ei 转成卷积的形式。首先把烦人的除法去掉,令 G(x)=1x2 ,特殊的, G(0)=0 ,同时令 q0=0 ,那么有

Ei=i=0jq(i)G(ji)i=jnq(i)G(ij)

前面已经是卷积的形式了,无需转化,后面的不是,我们考虑给他转成卷积的形式。

画个图看看:

image

他们是这样相乘的,但是卷积是这样相乘的:

image

所以我们考虑给 q(x) 转过来,变成 Q(x) ,所以后面的式子就变成了

i=0k=njQ(i)G(ki)

对两者 FFT 然后加起来即可。

二、 NTT

NTT ,中文 快速数论变换 是用来解决卷积过程中需要取模的快速加法卷积,说白了就是可以取模的 FFT。

这个东西就是直接把 wn1 替换成了 gmod1n ,其中 mod 是模数,g 是模数的一个原根,n 是序列长度 ,但是原根这个东西我也不会,目前只知道 998244353 的原根是 3 ,所以 NTT 到此为止。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define int ll
typedef pair<int,int> pii;
typedef unsigned long long ull;
#define mk make_pair
#define ps push_back
#define fi first
#define se second
const int N=1e6+10,inf=0x3f3f3f3f;
const ll mod=998244353,linf=0x3f3f3f3f3f3f3f3f,g=3;
inline ll read(){
char c=getchar();ll x=0;bool f=0;
while(!isdigit(c))f=c=='-'?1:0,c=getchar();
while(isdigit(c))x=(x<<1)+(x<<3)+(c^48),c=getchar();
return f?-x:x;
}
inline ll qpow(ll x,ll y){
ll ans=1;
while(y){
if(y&1)ans=ans*x%mod;
x=x*x%mod;y>>=1;
}
return ans;
}
ll ng=qpow(g,mod-2);// g 的逆元
int pos[N<<2];
inline void ntt(ll *f,int n,bool fl){
for(int i=0;i<n;++i)
if(i<pos[i])swap(f[i],f[pos[i]]);
for(int len=2,mid;len<=n;len<<=1){
int op=qpow(fl?g:ng,(mod-1)/len),tp;mid=len>>1;
for(int i=0;i<n;i+=len){
ll now=1;
for(int j=i;j<i+mid;++j){
tp=f[j+mid]*now%mod;
f[j+mid]=f[j]-tp;
if(f[j+mid]<0)f[j+mid]+=mod;
f[j]+=tp;
if(f[j]>=mod)f[j]-=mod;
now=now*op%mod;
}
}
}
}
int n,m;
ll f[N<<2],ff[N<<2];
signed main(){
#ifndef ONLINE_JUDGE
freopen("in.in","r",stdin);
freopen("out.out","w",stdout);
#endif
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
n=read(),m=read();
for(int i=0;i<=n;++i)
f[i]=read();
for(int i=0;i<=m;++i)
ff[i]=read();
m+=n;n=1;
while(n<=m)n<<=1;
for(int i=0;i<n;++i)
pos[i]=(pos[i>>1]>>1)|(i&1?n>>1:0);
ntt(f,n,0),ntt(ff,n,0);
for(int i=0;i<n;++i)
f[i]=f[i]*ff[i]%mod;
ntt(f,n,1);
ll ny=qpow(n,mod-2);
for(int i=0;i<=m;++i)
cout<<f[i]*ny%mod<<' ';
}

但是我的 NTT 好像没有 FFT 快???

三、 FWT

在 OI 中,FWT是用来解决有关位运算卷积的快速卷积算法。

即对于 ci=i=jkajbk ,在知道 a,b 的情况下,快速计算 c

或运算

此时 |

我们设 A,B,C 分别对应 a,b,c 进行了 FWT 后的序列,我们定义 Ai=i=i|jaj ,那么自然有 Ci=AiBi

考虑如何快速求 A ,我们考虑初始序列 a ,它的长度 n 是 2 的整数次幂,我们将 a 一分为二,左边为 a0 ,右边为 a1 ,两者没有任何关系 ,直接递归求出 A0,A1 ,此时需要需要合并 A0,A1A ,即,把 a0A1 的贡献考虑进来, a1A0 中的贡献考虑进来。

注意到将 a1A0 考虑时,其中 a1 的目前最高位上一定是 1 ,而 a0 中的数一定是 0 ,所以 a1 不可能向 A0 做贡献。

再看 a0A1 做贡献,注意到 a0j|a1i=a1i 当且仅当 a0j|a0i=a0i ,即能向 a0i 做贡献的都能向 a1i 做贡献,所以有

A=he(A0,A0+A1)

其中 + 表示多项式相加, he(x,y) 表示将 x,y 两个序列顺序连接。所以我们可以分治去求 A

类似的可以退出逆运算为

A=he(A0,A0A1)

也就可以在得到 C 之后反推出 c


感觉就和 FFT , NTT 一样, FWT(|) 找了一种特殊运算,使得可以分治 O(nlogn) 的去求值插值。同时分治的过程可以看成逐渐考虑二进制上每一位的影响。


与运算

类比或运算,这里直接给出式子

求值:

A=he(A0+A1,A1)

插值:

A=he(A0A1,A1)


异或运算

异或运算是 FWT 中最难理解的一个,是一种非常神奇的构造。

以下 xor

首先我们定义 xy=popcount(xy)mod2 ,然后我们先给出 (xy)(xz)=x(yz)

证明:

我们一位一位的去考虑,在一开始,两边都是 0,然后考虑新的一位,如果要做出改变,首先 x 在这位上得是 1,否则不用考虑。然后看 yz 的值,如果为 1 ,说明两个数一个是 1,一个是 0,所以此时左边右边的值都会改变,不影响等式成立。如果 yz=0 ,说明两者一样,此时两边的值都不会改变,也不影响等式成立。

综上, (xy)(xz)=x(yz)

接下来我们定义 Ai=ij=0ajij=1aj ,那么有

AiBi=(ij=0ajij=1aj)(ij=0bjij=1bj)=(ij=0ajik=0bk+ij=1ajik=1bk)(ij=0ajik=1bk+ij=1ajik=0bk)=i(jk)=0ajbki(jk)=1ajbk=Ci

接下来考虑如何快速计算 A ,我们仍然尝试将它分为 A0,A1 递归求解,接下来考虑合并。

首先举一个例子:

假设 A 长度为 8,我们写成二进制形式

image

然后我们一分为二,递归求解,但是注意递归到下一层后,他们在本层的第一位将不再考虑,也就是

image

这也就是为什么之前里面说分治的过程是逐渐考虑二进制位的影响。

理解了这里,下面就很好理解了。

对于 A ,分为 A0,A1 之后,我们新考虑一位,定义 x 为 考虑新一位的 x ,那么有 a0ia1j=a1ia1j,a0ia0j=a0ia0j,a1ia1j=(a1ia1j)1,a1ia0j=a0ia0j ,于是我们有

A=he(A0+A1,A0A1)

同时有逆推

A=he(A0+A12,A0A12)

模板题

P4717 【模板】快速莫比乌斯/沃尔什变换 (FMT/FWT)

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define int ll
typedef pair<int,int> pii;
typedef unsigned long long ull;
#define mk make_pair
#define ps push_back
#define fi first
#define se second
const int N=1e6+10,inf=0x3f3f3f3f;
const ll linf=0x3f3f3f3f3f3f3f3f,mod=998244353;
inline ll read(){
char c=getchar();ll x=0;bool f=0;
while(!isdigit(c))f=c=='-'?1:0,c=getchar();
while(isdigit(c))x=(x<<1)+(x<<3)+(c^48),c=getchar();
return f?-x:x;
}
inline void OR(int *f,int n,int op){
for(int len=2;len<=n;len<<=1){
int mid=len>>1;
for(int j=0;j<n;j+=len){
for(int k=j;k<j+mid;++k){
f[k+mid]=(f[k+mid]+f[k]*op)%mod;
}
}
}
}
inline void AND(int *f,int n,int op){
for(int len=2;len<=n;len<<=1){
int mid=len>>1;
for(int j=0;j<n;j+=len){
for(int k=j;k<j+mid;++k)
f[k]=(f[k]+f[k+mid]*op)%mod;
}
}
}
inline void XOR(int *f,int n,int op){
for(int len=2;len<=n;len<<=1){
int mid=len>>1,tp;
for(int j=0;j<n;j+=len){
for(int k=j;k<j+mid;++k){
tp=f[k];
f[k]=(f[k]+f[k+mid])*op%mod;
f[k+mid]=(tp-f[k+mid])*op%mod;
}
}
}
}
inline ll qpow(ll x,ll y){
ll ans=1;
while(y){
if(y&1)ans=ans*x%mod;
x=x*x%mod;y>>=1;
}
return ans;
}
int a[N],b[N],c[N],n;
signed main(){
#ifndef ONLINE_JUDGE
freopen("in.in","r",stdin);
freopen("out.out","w",stdout);
#endif
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
n=read();int m=1<<n;
for(int i=0;i<m;++i)
a[i]=read();
for(int i=0;i<m;++i)
b[i]=read();
OR(a,m,1),OR(b,m,1);
for(int i=0;i<m;++i)
c[i]=a[i]*b[i]%mod;
OR(c,m,-1);OR(a,m,-1),OR(b,m,-1);
for(int i=0;i<m;++i)
cout<<(c[i]+mod)%mod<<' ';
cout<<'\n';
AND(a,m,1);AND(b,m,1);
for(int i=0;i<m;++i)
c[i]=a[i]*b[i]%mod;
AND(a,m,-1),AND(b,m,-1),AND(c,m,-1);
for(int i=0;i<m;++i)
cout<<(c[i]+mod)%mod<<' ';
cout<<'\n';
XOR(a,m,1),XOR(b,m,1);
for(int i=0;i<m;++i)
c[i]=a[i]*b[i]%mod;
XOR(c,m,qpow(2,mod-2));
for(int i=0;i<m;++i)
cout<<(c[i]+mod)%mod<<' ';
}

小结

感觉三者都是通过构造找了一些特殊的点或特殊的运算,使得我们可以通过这写特殊的性质达到快速计算的效果,当然这一切的前提都是因为我们用到的 O(n) 点乘对点是没有要求的,这就支撑着我们可以通过构造特殊点来完成快速卷积。

UPD:感谢各位指出本篇的 Huge 量错误。

posted @   lzrG23  阅读(497)  评论(21编辑  收藏  举报
相关博文:
阅读排行:
· 2分钟学会 DeepSeek API,竟然比官方更好用!
· .NET 使用 DeepSeek R1 开发智能 AI 客户端
· 10亿数据,如何做迁移?
· 推荐几款开源且免费的 .NET MAUI 组件库
· c# 半导体/led行业 晶圆片WaferMap实现 map图实现入门篇
点击右上角即可分享
微信分享提示