面试算法题总结——动态规划小结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;

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