cf 1542e2 Abnormal Permutation Pairs (hard version)

有两个 \(n\) 个数的排列,\(p,q\) ,有多少对 \(p,q\) 使得

  1. \(p\) 的字典序比 \(q\) 小.
  2. \(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\) 可能有多种优化方式,在好写程度,时间复杂度,常熟大小,方面进行考虑之后再选择.

最后,这题也锻炼了我卡常的技巧,从取模到循环,到指针.

posted @ 2021-07-06 19:05  xyangh  阅读(36)  评论(0)    收藏  举报