LeetCode 127: Word Ladder

tags: String, BFS


Given two words(beginWord and endWord), and a dictionary's word list, find the length of shortest transformation sequence from beginWord to endWord, such that:

  1. Only one letter can be changed at a time.
  2. Each intermediate word must exist in the word list.

Note:

  • Return 0 if there is no such transformation sequence.
  • All words have the same length.
  • All words contain only lowercase alphabetic characters.

Function:

public int ladderLength(String beginWord, String endWord, Set<String> wordList) {
    // Your Code
}

題目分析

該題目是一道基于String旁理、LeetCode Medium的題目亥鸠。顯然可以用BFS算法求解:以beginWord為起點硬耍,以beginWord的下一跳為目標廣度優(yōu)先搜索wordList饺饭,并將符合條件的結(jié)果放入新的結(jié)果集中,然后對其進行新一輪的廣度優(yōu)先搜索,最終看是否能達到endWord聊疲。

下面會看到3個解法,分別是自己失敗且丑陋的解法沪悲、常見的解法以及高效的解法获洲。

算法1:自己的解法

public int ladderLength(String beginWord, String endWord, Set<String> wordList) {

    int result = 1;
    Set<String> wordListCopy = new HashSet<>(); // wordList的Clone,不影響原數(shù)據(jù)集的內(nèi)容
    wordListCopy.addAll(wordList);
    wordListCopy.add(endWord);
    ArrayList<String> curAL = new ArrayList<>(); // BFS的隊列
    curAL.add(beginWord);
    ArrayList<String> otherAL = new ArrayList<>(); // 存儲新加入的元素
    Set<String> remove = new HashSet<>(); // 3.wordListCopy中應(yīng)該刪除的元素集合

    while (!curAL.contains(endWord)) {
        otherAL.clear();
        for (String temp : curAL) {
            for (String s : wordListCopy) { // 判斷集合剩余的元素與隊列元素是否相鄰
                if (isNeighbor(temp, s)) { // 1.調(diào)用判斷neighbor方法殿如,TLE的關(guān)鍵
                    otherAL.add(s);
                    remove.add(s);
                }
            }
            remove.forEach(wordListCopy::remove); // 2.將新獲取的元素在wordListCopy中刪除
            remove.clear();
        }
        ArrayList<String> temp = curAL; // 4.我也不懂自己在做什么
        curAL = otherAL;
        otherAL = temp;
        result++; // 計算距離
        if (curAL.size() == 0)
            return 0;
    }
    return result;
}

public boolean isNeighbor(String s1, String s2) {
    int len = s1.length();
    char[] ch1 = s1.toCharArray();
    char[] ch2 = s2.toCharArray();
    int diff = 0;
    for (int i=0; i<len&&diff<2; i++) { // 逐個字符的判斷兩者的距離
        if (ch1[i]!=ch2[i]) {
            diff++;
        }
    }
    return diff <= 1;
}

雖然是幾個月之前寫的算法贡珊,但看上去還是那么親切,真是熱淚盈眶握截,簡直丑爆了飞崖。根據(jù)嚴重程度烂叔,依次分析代碼為什么TLE谨胞,為什么寫得這么丑陋。

  1. 判斷相鄰字符串是String類型的常見問題蒜鸡,開始看到他人代碼中對字符串的每一個字符遍歷26個字母感到不解胯努,認為自己的isNeighbor方法效率會更高,然而在嘗試將他人代碼的該模塊改成自己的isNeighbor時逢防,TLE叶沛。經(jīng)過仔細考慮,自己的想法太naive了:假設(shè)候選隊列中有k個字符串忘朝,wordList剩有c個字符串灰署,每個字符串的長度為l,字符遍歷的復(fù)雜度為k*l*26局嘁,而自己方法的復(fù)雜度為k*c*l溉箕。很顯然,后者比前者多了一個系數(shù)c悦昵,而且很可能c>>26肴茄,所以后者效率遠不及前者,造成TLE但指。
  2. 這一塊的代碼寫得很丑陋寡痰,原因是開始我嘗試在foreach循環(huán)中直接刪除wordListCopy中被訪問到的元素,但會拋出Concurrent ModificationException的異常棋凳。在當時編寫代碼時并不知道如何解決這個問題拦坠,所以改寫的這么丑陋。最近復(fù)習(xí)Java集合的過程中找到了問題的根源:集合的迭代器在遍歷過程中不允許修改集合信息剩岳,否則會拋出上述異常贞滨。下面兩個算法都給出了相對優(yōu)雅的解決方案。
  3. remove集合其實和curAL保持實時一致卢肃,所以完全沒必要申請這樣一個集合做莫名其妙的記錄疲迂。
  4. 這個地方我也無力吐槽了才顿。。尤蒿。

算法2:常見的解法

public int ladderLength(String beginWord, String endWord, Set<String> wordList) {
    Set<String> reached = new HashSet<>();
    reached.add(beginWord);
    wordList.add(endWord);
    int distance = 1;
    while(!reached.contains(endWord)) {
        Set<String> toAdd = new HashSet<>();
        for(String each : reached) {
            for (int i = 0; i < each.length(); i++) {
                char[] chars = each.toCharArray();
                for (char ch = 'a'; ch <= 'z'; ch++) {
                    chars[i] = ch;
                    String word = new String(chars);
                    if(wordList.contains(word)) {
                        toAdd.add(word);
                        wordList.remove(word);
                    }
                }
            }
        }
        distance ++;
        if(toAdd.size() == 0) return 0;
        reached = toAdd;
    }
    return distance;
}

