拉格朗日插值小记
关于拉格朗日插值:我只会最简单的形式喵。
就是给 个点值,就能在 的时间复杂度内求出当 的时候的值。
其标准形式是:
然后它更高深的应用我暂时不会喵。
但是也许能用在一些小清新 DP 式的优化。
主要在于如果发现式子有一个消不掉的巨大数字,并且能够确定整个结果是和它有关的多项式,就能先求出小值域下的结果,然后再插值回去。
CF995F Cowmpany Cowmpensation
题意: 个点的树,给每个点分配权值 ,父亲的权值必须大于儿子,求方案数。
表示整棵树的权值在 的方案数。
所有叶子的
感性理解一下,当在叶子上时, 是一个关于 的一次函数,也就是多项式的项数为 。而考虑叶子们的父亲,就是把儿子们乘起来,变成了次数为叶子数加一的多项式。也就是说,到根的时候,最多是一个关于 ,项数为 次的多项式。这样可以 求出一部分点值,再拉格朗日插值把 带进去。
#include <bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
typedef long long ll;
ll ksm(ll x,ll y)
{
ll ret=1;
while(y)
{
if(y&1) ret=ret*x%mod;
x=x*x%mod; y>>=1;
}
return ret;
}
int n,D;
const int maxn=6005;
int to[maxn],nxt[maxn],head[maxn],num;
void add(int x,int y){num++;to[num]=y;nxt[num]=head[x];head[x]=num;}
ll f[3002][3002],a[maxn],b[maxn];
void dfs(int p,int fa)
{
for(int i=head[p];i;i=nxt[i])
{
if(to[i]==fa) continue;
dfs(to[i],p);
}
for(int j=1;j<=n;j++)
{
ll ret=1;
for(int i=head[p];i;i=nxt[i])
if(to[i]!=fa) ret=ret*f[to[i]][j]%mod;
f[p][j]=(f[p][j-1]+ret)%mod;
}
}
ll LA(int n,int D,ll *a,ll *b)
{
ll ret=0;
for(int i=0;i<=n;i++)
{
ll x1=1,x2=1;
for(int j=0;j<=n;j++)
{
if(i==j) continue;
x1=x1*(D-a[j])%mod; x2=x2*(a[i]-a[j])%mod;
}
ret=(ret+x1*ksm(x2,mod-2)%mod*b[i]%mod)%mod;
}
return (ret%mod+mod)%mod;
}
int main()
{
scanf("%d%d",&n,&D);
for(int i=2;i<=n;i++)
{
int f; scanf("%d",&f);
add(f,i);
}
dfs(1,0);
for(int i=0;i<=n;i++) a[i]=i,b[i]=f[1][i];
printf("%lld\n",LA(n,D,a,b));
}
[省选联考 2022] 填树
首先题面善良地告诉你被染色的点是一条链。
那么很容易写出第一问的转移方程。
枚举最小值为 ,当前情况下的答案为 ,设 表示以 为根,所有符合条件的链的数量,。
表示在整棵树上最小值为 的情况下, 节点能到达的值域。,如果 则 。
然后在每个点上合并儿子的时候算一下当前链和前面已经合并的 匹配一下,统计到总答案里。
那么 。这样减一下是因为 可能统计重复,就是没有最小值取到 的情况。
然后我们猜测 可能是关于 的,项数为 的多项式。因为可以发现最终答案就是两条链 乘到一起然后在 处加起来的,而 是关于 的一次函数,假设 , 与 有如下关系:
上面就是为了向你展示 与每个点乘进去的权值的一次函数关系。
所以 是关于 的 项的多项式,不过每一项系数可能随 的大小改变。
但是如果我们把它想象成函数图像,就会明白能够使它发生转折的只有大约 个点,剩下的时候都是连续的函数。那么我们就可以考虑拉格朗日插值:我们找到所有可能使函数转折横坐标,将它排序之后一段一段地考虑当前的函数。假如当前这段的函数图像覆盖的范围不太大(小于等于 ),那么我们完全可以把这段里所有的函数值全求出来加和;而如果覆盖的范围很大,我们求出这一段的前 个点值,然后做一个前缀和,再用结尾的横坐标做拉格朗日插值,得到的数字相当于这一段的和。对于每一段的函数都求出来所有的和,那么总的和就是答案啦。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=1030;
const int mod=1e9+7;
ll ksm(ll x,ll y)
{
ll ret=1;
while(y)
{
if(y&1) ret=ret*x%mod;
x=x*x%mod; y>>=1;
}
return ret;
}
int n,K;
int to[maxn],nxt[maxn],head[maxn],num;
void add(int x,int y){num++; nxt[num]=head[x]; to[num]=y;head[x]=num;}
int l[maxn],r[maxn];int L,R;
ll f[maxn],g[maxn];ll tmpf,tmpg,ansf,ansg;
void dfs(int p,int fa)
{
ll ll=max(l[p],L),rr=min(r[p],R);
if(ll<=rr) f[p]=(rr-ll+1),g[p]=((ll+rr)*(rr-ll+1)/2)%mod;
else f[p]=0,g[p]=0;
int tmp1=ll<=rr?rr-ll+1:0;
int tmp2=ll<=rr?((ll+rr)*(rr-ll+1)/2)%mod:0;
tmpf=(tmpf+f[p])%mod;
tmpg=(tmpg+g[p])%mod;
for(int i=head[p];i;i=nxt[i])
{
if(to[i]==fa) continue;
dfs(to[i],p);
int v=to[i];
tmpf=(f[p]*f[v]%mod+tmpf)%mod;
tmpg=(f[p]*g[v]%mod+g[p]*f[v]%mod+tmpg)%mod;
f[p]=(f[p]+f[v]*tmp1%mod)%mod;
g[p]=(g[p]+g[v]*tmp1%mod+f[v]*tmp2%mod)%mod;
}
}
void calc(int w)
{
//printf("%d %d\n",L,R);
tmpf=0; tmpg=0;
dfs(1,0);
ansf=(ansf+w*tmpf%mod)%mod; ansg=(ansg+w*tmpg%mod)%mod;
}
ll la(int n,ll val,ll a[],ll b[])
{
ll ret=0;
for(int i=0;i<=n;i++)
{
ll xx1=1,xx2=1;
for(int j=0;j<=n;j++)
{
if(i==j) continue;
xx1=xx1*(val-a[j])%mod;
xx2=xx2*(a[i]-a[j])%mod;
}
ret=(ret+xx1*ksm(xx2,mod-2)%mod*b[i]%mod)%mod;
}
return ret;
}
int pos[maxn],m;
ll a[maxn],b[maxn],c[maxn];
signed main()
{
// freopen("tree.in","r",stdin);
// freopen("tree.out","w",stdout);
scanf("%d%d",&n,&K);
for(int i=1;i<=n;i++)
{
scanf("%d%d",&l[i],&r[i]);
pos[++m]=l[i],pos[++m]=max(0,l[i]-K),pos[++m]=r[i],pos[++m]=max(r[i]-K,0);
pos[0]=max(pos[0],r[i]+1);
}
sort(pos+0,pos+m+1);m=unique(pos,pos+m+1)-pos;
for(int i=1;i<n;i++)
{
int x,y; scanf("%d%d",&x,&y);
add(x,y); add(y,x);
}
for(int i=0;i<m;i++)
{
int j;
for(j=0;j<n+2;j++)
{
if(pos[i]+j==pos[i+1]) break;
L=pos[i]+j,R=pos[i]+j+K;
calc(1); L++; calc(-1);
a[j]=L-1,b[j]=ansf,c[j]=ansg;
}
if(pos[i]+j<pos[i+1])
{
ansf=la(j-1,pos[i+1]-1,a,b);
ansg=la(j-1,pos[i+1]-1,a,c);
}
}
printf("%lld\n%lld\n",(ansf%mod+mod)%mod,(ansg%mod+mod)%mod);
}
P3270 [JLOI2016]成绩比较
纪念一下我自己切出来的题 qwq
我们看到了恰好有 个人被碾压,考虑容斥。就是设 为钦定 个人被碾压,剩下的随意的方案数。
答案就是
然后就是考虑这个 怎么求。
这个意思就是先从不被碾压的人选出 个在 D 神前面,然后枚举 D 神的分数,算方案数,然后不同科目乘起来就行。
观察到后面的和式很像数列 次方和,它自然是一个 次多项式 qwq
然后后面的拉插 次预处理一下就行。
/*
/> フ
| _ _|
/`ミ _x 彡
/ |
/ ヽ /
/ ̄| | | |
| ( ̄ヽ__ヽ_)_)
\二つ
*/
#include <bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
typedef long long ll;
ll ksm(ll x,ll y)
{
ll ret=1;
while(y)
{
if(y&1) ret=ret*x%mod;
x=x*x%mod; y>>=1;
}
return ret;
}
int n,m,K;
int U[103],R[103];ll b[104],a[104],d[104],fac[103],inv[192],ans;
ll LA(ll n,ll m,ll *a,ll *b)
{
ll ret=0;
for(int i=1;i<=m;i++)
{
ll x1=1,x2=1;
for(int j=1;j<=m;j++)
{
if(i==j) continue;
x1=x1*(n-a[j])%mod; x2=x2*(a[i]-a[j])%mod;
}
ret=(ret+x1*ksm(x2,mod-2)%mod*b[i]%mod)%mod;
}
return ret;
}
ll C(ll n,ll m)
{
if(n<0||m<0||n<m) return 0;
return fac[n]*inv[m]%mod*inv[n-m]%mod;
}
int main()
{
//freopen("p.in","r",stdin);
scanf("%d%d%d",&n,&m,&K);
for(int i=1;i<=m;i++) scanf("%d",&U[i]);
for(int i=1;i<=m;i++) scanf("%d",&R[i]);
int up=0x3f3f3f3f;
for(int i=1;i<=m;i++)
{
ll tmp=0;
for(int j=1;j<=n+1;j++)
{
tmp=(tmp+ksm(j,n-R[i])*ksm(U[i]-j,R[i]-1)%mod)%mod;
b[j]=tmp; a[j]=j;
}
d[i]=LA(U[i],n+1,a,b);
//fprintf(stderr,"%lld\n",d[i]);
up=min(up,n-R[i]);
}
fac[0]=1;inv[0]=1;
for(int i=1;i<=n;i++) fac[i]=fac[i-1]*i%mod,inv[i]=ksm(fac[i],mod-2);
for(int i=K;i<=up;i++)
{
ll tmp=((i-K)&1?(mod-1):1)*C(n-1,i)%mod*C(i,K)%mod;
//fprintf(stderr,"%lld\n",tmp);
for(int j=1;j<=m;j++)
{
tmp=tmp*C(n-i-1,R[j]-1)%mod*d[j]%mod;
}
//fprintf(stderr,"%lld\n*******\n",tmp);
ans=(ans+tmp)%mod;
}
printf("%lld\n",(ans%mod+mod)%mod);
}
P7116 [NOIP2020] 微信步数
这题卡常,卡着卡着,拉插就消失了。
首先,如果走完 步之后没有坐标发生变化且存在期间没有走出界的起点,那么就一定走不出去。
我们考虑每个时间产生的贡献。显然就是这个时间点还剩下的点的个数。
我们把维度分开考虑,对于每一个时间点 ,假如维度 能保持在场地内的数量为 ,那么当前时间点能产生的贡献为 。
那么我们考虑计算每一个维度向左或向右走的最远距离 ,所以 。我们一步一步枚举,如果某一步 ,那么就结束,否则统计所有的乘积,就是答案。期望得分 30pts。
这样很慢的原因是每一轮都要一步一步枚举。而可以发现,从第二轮开始,每个维度增加或减少的坐标都相同。所以我们枚举步数 ,计算出还能走到这一步多少次。每一次就是一轮过去了。而这个的贡献就是 。其中 表示这一维度第一轮过后还剩多少人, 表示这一维度每轮要离开多少人, 表示再走 步 维度消失多少个。然后可以发现里面的乘积形式的东西是个与 有关的 次多项式。然后就可以拉插了。
但是问题就是拉插写不好就是 的太慢。其实我们发现 不会超过 ,所以我们预处理 ,然后把里面的多项式暴力乘在一起,然后直接算就行。
这样就没拉插什么事了 qwq
P4463 [集训队互测 2012] calc
这题先咕着,因为我不会证明它是个多项式...
P5469 [NOI2019] 机器人
一开始看错题自闭了好久
一个挺巧妙的区间 DP:由于区间内最高点左边的点不可能到右边来,所以设 表示 区间内最大值为 的方案数。转移就是枚举可以的起点 , , 必须是一个合法的转移点,也就是
然后发现 合法点不太多,打表发现不超过3000个 所以此时复杂度为 ,mx 为 的最大值。
我们观察 ,然后我们发现这个式子中 ,所以最后的式子一定是关于 的不超过 次多项式。那么我们可以直接拉格朗日插值。
然后我们考虑还像填树那样,把所有分界点都找出来,这样 保证是 ,然后每次都求出 个点值,拉插求前缀和即可。
然后是痛苦卡常时间。
upd on 2023/05/26
补一下关于如何拉格朗日插值系数。
首先考虑拉插的原理:我们知道了 个点的点值,然后需要构造一个多项式。一种构造方案是构造 个多项式,对于多项式 ,当 时等于 ,其余时候都等于 。那么可以显然的构造一个东西就是 ,这个东西当 时一定等于 ,但是当 时则不一定为 ,而是一个别的数字。所以我们将这个数字除掉然后再乘上 就行了。
所以
然后正常的拉插就将 带入求和就可以了。
那么我们想要求系数怎么办。
我们可以发现 和 都是常数,可以直接求。
剩下的一部分是一个多项式,如果对每个 都直接展开求系数是 的。所以我们考虑先 求出 的系数,然后对每个 除以 ,因为需要除的多项式比较特殊,可以在 的时间内求出来。所以求 的系数是 的。
然后求出的 个多项式对应项系数相乘就行。
#include <bits/stdc++.h>
using namespace std;
const int maxn=2e4+3;
typedef long long ll;
const int mod=998244353;
ll a[maxn],b[maxn];int n,K;
ll x[maxn],y[maxn];ll f[maxn],g[maxn],c[maxn],d[maxn];
ll ksm(ll x,ll y)
{
ll ret=1;
while(y)
{
if(y&1) ret=ret*x%mod;
x=x*x%mod; y>>=1;
}
return ret;
}
void solve()
{
for(int i=1;i<=n;i++)
{
g[i]=y[i];
for(int j=1;j<=n;j++)
{
if(i==j) continue;
g[i]=g[i]*ksm((x[i]-x[j]+mod)%mod,mod-2)%mod;
}
}
a[0]=1;
for(int i=1;i<=n;i++)
{
for(int j=i;j>=1;j--)
a[j]=(a[j]*(mod-x[i])%mod+a[j-1])%mod;
a[0]=a[0]*(mod-x[i])%mod;
}
for(int i=1;i<=n;i++)
{
for(int j=0;j<=n;j++) c[j]=a[j];
for(int j=n;j>=1;j--)
{
d[j-1]=c[j];
c[j-1]=(c[j-1]+c[j]*x[i]%mod)%mod;
}
// for(int j=0;j<=n;j++) cerr<<d[j]<<" ";
// cerr<<endl;
for(int j=0;j<n;j++) f[j]=(f[j]+d[j]*g[i]%mod)%mod;
}
}
int main()
{
//freopen("p.in","r",stdin);
scanf("%d%d",&n,&K);
for(int i=1;i<=n;i++)
{
scanf("%lld%lld",&x[i],&y[i]);
}
solve();
ll tmp=1,ans=0;
for(int i=0;i<n;i++) ans=(ans+tmp*f[i]%mod)%mod,tmp=tmp*K%mod;
printf("%lld\n",ans);
}
本文作者:cc0000
本文链接:https://www.cnblogs.com/cc0000/p/17133059.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步