肝了好多天-動態(tài)規(guī)劃十連-超細膩解析|刷題打卡

No.1

最簡單的動態(tài)規(guī)劃題目

侄女5歲現(xiàn)在開始學習加減法了,每次做數(shù)學都離不開她的小手指,超過5的就開始數(shù)另一個手的手指,真是汗顏啊

有一天,我問她

“1+1+1+1+1等于多少?”

“搬起小拇指,就開始數(shù)了,5!”

“那么再加一個1呢?”

“當然是6了” -脫口而出

“這次你怎么算這么快的?”

“剛剛不是5嗎,加1就是6了啊”

“所以你不需要重新計算,因為你記得之前的答案是5!動態(tài)規(guī)劃就是說:記住之前的東西可以節(jié)省時間”

玩歸玩,鬧歸鬧,別拿dp開玩笑!\color{red}{玩歸玩,鬧歸鬧,別拿dp開玩笑!}玩歸玩,鬧歸鬧,別拿dp開玩笑!

來瞅一眼科普中國科學百科的詞條解釋

動態(tài)規(guī)劃(Dynamic Programming,DP)是運籌學的一個分支,是求解決策過程最優(yōu)化的過程。20世紀50年代初,美國數(shù)學家貝爾曼(R.Bellman)等人在研究多階段決策過程的優(yōu)化問題時,提出了著名的最優(yōu)化原理,從而創(chuàng)立了動態(tài)規(guī)劃。動態(tài)規(guī)劃的應用極其廣泛,包括工程技術、經(jīng)濟、工業(yè)生產(chǎn)、軍事以及自動化控制等領域,并在背包問題、生產(chǎn)經(jīng)營問題、資金管理問題、資源分配問題、最短路徑問題和復雜系統(tǒng)可靠性問題等中取得了顯著的效果。

看不完的童鞋跳過,咱整點簡單點的

其實剛剛這道題應該算是最簡單的動態(tài)規(guī)劃題目了。

我們可以看出這道題有什么特點呢?

我們知道之所以最后一步加1會計算的那么快,大部分要歸功于之前計算過答案是5,如果我們把問題歸納為一個子問題,我想要計算每一步的答案,就可以列出一個方程:f(x) = f(x -1) + 1, 大家別對f(x)發(fā)怵,就把它當做一個簡單的方法。

其中,f(x)為第幾步的值,設定一個初始值,x > 0, 則第一步

f(1) = 1;

f(2) = f(1) + 1;

...

f(6) = f(5) + 1

在程序的世界里,用什么東東來存一個可以記錄之前結果值的數(shù)據(jù)結構呢?

顯而易見:數(shù)組唄。直接利用下標來存,巴適, 這就是動態(tài)規(guī)劃里的動態(tài),規(guī)劃就是限定一些邊界和初始化。

看到這里,老鐵,你就會動態(tài)規(guī)劃了,來看第二題。

No.2

leecode 322: 你有三種硬幣,2元、5元、7元,每種硬幣足夠多,買一本書需要27元,用最少的硬幣組合

怎么感覺像是回到了小學應用題?

--簡單分析一下: 最小硬幣組合 -> 盡量用大的硬幣

這傻不拉幾的題誰出的,這么簡單

7+7+7=21,21+2+2+2=27, 6枚硬幣

臥槽

7+5+5+5+5=27, 5枚硬幣

我們可以很暴力的想一想,從大往小的算,可以加起來不超過27,比如

7+7+7+7 > 27 (排除)

7+7+7+5 或者 7 +7 +7+2+2+2 6枚

....

窮舉太慢了,還會涉及到很多的重復計算,如果我想要27以內的任何值最小的硬幣組合呢,想想都頭大了吧。

既然計算機可以通過內存保存之前的內容,又快,很明顯,我們開一個數(shù)組來保存之前的狀態(tài)。

重點預警

1.1. 動態(tài)規(guī)劃組成部分1:確定狀態(tài)

簡單的說,解動態(tài)規(guī)劃的時候需要開一個數(shù)組,數(shù)組的每個元素f[i]或者f[i][j]代表什么,類似數(shù)學題中x, y, z代表什么

解動態(tài)規(guī)劃需要兩個意識:

最后一步

子問題

最后一步

剛剛第一題不是說了嘛,最后一步的計算結果是5。對于這道題,雖然我們不知道最優(yōu)策略是什么,但是最優(yōu)策略肯定是K枚硬幣,a1, a2, ....ak面值加起來是27

所以一定有一枚最后的硬幣:ak.

除掉這么硬幣,前面硬幣的面值加起來是27-ak

關鍵點1:

我們不關心前面的k-1枚硬幣是怎么拼出27-ak的(可能有一種拼法,也可能有100種拼法),而且我們現(xiàn)在甚至還不知道ak和K, 但是我們確定前面的硬幣拼出了27-ak

關鍵點2:

因為是最優(yōu)策略, 所以拼出27-ak的硬幣數(shù)一定要最少,否則這就不是最優(yōu)策略了

子問題

所以我們就要求:最少用多少枚硬幣可以拼出27-ak

原問題是最少用多少枚硬幣拼出27

我們將原問題轉化了一個子問題,而且規(guī)模更?。?7-ak

為了簡化定義, 我們設狀態(tài)f(x)=最少用多少枚硬幣拼出x

