Boggle

1. 問題描述

princeton_algs4第四周的編程作業(yè)Boggle论寨,一個(gè)拼字游戲。將棋盤上的字母按照限定的順序連接爽茴,如果得到給定字典中出現(xiàn)的單詞葬凳,則記分。

游戲說明

一個(gè)合法的單詞必須滿足以下條件:

  • 一個(gè)字母只能夠與它的上室奏、下火焰、左、右胧沫、左上昌简、右上、左下绒怨、右下8個(gè)方向上的字母相連纯赎。
  • 單詞中同一個(gè)位置的字母只能出現(xiàn)一次。
  • 單詞至少有三個(gè)字母南蹂。
  • 單詞必須存在于給定的字典中犬金。

我們要完成的任務(wù)就是:已知一個(gè)字典和一個(gè)棋盤(棋盤上每個(gè)位置的字母可以得到),找出該棋盤上的字母可以組成的所有合法單詞六剥。

2. 解決思路

2.1 對(duì)棋盤建模

我們考慮如何在棋盤上搜索單詞晚顷。既然每個(gè)單詞是沿著棋盤上的字母與其臨接的字母一個(gè)一個(gè)串起來(lái)的,那么我們可以把棋盤看成圖疗疟,棋盤上各位置的字母可以看成圖中的結(jié)點(diǎn)该默,從一個(gè)字母與其能達(dá)到的另一個(gè)字母之間建立一條邊,假設(shè)一個(gè)4*4大小的棋盤可以構(gòu)成如下的無(wú)向圖秃嗜。

根據(jù)棋盤構(gòu)成的圖

其實(shí)這里我們不用真正構(gòu)建一幅圖权均,我們的主要目的是以某個(gè)字母為起點(diǎn)時(shí),要很方便的得到它所有的臨接點(diǎn)來(lái)組成單詞锅锨,因此我們真正需要的是一個(gè)臨接表叽赊。這里我用了一個(gè)數(shù)組來(lái)存儲(chǔ)Bag類型的數(shù)據(jù),一個(gè)Bag存儲(chǔ)的是一個(gè)字母的所有臨接字母必搞。

rows = board.rows();
cols = board.cols();
adj = (Bag<Integer>[]) new Bag[rows*cols];  
        
for (int i = 0; i < rows; i++) {
    for (int j = 0; j < cols; j++) {
        int v = i*cols + j;
        adj[v] = new Bag<Integer>();
        if (checkWIsValid(i-1, j)) adj[v].add((i-1) * cols + j);
        if (checkWIsValid(i+1, j)) adj[v].add((i+1) * cols + j);
        if (checkWIsValid(i, j+1)) adj[v].add(i * cols + j+1);
        if (checkWIsValid(i, j-1)) adj[v].add(i * cols + j-1);
        if (checkWIsValid(i+1, j-1)) adj[v].add((i+1) * cols + j-1);
        if (checkWIsValid(i+1, j+1)) adj[v].add((i+1) * cols + j+1);
        if (checkWIsValid(i-1, j-1)) adj[v].add((i-1) * cols + j-1);
        if (checkWIsValid(i-1, j+1)) adj[v].add((i-1) * cols + j+1);
    }
}

2.2 改造深度優(yōu)先搜索

