面試算法題總結——動態規劃小結2

前言

動態規劃是是我目前覺得遇見的最難的題目了。說起來到處都有他,但是真正用的時候,如果變個形狀,又很難想到動態方程。所以,目前的動態規劃,還是要多總結,找到思路。
拋磚迎玉,先來說個簡單的:上臺階問題。現在有一層樓梯,共n層臺階。每次上臺階,可以上1層或者2層,問一共有多少層走完樓梯的方案。

入門

這個題應該都很熟悉,很明顯這個是斐波那契數列。怎麼推算出來,這是個斐波那契數列的呢?

假設我站在第k層臺階上面,要到達第k層臺階,可以由第k-1層臺階,走一步,和第k-2層臺階,走兩步,到達第k層。

在思考的過程中,可能最搞不明白的就是,爲什麼不考慮第k-3層,或者其他k-n層的臺階,走3層,或者走4層來到達第k層。(最長迴文子串也會這樣想,考慮k的時候,是否考慮string[i-1]和string[j-1])。
原因是因爲第k-1層臺階,和第k-2層臺階,已經考慮到了k-3層,和k-4層。(其中,k-1層考慮到了k-2和k-3層,原因就在於只能走1步和2步,但是k-1考慮到的k-2只是從 k-2 走 一步 到k-1這一情況)
這種狀態的轉移很饒人,就跟剛學一個知識點一樣,很不清楚。習慣就好了,想真正熟悉,估計只有多用,不斷重複了。

進階

下面說一下這個題目的進階,現在我能走的不是隻有1層或2層,而是能走a[1],a[2]…a[n]層,怎麼說?難道a[k] = a[1]+a[2]+…+a[k-1];沒錯,就是這樣!

for (int i = 0; i < n; i++) {
	int ans = 0;
	for (int j = a[i]; j <= k; j++) {
		ans = ans + dp[j - a[i]];
    }
	dp[i] = ans;
}
return dp[k];

從這裏面應該就能明白,爲什麼說菲波拉契數列是最簡單的動態規劃了。

進階之揹包

ok,先放下這個題,來看看另一個題,就是動態規劃的經典——揹包問題(關於揹包可以看之前的博客總結筆試中揹包問題的應用)——先來看看多重揹包的場景:被告容量爲V,一共有n中物品,每個價值爲a[i],體積爲b[i],問揹包最大的能裝多少(物品可以裝多次)。動態轉移方程如下:


for(int i=0;i<n;i++){
   for(int j=a[i];j<=V;j++){
        dp[j] = Math.max(dp[j],d[j-b[i]+a[i]]);
   }
}
return dp[V];

注意,這裏每個物品可以裝多次。爲什麼不採用貪心?貪心採用性價比最高的,從大到小排,從大往小裝(當然不對了,這樣裝,無法保證全局最優)。
如果是遞歸呢?每個揹包選或者不選,有2^n情況,顯然複雜度太大了。

重點來了

上面的揹包得到的的是能裝的最大價值,而無法拿到達到最大價值有多少種方案。
那麼,要求出多少方案怎麼做呢?
是不是可以這樣想,把走臺階看成揹包容量,這樣,狀態方程就和上面走臺階一樣了。
來個例子:
有兩種硬幣,第一種可以無限使用,第二種每個只能使用一次.。第一種硬幣爲n個,價值分別任a[0],a[1],a[2]…a[n-1],第二種硬幣爲m個,價值分別爲b[0],b[1]…b[n-1],求組合成價值爲k的硬幣數量