等等,我們還不知道最后的那枚硬幣ak是多少

很明顯,最后的那枚硬幣只能是2,5或者7

如果ak是2, f(27)應該是f(27-2)+1(1代表最后一枚硬幣2)

如果ak是5, f(27)應該是f(27-5)+1(1代表最后一枚硬幣5)

如果ak是7, f(27)應該是f(27-7)+1(1代表最后一枚硬幣7)

所以使用最少的硬幣數(shù)=f(27) = min{f(27-2)+1, f(27-5)+1, f(27-7)+1}

1.2. 動態(tài)規(guī)劃組成部分2:轉移方程

設狀態(tài)f(x)=最少用多少枚硬幣拼出x

對于任意的x : f(X) = min{f(X-2)+1, f(X-5)+1, f(X-7)+1}

1.3. 動態(tài)規(guī)劃組成部分2:初始條件和邊界情況

提出問題

x-2, x-5, x-7 小于0怎么辦?

什么時候停下來?

1.3.1

如果不能拼出Y, 就定義f[Y] = 正無窮

例如f[-1], f[-2] = 正無窮

例如:拼不出f[1]=min{f(-1)+1, f(-3)+1, f(-6)+1}

初始條件:f[0] = 0

2.4. 動態(tài)規(guī)劃組成部分2:計算順序

計算:f[1], f[2], ... f[27]

當我們計算到f[x], f[x-2], f[x-5], f[x-7] 都已經(jīng)得到結果了

如圖:

f[x] = 最少用多少枚硬幣拼出x

f[x] = ∞ 表示無法用硬幣拼出x

參考代碼

