Some 奇技淫巧 in OI.

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 ab 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 &lt; a &lt; P 0&lt;a&lt;P 0<a<P 0 &lt; b &lt; P 0&lt;b&lt;P 0<b<P
我们分类讨论来实现这个函数:

  • a + b &lt; P a+b&lt;P a+b<P
    显然我们不能计算 a + b a+b a+b,但可以计算 P − b P-b Pb,因此我们可以通过比较 a &lt; P − b a&lt;P-b a<Pb来判断是不是这个情况,显然此时return a + b;
  • a + b &gt; = P a+b&gt;=P a+b>=P
    首先还是判断 a &gt; = P − b a&gt;=P-b a>=Pb,然后只需要计算 a + b − P = a − ( P − b ) a+b-P=a-(P-b) a+bP=a(Pb)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啦!

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值