你沒見過的 “極速N皇后”

前幾天聽了 CMU 一大神的算法公開課,算是近距離見識了算法大神的冰山一角洋访,管中窺豹镣陕,可見一斑。

先稍微介紹一下大神姻政,方便感興趣的童鞋進一步了解他呆抑。

  • 姓名:王赟 Maigo

想了解更多的就自己谷歌吧。知乎專欄就夠看好久的了汁展。

我寫這篇文字的目的很簡單鹊碍,

  1. 分享給需要的和感興趣的同學(xué)
  2. 看了好幾遍視頻,還是想自己從頭到尾復(fù)述一遍以加深印象食绿,熟悉 bit 的操作侈咕。

算法的所有代碼,java 版本和 swift 版本器紧,我都放在了我的 Github 上耀销。當(dāng)然還有歡迎訂閱我的博客

為了找工作過 OA 關(guān)铲汪,也是沒辦法用 swift熊尉,更不想用 C++,還不如臨時學(xué) java 來的效率點桥状。所以這里就用 java 代碼來講解一下帽揪。我這里用的是 “15 queens”,共5個方法辅斟,第四個開始速度有了質(zhì)的提升转晰,但需要第三個方法的基礎(chǔ),反正后一個的提升都基于前一個士飒。

Queen 1

我自己的電腦上跑的時間是// Time elapsed: 28423ms查邢。用的是最基礎(chǔ)的回溯法(back tracking)。最后就不把所有的解都打印出來了酵幕,打印的時間其實和算法本身關(guān)系并不大扰藕,而且因為是15 皇后,容易死機芳撒。

public class Main {
    private static final int n = 15;
    private static int count = 0;
    private static int[] sol;
    
    public static void main(String[] args) {
        sol = new int[n];
        long tic = System.currentTimeMillis();
        DFS(0);
        long toc = System.currentTimeMillis();
        System.out.println("Total solutions: " + count);
        System.out.println("Time elapsed: " + (toc - tic) + "ms");
    }
    
    private static void DFS(int row) {
        for (int col = 0; col < n; col++) {
            boolean ok = true;
            // 注意下面這個循環(huán)邓深,后面會做改進
            for (int i = 0; i < row; i++) {
                if (col == sol[i] || i - row == sol[i] - col || i - row == col - sol[i]) {
                    ok = false;
                    break;
                }
            }
            if (!ok) continue;
            sol[row] = col;
            if (row == n - 1) {
                count++;
            } else {
                DFS(row + 1);
            }
        }
    }
}

Queen 2

Queen1 的時間是:// Time elapsed: 28423ms
這個方法的時間是:// Time elapsed: 16024ms

這里就需要開始介紹一個小竅門未桥,因為上面的方法,每次都要和之前的所有行相比較(看上面代碼的注釋部分)芥备,如果我們可以用一個 boolean 數(shù)組冬耿,記錄下每一列,每一個對角線的情況萌壳,就可以不用所有都比較了亦镶。那么如何求行和列呢

Paste_Image.png

如上圖,一共有 2n - 1 個對角線袱瓮,大神是語言研究所的博士生缤骨,他用的是中文的“豎”,“撇”和“捺”來表示兩種對角線尺借,可以很容易知道它們的 index 就是 0 -> 2n - 2绊起。所以上面的代碼注釋部分就可以改成:

int j = col + row, k = n - 1 + col - row;
if (shu[col] || pie[j] || na[k]) continue;

就是如果當(dāng)前列和對角線都被占用的話淌哟,就直接 continue侍瑟。當(dāng)然我們還需要設(shè)置清除。把已經(jīng)被占用的設(shè)為 true,一趟完事了就重新設(shè)為 false瘫里。

shu[col] = true; pie[j] = true; na[k] = true;
DFS(row + 1);
shu[col] = false; pie[j] = false; na[k] = false;

Queen 3

Queen1 :// Time elapsed: 28423ms
Queen2 :// Time elapsed: 16024ms
這個方法:// Time elapsed: 10302ms

從這個方法開始引入 bit array,關(guān)鍵點是:

用 32 位的 bit array(也就是一個整數(shù)(int)的長度)荡碾,代替 32 位長度的 boolean 谨读。