publicstaticintcoinChange(int[] A,intM ){int[] f =newint[M+1];intn = A.length; f[0] =0;inti,j;for(i =1; i<=M; i++) {? ? f[i] = Integer.MAX_VALUE;for(j =0; j< n;j++) {// 邊界條件判斷if(i >= A[j] && f[i - A[j]] != Integer.MAX_VALUE) {? ? ? ? ? ? f[i] = Math.min(f[i - A[j]] +1, f[i]);//? System.out.println(i + "=" +f[i]);}? ? } }if(f[M] == Integer.MAX_VALUE) {? ? f[M] = -1; }returnf[M];}@TestpublicvoidisCoinChange(){intxx = {1,2,3};intb =6;inti = coinChange(xx, b);? ? Assert.assertNotNull(i);}復制代碼

??

核心代碼就4行,是不是很簡單?\color{red}{核心代碼就4行,是不是很簡單~}核心代碼就4行,是不是很簡單?

No.3

leecode 62 :不同路徑

一個機器人位于一個 m x n 網(wǎng)格的左上角 (起始點在下圖中標記為 “Start” )。

機器人每次只能向下或者向右移動一步。機器人試圖達到網(wǎng)格的右下角(在下圖中標記為 “Finish” )。

問總共有多少條不同的路徑?

看了上面的解題步驟,按部就班的來

2.1. 動態(tài)規(guī)劃組成部分1:確定狀態(tài)

最后一步

無論機器人用何種方式到達右下角,總有最后挪動的一步:-向右或者向下

如果所示,我們設右下角的坐標為(m-1,n-1)

那么最后一步的前一步機器人的位置在(m-2,n-1)或者(m-1,n-2)

子問題

那么,如果機器人有x種方式從左上角走到(m-2,n-1), 有Y種方式從左上角走到(m-1,n-2), 則機器人有X+Y的方式走到(m-1,n-1)

問題轉化為,機器人有多少種 方式從左上角走到(m-2,n-1)或者(m-1,m-2)

如果走到是(m-2,n-1)如圖:

我們可以直接干掉最后一列

同理如果是走到(m-1,n-2)行就減少一行。

狀態(tài)

設f[i][j]為機器人有多少種方式從左上角走到(i,j)

2.2. 動態(tài)規(guī)劃組成部分2:轉移方程

對于任意一個格子:

f[i][j] = f[i-1][j] + f[i][j-1]

1? ? ? 2? ? ? 3

1代表機器人有多少種方式走到[i][j]

2代表機器人有多少種方式走到f[i-1][j]

3代表機器人有多少種方式走到f[i][j-1]

2.3. 動態(tài)規(guī)劃組成部分3:初始條件和邊界情況

初始條件:f[0][0]=1,因為機器人只有一個方式到左上角

邊界情況:i=0或j=0,則前一步只能有一個方向過來,也就是說第0行或者第0列,每走一步只有一種情況,則f[i][j] = 1,其他區(qū)域都滿足轉移方程。

3.4. 動態(tài)規(guī)劃組成部分4:計算順序

按行計算,為什么按行計算呢?

對于這道題來說,按行計算在計算到f[1][1]時,f[0][1]和f[1][0]都已經(jīng)計算了,同樣按列計算這兩坐標也計算了,不用再次計算。

f[0][0] =? 1

計算第0行:f[0][0],f[0][1],...,f[0][n-1]

計算第1行:f[1][0],f[1][1],...,f[1][n-1]

...

計算第m-1行:f[m-1][0],f[m-1][1],...,f[m-1][n-1]

時間復雜度:O(mn)

參考代碼

publicintuniquePaths(intm,intn){int[][] f =newint[m][n];inti,j;for(i =0; i

能看到這里的朋友,你已經(jīng)超過80%的人,可能現(xiàn)在你的腦袋開始有點暈了,刷題就是這樣,刷幾道就會頭疼,休息下就好了,這玩兒意兒看得就是堅持.

總結一下

什么題可以選擇動態(tài)規(guī)劃來做?

1.計數(shù)

有多少種方式走到右下角

有多少種方法選出k個數(shù)是的和是sum

2.求最大值最小值

從左上角走到右下角路徑的最大數(shù)字和

最長上升子序列長度

3.求存在性

取石子游戲,先手是否必勝

能不能選出k個數(shù)使得和是sum

好叻!接下來咱們整第四道題。

No.4

leecode 5. 最長回文子串

給定一個字符串 s,找到 s 中最長的回文子串。你可以假設 s 的最大長度為 1000。

示例 1:輸入: "babad"

輸出: "bab" 注意: "aba" 也是一個有效答案。

示例 2:輸入: "cbbd"

輸出: "bb"

看了之前的文章,我們就四步走吧

1.1. 動態(tài)規(guī)劃組成部分1:確定狀態(tài)

簡單的說,解動態(tài)規(guī)劃的時候需要開一個數(shù)組,數(shù)組的每個元素f[i]或者f[i][j]代表什么,類似數(shù)學題中x, y, z代表什么

在這道題中,我們定義f[i][j] 表示字符串 s 的第 i 到 j 個字母組成的串是否為回文串

解動態(tài)規(guī)劃需要兩個意識:

最后一步

子問題

最后一步

我們用示例一來講解,下圖是第一步和第二步的判斷過程,很明顯最后一步為下標為4的字母b與前面所有元素進行比較,得出最長的回文子串。

子問題

我們可以看到如果f[i] =f[j],要判定它是一個回文串,需要判定

f[i]j] = f[i+1]f[j-1] : 從i到j是一個回文串,那么從i+1到j-1一定也是一個回文串

也就是如上圖需要判定a=a

1.2. 動態(tài)規(guī)劃組成部分2:轉移方程

對于從i到j長度的字符串,判定它是一個回文串:

f[i]j] = f[i+1]f[j-1]

同時我們也知道,f[i+1][j-1]這是一個已知的,因為最后一步的上一步已經(jīng)將結果保存,也就是f[i+1][j-1] = f[i+2]f[j-2]

1.3. 動態(tài)規(guī)劃組成部分3:初始條件和邊界情況

當剩余判定字母個數(shù)<3 并且 f[i] = f[j],它一定是回文串。

對于字母本身來說f[i][i],從i到i的字符串,它也是回文。

1.4. 動態(tài)規(guī)劃組成部分4:計算順序

如上圖,我們用j去匹配0~i (i < j)

它的時間復雜度是On^2,由于是二維數(shù)據(jù)空間復雜度也是On^2

當然也有其他的解決辦法如中心擴散和馬拉車。

參考代碼

// 動態(tài)規(guī)劃publicStringlongestPalindrome(String s){// 特判intlen = s.length();if(len <2) {returns;? ? }intmaxLen =1;intbegin =0;// dp[i][j] 表示 s[i, j] 是否是回文串boolean[][] dp =newboolean[len][len];char[] charArray = s.toCharArray();for(inti =0; i < len; i++) {? ? ? ? dp[i][i] =true;// 對本身來說就是回文}for(intj =1; j < len; j++) {for(inti =0; i < j; i++) {if(charArray[i] != charArray[j]) {? ? ? ? ? ? ? ? dp[i][j] =false;? ? ? ? ? ? }else{if(j - i <3) {? ? ? ? ? ? ? ? ? ? dp[i][j] =true;? ? ? ? ? ? ? ? }else{? ? ? ? ? ? ? ? ? ? dp[i][j] = dp[i +1][j -1];// 要滿足這個條件,必需先滿足j - i > 3考慮邊界}? ? ? ? ? ? }// 只要 dp[i][j] == true 成立,就表示子串 s[i..j] 是回文,此時記錄回文長度和起始位置if(dp[i][j] && j - i +1> maxLen) {? ? ? ? ? ? ? ? maxLen = j - i +1;? ? ? ? ? ? ? ? begin = i;? ? ? ? ? ? }? ? ? ? }? ? }returns.substring(begin, begin + maxLen);}@TestpublicvoidislongestPalindrome(){? ? String i = longestPalindrome("babaxaxab");? ? Assert.assertNotNull(i);}復制代碼

準備來一道非常實用的題目?? ?? ??

No.5

leecode 10. 正則表達式匹配

給你一個字符串 s 和一個字符規(guī)律 p,請你來實現(xiàn)一個支持 '.' 和 '*' 的正則表達式匹配。

'.' 匹配任意單個字符

'*' 匹配零個或多個前面的那一個元素

所謂匹配,是要涵蓋 整個 字符串 s的,而不是部分字符串。

示例 1:

輸入:s = "aab" p = "cab"

輸出:true

解釋:因為 '*' 表示零個或多個,這里 'c' 為 0 個, 'a' 被重復一次。因此可以匹配字符串 "aab"。

示例 2:

輸入:s = "mississippi" p = "misisp*."

輸出:false

解釋:第二個*不能匹配si

2.1. 動態(tài)規(guī)劃組成部分1:確定狀態(tài)

wc,你倒是說說怎么確定要用動態(tài)規(guī)劃來做啊?

看題目,需要逐步匹配

沒有時間復雜度空間復雜度限制,你可以選擇>On的

跟前面題很類似,這里需要考慮* 和 .的情況。

嘗試根據(jù)步驟寫出轉移方程

在這道題中,我們定義f[i][j] 表示字符串 s 的前 i 個字符與 字符串p的前 j 個字符是否匹配

解動態(tài)規(guī)劃需要兩個意識:

最后一步

子問題

最后一步

我們用示例來講解 :s = "aaab" p = "aa*b"

根據(jù)題目意思,我們知道s要被全匹配,我們直接用s去匹配p字符串的每一個字符,這樣我們的最后一步就是用s字符串中的b字符去匹配p字符串的每一個字符,匹配最后一個字符為b是否相等。

子問題

有幾種情況需要討論,也就是用s去匹配p最后一個匹配的字符j

如果都是字符,只需要判斷:f[i][j] = f[i-1][j-1]

如果是“.” ,說明可以匹配s的最后任意一個字符,也只需:

f[i][j] = f[i-1][j-1]

如果是“*”,分兩種情況一種是匹配零個字符,一種是匹配1個或多個字符

如果是匹配零個字符:f[i][j] = f[i][j-2] ,因為如果j是*,我們就可以對p的j-1匹配任意次數(shù),當然零次就是j-2了。

? ? 如果是匹配1個或多個字符:f[i][j] = f[i-1][j],匹配 s 末尾的一個字符,將該字符扔掉,而該組合還可以繼續(xù)進行匹配,簡單點說,零個我們判定了,如果是一個我們扔掉一個字符,如果能匹配則保存,如果匹配兩個,我們是知道匹配s的上一個字符的結果的,直接匹配就行,同樣匹配多個也是如此。(匹配多個是根據(jù)前一個的匹配結果得出來的)

2.2. 動態(tài)規(guī)劃組成部分2:轉移方程

在子問題中我們分析的幾種情況就是轉移方程

2.3. 動態(tài)規(guī)劃組成部分3:初始條件和邊界情況

因為要涉及到i-1和j-1,注意邊界

boolean[][] f = new boolean[m + 1][n + 1];

f[0][0]=true

2.4. 動態(tài)規(guī)劃組成部分4:計算順序

用s去匹配p字符串的每一個字符

它的時間復雜度是Onm,由于是二維數(shù)據(jù)空間復雜度是Onm

參考代碼

publicbooleanisMatch(String s, String p){intm = s.length();intn = p.length();boolean[][] f =newboolean[m +1][n +1];? ? f[0][0] =true;for(inti =0; i <= m; ++i) {for(intj =1; j <= n; ++j) {if(p.charAt(j -1) =='*') {? ? ? ? ? ? ? ? f[i][j] = f[i][j -2];if(matches(s, p, i, j -1)) {? ? ? ? ? ? ? ? ? ? f[i][j] = f[i][j] || f[i -1][j];? ? ? ? ? ? ? ? }? ? ? ? ? ? }else{if(matches(s, p, i, j)) {? ? ? ? ? ? ? ? ? ? f[i][j] = f[i -1][j -1];? ? ? ? ? ? ? ? }? ? ? ? ? ? }? ? ? ? }? ? }returnf[m][n];}publicbooleanmatches(String s, String p,inti,intj){if(i ==0) {returnfalse;? ? }if(p.charAt(j -1) =='.') {returntrue;? ? }returns.charAt(i -1) == p.charAt(j -1);}@TestpublicvoidisisMatch(){booleani = isMatch("aa","a*");? ? Assert.assertNotNull(i);}復制代碼

No.6

繼續(xù)干,繼續(xù)干????????

leecode 32. 最長有效括號

給你一個只包含 '(' 和 ')' 的字符串,找出最長有效(格式正確且連續(xù))括號子串的長度。

示例 1:

輸入:s = "(()"

輸出:2

解釋:最長有效括號子串是 "()"

示例 2:

輸入:s = ")()())"

輸出:4

解釋:最長有效括號子串是 "()()"

示例 3:

輸入:s = ""

輸出:0

提示:

0 <= s.length <= 3 * 104

s[i] 為 '(' 或 ')'

看了之前的文章,我們就四步走吧

1.1. 動態(tài)規(guī)劃組成部分1:確定狀態(tài)

簡單的說,解動態(tài)規(guī)劃的時候需要開一個數(shù)組,數(shù)組的每個元素f[i]或者f[i][j]代表什么,類似數(shù)學題中x, y, z代表什么

wc,你倒是說說怎么確定要用動態(tài)規(guī)劃來做?。?/p>

看題目,需要逐步驗證最長長度

沒有時間復雜度空間復雜度限制,你可以選擇>=On的

跟前面題很類似,這里需要考慮生成括號的情況。

嘗試根據(jù)步驟寫出轉移方程

在這道題中,我們定義d[i]表示以下標 ii字符結尾的最長有效括號的長度

解動態(tài)規(guī)劃需要兩個意識:

最后一步

子問題

最后一步

我們用下圖來講解,i作為最后一個括號判斷,我們只對為左括號做判斷,左括號分兩種情況,具體看子問題拆分。

子問題

第一種情況為: ...()

因為括號前面可能還有有效的括號,之前我們定義了d[i] 表示下標i字符結尾的最長有效括號的長度,所以可以推導出:

d[i] = d[i-2] + 2

第二種情況為: ...))

>need-to-insert-img

如圖,下標為2:i - d[i-1] -1

提出一個疑問:在下標2和5之前可能存在多個有效括號,其實都是d[i-1],因為我們定義的:d[i-1] 表示下標i-1字符結尾的最長有效括號的長度

>need-to-insert-img

最長有效括號的長度 :

d[i] = x + y

這里x = d[i - d[i-1] -2]

這里y = d[i-1] + 2

因此 :d[i] = d[i - d[i-1] -2] + d[i-1] + 2

1.2. 動態(tài)規(guī)劃組成部分2:轉移方程

d[i] 表示下標i字符結尾的最長有效括號的長度

d[i] = d[i - d[i-1] -2] + d[i-1] + 2

1.3. 動態(tài)規(guī)劃組成部分3:初始條件和邊界情況

跟之前有些題一樣,我們需要判斷第i字符去判斷第i-1個字符,所以i從1開始遍歷,判斷數(shù)組i-2需要考慮越界。

① ...()? : i >=2

② ...))? :? i - d[i-1] > 0? 對應 :())

