Part.1 卡常
快读
- 适用范围:需要读入大量数据的数据结构题,在理论复杂度可以通过的情况下因为
出题人就是不肯开大一点的严苛时限而TLE。 - 原理:
scanf
内部会对读入格式做很多处理,如果我们明确读入的类型和格式,就可以直接利用读入单个字符的getchar()
函数了。因此加速了读入。 - 读入整数(正负皆可):
int read()
{
int x = 0, f = 0;
char c = getchar();
for (; c < '0' || c > '9'; c = getchar()) if (c == '-') f = 1;
for (; c >= '0' && c <= '9'; c = getchar()) x = (x << 1) + (x << 3) + (c ^ '0');
return f ? -x : x;
}
//使用了大量的位运算以提高效率
- 优化效果:还是很明显的,我们对比一下:
我用下面的程序生成了读入数据:
#include <cstdio>
#include <cstring>
#include <cstdlib>
int n = 10000000;
int main()
{
freopen("input", "w", stdout);
printf("%d\n", n);
for (int i = 1; i <= n; i++) printf("50000\n");
return 0;
}
使用头文件<windows.h>
里面的GetTickCount()
函数计时:
#include <cstdio>
#include <ctime>
#include <cstdlib>
#include <cstring>
#include <windows.h>
int read()
{
int x = 0, f = 0;
char c = getchar();
for (; c < '0' || c > '9'; c = getchar()) if (c == '-') f = 1;
for (; c >= '0' && c <= '9'; c = getchar()) x = (x << 1) + (x << 3) + (c ^ '0');
return f ? -x : x;
}
const int N = 1e7 + 7;
int n, a[N];
int main()
{
//freopen("input", "r", stdin);
int now = GetTickCount();
scanf("%d", &n);
for (int i = 1; i <= n; i++) a[i] = read();
printf("%d\n", GetTickCount() - now);
return 0;
}
运行10次,平均用时为686.5ms
。
那么如果用scanf
呢?
#include <cstdio>
#include <ctime>
#include <cstdlib>
#include <cstring>
#include <windows.h>
int read()
{
int x = 0, f = 0;
char c = getchar();
for (; c < '0' || c > '9'; c = getchar()) if (c == '-') f = 1;
for (; c >= '0' && c <= '9'; c = getchar()) x = (x << 1) + (x << 3) + (c ^ '0');
return f ? -x : x;
}
const int N = 3e7 + 7;
int n, a[N];
int main()
{
//freopen("input", "r", stdin);
int now = GetTickCount();
scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
printf("%d\n", GetTickCount() - now);
return 0;
}
运行5次,平均用时为4143.6ms
。
如果使用cin
读入,运行3次,平均用时17217.33ms
。
恐怖!
因此读入方式的选择是:
快读>scanf
>cin
- Tips:一般读入整数时才会使用快读,优化读入字符串时效果并不明显。
- 如果因为某些原因你不得不使用
cin
读入(例如忘记了怎么读入一行之类的)。请在读入所有数据之前加上这句:std::ios::sync_with_stdio(0);
(如果已经using namespace std;
就可以去掉std::
)。这句话可以加快cin
的读入效率,加上这句话后用cin
读入上面的数据,平均用时是9773.5ms
。
O2吸氧 && O3臭氧优化
- 适用范围:极广,凡是卡不过去的卡常题都能用,
程序越慢优化效果越明显 ,可能会导致奇怪的WA(我暂时没碰见过),对于大量使用STL
的程序优化效果更明显,甚至能让运行时间直接减半。 - 原理:
去问编译器吧我也不知道,通过编译器的一些骚操作让程序变快。 - 一部分OJ带有是否开启O2的选项,例如洛谷:
- 如果OJ没有这个选项的话,我们也可以在程序的开头加上这句话来开O2/O3:
#pragma GCC optimize(2)
或者#pragma GCC optimize(3)
。
碰见卡不过的卡常题,我通常喜欢把这四句都打上:
#pragma GCC optimize(2)
#pragma GCC optimize(3)
#pragma G++ optimize(2)
#pragma G++ optimize(3)
虽然很无耻,但是优化效果还是不错的。
- Tips:许多正规比赛不允许使用O2/O3优化,例如NOIP,所以比赛的时候还是不要以身试法了。仅限于对付恶心毒瘤的卡常题。不过有些比赛为避免常数对程序效率影响过大,会统一开O2编译,例如GDKOI。
此外,O2/O3还有一些神奇的功能,例如下面这段程序:
#include <cstdio>
int main()
{
int n = 1000000000, ans = 0;
for (int i = 1; i <= n; i++) ans++;
printf("%d\n", ans);
return 0;
}
直接编译运行,如果评测机比较快就大概跑个2s,但是如果把n再开大一点,就TLE
了,但是O2/O3能够识别这个弱智的程序并优化它,它直接将for(int i = 1; i <= n; i++) ans++;
这一句理解成了ans += n;
一般人我不告诉他的好东西
- 适用范围:极广,但是可能会出现负优化,全看人品。
- 原理:未知。
register
寄存器变量:
在定义一个非全局变量时,前面加上register
可标记它为寄存器变量,用它来定义经常使用的变量可以提高访问变量的速度。
不过它在大部分情况下是负优化,所以尽量别用吧。循环展开
促进CPU并发的黑科技:
下面这句话
int n = 30000000;
for (int i = 1; i <= n; i++) a[i] = -1;
如果写成这样
int n = 30000000;
for (int i = 1; i <= n; i += 4) a[i] = a[i + 1] = a[i + 2] = a[i + 3] = -1;
看上去并没有什么区别对吗?
可是运行一下,就有神奇的发现:其实时间差异并不大…
循环展开,因机而异。理论上来说,下面的语句"暗示"了编译器:“我卡不过这道题,给我优化一下!”。但是并非所有的评测机都那么开窍,所以还是别用这个优化了。
(所以我上面都是在说废话)
压位高精度
- 适用范围:时限比较极限的高精度题。
- 原理:将原来存10进制数改为存1000进制或10000进制数,减少计算的数的位数,以此加快高精度乘法效率的同时,也便于输出答案。
- 依照原理,我们把原来的高精度数当做是10000进制的,逢10000进1,输出的时候只需要把该位转换成10进制数输出。
Part.2 数论里的神奇方法
慢速乘
- 适用范围:当需要计算
a
∗
b
m
o
d
P
a*b\ mod\ P
a∗b mod P时,如果
a
,
b
a,b
a,b都在
1
0
18
10^{18}
1018级别,也就是它们的乘积连
long long
也存不下,但它们的和可以用long long
存下的时候,就可以使用慢速乘。 - 原理:仿照快速幂思路,以加代乘,做 l o g b log_b logb次加法。
- 有时候
a
,
b
a,b
a,b加起来也会超出
long long
范围,我们就需要一个函数:
typedef long long ll;
ll plus(ll a, ll b, ll P);
来计算
(
a
+
b
)
m
o
d
P
(a+b)\ mod\ P
(a+b) mod P。
这个函数的前提是要保证
0
<
a
<
P
0<a<P
0<a<P且
0
<
b
<
P
0<b<P
0<b<P。
我们分类讨论来实现这个函数:
-
a
+
b
<
P
a+b<P
a+b<P时
显然我们不能计算 a + b a+b a+b,但可以计算 P − b P-b P−b,因此我们可以通过比较 a < P − b a<P-b a<P−b来判断是不是这个情况,显然此时return a + b;
-
a
+
b
>
=
P
a+b>=P
a+b>=P时
首先还是判断 a > = P − b a>=P-b a>=P−b,然后只需要计算 a + b − P = a − ( P − b ) a+b-P=a-(P-b) a+b−P=a−(P−b)。return a - (P - b);
这个函数就出来了:
typedef long long ll;
ll plus(ll a, ll b, ll P)
{
if (a < P - b) return a + b;
return a - (P - b);
}
再仿照快速幂写一个慢速乘的过程:
typedef long long ll;
ll plus(ll a, ll b, ll P)
{
if (a < P - b) return a + b;
return a - (P - b);
}
ll multi(ll a, ll b, ll P) //calculate a * b % P
{
a %= P, b %= P;
ll ret = 0;
while (b)
{
if (b & 1) ret = plus(ret, a, P);
a = plus(a, a, P), b >>= 1;
}
return ret;
}
这样妈妈再也不用担心我乘爆long long
啦!