分塊入門&卡常小技巧
基本分塊
分塊是優美的暴力,就是把一個序列分成多塊來處理,每次維護塊,邊緣不是整塊的地方暴力處理\ 如果我們設塊長為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
如果是在洛谷上卡常,可以禁賽三年優化:
就是強制內聯,優化效果是普通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];
}