1.4. 動態(tài)規(guī)劃組成部分4:計算順序

從左往右

它的時間復雜度是On,空間復雜度也是On

當然也有其他的解決辦法如棧

參考代碼

public int longestValidParentheses(String s) {? ? int maxans = 0;? ? int[] dp = new int[s.length()];? ? for (int i = 1; i < s.length(); i++) {? ? ? ? if (s.charAt(i) == ')') {? ? ? ? ? ? if (s.charAt(i - 1) == '(') {? ? ? ? ? ? ? ? dp[i] = (i >= 2 ? dp[i - 2] : 0) + 2;? ? ? ? ? ? } else if (i - dp[i - 1] > 0 && s.charAt(i - dp[i - 1] - 1) == '(') {? ? ? ? ? ? ? ? dp[i] = dp[i - 1] + ((i - dp[i - 1]) >= 2 ? dp[i - dp[i - 1] - 2] : 0) + 2;? ? ? ? ? ? }? ? ? ? ? ? maxans = Math.max(maxans, dp[i]);? ? ? ? }? ? }? ? return maxans;}@Testpublic void isLongestValidParentheses() {? ? String s = "()(())";? ? longestValidParentheses(s);}復制代碼

No.7

這道題相對于就簡單一些啦---來自禿頂?shù)墓こ處??????????