位運算的基本操作是:與,或坛吁,異或劳殖,取反,左移拨脉,右移哆姻。很容易理解,這里瞬間就節(jié)省了超多的空間玫膀。也很容易就想到這里并不會節(jié)省時間很多時間矛缨,因為整個的流程是沒什么太大的區(qū)別的。所以上面的 boolean[] shu, pie, na; 就成了 int shu, pie, na;帖旨,默認為 0箕昭。上面的判斷條件也成了

if ((((shu >> col) | (pie >> (col + row)) | (na >> (n - 1 + col - row))) & 1) != 0) continue;

這個乍一看有點復(fù)雜,先看一下這個 bit 是如何模擬數(shù)組操作的解阅,其實就是讀和寫:

- 寫
把第 i 位置1:a |= (1 << i)
把第 i 位置0: a &= ~(1 << i)
把第 i 位取反:a ^= (1 << i)
- 讀
讀取第 i 位的值:(a >> i)&1

所以上面那個條件其實就是取第col位的值 (shu >> col) & 1落竹,每個都是這樣,所以就先把三個都|或起來货抄,再和 1 &與述召。操作是等價的朱转,存儲空間不同而已。后面的設(shè)置與清除也成了:

shu ^= (1 << col); pie ^= (1 << (row + col)); na ^= (1 << (n - 1 + col - row));

就是把shu的第col位置1积暖,本來是0肋拔,取反就成了1。用異或的好處就是清除和設(shè)置的代碼相同呀酸,更方便凉蜂。

Queen 4

Queen1 :// Time elapsed: 28423ms
Queen2 :// Time elapsed: 16024ms
Queen3 :// Time elapsed: 10302ms
這個方法:// Time elapsed: 1962ms

可以從上面這個時間比較看出來瞬間少了個數(shù)位。因為這一方法用到了系統(tǒng)級別的運算性誉,利用了 bit array 可以一步操作多位的優(yōu)勢窿吩,不像一般的 array,必須要一個一個操作错览,而 bit array 可以在一個整形的長度內(nèi)纫雁,O(1) 時間任意存取任意位置的數(shù)。為了更好的理解這個方法倾哺,這里我們需要介紹另一個 bit 的操作轧邪,取最后一個 1:

a & -a

懂負數(shù)的原理就很容易理解了,負數(shù)就是取反+1羞海。例子:

a = 0001100
-a = 1110011 + 1 = 1110100
a & -a = 0000100

枚舉 bit array 中的1忌愚,就是:

while(a != 0) {
    int p = a & -a; // p 就是取出來的 1
    a ^= p;
    Do something with p; 
}

之前是枚舉每個位置,然后檢查是否沖突却邓,而現(xiàn)在我們可以利用這點硕糊,直接枚舉不沖突的位置。那么在第 row 行腊徙,相應(yīng)的shu简十,pie,na中沖突的位置在哪里呢撬腾?

shu 沖突的位置:shu
pie 沖突的位置:pie >> row
na 沖突的位置:na >> (n - 1 - row)

就是之前式子 pie[row + col], na[n - 1 + col - row] 的一個變形螟蝙,用 bit array 來代替普通的 boolean array。所以現(xiàn)在這個 DFS 方法就成了:

private static void DFS(int row) {
        int ok = ((1 << n) - 1) & ~(shu | (pie >> row) | (na >> (n - 1 - row)));
        while (ok != 0) {
            int p = ok & -ok;
            ok ^= p;
            if (row == n - 1) {
                count++;
            } else {
                shu ^= p; pie ^= (p << row); na ^= (p << (n - 1 - row));
                DFS(row + 1);
                shu ^= p; pie ^= (p << row); na ^= (p << (n - 1 - row));
            }
        }
    }

Queen 5

Queen1 :// Time elapsed: 28423ms
Queen2 :// Time elapsed: 16024ms
Queen3 :// Time elapsed: 10302ms
Queen4 :// Time elapsed: 1962ms
這個方法:// Time elapsed: 1350ms

為了更好的理解這個方法民傻,這里有個 google developer 的文檔胰默,講的是 Propagation and backtracking。所謂的 Propagation饰潜,就是:

Propagation consists of taking some constraint—which might be a constraint of the original problem, or a constraint learned or hypothesized along the way—and applying it to variables.

