cf 1542e2 Abnormal Permutation Pairs (hard version)
有两个 \(n\) 个数的排列,\(p,q\) ,有多少对 \(p,q\) 使得
- \(p\) 的字典序比 \(q\) 小.
- \(p\) 的逆序对比 \(q\) 多.
模 \(10^9+7\).
字典序,可以枚举 \(p\) ,\(q\) 前面的相同串长度 \(l\) ,从 \(0\) 到 \(n-2\).
接着,用 \(dp(i,j)\) 表示长度为 \(i\) 的序列,有 \(j\) 个逆序对的方案数.
转移为:
\(dp(i,j)=\sum\limits_{j-i+1}^{j} dp(i-1,k)\). 可以用前缀和实现转移的 \(O(1)\) 优化.
此时,可以枚举不同位的取值差,再枚举逆序对个数即可. 详细来说,
\(p\) 选的数为 \(x\) ,\(q\) 选择的数为 \(y\). \(p\) 逆序对个数为 \(a\),\(q\) 逆序对个数为 \(b\),需要满足,
\(x+a>y+b\) 且 \(x<y\). 则 \(b>x+a-y\),所以,枚举 \(x,y\),之后再枚举 \(a\) ,\(b\) 的取值就是一个区间.
再观察,对于固定的 \(x-y\) ,可行的方案数也是固定的. 可以枚举 \(x-y\) ,再枚举 \(a\) ,乘上每种选值的方案数.
但是,时间复杂度是 \(O(n^4)\) 的,不可能通过 \(500\) 的数据范围.
此时,我就卡住了,我一直在纠结于如何优化枚举逆序对个数时的复杂度,没有思考过从 \(dp\) 设计上优化.
可以用 \(dp(i,j)\) 表示 \(p,q\) 长度为 \(i\) 时,\(p\) 的逆序对个数减去 \(q\) 的逆序对个数的差为 \(j\) 时的方案数.
(注意,此时 \(j\) 可能为负数,需要整体右移)
在 \(dp\) 转移上,如果 \(p\) , \(q\) 的选值都枚举的话,会达到 \(O(n^4)\) 的时间复杂度.
此时,我想到了两种优化.
优化1
增加一维,用 \(dp(i,j,0/1)\) 表示 \(p,q\) 填了前 \(i\) 位,逆序对差为 \(j\) ,\(0/1\) 分别代表第 \(i+1\) 位 \(p\) 是否已经选好.
得到转移,
\(dp(i,j,1)=\sum\limits_{j-i}^{j} dp(i,k,0)\)
\(dp(i,j,0)=\sum\limits_{j-i+1}^{j}dp(i-1,k,1)\).
都可以用前缀和优化.
时间复杂度:\(O(n^3)\)
空间复杂度:\(O(n^2)\)
step 1
然而,当我快乐地写完,提交上去时,tle了.
因为这最起码需要 4 个循环,光是循环就用了 \(10^9\) 次操作.
这是我原来 tle 的代码.
#include<bits/stdc++.h>
using namespace std;
int n,mod,add=500*500/2;
int C[510][510],P[510],ans=0;
int dp[2][500*500][2],tag[500*500],s[500*500];
inline void upd(int &x,int y){x=(x+y)%mod;}
int main(){
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n>>mod;
P[0]=1;
for(int i=1;i<510;i++)P[i]=1ll*P[i-1]*i%mod;
C[0][0]=1;
for(int i=1;i<510;i++){
C[i][0]=1;
for(int j=1;j<=i;j++){
upd(C[i][j],C[i-1][j-1]);
upd(C[i][j],C[i-1][j]);
}
}
upd(dp[0][add][0],1);
for(int i=0;i+1<n;i++){
int cur=i&1,nxt=cur^1;
memset(tag,0,sizeof(tag));
memset(dp[nxt],0,sizeof(dp[nxt]));
for(int j=0;j<(add<<1);j++){
if(dp[cur][j][0]==0)continue;
upd(tag[j],dp[cur][j][0]);
upd(tag[j+i+1],(mod-dp[cur][j][0])%mod);
}
for(int j=1;j<(add<<1);j++)upd(tag[j],tag[j-1]);
for(int j=0;j<(add<<1);j++)upd(dp[cur][j][1],tag[j]);
memset(tag,0,sizeof(tag));
for(int j=0;j<(add<<1);j++){
if(dp[cur][j][1]==0)continue;
upd(tag[j-i],dp[cur][j][1]);
upd(tag[j+1],(mod-dp[cur][j][1])%mod);
}
for(int j=1;j<(add<<1);j++)upd(tag[j],tag[j-1]);
for(int j=0;j<(add<<1);j++)upd(dp[nxt][j][0],tag[j]);
memset(s,0,sizeof(s));
for(int j=0;j<(add<<1);j++)s[j]=dp[nxt][j][0];
for(int j=1;j<(add<<1);j++)upd(s[j],s[j-1]);
//number of the left : n-i-1
for(int j=0;j<i+2;j++)for(int k=j+1;k<i+2;k++){
// k k-1 x
// j j-1 y
// x+k-1<y+j-1 -> x-y<j-k
int tmp=1ll*s[j-k+add-1]*C[n][n-i-2]%mod*P[n-i-2]%mod;
upd(ans,tmp);
}
}
cout<<ans<<endl;
return 0;
}
/*inline? ll or int? size? min max?*/
step 2
接着,我想到了几个优化.
我其中循环了 8 次,很多循环是没有先后依赖的关系可以合并在一起,最后只剩下4次循环.
我也不需要滚动. 在不滚动之后,memset 的清空其实也是不需要的.
从 jsoi 潜入计划 中学到%操作耗费时间,可以用 long long 保存,然后一次取模.
此时,加上 cf 的机子十分快,我可以优化到 3946 ms.
这是优化后的代码.
#include<bits/stdc++.h>
using namespace std;
int n,mod,add=500*500/2;
int C[510][510],P[510];
int dp[2][500*500];
long long tag[500*500],s[500*500],ans=0;
int main(){
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n>>mod;
P[0]=1;
for(int i=1;i<510;i++)P[i]=1ll*P[i-1]*i%mod;
C[0][0]=1;
for(int i=1;i<510;i++){
C[i][0]=1;
for(int j=1;j<=i;j++)C[i][j]=(C[i-1][j-1]+C[i-1][j])%mod;
}
dp[0][add]=1;
for(int i=0;i+1<n;i++){
for(int j=0;j<(add<<1);j++){
if(dp[0][j]==0)continue;
tag[j]+=dp[0][j];
tag[j+i+1]+=(mod-dp[0][j])%mod;
}
for(int j=0;j<(add<<1)-1;j++){
tag[j]%=mod;
dp[1][j]=tag[j];
tag[j+1]+=tag[j];
tag[j]=0;
}
for(int j=0;j<(add<<1);j++){
if(dp[1][j]==0)continue;
tag[j-i]+=dp[1][j];
tag[j+1]+=mod-dp[1][j];
}
for(int j=0;j<(add<<1)-1;j++){
tag[j]%=mod;
dp[0][j]=tag[j];
tag[j+1]+=tag[j];
tag[j]=0;
s[j]=dp[0][j];
if(j>0)s[j]+=s[j-1];
}
for(int j=0;j<i+2;j++)for(int k=j+1;k<i+2;k++){
// k k-1 x
// j j-1 y
// x+k-1<y+j-1 -> x-y<j-k
s[j-k+add-1]%=mod;
int tmp=1ll*s[j-k+add-1]*C[n][n-i-2]%mod*P[n-i-2]%mod;
ans+=tmp;
}
}
cout<<ans%mod<<endl;
return 0;
}
/*inline? ll or int? size? min max?*/
step 3
接着,注意到循环的上下界其实根据 \(i\) 的改变才增大,所以,对于 \(i\) 很小的情况,白白循环了很多次. 因此,可以优化循环的上下界.
此时,可以优化到 1606 ms.
#include<bits/stdc++.h>
using namespace std;
int n,mod,add=500*500/2;
int C[510][510],P[510];
int dp[2][500*500];
long long tag[500*500],s[500*500],ans=0;
int main(){
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n>>mod;
P[0]=1;
for(int i=1;i<510;i++)P[i]=1ll*P[i-1]*i%mod;
C[0][0]=1;
for(int i=1;i<510;i++){
C[i][0]=1;
for(int j=1;j<=i;j++)C[i][j]=(C[i-1][j-1]+C[i-1][j])%mod;
}
dp[0][add]=1;
for(int i=0;i+1<n;i++){
int num1=i*(i-1)/2,num2=num1+i;
for(int j=add-num1;j<=add+num1;j++){
tag[j]+=dp[0][j];
tag[j+i+1]+=mod-dp[0][j];
}
for(int j=add-num2-5;j<=add+num2+5;j++){
tag[j]%=mod;
dp[1][j]=tag[j];
tag[j+1]+=tag[j];
tag[j]=0;
}
for(int j=add-num2;j<=add+num2;j++){
tag[j-i]+=dp[1][j];
tag[j+1]+=mod-dp[1][j];
}
for(int j=add-num2-5;j<=add+num2+5;j++){
tag[j]%=mod;
s[j]=dp[0][j]=tag[j];
s[j]+=s[j-1];
tag[j+1]+=tag[j];
tag[j]=0;
}
for(int j=0;j<i+2;j++)for(int k=j+1;k<i+2;k++){
// k k-1 x
// j j-1 y
// x+k-1<y+j-1 -> x-y<j-k
s[j-k+add-1]%=mod;
int tmp=1ll*s[j-k+add-1]*C[n][n-i-2]%mod*P[n-i-2]%mod;
ans+=tmp;
}
}
cout<<ans%mod<<endl;
return 0;
}
/*inline? ll or int? size? min max?*/
step 4
接着,我其实后一个 \(O(n^2)\) 的循坏是不需要的,只需要枚举差即可,于是,优化到了 1216 ms.
#include<bits/stdc++.h>
using namespace std;
int n,mod,add=500*500/2;
int C[510][510],P[510];
int dp[2][500*500];
long long tag[500*500],s[500*500],ans=0;
int main(){
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n>>mod;
P[0]=1;
for(int i=1;i<510;i++)P[i]=1ll*P[i-1]*i%mod;
C[0][0]=1;
for(int i=1;i<510;i++){
C[i][0]=1;
for(int j=1;j<=i;j++)C[i][j]=(C[i-1][j-1]+C[i-1][j])%mod;
}
dp[0][add]=1;
for(int i=0;i+1<n;i++){
int num1=i*(i-1)/2,num2=num1+i;
for(int j=add-num1;j<=add+num1;j++){
tag[j]+=dp[0][j];
tag[j+i+1]+=mod-dp[0][j];
}
for(int j=add-num2-5;j<=add+num2+5;j++){
tag[j]%=mod;
dp[1][j]=tag[j];
tag[j+1]+=tag[j];
tag[j]=0;
}
for(int j=add-num2;j<=add+num2;j++){
tag[j-i]+=dp[1][j];
tag[j+1]+=mod-dp[1][j];
}
for(int j=add-num2-5;j<=add+num2+5;j++){
tag[j]%=mod;
s[j]=dp[0][j]=tag[j];
s[j]+=s[j-1];
tag[j+1]+=tag[j];
tag[j]=0;
}
for(int j=-(i+1);j<0;j++){
s[j+add-1]%=mod;
int tmp=1ll*s[j+add-1]*C[n][n-i-2]%mod*P[n-i-2]%mod*(i+1-abs(j)+1)%mod;
ans+=tmp;
}
}
cout<<ans%mod<<endl;
return 0;
}
/*inline? ll or int? size? min max?*/
step 5
接着,ycx 曾经告诉我,如果连续地访问数组的话,用指针循环取值会更好. 然后,就优化到了 1185 ms.
(其实也没有优化多少,因为需要维护的指针太多了,右移操作太多了)
#include<bits/stdc++.h>
using namespace std;
int n,mod,add=500*500/2;
int C[510][510],P[510];
int dp[2][500*500];
long long tag[500*500],s[500*500],ans=0;
int main(){
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n>>mod;
P[0]=1;
for(int i=1;i<510;i++)P[i]=1ll*P[i-1]*i%mod;
C[0][0]=1;
for(int i=1;i<510;i++){
C[i][0]=1;
for(int j=1;j<=i;j++)C[i][j]=(C[i-1][j-1]+C[i-1][j])%mod;
}
dp[0][add]=1;
for(int i=0;i+1<n;i++){
int num1=i*(i-1)/2,num2=num1+i;
long long *p_tag1=tag+add-num1,*p_tag2=tag+add-num1+i+1;
for(int *p_dp=dp[0]+add-num1;p_dp!=dp[0]+add+num1+1;p_dp++){
*p_tag1+=*p_dp;
*p_tag2+=mod-*p_dp;
p_tag1++;p_tag2++;
}
long long *p_tag=tag+add-num2-5,*nxt=p_tag;nxt++;
for(int *p_dp=dp[1]+add-num2-5;p_dp!=dp[1]+add+num2+5+1;p_dp++){
*p_tag%=mod;
*p_dp=*p_tag;
*nxt+=*p_tag;
*p_tag=0;
nxt++;p_tag++;
}
p_tag1=tag+add-num2-i;p_tag2=tag+add-num2+1;
for(int *p_dp=dp[1]+add-num2;p_dp!=dp[1]+add+num2+1;p_dp++){
*p_tag1+=*p_dp;
*p_tag2+=mod-*p_dp;
p_tag1++;p_tag2++;
}
p_tag=tag+add-num2-5;nxt=p_tag;nxt++;
long long *p_s=s+add-num2-5,*lst=p_s;lst--;
for(int *p_dp=dp[0]+add-num2-5;p_dp!=dp[0]+add+num2+5+1;p_dp++){
*p_tag%=mod;
*p_s=*p_dp=*p_tag;
*p_s+=*lst;
*nxt+=*p_tag;
*p_tag=0;
p_tag++;nxt++;p_s++;lst++;
}
for(int j=-(i+1);j<0;j++){
s[j+add-1]%=mod;
int tmp=1ll*s[j+add-1]*C[n][n-i-2]%mod*P[n-i-2]%mod*(i+1-abs(j)+1)%mod;
ans+=tmp;
}
}
cout<<ans%mod<<endl;
return 0;
}
/*inline? ll or int? size? min max?*/
这样,就把一个 4000ms+ 的代码优化到了 1185 ms.
优化2
考虑同时枚举两个序列第 \(i+1\) 的转移是什么样的.
会发现,
\(f(i+1,j)=\sum\limits_{j-i}^{j}\ (i-j+k+1)f(i,k)+\sum\limits_{j+1}^{j+i}\ (i+j+1-k)f(i,k)\).
\(f(i+1,j+1)=\sum\limits_{j-i+1}^{j+1}\ (i-j+k)f(i,k)+\sum\limits_{j+2}^{j+i+1}\ (i+j+2-k)f(i,k)\).
观察可得,相对于 \(f(i+1,j)\) ,\(f(i+1,j+1)\) 增加了 \(-\sum\limits_{j-i}^{j}f(i,k)+\sum\limits_{j+1}^{j+i+1} f(i,k)\).
此时,可以用前缀和维护.
step 1
如果,完全不顾常数地写出这个代码,是 3104 ms 的.
相较于上一个代码,是这个优化带来的先天优势,会比上个代码少了一半的常数.
#include<bits/stdc++.h>
using namespace std;
const int add=510*510/2;
int n,mod;
int C[510][510],P[510];
int dp[add<<1],s[add<<1];
int ans=0;
int main(){
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n>>mod;
C[0][0]=1;
for(int i=1;i<510;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;
}
P[0]=1;
for(int i=1;i<510;i++)P[i]=1ll*P[i-1]*i%mod;
dp[add]=1;
for(int i=0;i<n;i++){
for(int j=0;j<(add<<1);j++){
s[j]=dp[j];
if(j>0)s[j]+=s[j-1];
s[j]%=mod;
}
for(int j=0;j<=i;j++){
dp[0]=1ll*(j==0?s[j]:s[j]-s[j-1])*(i+1-j)%mod;
dp[0]%=mod;
}
for(int j=500;j+500<(add<<1);j++){
dp[j+1]=dp[j];
int tmp=(s[j]-s[j-i-1]+mod)%mod;
dp[j+1]+=(mod-tmp)%mod;
dp[j+1]%=mod;
dp[j+1]+=(s[j+i+1]-s[j]+mod)%mod;
dp[j+1]%=mod;
}
if(i>0){
for(int j=-i;j<0;j++){
int tmp=1ll*s[j+add-1]*C[n][n-i-1]%mod*P[n-i-1]%mod*(i+1+j)%mod;
ans+=tmp;
ans%=mod;
}
}
}
cout<<ans<<endl;
return 0;
}
/*inline? ll or int? size? min max?*/
step 2
稍微对循环和取模做些优化,就可以优化到 1731ms .
#include<bits/stdc++.h>
using namespace std;
const int add=510*510/2;
int n,mod;
int C[510][510],P[510];
long long dp[add<<1],s[add<<1];
long long ans=0;
int main(){
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n>>mod;
C[0][0]=1;
for(int i=1;i<510;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;
}
P[0]=1;
for(int i=1;i<510;i++)P[i]=1ll*P[i-1]*i%mod;
dp[add]=1;
for(int i=0;i<n;i++){
for(int j=0;j<(add<<1);j++){
s[j]=dp[j];
if(j>0)s[j]+=s[j-1];
}
for(int j=500;j+500<(add<<1);j++){
dp[j+1]=dp[j]-s[j]+s[j-i-1]-s[j]+s[j+i+1];
dp[j+1]%=mod;
}
if(i>0){
for(int j=-i;j<0;j++){
s[j+add-1]%=mod;
int tmp=1ll*s[j+add-1]*C[n][n-i-1]%mod*P[n-i-1]%mod*(i+1+j)%mod;
ans+=tmp;
}
}
}
cout<<ans%mod<<endl;
return 0;
}
/*inline? ll or int? size? min max?*/
小结
这个题目告诉我优化不仅还可能在状态上,思考需要关心 \(dp\) 相关的什么值,是否可以因此改变 \(dp\) 状态来得到更优的效果.
其次,对于 \(dp\) 可能有多种优化方式,在好写程度,时间复杂度,常熟大小,方面进行考虑之后再选择.
最后,这题也锻炼了我卡常的技巧,从取模到循环,到指针.