leecode 42. 接雨水

給定 n 個非負整數(shù)表示每個寬度為 1 的柱子的高度圖,計算按此排列的柱子,下雨之后能接多少雨水。

>need-to-insert-img

輸入:height = [0,1,0,2,1,0,1,3,2,1,2,1]

輸出:6

解釋:上面是由數(shù)組 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度圖,在這種情況下,可以接 6 個單位的雨水(藍色部分表示雨水)。

示例 2:

輸入:height = [4,2,0,3,2,5]

輸出:9

提示:

n == height.length

0 <= n <= 3 * 104

0 <= height[i] <= 105

看了之前的文章,我們就四步走吧

這道題相對而言,提升了難度,常規(guī)的題,我們的計算順序一般是從左到右,或者從右到左,亦或是從上到下。

1.1. 動態(tài)規(guī)劃組成部分1:確定狀態(tài)

簡單的說,解動態(tài)規(guī)劃的時候需要開一個數(shù)組,數(shù)組的每個元素f[i]或者f[i][j]代表什么,類似數(shù)學題中x, y, z代表什么

wc,你倒是說說怎么確定要用動態(tài)規(guī)劃來做?。?/p>

看題目,需要逐步驗證最長長度

沒有時間復雜度空間復雜度限制,你可以選擇>=On的

跟前面題很類似,這里需要考慮每一步最高的高度取低位高度,因為可能形成低洼

嘗試根據(jù)步驟寫出轉移方程

在這道題中,我們定義d[i]表示以下標 i字符結尾的最多的有效雨水

解動態(tài)規(guī)劃需要兩個意識:

最后一步

子問題

最后一步

我們可以利用填滿法來降低復雜度。去掉低洼的情況。

先從左到右來看,求出每個位置的最大高度(深度)

>need-to-insert-img

從右往左看,求出每個位置的最大高度

>need-to-insert-img

我們在來看它們重疊之后的效果

>need-to-insert-img

這樣每個位置的最小值 - 高度就是每個位置的蓄水值。

子問題

我們存儲了從左到右和從右到左每個位置的最大值

從重疊的圖中可以看到最后一個位置的最小值為2,減去高度,蓄水值為0.

1.2. 動態(tài)規(guī)劃組成部分2:轉移方程

ans += Math.min(left_max[i], right_max[i]) - height[i];

1.3. 動態(tài)規(guī)劃組成部分3:初始條件和邊界情況

1.4. 動態(tài)規(guī)劃組成部分4:計算順序

從左到右 + 從右到左

它的時間復雜度是On,空間復雜度也是On

