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

你可能感兴趣的:(ACM编程中的小技巧总结 (持续更新))