接下來(lái)我們可以使用深度優(yōu)先的方法在圖中搜索所有的合法單詞必指。首先判斷是否出現(xiàn)了合法單詞:當(dāng)前字符串長(zhǎng)度大于2,并且存在于與字典中恕洲。如果是合法單詞塔橡,就記錄下來(lái)并且繼續(xù)搜索梅割。這里有一個(gè)限制條件要注意:就是同一個(gè)位置的字母不能兩次出現(xiàn)在得到的字符串中。如果我們使用一般DFS中用一個(gè)boolean型數(shù)組marked[]來(lái)標(biāo)記當(dāng)前所遍歷到的結(jié)點(diǎn)時(shí)就會(huì)出現(xiàn)一個(gè)問題:原本marked標(biāo)記的是所有遍歷過的結(jié)點(diǎn)葛家,如果某個(gè)結(jié)點(diǎn)已經(jīng)遍歷過了户辞,則下次遍歷到它時(shí)就會(huì)跳過。而我們這里的需求是正在遍歷的結(jié)點(diǎn)們中不能重復(fù)出現(xiàn)同一個(gè)位置的結(jié)點(diǎn)癞谒。舉個(gè)例子:假如說 PINES 和 PIDS 都是合法的單詞底燎。我們沿著P----I----N----E----S一路遍歷下來(lái),得到了PINES這個(gè)合法單詞弹砚,這些正在被遍歷的點(diǎn)都應(yīng)該被標(biāo)記双仍,當(dāng)我們繼續(xù)遍歷到P——I——D時(shí),之前遍歷過的N桌吃,E朱沃,S結(jié)點(diǎn)這時(shí)候應(yīng)該被取消標(biāo)記,不然我們就遍歷不到S結(jié)點(diǎn)茅诱,就無(wú)法找出合法的單詞PIDS了逗物。改造后的marked[]與之前的區(qū)別就是,之前marked就是負(fù)責(zé)把所有遍歷過的點(diǎn)標(biāo)記出來(lái)瑟俭,而現(xiàn)在我們只標(biāo)記所有存在于當(dāng)前搜索出的路徑上的結(jié)點(diǎn)敬察,一旦某個(gè)節(jié)點(diǎn)不在當(dāng)前遍歷的路徑上時(shí),則立即取消標(biāo)記尔当。于是我的做法是除了之前的數(shù)組marked[]莲祸,新加了一個(gè)數(shù)據(jù)Stack<Integer> visitingDices用來(lái)記錄當(dāng)前路徑遍歷到的結(jié)點(diǎn)。一旦完成某個(gè)結(jié)點(diǎn)的遍歷椭迎,則立刻從棧頂彈出該結(jié)點(diǎn)锐帜,并取消相應(yīng)標(biāo)記。

private void searchValidWords(int v, Node x, String str, Stack<Integer> visitingDices) {
    if (str.length() > 2 && x != null && x.val == 1) {
        validWords.add(str);
    }   
    for (int w : adj[v]) {  
        char c = getLetterOnBoard(w);
        if (!marked[w] && x != null && x.next[c -'A'] != null) {
            visitingDices.push(w);
            marked[w] = true;
            if (c == 'Q') { 
                searchValidWords(w, x.next['Q'-'A'].next['U' - 'A'], str+"QU", visitingDices);
            }                   
            else {
                searchValidWords(w, x.next[c -'A'], str+c, visitingDices);
            }
            int index = visitingDices.pop();
            marked[index] = false;          
        } 
    }
}

2.3 對(duì)字典建模

最后還有一個(gè)大問題要解決:如何快速地在字典中查找一個(gè)單詞是否存在與該字典中畜号。由于這周主要講的數(shù)據(jù)結(jié)構(gòu)是前綴樹Trie,所以毫無(wú)疑問缴阎,我們應(yīng)該將字典存儲(chǔ)在Trie中。其實(shí)也可以這樣理解:我們用給定的字典構(gòu)建出一棵前綴樹(也叫字典樹)后简软,棋盤上以某個(gè)字母為起點(diǎn)用DFS遍歷到的字母順序蛮拔,就是在字典樹中前進(jìn)的方向,如果前進(jìn)到某個(gè)結(jié)點(diǎn)為空痹升,則說明在字典中不存在以DFS遍歷到的字符串為前綴的單詞建炫,因此對(duì)于棋盤來(lái)說也就沒有必要在當(dāng)前結(jié)點(diǎn)繼續(xù)用DFS搜索下去了。

在字典樹的查找

