【C++】代碼優化的推導與結論

0x00 前言

作者第一次寫這類文章,有錯誤歡迎指出!

有人把代碼優化稱為騙分,或許被人認為是毫無作用的東西,或許是被認為歪門邪道的方法。然而,就算是騙分所蘊含的知識可不少,同時騙分與打表有着本質上的差別。在比賽中,假設你一道題使用了 \(O(n^2)\) 的時間複雜度來做,或許只能拿 20 分;但是使用了一些優化後,或許能拿 50 分,甚至更高。不過值得一提的是,這些方法通常只用於 OI 賽制,因為 ACM 賽制下,如果得不到滿分,拿 99 分也不算做對。

0x10 讀寫優化

0x11 內存緩衝優化

輸入輸出,是每個程序必不可少的部分之一。如果把你要讀入的內容看成一條路,那麼讀入就是走這條路。然而,有一些讀入的方法沒有考慮到內存緩衝,比如 get() 。大家可以理解為 get() 在這條路上走的太快,導致到達終點時沒有剎住車,所以其在比賽時禁用。而 scanf通常比 cin 快的原因僅僅只是其內存緩衝處理的比較好。那麼如果我們關掉 cin 的內存緩衝,它的效率會高很多嗎?答案是肯定的。

cin.tie(0); //這條語句可以關掉 cin 的內存緩衝
cout.tie(0); //與 cin 同理,但作用於 cout

這兩句代碼放在主函數的第一句話時可以生效,但是這只是基礎操作,而且其在比賽時還禁用。所以,我們引來了下一個部分——快讀。

0x12 快速讀寫優化

除了 scanfcin 等常見的讀入工具,還有一種讀入工具:getchar()。這種讀入工具會在生效後讀入控制台輸入的第一個字符,包括空格,換行符等。因為其只讀入一個字符,不用考慮內存緩衝,又因為種種原因,其效率高的驚人。

int a=getchar()

例如以上代碼會讀入一個字符,但想讀入多個卻不行(如:我輸入了 10 ,但其卻只儲存了 1 )。

但如果我們寫一個循環,不斷地用 getchar() 讀入,只要讀入的還是數字就繼續讀入,效率會高嗎?答案是會。這種讀入方式被稱為快讀,不僅效率高,而且在比賽中也允許使用。

inline int read()
{
    int f=1,res=0; //res 是你輸入的值,f 用於判斷正負
    char c=getchar();
    while(c<'0' || c>'9'){ //先把奇怪的東西讀入了
    	if(c=='-') f=-1; //如果是負數,f 變為 -1
    	c=getchar();
    }
    while(c>='0' && c<='9'){
    	res=res*10+c-'0'; //讀入部分
    	c=getchar();
    }
    return res*f; //返回
}

這就是一個可以讀入整數的快讀模板,使用 int a=read() 可以快速讀入 a 。

練習: 用快讀完成 P1001

思考: 想一下能用快讀讀入字符串嗎?如果能,嘗試寫一個。

0x20 運算優化

0x21 位運算

眾所周知,電腦在完成我們交給它的任務時,總是使用二進制在計算。而位運算是直接在二進制時運算,顯然,它比四則運算效率會高不少(不妨通過學習一下 C++ 中的進制,以了解進制轉換與字符串的關係)。接下來,我將簡略介紹一下基礎的位運算,如果你已經掌握了基礎位運算,可以直接跳過。

  1. & 與 這是一種雙目運算符,當兩個參數都為 1 時,答案為 1 ;

  2. | 或 這是一種雙目運算符,當任何一個參數為 1 時,答案為 1 ;

  3. ~ 非 這是一種單目運算符,答案與參數相反;

  4. ^ 異或 這是一種雙目運算符,當兩個參數不同時,答案為 1 ;

  5. >> 右移 這是一種單目運算符,可以刪除最右位;

  6. << 左移 這是一種單目運算符,作用與右移相反。

注意,以上運算都是在二進制下完成,請看樣例:

4 & 7 表示 4 與 7 進行與運算,在二進制下標示為:

100 & 111 ,然後末位對齊,若高位不夠需要補 0 。

按以上與操作運算後,得到二進制數:100 ,轉換為十進制數就是 4 ,這就是位運算。值得一提的是,位運算不具備短路特性。

綜上,你可以使用位運算進行優化你的代碼,例如 a<<2 等價於 a*2 。同理, a>>2 等價於 a/2 。或許你會覺得位運算使用範圍少,其實在數學部分,狀態壓縮等程序中,可運用位運算的地方可多了。

