前言
動態規劃是是我目前覺得遇見的最難的題目了。說起來到處都有他,但是真正用的時候,如果變個形狀,又很難想到動態方程。所以,目前的動態規劃,還是要多總結,找到思路。
拋磚迎玉,先來說個簡單的:上臺階問題。現在有一層樓梯,共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;
}