利用之前學(xué)到的初坠,來限制接下來的。

這個方法中彭雾,我們把 shu碟刺,pie,na 都當(dāng)成形參薯酝,把當(dāng)前行學(xué)到的“不能放”的限制信息半沽,保留到下一行爽柒,從而限制下一行的決策。這也就是 Propagation 的意思者填。所以我們的代碼也就成了

public class Main {
    private static final int n = 15;
    private static int count = 0;
    
    public static void main(String[] args) {
        long tic = System.currentTimeMillis();
        DFS(0, 0, 0, 0);
        long toc = System.currentTimeMillis();
        System.out.println("Total solutions: " + count);
        System.out.println("Time elapsed: " + (toc - tic) + "ms");
    }
    
    private static void DFS(int row, int shu, int pie, int na) {
        // shu, pie, na 當(dāng)前行的位置不能放
        int ok = ((1 << n) - 1) & ~(shu | pie | na);
        while (ok != 0) {
            int p = ok & -ok;
            ok ^= p;
            if (row == n - 1) {
                count++;
            } else {
                // 把當(dāng)前行的 shu浩村,pie,na 設(shè)為 0占哟,從而之后的所有行都不能放
                DFS(row + 1, shu ^ p, (pie ^ p) >> 1, (na ^ p) << 1);
            }
        }
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末心墅,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子榨乎,更是在濱河造成了極大的恐慌怎燥,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,607評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蜜暑,死亡現(xiàn)場離奇詭異铐姚,居然都是意外死亡,警方通過查閱死者的電腦和手機肛捍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,239評論 3 395
  • 文/潘曉璐 我一進店門隐绵,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人拙毫,你說我怎么就攤上這事依许。” “怎么了恬偷?”我有些...
    開封第一講書人閱讀 164,960評論 0 355
  • 文/不壞的土叔 我叫張陵悍手,是天一觀的道長。 經(jīng)常有香客問我袍患,道長,這世上最難降的妖魔是什么竣付? 我笑而不...
    開封第一講書人閱讀 58,750評論 1 294
  • 正文 為了忘掉前任诡延,我火速辦了婚禮,結(jié)果婚禮上古胆,老公的妹妹穿的比我還像新娘肆良。我一直安慰自己,他們只是感情好逸绎,可當(dāng)我...
    茶點故事閱讀 67,764評論 6 392
  • 文/花漫 我一把揭開白布惹恃。 她就那樣靜靜地躺著,像睡著了一般棺牧。 火紅的嫁衣襯著肌膚如雪巫糙。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,604評論 1 305
  • 那天颊乘,我揣著相機與錄音参淹,去河邊找鬼醉锄。 笑死,一個胖子當(dāng)著我的面吹牛浙值,可吹牛的內(nèi)容都是我干的恳不。 我是一名探鬼主播,決...
    沈念sama閱讀 40,347評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼开呐,長吁一口氣:“原來是場噩夢啊……” “哼烟勋!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起筐付,我...
    開封第一講書人閱讀 39,253評論 0 276
  • 序言:老撾萬榮一對情侶失蹤神妹,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后家妆,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體鸵荠,經(jīng)...
    沈念sama閱讀 45,702評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,893評論 3 336
  • 正文 我和宋清朗相戀三年伤极,在試婚紗的時候發(fā)現(xiàn)自己被綠了蛹找。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,015評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡哨坪,死狀恐怖庸疾,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情当编,我是刑警寧澤届慈,帶...
    沈念sama閱讀 35,734評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站忿偷,受9級特大地震影響金顿,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜鲤桥,卻給世界環(huán)境...
    茶點故事閱讀 41,352評論 3 330
  • 文/蒙蒙 一揍拆、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧茶凳,春花似錦嫂拴、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,934評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至箱沦,卻和暖如春辩恼,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,052評論 1 270
  • 我被黑心中介騙來泰國打工运挫, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留状共,地道東北人。 一個月前我還...
    沈念sama閱讀 48,216評論 3 371
  • 正文 我出身青樓谁帕,卻偏偏與公主長得像峡继,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子匈挖,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,969評論 2 355

推薦閱讀更多精彩內(nèi)容