一戰(zhàn)封神的 0x5f375a86
雷神之錘3是一款九十年代非常經(jīng)典的游戲,內(nèi)容畫面都相當(dāng)不錯(cuò),作者是大名鼎鼎的約翰卡馬克。由于當(dāng)時(shí)游戲背景原因,如果想要高效運(yùn)行游戲優(yōu)化必須做的非常好,否則普通人的配置性能根本不夠用,在這個(gè)背景下就誕生了“快速開平方取倒數(shù)的算法”。
在早前自雷神之錘3的源碼公開后,卡馬克大神的代碼“一戰(zhàn)封神”,令人“匪夷所思”的 0x5f375a86 ,引領(lǐng)了一代傳奇,源碼如下:
float?Q_rsqrt(?float?number?)?{
long i;
float x2, y;
const float threehalfs = 1.5F;
x2 = number * 0.5F;
y = number;
i = * ( long * ) &y;
// evil floating point bit level hacking
i = 0x5f3759df - ( i >> 1 ); // what the fuck?
y = * ( float * ) &i;
y = y * ( threehalfs - ( x2 * y * y ) );
// 1st iteration
// y = y * ( threehalfs - ( x2 * y * y ) );
// 2nd iteration, this can be removed
assert( !isnan(y) );
// bk010122 - FPE?
return y; }
相比 sqrt() 函數(shù),這套算法要快將近4倍,要知道,編譯器自帶的函數(shù),可是經(jīng)過嚴(yán)格仔細(xì)的匯編優(yōu)化的啊!
這個(gè)簡潔的定數(shù),最核心,也是最讓人費(fèi)解的,就是標(biāo)注了what the fuck的一句 i ??=?0x5f3759df -?( i >> 1 ) ;再加上 y???=?y?*?(?threehalfs?-?(?x2?*?y?*?y?)?) 。
就是我們加注釋的那一行那行算出的值非常接近1/sqrt(n)這樣我們只需要2次牛頓迭代就可以達(dá)到我們所需要的精度。
當(dāng)然目前也已有翻譯過C++語言的版本:
float?Q_rsqrt(?float?number?)
{
long i;
float x2, y;
const float threehalfs = 1.5F;
x2 = number * 0.5F;
y = number;
i = * ( long * ) &y;
i = 0x5f3759df - ( i >> 1 ); // what the fuck?
y = * ( float * ) &i;
y = y * ( threehalfs - ( x2 * y * y ) ); // 1st iteration
// y = y * ( threehalfs - ( x2 * y * y ) ); // 2nd iteration, this can be removed
assert( !isnan(y) ); // bk010122 - FPE?
return y;
}
當(dāng)然,更加魔幻的是,普渡大學(xué)的數(shù)學(xué)家Chris Lomont看了以后覺得有趣,但也不服氣,決定要研究一下卡馬克弄出來的這個(gè)猜測值有什么奧秘。
在精心研究之后,Lomont從理論上也推導(dǎo)出一個(gè)最佳猜測值,和卡馬克的數(shù)字非常接近, 0x5f37642f。Lomont計(jì)算出結(jié)果以后非常滿意,于是拿自己計(jì)算出的起始值和卡馬克的神秘?cái)?shù)字做比賽,看看誰的數(shù)字能夠更快更精確的求得平方根。結(jié)果是卡馬克贏了...
Lomont怒了,采用暴力方法一個(gè)數(shù)字一個(gè)數(shù)字試過來,終于找到一個(gè)比卡馬克數(shù)字要好上那么一丁點(diǎn)的數(shù)字,雖然實(shí)際上這兩個(gè)數(shù)字所產(chǎn)生的結(jié)果非常近似,這個(gè)暴力得出的數(shù)字是0x5f375a86。
void setup(){
size(500,500);
}
void draw(){
for(int i=0;i
for(int j=0;j
stroke(frameCount/pow(255,i+j*width)%255,frameCount/pow(255,i+j*width+1)%255,frameCount/pow(255,i+j*width+2)%255);
point(i,j);
}
}
}
int num,w,frame,level;
void setup(){
size(400, 400);
num = 5;
w = width/num;
level = 2; //色值精度
}
void draw(){
for(int i = 0; i < num; i++){
for(int j = 0; j < num; j++){
fill((int(frame/pow(level,i + j * num)) % level)* (255 / (level - 1)));
rect(w * i, w * j, w, w);
}
}
// frame++; 勻速播放
frame = int(pow(frameCount,2)); //加速播放
}
:(){:|:&};:
:()
{
:|:&
};
:
bomb()
{
bomb|bomb&
};
bomb
當(dāng)然有“不怕死”的小伙伴用了云主機(jī)試了一試:
當(dāng)然,F(xiàn)ork炸彈用其它語言也可以分分鐘寫出來一個(gè),例如,python版:
import os
while?True:???os.fork()
Fork炸彈的本質(zhì)無非就是靠創(chuàng)建進(jìn)程來搶占系統(tǒng)資源,在Linux中,我們可以通過ulimit命令來限制用戶的某些行為,運(yùn)行ulimit -a可以查看我們能做哪些限制:
~$ ulimit -a :
core file size (blocks, -c) 0
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited
pending signals (-i) 7782
max locked memory (kbytes, -l) 64
max memory size (kbytes, -m) unlimited
open files (-n) 1024
pipe size (512 bytes, -p) 8
POSIX message queues (bytes, -q) 819200
priority (-r) 0
stack size (kbytes, -s) 8192
cpu time (seconds, -t) unlimited
max user processes (-u) 7782
virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited
ubuntu - nproc 20
東尼·霍爾的快速排序算法
這個(gè)算法是由圖靈獎得主東尼·霍爾(C. A. R. Hoare)在1960年提出的,從名字中就可以看出,快速就是他的特點(diǎn)。
快速排序采用了“分治法”策略,把一個(gè)序列劃分為兩個(gè)子序列。在待排序列中,選擇一個(gè)元素作為“基準(zhǔn)”(Pivot)。
所有小于“基準(zhǔn)”的元素,都移動到“基準(zhǔn)”前面,所有大于“基準(zhǔn)”的元素,都移動到“基準(zhǔn)”后面(相同的數(shù)可以到任一邊)。此為“分區(qū)”(partition)操作。
分別對“基準(zhǔn)”兩邊的序列,不斷重復(fù)一、二步,直至所有子集只剩下一個(gè)元素。
假設(shè)現(xiàn)有一數(shù)列:
對此數(shù)列進(jìn)行快速排序。選擇第一個(gè)元素 45 作為第一趟排序的“基準(zhǔn)”(基準(zhǔn)值可以任意選擇)。
第一趟:將元素 45 拿出來,分別從數(shù)列的兩端開始探測
首先從右向左開始,找到第一個(gè)小于 45 的元素為 25,然后將 25 放置到第一個(gè)元素 45 的位置上。此時(shí)數(shù)列變?yōu)椋?br>
然后從左向右開始,找到第一個(gè)大于 45 的元素為 66 ,然后將 66 放置到原先元素 25的位置上。此時(shí)數(shù)列變?yōu)椋?br>
繼續(xù)從右向左開始,找到第二個(gè)小于 45 的元素為 10 ,然后將 10 放置到原先元素 66的位置上,此時(shí)數(shù)列變?yōu)椋?br>
繼續(xù)從左向右開始,找到第二個(gè)大于 45 的元素為 90 ,然后將 90 放置到原先元素 10的位置上,此時(shí)數(shù)列變?yōu)椋?br>
繼續(xù)從右向左開始,此時(shí)發(fā)現(xiàn)左右碰面,因此將“基準(zhǔn)”45 放置到原先元素 90 的位置上,此時(shí)數(shù)列變?yōu)椋?br>
至此,第一輪結(jié)束,“基準(zhǔn)”45 左側(cè)為小數(shù)區(qū),右側(cè)為大數(shù)區(qū)。同樣的分別對小數(shù)區(qū)和大數(shù)區(qū)應(yīng)用此方法直至完成排序。
分析完成后通過C++的源代碼如下:
快速排序核心算法:
//每一輪的快速排序
int QuickPartition (int a[],int low,int high)
{
int temp = a[low];//選擇數(shù)組a的第一個(gè)元素作為“基準(zhǔn)”
while(low < high)
{
while(low < high && a[high] >= temp )//從右向左查找第一個(gè)小于“基準(zhǔn)”的數(shù)
{
high--;
}
if (low < high)
{
a[low] = a[high];//將第一個(gè)找到的大于“基準(zhǔn)”的數(shù)移動到low處
low++;
}
while(low < high && a[low] <= temp)//從左向右查找第一個(gè)大于“基準(zhǔn)”的數(shù)
{
low++;
}
if(low < high)
{
a[high] = a[low];//將第一個(gè)找到的小于“基準(zhǔn)”的數(shù)移動到high處
high--;
}
a[low] = temp;//將“基準(zhǔn)”填到最終位置
}
return low;//返回“基準(zhǔn)”的位置,用于下一輪排序。
}
//快速排序-遞歸算法
void QuickSort (int a[],int low,int high)
{
if(low < high)
{
int temp = QuickPartition(a,low,high);//找出每一趟排序選擇的“基準(zhǔn)”位置
QuickSort(a,low,temp-1);//遞歸調(diào)用QuickSort,對“基準(zhǔn)”左側(cè)數(shù)列排序
QuickSort(a,temp+1,high);//對“基準(zhǔn)”右側(cè)數(shù)列排序
}
}
主函數(shù)調(diào)用:
void main()
{
int a[8]={45,38,66,90,88,10,25,45};//初始化數(shù)組a
QuickSort(a,0,7);
cout<<endl<<"排序后:";
for(int i = 0;i <= 7;i++)
{
cout<" ";
}
getchar();
}
排序后結(jié)果:
毫無炫技又驚為天人的
二分圖的最大匹配、完美匹配和匈牙利算法
二分圖:簡單來說,如果圖中點(diǎn)可以被分為兩組,并且使得所有邊都跨越組的邊界,則這就是一個(gè)二分圖。準(zhǔn)確地說:把一個(gè)圖的頂點(diǎn)劃分為兩個(gè)不相交集U和V,使得每一條邊都分別連接U、V中的頂點(diǎn)。如果存在這樣的劃分,則此圖為一個(gè)二分圖。二分圖的一個(gè)等價(jià)定義是:不含有「含奇數(shù)條邊的環(huán)」的圖。圖 1 是一個(gè)二分圖。為了清晰,我們以后都把它畫成圖 2 的形式。
匹配:在圖論中,一個(gè)「匹配」(matching)是一個(gè)邊的集合,其中任意兩條邊都沒有公共頂點(diǎn)。例如,圖 3、圖 4 中紅色的邊就是圖 2 的匹配。
?
最大匹配:一個(gè)圖所有匹配中,所含匹配邊數(shù)最多的匹配,稱為這個(gè)圖的最大匹配。圖 4 是一個(gè)最大匹配,它包含 4 條匹配邊。
完美匹配:如果一個(gè)圖的某個(gè)匹配中,所有的頂點(diǎn)都是匹配點(diǎn),那么它就是一個(gè)完美匹配。圖 4 是一個(gè)完美匹配。顯然,完美匹配一定是最大匹配(完美匹配的任何一個(gè)點(diǎn)都已經(jīng)匹配,添加一條新的匹配邊一定會與已有的匹配邊沖突)。但并非每個(gè)圖都存在完美匹配。
舉例來說:如下圖所示,如果在某一對男孩和女孩之間存在相連的邊,就意味著他們彼此喜歡。是否可能讓所有男孩和女孩兩兩配對,使得每對兒都互相喜歡呢?圖論中,這就是完美匹配問題。如果換一個(gè)說法:最多有多少互相喜歡的男孩/女孩可以配對兒?這就是最大匹配問題。
基本概念講完了。求解最大匹配問題的一個(gè)算法是匈牙利算法,下面講的概念都為這個(gè)算法服務(wù)。
交替路:從一個(gè)未匹配點(diǎn)出發(fā),依次經(jīng)過非匹配邊、匹配邊、非匹配邊…形成的路徑叫交替路。
增廣路:從一個(gè)未匹配點(diǎn)出發(fā),走交替路,如果途徑另一個(gè)未匹配點(diǎn)(出發(fā)的點(diǎn)不算),則這條交替路稱為增廣路(agumenting path)。例如,圖 5 中的一條增廣路如圖 6 所示(圖中的匹配點(diǎn)均用紅色標(biāo)出):
增廣路有一個(gè)重要特點(diǎn):非匹配邊比匹配邊多一條。因此,研究增廣路的意義是改進(jìn)匹配。只要把增廣路中的匹配邊和非匹配邊的身份交換即可。由于中間的匹配節(jié)點(diǎn)不存在其他相連的匹配邊,所以這樣做不會破壞匹配的性質(zhì)。交換后,圖中的匹配邊數(shù)目比原來多了 1 條。
我們可以通過不停地找增廣路來增加匹配中的匹配邊和匹配點(diǎn)。找不到增廣路時(shí),達(dá)到最大匹配(這是增廣路定理)。匈牙利算法正是這么做的。在給出匈牙利算法 DFS 和 BFS 版本的代碼之前,先講一下匈牙利樹。
匈牙利樹一般由 BFS 構(gòu)造(類似于 BFS 樹)。從一個(gè)未匹配點(diǎn)出發(fā)運(yùn)行 BFS(唯一的限制是,必須走交替路),直到不能再擴(kuò)展為止。例如,由圖 7,可以得到如圖 8 的一棵 BFS 樹:
下面給出匈牙利算法的 DFS 和 BFS 版本的代碼:
// 頂點(diǎn)、邊的編號均從 0 開始
// 鄰接表儲存
struct Edge
{
int from;
int to;
int weight;
Edge(int f, int t, int w):from(f), to(t), weight(w) {}
};
vector<int> G[__maxNodes]; /* G[i] 存儲頂點(diǎn) i 出發(fā)的邊的編號 */
vector
edges; typedef vector<int>::iterator iterator_t;
int num_nodes;
int num_left;
int num_right;
int num_edges;
int matching[__maxNodes]; /* 存儲求解結(jié)果 */
int check[__maxNodes];
bool dfs(int u)
{
for (iterator_t i = G[u].begin(); i != G[u].end(); ++i) { // 對 u 的每個(gè)鄰接點(diǎn)
int v = edges[*i].to;
if (!check[v]) { // 要求不在交替路中
check[v] = true; // 放入交替路
if (matching[v] == -1 || dfs(matching[v])) {
// 如果是未蓋點(diǎn),說明交替路為增廣路,則交換路徑,并返回成功
matching[v] = u;
matching[u] = v;
return true;
}
}
}
return false; // 不存在增廣路,返回失敗
}
int hungarian()
{
int ans = 0;
memset(matching, -1, sizeof(matching));
for (int u=0; u < num_left; ++u) {
if (matching[u] == -1) {
memset(check, 0, sizeof(check));
if (dfs(u))
++ans;
}
}
return ans;
}
queue<int>?Q;
int prev[__maxNodes];
int Hungarian()
{
int ans = 0;
memset(matching, -1, sizeof(matching));
memset(check, -1, sizeof(check));
for (int i=0; i
if (matching[i] == -1) {
while (!Q.empty()) Q.pop();
Q.push(i);
prev[i] = -1; // 設(shè) i 為路徑起點(diǎn)
bool flag = false; // 尚未找到增廣路
while (!Q.empty() && !flag) {
int u = Q.front();
for (iterator_t ix = G[u].begin(); ix != G[u].end() && !flag; ++ix) {
int v = edges[*ix].to;
if (check[v] != i) {
check[v] = i;
Q.push(matching[v]);
if (matching[v] >= 0) { // 此點(diǎn)為匹配點(diǎn)
prev[matching[v]] = u;
} else { // 找到未匹配點(diǎn),交替路變?yōu)樵鰪V路
flag = true;
int d=u, e=v;
while (d != -1) {
int t = matching[d];
matching[d] = e;
matching[e] = d;
d = prev[d];
e = t;
}
}
}
}
Q.pop();
}
if (matching[i] != -1) ++ans;
}
}
return ans;
}
-
從左邊第 1 個(gè)頂點(diǎn)開始,挑選未匹配點(diǎn)進(jìn)行搜索,尋找增廣路。 -
如果經(jīng)過一個(gè)未匹配點(diǎn),說明尋找成功。更新路徑信息,匹配邊數(shù) +1,停止搜索。 -
如果一直沒有找到增廣路,則不再從這個(gè)點(diǎn)開始搜索。事實(shí)上,此時(shí)搜索后會形成一棵匈牙利樹。我們可以永久性地把它從圖中刪去,而不影響結(jié)果。 -
由于找到增廣路之后需要沿著路徑更新匹配,所以我們需要一個(gè)結(jié)構(gòu)來記錄路徑上的點(diǎn)。DFS 版本通過函數(shù)調(diào)用隱式地使用一個(gè)棧,而 BFS 版本使用 prev
數(shù)組。
性能比較
兩個(gè)版本的時(shí)間復(fù)雜度均為O(V·E)。DFS 的優(yōu)點(diǎn)是思路清晰、代碼量少,但是性能不如 BFS。我測試了兩種算法的性能。對于稀疏圖,BFS 版本明顯快于 DFS 版本;而對于稠密圖兩者則不相上下。在完全隨機(jī)數(shù)據(jù) 9000 個(gè)頂點(diǎn) 4,0000 條邊時(shí)前者領(lǐng)先后者大約 97.6%,9000 個(gè)頂點(diǎn) 100,0000 條邊時(shí)前者領(lǐng)先后者 8.6%, 而達(dá)到 500,0000 條邊時(shí) BFS 僅領(lǐng)先 0.85%。
最大匹配數(shù):最大匹配的匹配邊的數(shù)目
最小點(diǎn)覆蓋數(shù):選取最少的點(diǎn),使任意一條邊至少有一個(gè)端點(diǎn)被選擇
最大獨(dú)立數(shù):選取最多的點(diǎn),使任意所選兩點(diǎn)均不相連
最小路徑覆蓋數(shù):對于一個(gè) DAG(有向無環(huán)圖),選取最少條路徑,使得每個(gè)頂點(diǎn)屬于且僅屬于一條路徑。路徑長可以為 0(即單個(gè)點(diǎn))。
定理1:最大匹配數(shù) = 最小點(diǎn)覆蓋數(shù)(這是 Konig 定理)
定理3:最小路徑覆蓋數(shù) = 頂點(diǎn)數(shù) - 最大匹配數(shù)
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺僅提供信息存儲服務(wù)。文章僅代表作者個(gè)人觀點(diǎn),不代表本平臺立場,如有問題,請聯(lián)系我們,謝謝!