5.最長迴文子串
- 暴力法,用雙重for循環列舉所有子串(On2),判斷每個子串是否是迴文(On)
- 中心擴展法,以每個字母爲中心,向外擴展。
- 動態規劃。dp[i][j]表示從i到j這個子串是不是迴文(bool)。
dp[i][j] = s[i]==s[j] && dp[i+1][j-1]
class Solution:
def longestPalindrome(self, s: str) -> str:
dp=[]
for _ in range(len(s)):
dp.append([0]*len(s))
res=''
for i in range(len(s)-1,-1,-1):
for j in range(i, len(s)):
dp[i][j]=(s[j]==s[i])and(j-i<2 or dp[i+1][j-1]) # j-i<2代表相鄰或重疊
if dp[i][j] and j-i+1>len(res):
res=s[i:j+1]
return res
62. 不同路徑
同劍指offer中最大禮物價值。二維數組dp[i][j]記錄從左上角到達(i,j)有多少路徑。
- 動態規劃,從左至右,從上到下依次加。
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
dp=[]
for _ in range(m):
dp.append([0]*n)
dp[0][0]=1
for i in range(m):
for j in range(n):
if i==0 or j==0:
dp[i][j]=1
continue
dp[i][j]=dp[i-1][j]+dp[i][j-1]
return dp[m-1][n-1]
還可以用一位數組優化。
63. 不同路徑II
在62題的基礎上,增加了障礙物,遇到障礙物,則dp[i][j]爲0。預先處理第一行和第一列的數據。
class Solution:
def uniquePathsWithObstacles(self, obstacleGrid: List[List[int]]) -> int:
m,n=len(obstacleGrid), len(obstacleGrid[0])
dp=[]
for _ in range(m):
dp.append([0]*n)
for i in range(m):
for j in range(n):
if i==0:
if j==0:
if obstacleGrid[i][j]:
return 0
dp[0][0]=1
continue
else:
if obstacleGrid[i][j]:
dp[i][j]=0
else:
dp[i][j]=dp[i][j-1]
continue
if j==0:
if obstacleGrid[i][j]:
dp[i][j]=0
else:
dp[i][j]=dp[i-1][j]
continue
if obstacleGrid[i][j]:
dp[i][j]=0
else:
dp[i][j]=dp[i][j-1]+dp[i-1][j]
return dp[m-1][n-1]
64.最小路徑
同最大禮物問題
72. 編輯距離
對“dp[i-1][j-1] 表示替換操作,dp[i-1][j] 表示刪除操作,dp[i][j-1] 表示插入操作。”的補充理解:
以 word1 爲 “horse”,word2 爲 “ros”,且 dp[5][3] 爲例,即要將 word1的前 5 個字符轉換爲 word2的前 3 個字符,也就是將 horse 轉換爲 ros,因此有:
(1) dp[i-1][j-1],即先將 word1 的前 4 個字符 hors 轉換爲 word2 的前 2 個字符 ro,然後將第五個字符 word1[4](因爲下標基數以 0 開始) 由 e 替換爲 s(即替換爲 word2 的第三個字符,word2[2])
(2) dp[i][j-1],即先將 word1 的前 5 個字符 horse 轉換爲 word2 的前 2 個字符 ro,然後在末尾補充一個 s,即插入操作
(3) dp[i-1][j],即先將 word1 的前 4 個字符 hors 轉換爲 word2 的前 3 個字符 ros,然後刪除 word1 的第 5 個字符
class Solution:
def minDistance(self, word1: str, word2: str) -> int:
m,n=len(word1),len(word2)
if m*n == 0:
return m+n
dp=[[0]*(n+1) for _ in range(m+1)]
for i in range(m+1):
dp[i][0]=i
for j in range(n+1):
dp[0][j]=j
for i in range(m):
for j in range(n):
if word1[i]== word2[j]:
dp[i+1][j+1]=dp[i][j]
else:
dp[i+1][j+1]=min(
1+dp[i+1][j], # 添加
1+dp[i][j+1], # 刪除
1+dp[i][j] # 替換
)
return dp[-1][-1]
264. 醜數
- 三指針法,前面醜數的2,3,5倍中,且超過最後一個醜數,選擇最小的作爲新醜數。
class Solution:
def nthUglyNumber(self, n: int) -> int:
if n <=0:
return None
index = 1
n2 = n3 = n5 = 0
res = [1]
while index < n:
start = min(2*res[n2], 3*res[n3], 5*res[n5])
res.append(start)
while 2 * res[n2] <= start:
n2 += 1
while 3 * res[n3] <= start:
n3 += 1
while 5 * res[n5] <= start:
n5 += 1
index += 1
return res[-1]
279. 完全平方數
dp[i]爲將i拆分爲完全平方數字所需要的最小數目。
class Solution:
def numSquares(self, n: int) -> int:
if n <=0:
return 0
dp = [0]*(n+1)
for i in range(1, n+1):
j = 1
dp[i]=i
while (i - j**2) >= 0:
dp[i] = min(dp[i], dp[i-j**2]+1)
j += 1
return dp[-1]
300. 最長上升子序列
dp[i] 爲從0-i範圍內,以i爲終點的最大上升子序列。因爲最大上升子序列的終點未必是在n-1的位置上,所以返回的是max(dp)。
dp[i] = max(dp[i-k]+1) if nums[i] > nums[i-k] else 1, k in {1, i-1}
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
if not nums:
return 0
dp = [1] * (len(nums)+1)
dp[0] = 0
for i in range(1, len(nums)+1):
temp = 1
for j in range(1, i):
if nums[i-1] > nums[i-1-j]:
temp=max(temp, dp[i-j]+1)
dp[i] = temp
return max(dp)
304. 二維區域和檢索
- dp[i][j] = 前面和上面的值相加,加上當期位置的數字,再減去重複的部分dp[i-1][j-1]
class NumMatrix:
def __init__(self, matrix: List[List[int]]):
self.matrix = matrix
self.m = len(matrix)
self.flag=True
if not matrix:
self.flag=False
return
self.n = len(matrix[0])
self.dp = [[0]*(self.n+1) for _ in range(self.m+1)]
for i in range(1,self.m+1):
for j in range(1, self.n+1):
self.dp[i][j] = self.matrix[i-1][j-1]+self.dp[i-1][j] + self.dp[i][j-1] - self.dp[i-1][j-1]
def sumRegion(self, row1: int, col1: int, row2: int, col2: int) -> int:
if not self.flag:
return 0
return self.dp[row2+1][col2+1] - self.dp[row1][col2+1] - self.dp[row2+1][col1] + self.dp[row1][col1]
96. 不同的二叉搜索樹
- G(n): 是長度爲n的序列的不同二叉搜索樹個數;
- F(i,n):以i爲根的二叉搜索樹個數。總共有n個節點
- 而F(i,n)怎麼定義? 當以i爲根,i左邊的全部是左子樹部分,i右邊的全部是右子樹部分。F(i,n) = G(i-1)*G(n-i). (左子樹的個數 * 右子樹的個數)
class Solution:
def numTrees(self, n: int) -> int:
G = [0]*(n+1)
G[0], G[1] = 1, 1
for i in range(2, n+1):
for j in range(1, i+1):
G[i] += G[j-1] * G[i-j]
return G[n]
95 不用的二叉搜索樹II
這題是基於第96題。96題只需要求出有多少不同組合的二叉樹數目。但這道題需要我們生成這些二叉樹,就是需要記錄下來。
其實這道題,我感覺不算是動態規劃的題目了,因爲代碼的形式更加向全排列或者說回溯法。
- 1到n都能是根節點,所以放到for循環中,當i爲根節點,i左邊是左子樹,i右邊是右子樹。
- 對於左子樹的根節點,可以在1至i-1中選擇,這又是一個for循環,右子樹同理。因此,for循環內部是for循環,整個代碼形式是for之中用遞歸。分別對左邊和右邊遞歸,得到做子樹的根和右子樹的根。可以看成是由底至頂,先搭建最底下的。
class Solution:
def generateTrees(self, n: int) -> List[TreeNode]:
def core(start, end):
res = []
if start > end:
return [None,]
for i in range(start, end +1):
left = core(start, i-1)
right = core(i+1, end)
for l in left:
for r in right:
node=TreeNode(i)
node.left=l
node.right=r
res.append(node)
return res
return core(1, n) if n else []
91. 解碼方法
典型的動態規劃問題,A-Z對應了1-26,輸入數字字符串,求出解碼方法的最大數目。遞歸方程是 f[n] = f[n-1] + g* f[n-2], 如果s[n-1:n+1]在10-26之間,g爲1.
class Solution:
def numDecodings(self, s: str) -> int:
size = len(s)
#特判
if size == 0:
return 0
dp = [0]*(size+1)
dp[0] = 1
for i in range(1,size+1):
t = int(s[i-1])
if t>=1 and t<=9:
dp[i] += dp[i-1] #最後一個數字解密成一個字母
if i >=2:#下面這種情況至少要有兩個字符
t = int(s[i-2])*10 + int(s[i-1])
if t>=10 and t<=26:
dp[i] += dp[i-2]#最後兩個數字解密成一個一個字母
return dp[-1]
120. 三角形最小路徑和
下面的代碼沒有采用空間優化。dp是和輸入大小一樣的空間。實際上,dp可以只用一個一維的大小爲n的數組。
class Solution:
def minimumTotal(self, triangle: List[List[int]]) -> int:
dp=[]
height=len(triangle)
for h in range(height):
dp.append([0]*(h+1))
dp[0][0]=triangle[0][0]
for i in range(1,height):
for j in range(i+1):
if j==0: # 處理最左邊,只有一個選擇
dp[i][j]=dp[i-1][0]+triangle[i][j]
continue
if j== i: # 處理最右邊, 也只有一個選擇
dp[i][j]=dp[i-1][-1]+triangle[i][j]
continue
dp[i][j]=min(dp[i-1][j-1]+triangle[i][j],
dp[i-1][j]+triangle[i][j])
return min(dp[-1])
139. 單詞拆分
我採用的是回溯法,即暴力法,並且用mask剪枝。
思路是:dic記錄下有什麼單詞需要匹配。start指針指向單詞開頭位置,end指向單詞尾部位置。滑動end直到匹配到某個字符,如果匹配不到,start這個位置會被mask記錄爲失敗匹配,實現剪枝。
class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
dic={}
mask=[1]*(len(s)+1)
for l in wordDict:
dic[l]=0
return self.unfold(s,dic,0,mask)
def unfold(self,s,c,start,mask):
if mask[start] == 0:
return False
if start==len(s):
return True
for end in range(start, len(s)):
word=s[start:end+1]
if word in c:
c[word]+=1
if self.unfold(s,c,end+1,mask):
return True
c[word]-=1
mask[start]=0
return False
152. 乘法最大子數組
求一個數組內,最大的連續子數組的乘積
因爲涉及到負數,其實這道題還是不好處理。所以需要兩個數組作爲dp,一個記錄爲i爲結尾的連續數組乘積的最小值,另一個記錄以i爲結尾的乘積最大值。
其實這兩個數組可以合併爲dp[i][k],k爲0和1。dp[i][1]的含義是:爲i爲結尾的連續數組的乘積的最小值。
還可以優化空間,只用兩個變量就行了。
class Solution:
def maxProduct(self, nums: List[int]) -> int:
if len(nums)<=1:
return nums[0]
res=nums[0]
minv=[0]*len(nums)
minv[0]=res # 初始化
maxv=[0]*len(nums)
maxv[0]=res # 初始化第1個位置
for i in range(1,len(nums)):
temp1=nums[i]*maxv[i-1]
temp2=nums[i]*minv[i-1]
minv[i]=min(temp2,temp1,nums[i])
maxv[i]=max(temp1,temp2,nums[i])
res=max(res,maxv[i])
return res
377.組合總數IV
這道題和零錢兌換有點像,只不過零錢兌換要求的是,能兌換出target的最少硬幣數目。這道題則要求能兌換出零錢的最大組合數目。
核心就是更改遞歸方程:
class Solution:
def combinationSum4(self, nums: List[int], target: int) -> int:
dp = [0] * (target+1)
nums.sort()
dp[0] = 1 # 當i == nums[j]時, 有一個直接用現有硬幣兌換的組合
for i in range(1, target+1 ):
temp = 0
for j in range(len(nums)):
if i - nums[j] < 0: # 邊界條件
continue
temp += dp[i - nums[j]]
dp[i] = temp
return dp[-1]
416.分割等和子集
先貼一段暴力回溯法,當然超時了
動態規劃的思路是:dp[i][j]代表前i個數字中有沒有和爲j的組合。
遞歸方程爲:dp[i][j] = dp[i-1][j] or dp[i-1][j-nums[i]]
當不選擇nums[i]時,爲dp[i-1][j] 當選擇nums[i],說明從第0到第i-1的數字中有和爲j-nums[i]