該解法是BFS算法的經(jīng)典實現(xiàn)郑气,簡單優(yōu)雅、略帶粗暴腰池。

  1. 簡單優(yōu)雅:算法中使用2個集合實現(xiàn)了當前隊列和候選隊列尾组,對相鄰字符串的求解使用字符遍歷的方法保證了效率,同時由于字符遍歷的方法不需要wordList的迭代器示弓,因此可以在循環(huán)中調(diào)用remove方法讳侨。
  2. 略帶粗暴:調(diào)用wordListremove方法會對傳入?yún)?shù)的內(nèi)容產(chǎn)生修改,可能造成非預(yù)期的結(jié)果奏属。算法3提供了另一種"刪除元素的思路"跨跨。

算法3:高效的解法

public int wordLadder(String beginWord, String endWord, Set<String> wordList) {
    Set<String> beginSet = new HashSet<>();
    Set<String> endSet = new HashSet<>();

    int len = 1;
    HashSet<String> visited = new HashSet<>();

    beginSet.add(beginWord);
    endSet.add(endWord);
    while (!beginSet.isEmpty() && !endSet.isEmpty()) {
        if (beginSet.size() > endSet.size()) {
            Set<String> set = beginSet;
            beginSet = endSet;
            endSet = set;
        }

        Set<String> temp = new HashSet<>();
        for (String word : beginSet) {
            char[] chs = word.toCharArray();

            for (int i=0; i<chs.length; i++) {
                for (char c='a'; c<='z'; c++) {
                    char old = chs[i];
                    chs[i] = c;
                    String target = String.valueOf(chs);

                    if (endSet.contains(target)) {
                        return len + 1;
                    }

                    if (!visited.contains(target) && wordList.contains(target)) {
                        temp.add(target);
                        visited.add(target);
                    }
                    chs[i] = old;
                }
            }
        }
        beginSet = temp;
        len++;
    }
    return 0;
}

該解法的主要思想是從兩端進行廣度優(yōu)先搜索,也即維護兩個當前隊列囱皿,每次遍歷較小的隊列勇婴,查找是否在另一個隊列(找到路徑)、是否在剩余的wordList中(形成新的候選隊列)嘱腥。雖然我不知道這種思路的效率有沒有定理支持耕渴,但直觀的感受是算法2中的思想類似于單點的水波需要擴很大才能到達目標點橱脸,而算法3在起點和終點都產(chǎn)生水波,這樣可以擴較小的范圍就碰到彼此,也即找到一條可達路徑艾帐。假設(shè)wordList字符串的個數(shù)總共有n個,字符串的長度為l,則該算法的最壞情況就是遍歷所有的字符串仍沒有找到路徑今野,此時的時間復(fù)雜度為O(n*l)条霜,超過Java AC的90%。

總結(jié)

該題目難度不大旋圆,自己的實現(xiàn)卻讓人哭笑不得或南,需要從以下幾點加強自己的編碼訓(xùn)練:

  • 字符串相關(guān)算法;
  • 對常見算法——如BFS——的實現(xiàn)應(yīng)倒背如流;
  • 努力編寫簡單優(yōu)雅的代碼。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末要门,一起剝皮案震驚了整個濱河市封豪,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,734評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異居暖,居然都是意外死亡顽频,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,931評論 3 394
  • 文/潘曉璐 我一進店門太闺,熙熙樓的掌柜王于貴愁眉苦臉地迎上來糯景,“玉大人,你說我怎么就攤上這事省骂◇盎矗” “怎么了?”我有些...
    開封第一講書人閱讀 164,133評論 0 354
  • 文/不壞的土叔 我叫張陵钞澳,是天一觀的道長怠惶。 經(jīng)常有香客問我,道長轧粟,這世上最難降的妖魔是什么策治? 我笑而不...
    開封第一講書人閱讀 58,532評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮兰吟,結(jié)果婚禮上通惫,老公的妹妹穿的比我還像新娘。我一直安慰自己混蔼,他們只是感情好履腋,可當我...
    茶點故事閱讀 67,585評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著惭嚣,像睡著了一般遵湖。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上料按,一...
    開封第一講書人閱讀 51,462評論 1 302
  • 那天奄侠,我揣著相機與錄音,去河邊找鬼载矿。 笑死垄潮,一個胖子當著我的面吹牛烹卒,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播弯洗,決...
    沈念sama閱讀 40,262評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼旅急,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了牡整?” 一聲冷哼從身側(cè)響起藐吮,我...
    開封第一講書人閱讀 39,153評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎逃贝,沒想到半個月后谣辞,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,587評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡沐扳,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,792評論 3 336
  • 正文 我和宋清朗相戀三年泥从,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片沪摄。...
    茶點故事閱讀 39,919評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡躯嫉,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出杨拐,到底是詐尸還是另有隱情祈餐,我是刑警寧澤,帶...
    沈念sama閱讀 35,635評論 5 345
  • 正文 年R本政府宣布哄陶,位于F島的核電站帆阳,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏奕筐。R本人自食惡果不足惜舱痘,卻給世界環(huán)境...
    茶點故事閱讀 41,237評論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望离赫。 院中可真熱鬧,春花似錦塌碌、人聲如沸渊胸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,855評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽翎猛。三九已至,卻和暖如春接剩,著一層夾襖步出監(jiān)牢的瞬間切厘,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,983評論 1 269
  • 我被黑心中介騙來泰國打工懊缺, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留疫稿,地道東北人。 一個月前我還...
    沈念sama閱讀 48,048評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像遗座,于是被迫代替她去往敵國和親舀凛。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,864評論 2 354

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