1. 程式人生 > 其它 >P5046 [Ynoi2019 模擬賽] Yuno loves sqrt technology I(分塊+卡常)

P5046 [Ynoi2019 模擬賽] Yuno loves sqrt technology I(分塊+卡常)

分塊+定址優化

洛谷題面傳送門

zszz,lxl 出的 DS 都是卡常題(

首先由於此題強制線上,因此考慮分塊,我們那麼待查詢區間 \([l,r]\) 可以很自然地被分為三個部分:

  • 左散塊
  • 中間的整塊
  • 右散塊

那麼這樣一來區間逆序對的來源可以有以下幾種:

  • 左散塊內部的區間逆序對
  • 右散塊內部的區間逆序對
  • 每個整塊內部的區間逆序對
  • 左散塊與中間整塊之間的逆序對
  • 右散塊與中間整塊之間的逆序對
  • 中間整塊兩兩之間的逆序對
  • 左散塊與右散塊之間的逆序對

對於前三種情況,我們可以記錄這樣兩個陣列:

  • \(pre_j\)\(j\) 所在整塊左端點到 \(j\) 這段區間內逆序對個數
  • \(suf_j\)\(j\)\(j\)
    所在整塊右端點這段區間內逆序對個數

對於第四、五兩種情況,顯然是要求一個區間內比某個數小/大的數的個數,這個可以通過維護以下陣列求出:

  • \(lft_{i,j}\):在前 \(j\) 個整塊中有多少個數 \(>a_i\)
  • \(rit_{i,j}\):在第 \(j\) 個整塊到第 \(cnt\) 個整塊中有多少個數 \(<a_i\)

求出這兩東東之後第四、五種情況一邊字首和帶走即可。

對於第六種情況我們也類似地維護以下陣列:

  • \(bl_{i,j}\):第 \(i\) 個整塊與前 \(j\) 個整塊之間有多少個逆序對

對於第七種情況貌似沒有什麼方法直接處理,不過考慮一個 trick 叫歸併排序,對於兩個排好序的有序陣列,我們是可以線上性時間內求出它們排好序後的逆序對個數的。因此我們維護每個塊排好序的陣列,那麼我們在 \(l\)

所在的塊與 \(r\) 所在的塊中跑一遍雙針,即在第 \(l\) 個塊排好序的陣列中列舉一個 \(a_i\),那麼顯然在第 \(r\) 個塊排好序的陣列中,滿足 \(a_j<a_i\)\(j\) 顯然組成一段字首,那麼我們就雙針找出這段字首,然後統計一下這段字首中有多少個位置 \(\le r\) 即可。

上述情況都是針對 \(l,r\) 不在同一個整塊中的情況,對於 \(l,r\) 在同一個整塊中的情況,我們設 \(l,r\) 所在的整塊為 \([L,R]\),那麼區間 \([l,r]\) 的逆序對個數就可以用 \([L,r]\) 的逆序對個數減去 \([L,l-1]\) 中逆序對的個數,再減去 \([L,l-1]\)

\([l,r]\) 之間的逆序對個數,前兩個值都可以通過我們之前維護的 \(pre\) 陣列求出,至於第三個……相信聰明的讀者們如果明白了上面的第七種情況,也能很快知道這個怎麼維護。

時間複雜度 \((n+m)\sqrt{n}\),然鵝由於實現的不好,它 TLE 了,只過了第 5 個測試點。

附:20pts 的程式碼:

const int MAXN=1e5;
const int BLK=500;
int n,qu,a[MAXN+5],pre[MAXN+5],suf[MAXN+5],buc[MAXN+5];
pii b[BLK+5][BLK+5];int sum[BLK+5][MAXN+5];
int lft[MAXN+5][BLK+5],rit[MAXN+5][BLK+5];
int bl[BLK+5][BLK+5];
int blk_cnt=0,blk_sz=0,L[BLK+5],R[BLK+5],bel[MAXN+5];
int t[MAXN+5];
void add(int x,int v){for(int i=x;i<=n;i+=(i&(-i))) t[i]+=v;}
int ask(int x){int ret=0;for(int i=x;i;i&=(i-1)) ret+=t[i];return ret;}
ll query(int l,int r){
	if(l<1||l>n||r<1||r>n||l>r) return 0;
	if(bel[l]==bel[r]){
		ll sum=pre[r];
		if(l^L[bel[l]]) sum-=pre[l-1];
		for(int i=1,c=0;i<=R[bel[l]]-L[bel[l]]+1;i++){
			if(b[bel[l]][i].se<l) sum-=c;
			c+=(l<=b[bel[l]][i].se&&b[bel[l]][i].se<=r);
		} return sum;
	} else {
		ll sum=0;sum+=suf[l];sum+=pre[r];
		for(int i=bel[l]+1;i<bel[r];i++) sum+=pre[R[i]];
		for(int i=bel[l]+1;i<bel[r];i++) sum+=bl[i][i-1]-bl[i][bel[l]];
		for(int i=l;i<=R[bel[l]];i++) sum+=rit[i][bel[l]+1]-rit[i][bel[r]];
		for(int i=L[bel[r]];i<=r;i++) sum+=lft[i][bel[r]-1]-lft[i][bel[l]];
		for(int i=1,j=1,c=0;i<=R[bel[l]]-L[bel[l]]+1;i++){
			while(j<=R[bel[r]]-L[bel[r]]+1&&b[bel[r]][j].fi<b[bel[l]][i].fi){
				c+=(b[bel[r]][j].se<=r);j++;
			} if(b[bel[l]][i].se>=l) sum+=c;
		}
		return sum;
	}
}
int main(){
	scanf("%d%d",&n,&qu);
	blk_sz=400;blk_cnt=(n-1)/blk_sz+1;
	for(int i=1;i<=n;i++) scanf("%d",&a[i]);
	for(int i=1;i<=blk_cnt;i++){
		L[i]=(i-1)*blk_sz+1;R[i]=min(i*blk_sz,n);
		for(int j=L[i];j<=R[i];j++) bel[j]=i;
	}
	for(int i=1;i<=n;i++) b[bel[i]][i-L[bel[i]]+1]=mp(a[i],i);
	for(int i=1;i<=blk_cnt;i++) sort(b[i]+1,b[i]+R[i]-L[i]+2);
	for(int i=1;i<=n;i++) sum[bel[i]][a[i]]++;
	for(int i=1;i<=blk_cnt;i++) for(int j=1;j<=n;j++)
		sum[i][j]+=sum[i][j-1];
	for(int i=1;i<=blk_cnt;i++){
		memset(t,0,sizeof(t));ll sum=0;
		for(int j=L[i];j<=R[i];j++){
			pre[j]=(sum+=j-L[i]-ask(a[j]));
			add(a[j],1);
		} memset(t,0,sizeof(t));sum=0;
		for(int j=R[i];j>=L[i];j--){
			suf[j]=(sum+=ask(a[j]));
			add(a[j],1);
		}
	}
	for(int i=1;i<=blk_cnt;i++){
		for(int j=1;j<=n;j++) buc[j]+=sum[i][j];
		for(int j=R[i]+1;j<=n;j++) lft[j][i]=R[i]-buc[a[j]];
	} memset(buc,0,sizeof(buc));
	for(int i=blk_cnt;i;i--){
		for(int j=1;j<=n;j++) buc[j]+=sum[i][j];
		for(int j=L[i]-1;j;j--) rit[j][i]=buc[a[j]-1];
	}
	for(int i=1;i<=blk_cnt;i++) for(int j=1;j<i;j++) for(int k=L[i];k<=R[i];k++) bl[i][j]+=lft[k][j];
	ll pre=0;
	while(qu--){
		ll l,r;scanf("%lld%lld",&l,&r);l^=pre;r^=pre;
		printf("%lld\n",pre=query(l,r));
	}
	return 0;
}

然後就到了奇淫的卡常時間了,我首先玄學般地三分了下塊長並加了個 IO 優化,然鵝並沒有什麼卵用,該 T 的還是 T 掉了。

其次我們注意到再求 \(lft,rit\) 的過程用到了維護第 \(i\) 個塊 occurrence 的字首和陣列 \(sum_{i,j}\),事實上這東西大可不必提前預處理出來,這樣耗時間又耗空間,因此我將它改為,維護一個臨時陣列 \(c_j\),掃到第 \(i\) 塊時再求出這一塊的字首和,我還加了一個小小的剪枝,就是求出這一塊中 \(a_i\) 的最小值 \(mn\),求字首和時從 \(mn\) 開始列舉,正確性顯然,但是似乎用處不大。

接下來我發現這個 \(lft\)\(rit\) 陣列定義與功能類似,都可以求某個字首小於某個數的個數,事實上它們完全可以合併成一個數組,因此我就把求 \(rit\) 那一部分的 \(n\sqrt{n}\) 刪掉,然後幾個細節稍微改了改,常數確實小了不少,第五個測試點又原來 546ms 變成了 371ms,第四個點時而 A 時而 T,剩下三個點仍然 TLE。

附:40pts 的程式碼:

using namespace fastio;
const int MAXN=1e5;
const int BLK=500;
int n,qu,a[MAXN+5],pre[MAXN+5],suf[MAXN+5],buc[MAXN+5];
pii b[BLK+5][BLK+5];int c[MAXN+5];
int lt[MAXN+5][BLK+5],bl[BLK+5][BLK+5];
int blk_cnt=0,blk_sz=0,L[BLK+5],R[BLK+5],bel[MAXN+5];
int t[MAXN+5];
void add(int x,int v){for(int i=x;i<=n;i+=(i&(-i))) t[i]+=v;}
int ask(int x){int ret=0;for(int i=x;i;i&=(i-1)) ret+=t[i];return ret;}
ll query(int l,int r){
	if(l<1||l>n||r<1||r>n||l>r) return 0;
	if(bel[l]==bel[r]){
		ll sum=pre[r];
		if(l^L[bel[l]]) sum-=pre[l-1];
		for(int i=1,c=0;i<=R[bel[l]]-L[bel[l]]+1;i++){
			if(b[bel[l]][i].se<l) sum-=c;
			c+=(l<=b[bel[l]][i].se&&b[bel[l]][i].se<=r);
		} return sum;
	} else {
		ll sum=0;sum+=suf[l];sum+=pre[r];
		for(int i=bel[l]+1;i<bel[r];i++) sum+=pre[R[i]]+bl[i][i-1]-bl[i][bel[l]];
		for(int i=l;i<=R[bel[l]];i++) sum+=lt[i][bel[r]-1]-lt[i][bel[l]];
		for(int i=L[bel[r]];i<=r;i++) sum+=(L[bel[r]]-R[bel[l]]-1)-(lt[i][bel[r]-1]-lt[i][bel[l]]);
		for(int i=1,j=1,c=0;i<=R[bel[l]]-L[bel[l]]+1;i++){
			while(j<=R[bel[r]]-L[bel[r]]+1&&b[bel[r]][j].fi<b[bel[l]][i].fi){
				c+=(b[bel[r]][j].se<=r);j++;
			} if(b[bel[l]][i].se>=l) sum+=c;
		}
		return sum;
	}
}
int main(){
	read(n);read(qu);
	blk_sz=450;blk_cnt=(n-1)/blk_sz+1;
	for(int i=1;i<=n;i++) read(a[i]);
	for(int i=1;i<=blk_cnt;i++){
		L[i]=(i-1)*blk_sz+1;R[i]=min(i*blk_sz,n);
		for(int j=L[i];j<=R[i];j++) bel[j]=i;
	}
	for(int i=1;i<=n;i++) b[bel[i]][i-L[bel[i]]+1]=mp(a[i],i);
	for(int i=1;i<=blk_cnt;i++) sort(b[i]+1,b[i]+R[i]-L[i]+2);
	for(int i=1;i<=blk_cnt;i++){
		memset(t,0,sizeof(t));ll sum=0;
		for(int j=L[i];j<=R[i];j++){
			pre[j]=(sum+=j-L[i]-ask(a[j]));
			add(a[j],1);
		} memset(t,0,sizeof(t));sum=0;
		for(int j=R[i];j>=L[i];j--){
			suf[j]=(sum+=ask(a[j]));
			add(a[j],1);
		}
	}
	for(int i=1;i<=blk_cnt;i++){
		int mn=n+1,s=0;
		for(int j=L[i];j<=R[i];j++) c[a[j]]++,chkmin(mn,a[j]);
		for(int j=mn;j<=n;j++) s+=c[j],buc[j]+=s;
		for(int j=L[i];j<=R[i];j++) c[a[j]]--;
		for(int j=1;j<=n;j++) lt[j][i]=buc[a[j]-1];
	}
	for(int i=1;i<=blk_cnt;i++) for(int j=1;j<i;j++)
		for(int k=L[i];k<=R[i];k++) bl[i][j]+=R[j]-lt[k][j];
	ll pre=0;
	while(qu--){
		ll l,r;read(l);read(r);l^=pre;r^=pre;
		print(pre=query(l,r),'\n');
	} print_final();
	return 0;
}

然後就到了最奇怪的部分了,我注意到詢問的時候寫了這樣一句話:

for(int i=l;i<=R[bel[l]];i++) sum+=lt[i][bel[r]-1]-lt[i][bel[l]];

然後我就靈機一動,嘗試交換 \(lt\) 的兩維,並尋思這樣效率會加快多少。

然鵝出乎意料的是,作了這樣一個小小的變動後,它竟然 AC 了!而且竟然還搶到了(目前,截至 2021.7.18)的最優解。

為什麼我會想到作這樣一個變動?注意到在上面的語句中,後面一維的值與列舉變數 \(i\) 無關,因此如果交換這兩維,後面一維訪問的下標就是一段連續的區間,訪問的地址也是一段完整的區間,這樣就能大大提高訪問速度。

AC 程式碼:

using namespace fastio;
const int MAXN=1e5;
const int BLK=500;
int n,qu,a[MAXN+5],pre[MAXN+5],suf[MAXN+5],buc[MAXN+5];
pii b[BLK+5][BLK+5];int c[MAXN+5];
int lt[BLK+5][MAXN+5],bl[BLK+5][BLK+5];
int blk_cnt=0,blk_sz=0,L[BLK+5],R[BLK+5],bel[MAXN+5];
int t[MAXN+5];
void add(int x,int v){for(int i=x;i<=n;i+=(i&(-i))) t[i]+=v;}
int ask(int x){int ret=0;for(int i=x;i;i&=(i-1)) ret+=t[i];return ret;}
ll query(int l,int r){
	if(l<1||l>n||r<1||r>n||l>r) return 0;
	if(bel[l]==bel[r]){
		ll sum=pre[r];
		if(l^L[bel[l]]) sum-=pre[l-1];
		for(int i=1,c=0;i<=R[bel[l]]-L[bel[l]]+1;i++){
			if(b[bel[l]][i].se<l) sum-=c;
			c+=(l<=b[bel[l]][i].se&&b[bel[l]][i].se<=r);
		} return sum;
	} else {
		ll sum=0;sum+=suf[l];sum+=pre[r];
		for(int i=bel[l]+1;i<bel[r];i++) sum+=pre[R[i]]+bl[i][i-1]-bl[i][bel[l]];
		for(int i=l;i<=R[bel[l]];i++) sum+=lt[bel[r]-1][i]-lt[bel[l]][i];
		for(int i=L[bel[r]];i<=r;i++) sum+=(L[bel[r]]-R[bel[l]]-1)-(lt[bel[r]-1][i]-lt[bel[l]][i]);
		for(int i=1,j=1,c=0;i<=R[bel[l]]-L[bel[l]]+1;i++){
			while(j<=R[bel[r]]-L[bel[r]]+1&&b[bel[r]][j].fi<b[bel[l]][i].fi){
				c+=(b[bel[r]][j].se<=r);j++;
			} if(b[bel[l]][i].se>=l) sum+=c;
		}
		return sum;
	}
}
int main(){
	read(n);read(qu);
	blk_sz=460;blk_cnt=(n-1)/blk_sz+1;
	for(int i=1;i<=n;i++) read(a[i]);
	for(int i=1;i<=blk_cnt;i++){
		L[i]=(i-1)*blk_sz+1;R[i]=min(i*blk_sz,n);
		for(int j=L[i];j<=R[i];j++) bel[j]=i;
	}
	for(int i=1;i<=n;i++) b[bel[i]][i-L[bel[i]]+1]=mp(a[i],i);
	for(int i=1;i<=blk_cnt;i++) sort(b[i]+1,b[i]+R[i]-L[i]+2);
	for(int i=1;i<=blk_cnt;i++){
		memset(t,0,sizeof(t));ll sum=0;
		for(int j=L[i];j<=R[i];j++){
			pre[j]=(sum+=j-L[i]-ask(a[j]));
			add(a[j],1);
		} memset(t,0,sizeof(t));sum=0;
		for(int j=R[i];j>=L[i];j--){
			suf[j]=(sum+=ask(a[j]));
			add(a[j],1);
		}
	}
	for(int i=1;i<=blk_cnt;i++){
		int mn=n+1,s=0;
		for(int j=L[i];j<=R[i];j++) c[a[j]]++,chkmin(mn,a[j]);
		for(int j=mn;j<=n;j++) s+=c[j],buc[j]+=s;
		for(int j=L[i];j<=R[i];j++) c[a[j]]--;
		for(int j=1;j<=n;j++) lt[i][j]=buc[a[j]-1];
	}
	for(int i=1;i<=blk_cnt;i++) for(int j=1;j<i;j++)
		for(int k=L[i];k<=R[i];k++) bl[i][j]+=R[j]-lt[j][k];
	ll pre=0;
	while(qu--){
		ll l,r;read(l);read(r);l^=pre;r^=pre;
		print(pre=query(l,r),'\n');
	} print_final();
	return 0;
}