1. 程式人生 > 其它 >分塊入門&卡常小技巧

分塊入門&卡常小技巧

分塊

基本分塊

分塊是優美的暴力,就是把一個序列分成多塊來處理,每次維護塊,邊緣不是整塊的地方暴力處理\ 如果我們設塊長為B,則有\ 維護複雜度為\frac{n}{B}+B\ 查詢複雜度為\frac{n}{B}+B\ 結合數學知識,我們很容易發現B=\sqrt n時,效率最高,總複雜度為O((n+m)\sqrt n),這就比一般的暴力優美多了

普通分塊的優化

我們容易發現,對於某些詢問,我們都需要暴力處理超過\sqrt n的邊緣區間,這意味著我們的常數大了2倍多,那麼我們可以考慮卡塊長

離線分塊(也許比莫隊難卡一點)

我們可以考慮將塊長修改(自己把常數擴大你就卡不了我),那麼修改為多少比較合適呢?

我們再來看看問題,對於如圖的詢問(紅線是詢問,綠線是\sqrt n的分界線): 考慮優化常數,常數由兩部分組成:左右的散塊和中間的塊數。

但如果我們修改塊數的話我們的分塊複雜度難以分析,可能會導致負優化,所以更普遍的,我們考慮減小散塊的總長度。

於是,問題可以轉化為在一個區間上打T(通常為\sqrt n,不過是因題而異的)個標記,對於2M個有移動方向的pointer,最小化移動距離。

我們並不需要去完成這題的正解,對於卡常來說,極優解就夠了,其實等長分塊就是一種寬泛的極優解,是隨機資料下的最優解。

不過如果題目支援離線,且查詢答案與塊長無關或關聯度低(比如\log{n}等),我們可以考慮如下貪心:

1.如果一個區間中沒有詢問的端點,區間可以無限大(所有詢問要麼不經過它) 2.同一個塊中所有詢問的移動距離不能超過k\sqrt n,把這個移動距離,其中k通常為(0.8,2)的常數

這樣做,塊數T不會超過\frac{n+m}{{(k\sqrt n)}^{\frac{1}{2}}}大概是n^{\frac{3}{4}}級別的,在極端情況下比普通分塊劣,但大多數情況下比普通分塊優

給出一組可以卡掉的資料: 圖中的'D'或'O'是指\frac{n+m}{{(k\sqrt n)}^{\frac{1}{2}}},圖中的k是一個較小的隨機數

暴力重構的分塊

有的題目,我們需要維護區間的某些特徵,在修改後需要暴力重構 f(len)為重構一個長為len的塊的複雜度,g(len)為查詢一個長為len的塊的答案的複雜度 顯然,我們可以在詢問是將區間寫成開區間來微微剪枝,程式碼非常簡單,改動之處可以手寫:


ask(itn L=l-1,itn R=r+1)
{
int block_l=of[L],block_r=of[R];
//只寫右邊的情況
for(int i=block[block_r].L;i<R;++i)
;//統計/重構
}

特別的,如果不想改變碼風,在f(len)\geq f(len-1)+f(1)並且g(len)很小時可以在每塊首尾插入兩個長為1的塊(類比虛點),統計答案時一併統計就可以了

常數塊長 or 隨機塊長

如果你常常對自己的常數沒有信心,那麼不妨寫下:

block_len=1775;//看個人喜好

或者

block_len=sqrt(n)+k;//也許是別的,一定加k,否則會有負塊長(真虛塊) 
blen=0;//防止UB
for(int i(1);i<=n;i+=blen)
{
block[++tot].L=i,
block[tot].R=min(i+(blen=block_len+randBewteen(-k,k))-1,n);//小心越界
for( block[tot].L -> block[tot].R ) ;//預處理
}

上面就是瞎搞了

卡常

讀寫

基本的讀寫

讀入

scanf比cin快

getchar(),gets,getline差異不大

寫出

