Some 奇技淫巧 in OI.
阿新 • • 發佈:2018-12-13
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 數論裡的神奇方法
慢速乘
- 適用範圍:當需要計算時,如果都在級別,也就是它們的乘積連
long long
也存不下,但它們的和可以用long long
存下的時候,就可以使用慢速乘。 - 原理:仿照快速冪思路,以加代乘,做次加法。
- 有時候加起來也會超出
long long
範圍,我們就需要一個函式:
typedef long long ll;
ll plus(ll a, ll b, ll P);
來計算。 這個函式的前提是要保證且。 我們分類討論來實現這個函式:
- 時
顯然我們不能計算,但可以計算,因此我們可以通過比較來判斷是不是這個情況,顯然此時
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);
}
再仿照快速冪寫一個慢速乘的過程:
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
啦!