當然也有其他的解決辦法如棧

參考代碼

publicinttrap(int[] height){if(height ==null|| height.length ==0)return0;intans =0;intsize = height.length;int[] left_max =newint[size];int[] right_max =newint[size];? ? left_max[0] = height[0];for(inti =1; i < size; i++) {? ? ? ? left_max[i] = Math.max(height[i], left_max[i -1]);? ? }? ? right_max[size -1] = height[size -1];for(inti = size -2; i >=0; i--) {? ? ? ? right_max[i] = Math.max(height[i], right_max[i +1]);? ? }for(inti =1; i < size -1; i++) {? ? ? ? ans += Math.min(left_max[i], right_max[i]) - height[i];? ? }returnans;}@Testpublicvoidistrap(){int[] candidates = {2,0,6,1};inti = trap(candidates);? ? Assert.assertNotNull(i);}復制代碼

No.8

這道題跟第五題很類似,如果看懂了第五題,那么這題肯定是信手拈來????????

leecode 44. 通配符匹配

給定一個字符串 (s) 和一個字符模式 (p) ,實現(xiàn)一個支持 '?' 和 '*' 的通配符匹配。

'?' 可以匹配任何單個字符。

'*' 可以匹配任意字符串(包括空字符串)。

兩個字符串完全匹配才算匹配成功。

說明:

s 可能為空,且只包含從 a-z 的小寫字母。

p 可能為空,且只包含從 a-z 的小寫字母,以及字符 ? 和 *。

示例 1:

輸入:

s = "aa"

p = "a"

輸出: false

解釋: "a" 無法匹配 "aa" 整個字符串。

示例 2:

輸入:

s = "aa"

p = "*"

輸出: true

解釋: '*' 可以匹配任意字符串。

示例 3:

輸入:

s = "cb"

p = "?a"

輸出: false

解釋: '?' 可以匹配 'c', 但第二個 'a' 無法匹配 'b'。

示例 4:

輸入:

s = "adceb"

p = "ab"

輸出: true

解釋: 第一個 '' 可以匹配空字符串, 第二個 '' 可以匹配字符串 "dce".

示例 5:

輸入:

s = "acdcb"

p = "a*c?b"

輸出: false

看了之前的文章,我們就四步走吧

1.1. 動態(tài)規(guī)劃組成部分1:確定狀態(tài)

簡單的說,解動態(tài)規(guī)劃的時候需要開一個數(shù)組,數(shù)組的每個元素f[i]或者f[i][j]代表什么,類似數(shù)學題中x, y, z代表什么

wc,你倒是說說怎么確定要用動態(tài)規(guī)劃來做啊?

看題目,需要逐步驗證最長長度

沒有時間復雜度空間復雜度限制,你可以選擇>=On的

跟前面題很類似,這里需要考慮* 和 ?的情況。

嘗試根據(jù)步驟寫出轉移方程

在這道題中,我們定義f[i][j] 表示字符串 s 的前 i 個字符與 字符串p的前 j 個字符是否匹配

解動態(tài)規(guī)劃需要兩個意識:

最后一步

子問題

最后一步

我們用示例來講解 :s = "aaab" p = "aa*b"

根據(jù)題目意思,我們知道s和p要全匹配,我們直接用s去匹配p字符串的每一個字符,這樣我們的最后一步就是用s字符串中的b字符去匹配p字符串的每一個字符,匹配最后一個字符為b是否相等。

子問題

這是 '動態(tài)規(guī)劃Day two - 正則表達式匹配' 的子問題分析

有幾種情況需要討論,也就是用s去匹配p最后一個匹配的字符j

如果都是字符,只需要判斷:f[i][j] = f[i-1][j-1]

如果是“.” ,說明可以匹配s的最后任意一個字符,也只需:

f[i][j] = f[i-1][j-1]

如果是“*”,分兩種情況一種是匹配零個字符,一種是匹配1個或多個字符

如果是匹配零個字符:f[i][j] = f[i][j-2] ,因為如果j是*,我們就可以對p的j-1匹配任意次數(shù),當然零次就是j-2了。

如果是匹配1個或多個字符:f[i][j] = f[i-1][j],匹配 s 末尾的一個字符,將該字符扔掉,而該組合還可以繼續(xù)進行匹配,簡單點說,零個我們判定了,如果是一個我們扔掉一個字符,如果能匹配則保存,如果匹配兩個,我們是知道匹配s的上一個字符的結果的,直接匹配就行,同樣匹配多個也是如此。(匹配多個是根據(jù)前一個的匹配結果得出來的)

這道題基本上是如出一轍

有幾種情況需要討論,也就是用s去匹配p最后一個匹配的字符j

如果都是字符,只需要判斷:f[i][j] = f[i-1][j-1]

如果是“?” ,說明可以匹配s的最后任意一個字符,也只需:

f[i][j] = f[i-1][j-1]

如果是“”,分兩種情況,第一種是不需要使用,第二種是需要使用*

實例 :s = "abc"? p = "a*"。

在對s串的a遍歷p串的*時,剛好滿足dp[i][j] = dp[i][j-1] 此時i=1,j=2,

dp[1][2] = dp[1][1]=true。? ---不需要使用*

在對s串的b遍歷p串的時,剛好滿足dp[i][j] = f[i-1][j],因為是可以匹配任意一個字母的。

