ACM編程中的小技巧總結 (持續更新)

ACM中有很多小技巧和有趣的寫法。雖然無法改變算法的複雜度,但是卻可以縮短代碼長度、減少尋址時間和冗餘狀態等等。

在此對寫程序的時候一些小技巧以及一些函數的簡潔寫法進行總結,以後也會不斷更新。

當然很多函數它本來就這麼短,反正大概我知道的一行函數我都會記下來。

不過很多技巧我只是從實用的角度出發,如果要跟我討論嚴謹證明的話,麻煩您。。出門。。左轉。。。。Google...

其中可能借鑑了一些大牛的寫法,望見諒。


PS:關於位運算優化,強烈大家去看Matrix67的《位運算簡介及實用技巧》系列:(一)(二)(三)(四)


一、簡潔寫法


1、求逆元

int inv(int x)  
{  
    return x <= 1 ? x : (MOD - MOD / x) * inv(MOD % x) % MOD;  
} 
// x = 0 時無逆元


 inv[1] = 1;
 for(int i = 2; i <= n; i ++)
 {
     inv[i] = (-mod / i) * inv[mod % i];
     inv[i] = (inv[i] % mod + mod) % mod;
 }
// 求1~n的所有逆元

2、並查集

int findp(int x) 
{
 return p[x] == -1 ? p[x] : p[x] = findp(p[x]); 
}

3、狀壓預處理

eg:不能出現兩個相鄰的1

if (((mask >> 1) & mask)||((mask << 1) & mask))
{
  return false;
}
else
{
  return true;
}


4、枚舉子集

eg: mask的第x位爲0表示x必須不在子集中(原集合中不含這個元素):

for (int mask1 = mask; mask1 >= 0; mask1 = (mask1 - 1) & mask)

eg: mask的第x位爲1表示x必須在子集中:

for (int mask1 = mask; mask1 < (1 << m); mask1 = (mask1 + 1) | mask)

5、找出二進制中恰好含有 k個1的所有數

for (int mask = 0; mask < 1 << n; )
{
	int tmp = mask & -mask;
	mask = (mask + tmp) | (((mask ^ (mask + tmp)) >> 2) / tmp);
}

6、進制轉換

void convert(int x)
{
    if (x)
    {
        convert(x / k);
        ans[tot ++] = ch(x % k);
}
//倒序

7、g++內置位運算函數

介紹四種GCC內置位運算函數:

1)int __builtin_ffs (unsigned int x)
返回x的最後一位1的是從後向前第幾位,比如7368(1110011001000)返回4。
2)int __builtin_clz (unsigned int x)
返回前導的0的個數。
3)int __builtin_ctz (unsigned int x)
返回後面的0個個數,和__builtin_clz相對。
4)int __builtin_popcount (unsigned int x)
返回二進制表示中1的個數。
5)int __builtin_parity (unsigned int x)
返回x的奇偶校驗位,也就是x的1的個數模2的結果。

此外,這些函數都有相應的usigned long和usigned long long版本,只需要在函數名後面加上l或ll就可以了,比如int __builtin_clzll。
感謝張文泰大牛。


8、快速max && 快速min

int fastMax(int x, int y)  { return (((y - x) >> (32 - 1)) & (x ^ y)) ^ y; }
int fastMin(int x, int y)  { return (((y - x) >> (32 - 1)) & (x ^ y)) ^ x; }
這個其實是隻能用於特定的編譯環境的,我還沒仔細研究,目前只知道CF可以用。

不過光是看看都覺得很炫酷啊~

二、小技巧

1、開數組時,如果按行訪問,各維應按照上界由小到大排列,a[10][1000000]要比 a[1000000][10]的尋址時間少很多。按列訪問則相反。原理是儘量減少缺頁中斷。儘量少使用2的冪次的數組下標也是相同的原理。

2、BFS時,每個節點只會入隊一次,因此循環隊列的隊首和隊尾指針不必取餘。

3、二分匹配時,應從點數少的一邊開始搜。

二分匹配不必每次都初始化vis[]數組,只要記錄每個節點上一次被修改的時候是由哪個點作爲增廣路的起點的。

4、 對實數二分應採用for(int loop = 0; loop < 64; loop ++)之流而不是 while(r - l < eps)

5、對實數操作時,能加就不減,能乘就不除。保留精度。

6、由於C++把實數保存成二進制小數,所以C++的round不是四捨五入,而是四捨六入五留雙(大概吧,反正不是四捨五入)

int的強制類型轉換是截尾而不是floor(),(int) -1.5 = -1。 

7、C++擴棧指令:#pragma comment(linker,"/STACK:102400000,102400000")   中間的數字寫多少自己斟酌一下。

8、寫大數的時候,可以用int把大數分成一段一段的,比如說18位的數就分成兩個int,然後用10^9進制去運算,最後分段%09d輸出。

9、樹形DP如果爆棧可以用BFS代替DFS。從上往下更新自然是BFS。從下往上更新就是先由根BFS一遍,或者至少要確定每個點的父節點是誰,然後類似拓撲排序的過程,當一個點的所有子節點都被更新完畢後,再把這個點入隊。

10、sync_with_stdio(false); 關閉緩衝區同步,有效提高cin/cout的效率。但關閉後不能混用cin/cout和stdin/stdout。

11、當每個case有一個bool型的vis數組時,可以不必初始化。把vis開成int。要把vis[]置爲真的時候就令vis[] = cas,要置爲假的時候就令vis[] = 0。要判斷是否爲真就是vis[] == cas ? true: false; cas是指當前是第幾個案例,1-based。


三、小知識點

1、tarjan法求SCC得出的結果是遵循拓撲序的。

2、判斷樹的同構,只需判斷樹的括號序列是否循環同構(最小表示法)。

3、完全圖的生成樹個數是 n ^ (n - 2)

4、(q ^ (p - 1)) MOD p = 1,p、q互質。

5、衆所周知找負環應該用迭代深搜SPFA,但是懶得寫的話可以用棧來代替隊列存儲節點,一樣有比較好的效果。

SPFA懶得寫SLF和LLL的話,用優先隊列代替普通隊列亦可。

inq[]標記是必加的,不止是效率問題,多次入隊可能隊列長度會超過你開的數組大小

6、tarjan求BCC / SCC時,把low[u]初始化成時間戳,搜索子節點的時候不要立即更新low[u],而是用一個臨時變量來保存。這樣還在棧中的節點的low[u]就自然等於時間戳,省去了一些判斷。(這裏是依照BYVoid的理解來說。貌似那些判斷其實本來就可以不要的。

7、圖論如果是做稠密圖的話一般來說鄰接矩陣比鄰接表效率高很多,當然前提是你存得下。

8、查分約束。求的是最小值就求最長路,求的是最大值就求最短路。

9、DAG上的很多問題可以直接用拓撲排序解決,比如最短路。

10、n位二進制數中有 k 個 1 的數的個數是C(n, k)。

11、DP中如果先枚舉一個子集,再枚舉這個子集的子集,複雜度是O(3 ^ n)。另外,如果枚舉子集是爲了得到它和它的補集,那麼根據它和它的補集中必有一個不超過 1 << (n - 1),枚舉量就可以少一半。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章