ACSX: Dec, 2022
s3mple
给定模数 \(P\) 和不超过 \(10\) 组 \(n,X\),请问有多少个 \(n\) 的排列 \(p\),满足 \(X=\sum_{i=1}^n v_i\),其中 \(v_i=\min_{p_j>p_i}|i-j|\)。
\(n\le 200,X,P\le 10^9\)
容易想到笛卡尔树,对于一个节点,它的 \(v\) 就是 \(1+(左右子树大小的较小值)\),其中这个 "1+" 可以变成把 X-=n,就可以扔掉了。
于是就有朴素的 \(O(n^2x^2)\) DP 做法(如果取 \(x=O(n^2)\) 就是 \(O(n^6)\))。设 \(f(i,j)\) 表示子树大小为 \(i\),\(\sum v'=j\),填入 \(1\sim i\) 的方案数。\(f(i,j)=\sum_{k=0}^{i-1}\sum_{p=0}^{j}f(k,p)f(i-1-k,j-p-\min(k,i-1-k)){i-1\choose k}\)。可以获得 ≈60 分的部分分。(但是赛时我以为只能得 20 分就写了一个更好写的 20 分 next_permutation)
考虑转移方程,发现 \(p+(j-p-\min(k,i-1-k))=j-\min(k,i-1-k)(定值)\),这很像卷积,所以把 \(f(i)\) 的各项看成多项式对应次数项的系数,枚举 \(k\),就是 \(f(k)\) 和 \(f(i-1-k)\) 两个多项式的卷积。暴力卷积还是不行,其他卷积方式也不够快,我们可以考虑【套路】多项式卷积可以只维护每个多项式的点值,卷就是对应相乘,这样就可以 \(O(X)\) 地维护:\(f(i)=\sum_{k=0}^{i-1}f(i)*f(n-1-i)\cdot x^{\min(k,i-1-k)}\)(还要乘 \(x^{\min(k,i-1-k)}\) 的原因是,卷起来对应的是 \(j-\min(k,i-1-k)\),但实际上应该对应 \(j\))。考虑到 \(X\) 比较大,似乎没什么用,而部分分中有 \(X\) 比较小的提示,经过打表可以发现,\(X\) 的有效值是 \(750\) 左右。这样就可以 \(O(750n^2)\) 地求出每个 \(f(i)\) 的点值表示。
问题是,我们要输出的答案相当于是多项式系数,点值表示在模 \(P\) 意义下如何转为系数表示呢?这是拉格朗日插值的经典算法,考虑公式 \(f(x)=\sum_{i=1}^ny_i\prod_{j\ne i}{x-x_j\over x_i-x_j}\),原先我们把 \(x\) 代入差值,如今我们从另一个角度考虑,把它化简成一个关于 \(x\) 的多项式,具体方法是,分母和 \(y_i\) 都是常数,上面的 \(\prod_{j\ne i} (x-x_j)\) 可以预处理出 \(\prod_{j=1}^n(x-x_j)\) 的系数表示,然后每次做 \(O(n)\) 的多项式除法,除以 \((x-x_i)\),最后把所有 \(n\) 个得数加起来。 这样一来,便可以对于每一组询问在 \(O(800^2)\) 的复杂度内解决。
总复杂度 \(O(750n^3+10\times 750^2)\)。
#include <bits/stdc++.h>
using namespace std;
const int N=205,kM=780;
int n,axe,mod,f[N][kM],C[N][N],X[kM],T[kM],res[kM],inv[kM],jc[kM],ijc[kM],mi[kM][N];
inline void add(int &x,int y){(x+=y)>=mod&&(x-=mod);}
inline int qp(int a,int b){
int c=1;for(;b;b>>=1,a=1ll*a*a%mod)if(b&1)c=1ll*c*a%mod;return c;
}
void solve(){
if(axe<n||axe>770){puts("0");return;}
axe-=n;
int ans=0;
for(int i=1;i<=770;i++){
int o=f[n][i];
for(int j=1;j<=770;j++)if(i!=j){
if(i>j)o=1ll*o*inv[i-j]%mod;
else o=1ll*o*(mod-1ll)%mod*inv[j-i]%mod;
}
for(int j=0;j<=770;j++)T[j]=X[j],res[j]=0;
for(int j=770;j;j--){
res[j-1]=T[j],add(T[j-1],1ll*i*T[j]%mod);
}
add(ans,1ll*o*res[axe]%mod);
}
cout<<ans<<'\n';
}
int main(){
cin>>mod;
jc[0]=ijc[0]=inv[0]=1;
for(int i=1;i<=770;i++)f[1][i]=1,f[0][i]=1,jc[i]=1ll*jc[i-1]*i%mod;
for(int i=1;i<=770;i++){
mi[i][0]=1;
for(int j=1;j<=200;j++)mi[i][j]=1ll*mi[i][j-1]*i%mod;
}
ijc[770]=qp(jc[770],mod-2);
for(int i=769;i;i--)ijc[i]=ijc[i+1]*(i+1ll)%mod;
for(int i=1;i<=770;i++)inv[i]=1ll*ijc[i]*jc[i-1]%mod;
C[0][0]=1;
for(int i=1;i<=200;i++){
C[i][0]=1;
for(int j=1;j<=i;j++)C[i][j]=(C[i-1][j]+C[i-1][j-1])%mod;
}
for(int en=2;en<=200;en++){
for(int i=0;i<en;i++){
for(int j=1;j<=770;j++)add(f[en][j],1ll*f[i][j]*f[en-1-i][j]%mod*C[en-1][i]%mod*mi[j][min(i,en-1-i)]%mod);
}
}
//Now convert f[n] from dianzhi representation to xishu representation
//First calculate (x-1)*(x-2)*(x-3)*...*(x-770), namely polynomial X
//Then, for each i<=770:
// first preprocess o=yi*\prod_{i!=j}(1/(i-j))
// then get X'=X/(x-i)
//\sum o*X' will be the final polinomial
for(int i=0;i<=770;i++)X[i]=0; X[0]=1;
for(int i=1;i<=770;i++){
for(int j=770;j;j--)X[j]=(X[j-1]+(mod-1ll)*i%mod*X[j]%mod)%mod;
X[0]=(mod-1ll)*i%mod*X[0]%mod;
}
while(cin>>n>>axe)solve();
}
积木(cyj)
现在有 \(n(n\le 2\times 10^5)\) 堆积木,第 \(i\) 堆积木有 \(a_i\) 个蓝色的,\(b_i\) 个绿色的,\(c_i\) 个红色的(\(0\le a_i,b_i,c_i\le 150\)),想要任意选取
两堆积木 \(i,j(i<j)\),⽤完这两堆之中的所有积木,然后建出⼀座高楼,他现在想要知道有多少个建造方案。两个方案不同,当且仅当
选取的积木堆不同或者最后建成的高楼某⼀层颜色不同,总方案数对 \(10^8+7\) 取模。
所求即为 \(\sum_{i<j}{a_i+b_i+c_i+a_j+b_j+c_j\choose a_i+b_i+a_j+b_j}{a_i+b_i+a_j+b_j\choose a_i+a_j}\)。
考虑在三维空间中从 \((-a_i,-b_i,-c_i)\) 走到 \((a_j,b_j,c_j)\) 的答案就是后面那一串。所以可以对于每个 \(i\),把 \((-a_i,-b_i,-c_i)\)++;然后一遍 DP 求出 \((+x,+y,+z)\) 的答案,注意会多算从 \((-a_i,-b_i,-c_i)\) 走到 \((+a_i,+b_i,+c_i)\) 所以减掉,所有求和 ÷2 就是答案。
#include <bits/stdc++.h>
using namespace std;
const int N=2e5+5,mod=1e8+7;
int n,a[N],b[N],c[N],f[303][303][303],jc[152*6],ijc[152*6];
inline int read(){
register char ch=getchar();register int x=0;
while(ch<'0'||ch>'9')ch=getchar();
while(ch>='0'&&ch<='9')x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
return x;
}
inline int qp(int a,int b){
int c=1;for(;b;b>>=1,a=1ll*a*a%mod)if(b&1)c=1ll*c*a%mod;
return c;
}
int main(){
freopen("cyj.in","r",stdin);freopen("cyj.out","w",stdout);
n=read();
for(int i=1;i<=n;i++)a[i]=read(),b[i]=read(),c[i]=read(),f[151-a[i]][151-b[i]][151-c[i]]++;
jc[0]=ijc[0]=1;for(int i=1;i<=900;i++)jc[i]=1ll*jc[i-1]*i%mod,ijc[i]=qp(jc[i],mod-2);
int ans=0;
for(int i=1;i<=301;i++)
for(int j=1;j<=301;j++)
for(int k=1;k<=301;k++)
f[i][j][k]=((long long)f[i][j][k]+f[i-1][j][k]+f[i][j-1][k]+f[i][j][k-1])%mod;
for(int i=1;i<=n;i++)(ans+=f[151+a[i]][151+b[i]][151+c[i]]-1ll*jc[a[i]+b[i]+c[i]+a[i]+b[i]+c[i]]*ijc[a[i]+a[i]]%mod*ijc[b[i]+b[i]]%mod*ijc[c[i]+c[i]]%mod)%=mod;
cout<<(mod+1ll>>1)*(ans+mod)%mod;
}
Arg
【套路】最长上升子序列的另一种 DP 方法(可以让 DP 数组严格单调递增):设 \(f_i\) 表示当前长度为 \(i\) 的上升子序列的结尾元素最小数值。
【套路】严格单调递增数列可以直接状压。
由于我们后续要选择元素,所以还要记录当前已经选了的数是多少,这个跟 \(f\) 的状压合并就是三进制数,\(3^15\),所以设 \(f(i,s_3)\) 表示前 \(i\) 个位置,状态是 \(s_3\),有几种方案。
#include <bits/stdc++.h>
using namespace std;
int n,m,ans,a[20],f[15000000],_3[20],bk[20],ord[15000000],trans2[15000000],trans3[1<<16];
inline int ppc(int s){
int cnt=0;
while(s)cnt++,s-=s&-s;
return cnt;
}
inline int Comb(int s,int t){
return trans3[s]+trans3[t];
}
int main(){
freopen("arg.in","r",stdin);freopen("arg.out","w",stdout);
cin>>n>>m;
memset(bk,-1,sizeof bk);
for(int i=1,tmp=0;i<=m;i++)cin>>a[i],bk[a[i]-1]=tmp,tmp|=(1<<(a[i]-1));
int _3n=1;for(int i=1;i<=n;i++)_3n*=3;
f[0]=1;
for(int i=0;i<_3n;i++){
ord[i]=i;
for(int tmp=i,o=0;tmp;tmp/=3,o++){
if(tmp%3)trans2[i]|=1<<o;
}
}
sort(ord,ord+_3n,[](int x,int y){
return trans2[x]<trans2[y];
});
for(int i=0;i<(1<<n);i++){
int res=0;
for(int j=0,k=1;j<n;j++,k*=3)if(i>>j&1)res+=k;
trans3[i]=res;
}
for(int Y=0;Y<_3n;Y++){
int i=ord[Y];
int s=0,t=0;
for(int tmp=i,o=0;tmp;tmp/=3,o++){
if(tmp%3)s|=1<<o;
if(tmp%3==2)t|=1<<o;
}
for(int j=0;j<n;j++)if(!(s>>j&1)&&(bk[j]==-1||((bk[j]&s)==bk[j]))){
int ttt=(t^(t&((1<<j)-1))),tt=t-(ttt&-ttt)+(1<<j);
f[Comb(s|(1<<j),tt)]+=f[i];
}
if(s==(1<<n)-1&&ppc(t)==m)ans+=f[i];
}
cout<<ans;
}