dp[2][2] = dp[1][2]=true。---需要使用*

在對s串的c遍歷p串的時,剛好滿足dp[i][j] = f[i-1][j],因為是可以匹配任意一個字母的。

dp[3][2] = dp[2][2]=true。---不需要使用*

1.2. 動態(tài)規(guī)劃組成部分2:轉移方程

轉移方程可以詳見子問題分析

1.3. 動態(tài)規(guī)劃組成部分3:初始條件和邊界情況

因為要涉及到i-1和j-1,注意邊界

boolean[][] f = new boolean[m + 1][n + 1];

f[0][0]=true

當f[0][j]時,需要判斷j是否為*,因為*可以匹配null串

1.4. 動態(tài)規(guī)劃組成部分4:計算順序

用s串的每一個字符去匹配p串的每一個字符

它的時間復雜度是Onm,空間復雜度也是Onm

參考代碼

publicbooleanisWildcardMatch(String s, String p){intm = s.length();intn = p.length();boolean[][] dp =newboolean[m +1][n +1];? ? dp[0][0] =true;for(inti =1; i <= n; ++i) {if(p.charAt(i -1) =='*') {? ? ? ? ? ? dp[0][i] =true;? ? ? ? }else{break;? ? ? ? }? ? }for(inti =1; i <= m; ++i) {for(intj =1; j <= n; ++j) {if(p.charAt(j -1) =='*') {? ? ? ? ? ? ? ? dp[i][j] = dp[i][j -1] || dp[i -1][j];? ? ? ? ? ? }elseif(p.charAt(j -1) =='?'|| s.charAt(i -1) == p.charAt(j -1)) {? ? ? ? ? ? ? ? dp[i][j] = dp[i -1][j -1];? ? ? ? ? ? }? ? ? ? }? ? }returndp[m][n];}@TestpublicvoidisisWildcardMatch(){booleani = isWildcardMatch("123","1*");? ? Assert.assertNotNull(i);}復制代碼

No.9

這道題就很簡單了,放松一下~????????

63. 不同路徑 II

一個機器人位于一個 m x n 網(wǎng)格的左上角 (起始點在下圖中標記為“Start” )。

機器人每次只能向下或者向右移動一步。機器人試圖達到網(wǎng)格的右下角(在下圖中標記為“Finish”)。

現(xiàn)在考慮網(wǎng)格中有障礙物。那么從左上角到右下角將會有多少條不同的路徑?

>need-to-insert-img

網(wǎng)格中的障礙物和空位置分別用 1 和 0 來表示。

示例 1:

>need-to-insert-img

輸入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]]

輸出:2

解釋:

3x3 網(wǎng)格的正中間有一個障礙物。

從左上角到右下角一共有 2 條不同的路徑:

\1. 向右 -> 向右 -> 向下 -> 向下

\2. 向下 -> 向下 -> 向右 -> 向右

示例 2:

>need-to-insert-img

輸入:obstacleGrid = [[0,1],[0,0]]

輸出:1

提示:

m == obstacleGrid.length

n == obstacleGrid[i].length

1 <= m, n <= 100

obstacleGrid[i][j] 為 0 或 1

本題與

---

我是怎么向5歲侄女解釋動態(tài)規(guī)劃的?

---

里的不同路徑1,大致相同,只是增加了障礙物,我們做一下沒有障礙物的判斷就行了。

2.1. 動態(tài)規(guī)劃組成部分1:確定狀態(tài)

最后一步

無論機器人用何種方式到達右下角,總有最后挪動的一步:-向右或者向下

如果所示,我們設右下角的坐標為(m-1,n-1)

那么最后一步的前一步機器人的位置在(m-2,n-1)或者(m-1,n-2)

子問題

那么,如果機器人有x種方式從左上角走到(m-2,n-1), 有Y種方式從左上角走到(m-1,n-2), 則機器人有X+Y的方式走到(m-1,n-1)

問題轉化為,機器人有多少種 方式從左上角走到(m-2,n-1)或者(m-1,m-2)

如果走到是(m-2,n-1)如圖:

>need-to-insert-img

我們可以直接干掉最后一列

同理如果是走到(m-1,n-2)行就減少一行。

狀態(tài)

設f[i][j]為機器人有多少種方式從左上角走到(i,j)

2.2. 動態(tài)規(guī)劃組成部分2:轉移方程

對于任意一個格子:

f[i][j] = f[i-1][j] + f[i][j-1]

1? ? ? ? ? 2? ? ? ? ? ? 3

1代表機器人有多少種方式走到[i][j]

2代表機器人有多少種方式走到f[i-1][j]

3代表機器人有多少種方式走到f[i][j-1]

2.3. 動態(tài)規(guī)劃組成部分3:初始條件和邊界情況

初始條件:f[0][0]=1,因為機器人只有一個方式到左上角

邊界情況:i=0或j=0,則前一步只能有一個方向過來,也就是說第0行或者第0列,每走一步只有一種情況,則f[i][j] = 1,其他區(qū)域都滿足轉移方程。

如果遇到障礙物,f[i][j] = 0。

3.4. 動態(tài)規(guī)劃組成部分4:計算順序

按行計算,為什么按行計算呢?