printf比cout慢

endl會自動重新整理快取區,不要用

ios::sync_with_stdio(flase)有可能會報錯,謹慎使用

cout效率其實很高,即使不關流同步

下面是一些資料: putchar 951ms

puts 608ms

cout(字串)483ms

cout(數字,字元強轉為了int,可能不嚴謹)592ms

printf(字串)18145ms

printf(值)18252ms

以上資料均為輸出5e6個"n1\n"

更強的讀寫

普通讀入優化

template<typename Tp_>
inline void read_(Tp_ &x)
{
x=0;
int fi(1);
op=getchar();
while((op<'0'||op>'9')&&op!='-') op=getchar();
if(op=='-') op=getchar(),fi==;
while(op>='0'&&op<='9') x=x*10+(op^48),op=getchar();
if(!fi) x=-x;
return;
}

關於應該寫成x*10還是(x<<3)+(x<<1),其實大致是沒有影響的,至少影響小於評測姬波動 如果追求更小的常數,可以嘗試使用fread

char B[MAXSIZE],*S=B,*T=B;
#define getchar() (S==T&&(T=(S=B)+fread(B,1,MAXSIZE,stdin),S==T)?EOF:*S++)

一般來說,MAXSIZE設成100000或(1<<15)可以在時間和空間間平衡,從趨勢上講,MAXSIZE越大,讀入越快,但增加速度趨緩,大概開到(1<<21)之後的優化效果就小於評測姬波動了

快寫優化

char outputChar[700000];//注意,應從0開始存
int outputLen=0;//定義

outputChar[outputLen++]=the_char_need_to_put_out;

int main()
{
...
fwrite(outputChar,1,outputLen,stdout);
return 0;
}

原理大約是模擬快取區之類的,除此以外還有fputs的寫法(據說更快?)

神祕優化

register

據說在c++11之後就沒有用了,但實測還是快了,而且快了很多(2433ms->2074ms,對同一個值反覆操作) 但不要濫用,在洛谷上測出來呼叫次數在1e4以下基本是負優化(如果要進行一些奇妙操作就說不準了,呼叫次數<10則幾乎不影響常數) 優化const幾乎肯定是負優化,手動把迴圈變數放到函式最前開register則可以略微加強效果

inline

對遞迴函式沒有優化效果(可能是負優化)

對遞推函式則可以略微優化1e6次-20ms

如果是在洛谷上卡常,可以禁賽三年優化:

#define inline __inline__ __attribute__((always_inline))

就是強制內聯,優化效果是普通inline的兩三倍

()

據說,賦初值時寫()比寫=要快,不過實測中不大看得出來,可以當做心理安慰

型別轉換效率比用單位元去乘更慢一些,具體地

(ll)a*b%p

可以優化為

1ll*a*b%p

大概可以塊5%-10%

其他優化

八聚氧,火車頭

這個一查就有了,太長了,不放了

值得一提的是標頭檔案vector中開啟了O2優化

++,++

x++會先返回x加之前的值,在彙編中比++x多一句,所以會慢一點,不過不用刻意去追求就是了

連續訪問記憶體

雖然我們認為對任意地址的訪問耗時是相等的,但實際上不是,因為讀取記憶體比讀取cache慢,所以寫程式碼時應當注意自己訪問的記憶體儘量連續,跳躍距離儘可能小

比如在進行矩陣乘法時,我們可以樸素地寫:

for(int i(1);i<=n;++i)
for(int j(1);j<=n;++j)
for(int k(1);k<=n;++k)
c[i][j]+=a[i][k]*b[k][j];

考慮優化常數(減小記憶體跳躍距離):

for(int i=1;i<=n;++i)
for(int k=1;k<=n;++k)
{
s=a[i][k];
for(int j=1;j<=n;++j)
c[i][j]+=s*b[k][j];
}

在計算500階矩陣乘法時快了30-50ms,不過波動挺大的