練習:用位運算完成下列題目。

例: 將數字 a 的第 k 位設置為 0 用位運算為:a=~(1<<k)&a;

讀取數字 a 的第 k 位:

將數字 a 的第 k 位取反:

0x22 位運算對儲存的優化

在你了解計算機是如何儲存數碼之後,或許可以在運算之中找到等價的位運算。不過這類優化運用範圍較小,可以只作為了解為主。值得一提的是,計算機儲存整數和浮點數是分開來的,而計算機儲存出來的浮點數是無限精度的,所以這裡只介紹整數的儲存。

1.原碼

這種記錄方法將一串二進制數的首位作為標記,記錄其的正負(負數首位為 1 ,正數首位為 0 )。然而這樣運算出來的結果肯定不對,需要特殊判斷,所以無法作為正式的方法使用。

2.反碼

簡單來說,反碼就是首位符號位與原碼規則相同,但是若數字為負數,其它位全部取反。但因為種種原因,其並不是主要的儲存方法。

3.補碼

主流的儲存方法。其儲存方法與反碼類似,不同在於,如果儲存結果為負數,會給答案加負一。

0x23 位運算對空間的優化

此類優化針對於空間上的優化,基於位運算,不過其優化較小,對於實際應用用處不算特別大。比如交換兩個整數變量的值,我們一般會使用:

int t=a;
a=b;
b=t;

顯然,這樣會浪費 \(O(1)\) 的空間複雜度,如果想節約這點可以忽略不計的空間複雜度,可以使用:

a=a^b;
b=b^a;
a=a^b;

這種技巧用處或許不算大,但是有些技巧卻十分有用。比如在二分時,我們求出 mid 通常會使用 mid=(l+r)/2 ,但使用位運算只要 mid(l+r)>>2 即可,通常情況下,它們是等價的,但對於大數據,這種方法可以避免空間超限。總而言之,位運算更多的優化在於時間複雜度上(特別是狀壓)。

0x30 數據優化

0x31 寄存器優化

有人喜歡在循環時增加寄存器( regsiter ),或許認為這樣會更快,但在實際運行中真的是這樣嗎?首先,我們要知道寄存器是 CPU 特殊儲存器,其無地址,無變量。使用 register 可以直接將變量儲存在 CPU 的寄存器中,通常情況下, register 的確可以增加程序的運行速度,但是寄存器在很多運行環境中是禁用的,甚至還會出現 「負優化」 的情況,因此,我並不推薦各位使用寄存器。出於標程,一下放下寄存器的使用實例。

register int a,b;
cin>>a>>b;
for(register int i=1;i<=b;i++) a*=a;
cout<<a;

使用寄存器的方法只需在定義變量時聲明即可。

0x32 地址優化

眾所周知,系統新建變量時會開闢一個地址,而如果這個變量不再使用,則會釋放(具體原理是簡單化的 「棧原理」 )。而開闢地址需要時間,回憶學習鏈表時,曾有使用 new 來新建節點的方法,但效率極低。我們可以通過這種原理來做一些優化。

for(int i=1;i<=100;i++) cout<<"Luogu!"<<endl;

以上的循環定義了一個僅可以在這個 for 循環中使用的臨時變量 i ,若需要處理的數據十分多,這樣會浪費很多時間複雜度,因此,我們可以改進:

int i;
for(i=1;i<=n;i++) cout<<"Luogu!"<<endl;

這種優化方法確實有用,且不僅是在空間上。進一步,我們可以更多的使用全局變量和宏定義,以此增加程序的效率。例如以下代碼:

for(int i=1;i<=999;i++)
	for(int j=1;j<=999;j++)
		for(int k=1;k<=999;k++)
			cout<<"Luogu!"<<endl;

像這種代碼想要不超時基本不可能,但如果加上了宏定義就不一定了,如下:

#define fir(i,a,b) for(i=a;i<=b;i++)
......
int i,j,k;
fir(i,1,999)
	fir(j,1,999)
		fir(k,1,999)
			cout<<"Luogu!"<<endl;

雖然這樣能提高效率,但想要不超時還是很困難,不過從此可以看出,全局變量和宏定義確實能提高代碼效率。

0xFF 結語

如果你已經掌握了這些代碼複雜度優化方法,相信你在比賽中至少能多拿個 20 分。

作為學生黨,並沒有很多的時間來完成博客,甚至還會有很多寫錯的地方。這類文章(包括這篇)以後會持續更新,所以暫時先完結了。

同步發佈於:CSDN