對于這道題來說,按行計算在計算到f[1][1]時,f[0][1]和f[1][0]都已經(jīng)計算了,同樣按列計算這兩坐標也計算了,不用再次計算。

f[0][0] =? 1 如果第一個是障礙物f[0][0]=0

計算第0行:f[0][0],f[0][1],...,f[0][n-1]

計算第1行:f[1][0],f[1][1],...,f[1][n-1]

...

計算第m-1行:f[m-1][0],f[m-1][1],...,f[m-1][n-1]

時間復雜度:O(mn)

>need-to-insert-img

參考代碼

classSolution{publicintuniquePathsWithObstacles(int[][] obstacleGrid){if(obstacleGrid ==null|| obstacleGrid.length ==0) {return0;? ? }// 定義 dp 數(shù)組并初始化第 1 行和第 1 列。intm = obstacleGrid.length, n = obstacleGrid[0].length;int[][] dp =newint[m][n];for(inti =0; i < m && obstacleGrid[i][0] ==0; i++) {? ? ? ? dp[i][0] =1;? ? }for(intj =0; j < n && obstacleGrid[0][j] ==0; j++) {? ? ? ? dp[0][j] =1;? ? }// 根據(jù)狀態(tài)轉移方程 dp[i][j] = dp[i - 1][j] + dp[i][j - 1] 進行遞推。for(inti =1; i < m; i++) {for(intj =1; j < n; j++) {if(obstacleGrid[i][j] ==0) {? ? ? ? ? ? ? ? dp[i][j] = dp[i -1][j] + dp[i][j -1];? ? ? ? ? ? }? ? ? ? }? ? }returndp[m -1][n -1];}復制代碼

簡單說一下滾動數(shù)組的版本,當我們知道當前位置的最多路徑數(shù)時,我們去求下一個位置的路徑數(shù),只需要知道左邊和上邊的可以了,空間復雜度為o(m)

參考代碼

publicintuniquePathsWithObstacles(int[][] obstacleGrid){intn = obstacleGrid.length, m = obstacleGrid[0].length;int[] f =newint[m];? ? f[0] = obstacleGrid[0][0] ==0?1:0;for(inti =0; i < n; ++i) {for(intj =0; j < m; ++j) {if(obstacleGrid[i][j] ==1) {? ? ? ? ? ? ? ? f[j] =0;? ? ? ? ? ? }elseif(j -1>=0&& obstacleGrid[i][j -1] ==0) {? ? ? ? ? ? ? ? f[j] += f[j -1];? ? ? ? ? ? }? ? ? ? }? ? }returnf[m -1];}復制代碼

No.10

看到這次分享的最后一題啦,能看到這里的人呀,都是人才。????????

來一道比較簡單的題目吧?。?!

leecode 64. 最小路徑和

給定一個包含非負整數(shù)的 m?x?n?網(wǎng)格?grid ,請找出一條從左上角到右下角的路徑,使得路徑上的數(shù)字總和為最小。

說明:每次只能向下或者向右移動一步。


示例 1:

輸入:grid = [[1,3,1],[1,5,1],[4,2,1]]

輸出:7

解釋:因為路徑 1→3→1→1→1 的總和最小。

示例 2:

輸入:grid = [[1,2,3],[4,5,6]]

輸出:12


提示:

m == grid.length

n == grid[i].length

1 <= m, n <= 200

0 <= grid[i][j] <= 100

看了之前的題解,想想就明白了,這里用動態(tài)規(guī)劃解題,考慮一下幾點:

邊界處理

中間的路徑

這里的結果會有M*N次所以每個格子的結果都會作比較,我們取較小值就可以了

dp[i][j] 表示從左上角出發(fā)到 (i,j)(i,j) 位置的最小路徑和

初始條件:dp[0][0]=grid[0][0]

當 i>0 j=0時dp[i][0]=dp[i-1][0] + grid[i][0]; // grid[i][0]為最后一個格子的值

當 i=0 j>0時dp[0][j-1]=dp[0][j-1] + grid[0][j];

當 i>0 j>0時dp[i-1][j-1]=min(dp[i-1][j],dp[i][j-1])? + grid[i][j];

參考代碼:

classSolution{publicintminPathSum(int[][] grid){if(grid ==null|| grid.length ==0|| grid[0].length ==0) {return0;? ? ? ? }introws = grid.length, columns = grid[0].length;int[][] dp =newint[rows][columns];? ? ? ? dp[0][0] = grid[0][0];for(inti =1; i < rows; i++) {? ? ? ? ? ? dp[i][0] = dp[i -1][0] + grid[i][0];? ? ? ? }for(intj =1; j < columns; j++) {? ? ? ? ? ? dp[0][j] = dp[0][j -1] + grid[0][j];? ? ? ? }for(inti =1; i < rows; i++) {for(intj =1; j < columns; j++) {? ? ? ? ? ? ? ? dp[i][j] = Math.min(dp[i -1][j], dp[i][j -1]) + grid[i][j];? ? ? ? ? ? }? ? ? ? }returndp[rows -1][columns -1];? ? }}

作者:汀雨筆記

鏈接:https://juejin.cn/post/6937193443953393700

來源:掘金

著作權歸作者所有。商業(yè)轉載請聯(lián)系作者獲得授權,非商業(yè)轉載請注明出處。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容