1. 程式人生 > >Some 奇技淫巧 in OI.

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 數論裡的神奇方法

慢速乘

  • 適用範圍:當需要計算abmodPa*b\ mod\ P時,如果a,ba,b都在101810^{18}級別,也就是它們的乘積連long long也存不下,但它們的和可以用long long存下的時候,就可以使用慢速乘。
  • 原理:仿照快速冪思路,以加代乘,做logblog_b次加法。
  • 有時候a,ba,b加起來也會超出long long範圍,我們就需要一個函式:
typedef long long ll;
ll plus(ll a, ll b, ll P);

來計算(a+b)modP(a+b)\ mod\ P。 這個函式的前提是要保證0&lt;a&lt;P0&lt;a&lt;P0&lt;b&lt;P0&lt;b&lt;P。 我們分類討論來實現這個函式:

  • a+b&lt;Pa+b&lt;P時 顯然我們不能計算a+ba+b,但可以計算PbP-b,因此我們可以通過比較a&lt;Pba&lt;P-b來判斷是不是這個情況,顯然此時return a + b;
  • a+b&gt;=Pa+b&gt;=P時 首先還是判斷a&gt;=Pba&gt;=P-b,然後只需要計算a+bP=a(Pb)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啦!