for(int i=0;i<n;i++){
	for(int j=a[i];j<=k;j++{
		dp1[j] = dp1[j]+dp1[j-a[i]];
	}
}
for(int i=0;i<m;i++){
	for(int j=k;j>=a[i];j--){
		dp2[j] = dp2[j-a[i]]+dp2[j];
	}
}
for(int i=1;i<=k;i++){
	ans[k] += dp1[i]*dp[k-i];
}
return ans[k];

如果要求打印所有路徑呢?這就必須要用到dfs了。

高級進階

還是來給個例題。leetcode上面
單詞接龍 II
這個題,是單詞接龍 I的變形。
這個題目其實與動態規劃可以說有關,也可以說是無關(爲什麼,等會說明),也可以當做一個搜索題,放在這裏是爲了把相似的題目放在一起,便於總結。
首先,需要基於單詞接龍 I得到最短的路徑的單詞個數(兩種方法,1.bfs。2.動態規劃),其次,需要基於搜索(寬搜或者廣搜都可以,網上面的教程大多是基於廣搜,所以樓主等會給一下寬搜的代碼)。
這裏暫時先來討論如何得到最短路徑的單詞數。寬搜就不說了。那麼動態規劃怎麼做呢?
不知道大家時候還記得最短路算法。最短路算法中,有一個很有代表性的算法,bellman_ford算法(dijkstra基於貪心),其中,基於bellman_ford算法的優化版本,也可以說是bellman_ford和dijkstra的結合版本,SPFA(中國人發明的,挺牛的),就可以運用到這個題目中。
怎麼看成是最短路呢?題目要求,只能相差一個字母,所以,可以把,相差一個字母的兩個字符串之間的距離,看做1,相差不是1的,可以把其看成是無限遠,然後利用這個1去更新其他的距離。
具體代碼就不給了,這個不難,只要理解了SPFA的概念,這個題目的答案就很好寫出來。
下面再來看 單詞接龍 II 的解法。
這個網上大部分都是基於dfs,樓主這裏給出bfs。需要說明的是,這個題目有點卡數據。採用bfs時,如果從beginWord開始搜索,會超時,但是,從endWord開始搜索,就可以通過,注意這一點就行了。
一下是AC代碼:


    class T {
        int x;//記錄這個單詞的位置
        String ts;//記錄單詞
        List<String> list;//記錄走到當前單詞,路徑上的所有單詞

        public T(int x, String ts, List<String> list) {
            this.x = x;
            this.ts = ts;
            this.list = list;
        }
    }

    /**
     * 記錄下單詞是否只差一個,注意,必須差一個,兩個單詞完全相同也不可以
     * 如果兩個單詞完全相同,相當於重複了,當做一個單詞算
     */
    boolean isT(String ts, String ts2) {
        int s = 0;
        for (int i = 0; i < ts.length(); i++) {
            if (ts.charAt(i) != ts2.charAt(i)) {
                s++;
            }
            if (s > 1) {
                return false;
            }
        }
        return s == 1;
    }

    class Arr {//記錄鄰接矩陣(即Arr[i].slist代表與第i個單詞相差一個字母的所有單詞)
        List<String> slist;//
        List<Integer> ilist;//對應單詞的相對位置

        public Arr(List<String> slist, List<Integer> ilist) {
            this.slist = slist;
            this.ilist = ilist;
        }
    }

    public List<List<String>> findLadders(String beginWord, String endWord, List<String> wordList) {
        if (beginWord == null || beginWord.length() == 0 || endWord == null || endWord.length() == 0)
            return null;
        int len = wordList.size();
        Arr[] listArrArr = new Arr[len + 1];
        //構建動態表Arr[len+1]
        for (int i = 0; i < len; i++) {
            listArrArr[i] = new Arr(new ArrayList<String>(), new ArrayList<Integer>());
            for (int j = 0; j < len; j++) {
                if (j != i && isT(wordList.get(i), wordList.get(j))) {
                    listArrArr[i].slist.add(wordList.get(j));
                    listArrArr[i].ilist.add(j);
                }
            }
        }
        //把beginWord,當做第len個單詞
        listArrArr[len] = new Arr(new ArrayList<String>(), new ArrayList<Integer>());
        for (int i = 0; i < len; i++) {
            if (isT(beginWord, wordList.get(i))) {
                listArrArr[len].slist.add(wordList.get(i));
                listArrArr[len].ilist.add(i);
            }
        }
        //這個單詞二維list,記錄沒有過前的所有答案()
        List<List<String>> preList = new ArrayList<>();
        int min = Integer.MAX_VALUE;//記錄最短路徑的單詞數
        //可能這裏有疑問,既然已經記錄了最短路徑單詞數,爲什麼還要過濾preList呢
        //原因在於,可能到達最短路徑的時候,"下一層"的單詞已經放如隊列中了
        Queue<T> queue = new LinkedList<>();
        ArrayList<String> tsi = new ArrayList<>();
        tsi.add(endWord);

        int anspos = wordList.indexOf(endWord);//記錄endWord出現的位置
        if (anspos != -1)//說明能達到beginWord
            queue.add(new T(anspos, endWord, tsi));
        Set<String> set = new HashSet<>();//這裏Set,用來記錄所有已經走過的路
        /*
        舉個例子,比如答案是
        [tad ted red rex],[tad,red,
         */
        boolean isGetAns = false;
        while (!queue.isEmpty()) {
            T t = queue.poll();
            set.add(t.ts);//注意,只有到了這裏,才能加入set。以後如果還能通過這個t.ts變換,也不是最優解,
            // 所以通過這裏,讓以後不能再使用這個單詞。
            if (listArrArr[len].ilist.contains(t.x)) {//最終答案鄰接表是否包含,包含說明是一個解。
                t.list.add(beginWord);
                min = Math.min(t.list.size(), min);
                if (!preList.contains(t.list))//可能有重複的,重複的去掉
                    preList.add(new ArrayList<>(t.list));
                isGetAns = true;//得到最優解之後,不再往queue裏面添加任何東西。
                continue;
            }
            if (!isGetAns) {
                for (int i = 0; i < listArrArr[t.x].slist.size(); i++) {
                    //這是其中很大的一個優化,就是把能到達的單詞預存到這個數組中。而不是採用遍歷所有的數組的方式
                    if (!set.contains(listArrArr[t.x].slist.get(i))) {
                        List<String> sArr = new ArrayList<>(t.list);
                        sArr.add(listArrArr[t.x].slist.get(i));
                        //set.add(listArrArr[t.x].slist.get(i));不能在這裏添加入set
                        queue.add(new T(listArrArr[t.x].ilist.get(i), listArrArr[t.x].slist.get(i), sArr));
                    }
                }
            }
        }
        //由於是從後往前搜索,所以最後順序需要倒過來
        List<List<String>> ansLL = new ArrayList<>();
        for (int i = 0; i < preList.size(); i++) {
            if (preList.get(i).size() == min) {
                ArrayList<String> tsList = new ArrayList<>();
                int jlen = preList.get(i).size();
                for (int j = 0; j < preList.get(i).size(); j++) {
                    tsList.add(preList.get(i).get(jlen - j - 1));
                }
                ansLL.add(tsList);
            }
        }
        return ansLL;

    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章