多项式全家桶 - 常系数齐次线性递推
常系数齐次线性递推
注意:这篇是一个不用很高线性代数基础的理解方法
(图解党狂喜)
问题描述
已知,并已知递推系数
求
常规的矩阵快速幂过不去了。我们需要一个带 的做法。
换一个思路
考虑把转移看成一个带权 DAG 图:点 和点 有一个有向边,边权为 。(,后面如果没提到 的取值范围就是这个范围)
然后我们来考虑点 的状态把点 算了多少遍。假设我们能求出来“多少遍”,那直接就可以求出 了:每个 ,乘以对应的遍数,加起来,就行了。
然后我们考虑用拓扑排序求这个东西。设 表示遍数数组。
我们的边界条件是 :一遍,
对于每个当前入度为 的点 ,遍历它的出边 ,设边权为 。那么 c[v]+=c[u]*w
,显然。
然后这么做一遍就可以得到 ,对应的乘起来即可。复杂度
然后你会问:艹这nm不就是直接递推么
是的
但是我们反过来考虑了个寂寞 ... 吗?
魔法
考虑 的生成函数,设为 。
一开始我们知道这玩意就是 (在 位置有一个 ,其余为 )
我们令拓扑排序删边的时候,如果一个点的出度为 (即出边被删没了),且不是 中的点,就把这个点也删除,同时标记它的 为 (这样显然没问题,因为这个点已经不用了)
然后这样显然只会剩下 有可能有值(其它都是 )
考虑一次拓扑排序做了甚么:假设当前入度为 的点是 ,那相当于:
- 标记 加上
- 然后标记 为 ,即 减去
一共操作了 个数,并且操作数都是 的倍数。
在生成函数上考虑,它相当于减去了 倍的:
这个东西的系数是 ,然后后面都是 。把后面的 去掉,只保留这 项系数,构成的多项式被称作这个递推式的 特征多项式。设它为
注:如果您有了解过斐波那契的通项是怎么推的,那您应该接触过这个“特征多项式”的定义
斐波那契的特征多项式为:
然后每次我们就是把 和当前 的最高位(设为 )对齐,然后减去 的 倍 —— 这是拓扑排序干的事情。
我们发现它的本质就是在求余数
回想一下小学竖式计算除法的过程,又因为 的最高位为 ,所以减去 的 倍其实就是在做带余除法求余数
所以,
然后我们只要求出 的值,就可以知道遍数数组 ,对应的乘以初始值 ,就得到了
然后这个 怎么算呢? 老大了
考虑多项式快速幂 —— 不用 ,而是普通的倍增快速幂,参考整数的写法,并把“模”的操作改成多项式取模就可以了。
#include <bits/stdc++.h>
using namespace std;
namespace Flandre_Scarlet
{
#define N 2000006
#define GG 3
#define GI 332748118
#define mod 998244353
#define int long long
#define F(i,l,r) for(int i=l;i<=r;++i)
#define D(i,r,l) for(int i=r;i>=l;--i)
#define Fs(i,l,r,c) for(int i=l;i<=r;c)
#define Ds(i,r,l,c) for(int i=r;i>=l;c)
#define MEM(x,a) memset(x,a,sizeof(x))
#define FK(x) MEM(x,0)
#define Tra(i,u) for(int i=G.st(u),v=G.to(i);~i;i=G.nx(i),v=G.to(i))
#define p_b push_back
#define sz(a) ((int)a.size())
#define all(a) a.begin(),a.end()
#define iter(a,p) (a.begin()+p)
#define PUT(a,n) F(i,1,n) printf("%d ",a[i]); puts("");
int I() {char c=getchar(); int x=0; int f=1; while(c<'0' or c>'9') f=(c=='-')?-1:1,c=getchar(); while(c>='0' and c<='9') x=(x<<1)+(x<<3)+(c^48),c=getchar(); return ((f==1)?x:-x);}
template <typename T> void Rd(T& arg){arg=I();}
template <typename T,typename...Types> void Rd(T& arg,Types&...args){arg=I(); Rd(args...);}
void RA(int *p,int n) {F(i,1,n) *p=I(),++p;}
int qpow(int a,int b,int m=mod) {int r=1; while(b) {if (b&1) r=r*a%m; a=a*a%m,b>>=1;} return r;}
int pgg[30],pgi[30];
void init()
{
F(i,0,23)
{
pgg[i]=qpow(GG,(mod-1)>>i);
pgi[i]=qpow(GI,(mod-1)>>i);
}
}
#define ad(x,y) ((x)+(y)>mod?(x)+(y)-mod:(x)+(y))
#define dc(x,y) ((x)-(y)<0?(x)-(y)+mod:(x)-(y))
namespace poly
{
int w[N],r[N];
void NTT(int f[N],int lim,int type)
{
F(i,0,lim-1) if (i<r[i]) swap(f[i],f[r[i]]);
for(int mid=1,pp=1;mid<lim;mid<<=1,++pp)
{
int Wn=(type==1?pgg[pp]:pgi[pp]);
w[0]=1; F(i,1,mid-1) w[i]=w[i-1]*Wn%mod;
for(int i=0;i<lim;i+=(mid<<1))
{
for(int j=0;j<mid;++j)
{
register int y=f[i|mid|j]*w[j]%mod;
f[i|mid|j]=dc(f[i|j],y);
f[i|j]=ad(f[i|j],y);
}
}
}
if (type==-1)
{
int ivv=qpow(lim,mod-2);
F(i,0,lim-1) f[i]=f[i]*ivv%mod;
}
}
#define REV F(i,0,lim-1) r[i]=((r[i>>1]>>1)|((i&1)?len:0));
int pool[10][N];
inline void Inv(int f[N],int g[N],int n) // 求逆, pool 0~2
{
int *iv=pool[0],*a=pool[1],*b=pool[2];
F(i,0,4*n) iv[i]=a[i]=b[i]=0;
iv[0]=qpow(f[0],mod-2);
int len=1,lim=1;
for(len=1;len<=(n<<1);len<<=1)
{
lim=len<<1;
F(i,0,4*len) a[i]=b[i]=0;
F(i,0,len-1) a[i]=f[i],b[i]=iv[i];
REV;
NTT(a,lim,1); NTT(b,lim,1);
F(i,0,lim-1) a[i]=(2*b[i]-a[i]*b[i]%mod*b[i]%mod+mod)%mod;
NTT(a,lim,-1);
F(i,0,len-1) iv[i]=a[i];
}
F(i,0,n-1) g[i]=iv[i];
F(i,n,lim-1) g[i]=0;
}
inline void mul(int f[N],int g[N],int n,bool flag=1) // *=, pool 3~4
// flag: 是否对n取膜
{
int len=1,lim=1; while(len<=(n<<1)) len=lim,lim<<=1;
int *a=pool[3],*b=pool[4];
F(i,0,lim-1) a[i]=b[i]=0;
F(i,0,n-1) a[i]=f[i],b[i]=g[i];
REV;
NTT(a,lim,1); NTT(b,lim,1);
F(i,0,lim-1) a[i]=a[i]*b[i]%mod;
NTT(a,lim,-1);
F(i,0,2*n-1) f[i]=a[i]; F(i,2*n,lim) f[i]=0;
if (flag) F(i,n,2*n-1) f[i]=0;
}
void Mod(int f1[N],int f2[N],int n,int m,int R[N]) // 多项式取模, pool 5~6
// 这里只保留了余数, 商开到 pool 里而不是传参数修改了
{
int *a=pool[5],*b=pool[6],*Q=pool[7];
F(i,0,4*n) a[i]=b[i]=0;
F(i,0,n-1) a[i]=f1[n-1-i]; F(i,n-m+1,n-1) a[i]=0; // a=f1_r%(x^(n-m+1))
F(i,0,m-1) b[i]=f2[m-1-i]; F(i,n-m+1,m-1) b[i]=0;
Inv(b,b,n-m+1);
mul(a,b,n-m+1);
F(i,0,n-m) Q[i]=a[n-m-i];
F(i,0,4*n) a[i]=b[i]=0;
F(i,0,n-1) a[i]=f1[i];
F(i,0,m-1) b[i]=f2[i];
int len=1,lim=1; while(len<=n) len=lim,lim<<=1;
REV;
NTT(b,lim,1); NTT(Q,lim,1);
F(i,0,lim-1) b[i]=b[i]*Q[i]%mod;
NTT(b,lim,-1); NTT(Q,lim,-1);
F(i,0,m-2) R[i]=(a[i]-b[i]%mod+mod)%mod; F(i,m-1,lim-1) R[i]=0;
}
void PowMod(int f[N],int p,int g[N],int n,int h[N]) // f^p%g, g有n项, 保存在h
{
int *res=pool[8];
F(i,0,8*n) res[i]=0;
res[0]=1; // res=1
while(p)
{
if (p&1)
{
mul(res,f,n,0); // res*=f
Mod(res,g,2*n,n,res); // res%=g
}
mul(f,f,n,0); // f=f*f
Mod(f,g,2*n,n,f); // f%=g
p>>=1;
}
F(i,0,n-2) h[i]=res[i]; F(i,n,8*n) h[i]=0;
}
}
int n,k;
int a[N],t[N];
void Input()
{
Rd(n,k);
F(i,1,k) t[i]=(I()%mod+mod)%mod;
F(i,0,k-1) a[i]=(I()%mod+mod)%mod;
}
int f[N],g[N],c[N];
void Sakuya()
{
init();
f[1]=1; // 这玩意就是 x
F(i,1,k) g[k-i]=(mod-t[i]); g[k]=1; // 特征多项式
// f^n%g -> c
poly::PowMod(f,n,g,k+1,c);
int ans=0;
F(i,0,k-1) ans+=a[i]*c[i]%mod;
ans%=mod;
printf("%lld\n",ans);
}
void IsMyWife()
{
Input();
Sakuya();
}
}
#undef int //long long
int main()
{
Flandre_Scarlet::IsMyWife();
getchar();
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】