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),列舉量就可以少一半。