道理大概就是這樣疼蛾。我最開始直接調(diào)用了algs4包中的APITrieST.java(R-way tries)TST.java(ternary search tries)肛跌,不幸的是都掛掉了,這次的作業(yè)對(duì)時(shí)間復(fù)雜度卡的很嚴(yán)。于是乎發(fā)現(xiàn)有一個(gè)地方比較耗時(shí):原先我在每次用DFS搜索時(shí)衍慎,首先判斷字典中是否出現(xiàn)了合法單詞時(shí)都是直接調(diào)用keysWithPrefix函數(shù)并且判斷它的value不為空转唉,然而這個(gè)操作每次都從樹根開始查找,這就好比你第一遍DFS時(shí)得到字符串"P",然后在字典樹中從Root開始查找稳捆,存在鍵值為P的結(jié)點(diǎn)赠法,好的繼續(xù)第二遍DFS得到字符串"PI",字典樹又從Root開始找起,先找到P乔夯,再?gòu)腜的子結(jié)點(diǎn)中找到鍵值為I的子結(jié)點(diǎn)期虾。到第三遍在字典樹中查找時(shí),繼續(xù)從Root查找驯嘱,因此這個(gè)步驟存在很多冗余操作。解決方法是DFS時(shí)喳坠,記錄下當(dāng)前遍歷到的樹中的結(jié)點(diǎn)鞠评。所以你可以看到在上面查找合法單詞的函數(shù)中,有一個(gè)參數(shù)Node x 就是在記錄字典樹中當(dāng)前遍歷到的結(jié)點(diǎn)壕鹉。
字典樹的構(gòu)建代碼如下:

public BoggleSolver(String[] dictionary) {
    root = new Node();
    for (int i = 0; i < dictionary.length; i++) {
        put(dictionary[i]);
    }
}
    
private static class Node {
    private int val = 0;
    private Node[] next = new Node[26];
}
    
private int get(String key) {
    Node x = get(root, key, 0);
    if (x == null) return 0;
    return x.val;
}
        
private Node get(Node x, String key, int d) {
    if (x == null) return null;
    if (d == key.length()) return x;
    int c = key.charAt(d) - 'A';
    return get(x.next[c], key, d+1);
        }
    
        private void put(String key) {
            root = put(root, key, 0);
}
    
private Node put(Node x, String key, int d) {
    if (x == null) x = new Node();
    if (d == key.length()) {
        x.val = 1;
            return x;
        }
        int c = key.charAt(d) - 'A';
        x.next[c] = put(x.next[c], key, d+1);
        return x;
    }

3. 代碼

查看完整代碼戳這里

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末剃幌,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子晾浴,更是在濱河造成了極大的恐慌负乡,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,602評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件脊凰,死亡現(xiàn)場(chǎng)離奇詭異抖棘,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)狸涌,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,442評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門切省,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人帕胆,你說我怎么就攤上這事朝捆。” “怎么了懒豹?”我有些...
    開封第一講書人閱讀 152,878評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵芙盘,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我脸秽,道長(zhǎng)儒老,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,306評(píng)論 1 279
  • 正文 為了忘掉前任记餐,我火速辦了婚禮贷盲,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己巩剖,他們只是感情好铝穷,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,330評(píng)論 5 373
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著佳魔,像睡著了一般曙聂。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上鞠鲜,一...
    開封第一講書人閱讀 49,071評(píng)論 1 285
  • 那天宁脊,我揣著相機(jī)與錄音,去河邊找鬼贤姆。 笑死榆苞,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的霞捡。 我是一名探鬼主播坐漏,決...
    沈念sama閱讀 38,382評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼碧信!你這毒婦竟也來(lái)了赊琳?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,006評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤砰碴,失蹤者是張志新(化名)和其女友劉穎躏筏,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體呈枉,經(jīng)...
    沈念sama閱讀 43,512評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡趁尼,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,965評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了猖辫。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片弱卡。...
    茶點(diǎn)故事閱讀 38,094評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖住册,靈堂內(nèi)的尸體忽然破棺而出婶博,到底是詐尸還是另有隱情,我是刑警寧澤荧飞,帶...
    沈念sama閱讀 33,732評(píng)論 4 323
  • 正文 年R本政府宣布凡人,位于F島的核電站,受9級(jí)特大地震影響叹阔,放射性物質(zhì)發(fā)生泄漏挠轴。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,283評(píng)論 3 307
  • 文/蒙蒙 一耳幢、第九天 我趴在偏房一處隱蔽的房頂上張望岸晦。 院中可真熱鬧欧啤,春花似錦、人聲如沸启上。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,286評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)冈在。三九已至倒慧,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間包券,已是汗流浹背纫谅。 一陣腳步聲響...
    開封第一講書人閱讀 31,512評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留溅固,地道東北人付秕。 一個(gè)月前我還...
    沈念sama閱讀 45,536評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像侍郭,于是被迫代替她去往敵國(guó)和親询吴。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,828評(píng)論 2 345

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