【LeetCode】ダイナミックプログラミングの原理とプログラミング実践

(Datawhaleは8月に共同で研究しました)私が見た多くの経験の中で、動的プログラミングの頻度は非常に高いです。動的プログラミングのアイデアを習得すると、実際に多くの問題を解決できます。この記事では主に動的プログラミングの原理を紹介し、動的プログラミングで解決できるLeetCodeの多くの問題を解決します。

【LeetCode】シリーズ記事

LeetCodeの分割統治アルゴリズムの原理とプログラミングの練習 20200819に公開の原則とプログラミングの練習
LeetCodeの動的なプログラミング方法は 20200822に公開します

1.動的プログラミングの原理

  • 本旨

与えられた問題を解決するには、さまざまな部分(つまり、副問題)を解決し、副問題の解に基づいて元の問題の解を得る必要があります。動的プログラミングは、再帰的な問題を最適化するためによく使用されます。同じサブ問題の多くを解決するために再帰的な方法が使用されている場合、動的計画法のアイデアは計算量を減らすことができます。動的計画法は、各副問題を1回だけ解決し、自然剪定の機能を備えているため、計算量が削減されます。特定の副問題の解が計算されると、それは記憶されて保存されるため、次回は同じ副問題の解が必要になりますメーターを直接確認してください。

  • 一歩
  1. 動的プログラミングのステータスを決定する

  2. 状態遷移方程式を書く(状態遷移表を描く)

  3. 初期化条件を検討する

  4. 出力状態を考慮する

  5. 時間と空間の複雑さの最適化を検討(ボーナス)


第二に、実際のプログラミング

2.1例:質問300の最も長い昇順のサブシーケンス
  • タイトル説明

順序付けされていない整数配列を指定して、最も長い昇順のサブシーケンスの長さを見つけます。

  • 問題解決のアイデア

ステップ1:動的プログラミングのステータスを確認する

このトピックでは、1次元配列dpを直接使用して遷移状態を格納できます。dp[i]は、nums [i]で終わる最も長く増加するサブシーケンスの長さとして定義できます。


ステップ2:適切な状態遷移方程式を書く

数学の帰納的思考を使用して、正確な状態方程式を記述します。dp[i]の現在の長さをdp [i]と比較して、新しいサブシーケンス長を生成します。jを使用して、iより小さいすべてのグループのインデックスを表します。次のコード式で表すことができます

for i in range(len(nums)):
    for j in range(i):
    	if nums[i]>nums[j]:
    		dp[i]=max(dp[i],dp[j]+1)

ステップ3:初期条件を検討する

境界値の考慮事項は、主に3つの場所に分かれています。

(1)dp配列全体の初期値;(2)2次元のdp配列の位置i = 0およびj = 0

(3)dpストレージ状態の長さは、配列全体の長さまたは配列の長さ+ 1であり、特別な注意が必要です。

補足:一般的に使用されるいくつかのPython初期化メソッド

# 产生全为1且长度为n的数组
dp=[1 for _ in range(n)]
dp=[1]*n

# 产生全为0,长度为m,宽度为n的二维矩阵
dp=[[0 for _ in range(n)] for _ in range(m)]
dp=[[0]*n for _ in range(m)]

ステップ4:出力状態を検討する

配列のどの値が必要ですか:

(1)dp配列の最後の値を出力として返します。これは通常、2次元のdp問題に対応しています。

(2)dp配列の最大数を返します。これは通常、レコード最大値の問題に対応しています。

(3)保存された最大値を、通常はMaxval = max(Maxval、dp [i])の形式で返します。

この質問に対する動的計画法の標準的な答え

class Solution(object):
	def lengthOfLIS(self, nums: List[int]) -> int:
    	    if not nums:return 0  # 判断边界条件
        	dp=[1]*len(nums)      # 初始化dp数组状态
        	for i in range(len(nums)):
            	for j in range(i):
                	if nums[i]>nums[j]:   # 根据题目所求得到状态转移方程
                    	dp[i]=max(dp[i],dp[j]+1)
        	return max(dp)  # 确定输出状态

ステップ5:時間と空間の複雑さの最適化を検討する(ボーナス)

dpリストをトラバースする以前の方法では、O(N)O(N)が必要ですO N 、各dp [i]の計算にはO(N)O(N)が必要O N 時間なので、全体の複雑さはO(N 2)O(N ^ 2)O N2dpリストをトラバースする時間の複雑さを減らすことはできませんが、各ラウンドで[0、i]のdp [i]要素をトラバースする時間の複雑さは、設計状態の定義で考慮することができるため、dp全体は、二分法を使用できるソートされたリストになります時間の複雑さをO(N log N)O(NlogN)に減らすにはO N l o g N


テンプレートの概要:

上記の方法に従って、テンプレートとして要約し、実際の戦闘を行うことができます。

for i in range(len(nums)):
	for j in range(i):
		dp[i]=最值(dp[i], dp[j], ...)
2.2問題674:最長の連続増加シーケンス
  • タイトル説明

整数の並べ替えられていない配列が与えられた場合、最長かつ連続的に増加するシーケンスを見つけます。

  • 標準的な答え
def findLengthOfLCIS(self, nums: List[int]) -> int:
        if not nums:return 0  # 判断边界条件
        dp=[1]*len(nums)      # 初始化dp数组状态
        # 注意需要得到前一个数,所以从1开始遍历,否则会超出范围
        for i in range(1,len(nums)): 
        	if nums[i]>nums[i-1]: # 根据题目所求得到状态转移方程
                    dp[i]=dp[i-1]+1
                else:
                    dp[i]=1
        return max(dp)  # 确定输出状态
2.3質問5の最長回文部分文字列
  • タイトル説明

文字列sを指定して、sで最も長い回文の部分文字列を見つけます。sの最大長は1000と想定できます。

  • 標準的な答え
def longestPalindrome(self, s: str) -> str:
	length=len(s)
	if length<2:  # 判断边界条件
		return s
	dp=[[False for _ in range(length)]for _ in range(length)] # 定义dp状态矩阵
	# 定义初试状态,这步其实可以省略
	# for i in range(length):
	# dp[i][i]=True
	max_len=1
	start=0 # 后续记录回文串初试位置
	for j in range(1,length):
		for i in range(j):
		# 矩阵中逐个遍历
			if s[i]==s[j]:
				if j-i<3:
					dp[i][j]=True
				else:
					dp[i][j]=dp[i+1][j-1]
			if dp[i][j]: # 记录位置,返回有效答案
				cur_len=j-i+1
				if cur_len>max_len:
					max_len=cur_len
					start=i
	return s[start:start+max_len]
2.4質問516の最長回文サブシーケンス
  • タイトル説明

文字列sを指定して、sで最も長い回文の部分文字列を見つけます。sの最大長は1000と想定できます。

  • 標準的な答え
def longestPalindromeSubseq(self, s: str) -> int:
        n=len(s)
        dp=[[0]*n for _ in range(n)]  # 定义动态规划状态转移矩阵
        for i in range(n):  # 初始化对角线,单个字符子序列就是1
            dp[i][i]=1
        for i in range(n,-1,-1):  # 从右下角开始往上遍历
            for j in range(i+1,n):
                if s[i]==s[j]:   # 当两个字符相等时,直接子字符串加2
                    dp[i][j]= dp[i+1][j-1]+2  
                else:           # 不相等时,取某边最长的字符
                    dp[i][j]=max(dp[i][j-1],dp[i+1][j])
        return dp[0][-1]   # 返回右上角位置的状态就是最长
2.5問題72距離の編集
  • タイトル説明

2つのワードword1とword2が与えられた場合、word1をword2に変換するために使用されるオペランドの最小数を計算します。

  • 標準的な答え
def minDistance(self, word1, word2):
# m,n 表示两个字符串的长度
	m=len(word1) 
	n=len(word2)
	# 构建二维数组来存储子问题
	dp=[[0 for _ in range(n+1)] for _ in range(m+1)]
	# 考虑边界条件,第一行和第一列的条件
	for i in range(n+1):
		dp[0][i]=i  # 对于第一行,每次操作都是前一次操作基础上增加一个单位的操作
	for j in range(m+1):
		dp[j][0]=j # 对于第一列也一样,所以应该是1,2,3,4,5...
	for i in range(1,m+1):  # 对其他情况进行填充
		for j in range(1,n+1):
			if word1[i-1]==word2[j-1]: # 当最后一个字符相等的时候,就不会产生任何操作代价,所以与dp[i-1][j-1]一样
				dp[i][j]=dp[i-1][j-1]
			else:
				dp[i][j]=min(dp[i-1][j],dp[i][j-1],dp[i-1][j-1])+1 # 分别对应删除,添加和替换操作
	return dp[-1][-1] # 返回最终状态就是所求最小的编辑距离
2.6問題198
  • タイトル説明

あなたはプロの泥棒で、通り沿いに家を盗もうとしています。各部屋には一定量の現金が隠されています。盗難に影響する唯一の制約は、隣接する家に相互接続された盗難防止システムが装備されていることです。隣接する2つの家が同じ夜に泥棒に侵入された場合、システムは自動的に警告します。

  • 標準的な答え
def rob(self, nums):  
	if(not nums):   # 特殊情况处理
		return 0
	if len(nums)==1:
		return nums[0]
	n=len(nums)
	dp=[0]*n    # 初始化状态转移数组
	dp[0]=nums[0]  # 第一个边界值处理
	dp[1]=max(nums[0],nums[1]) # 第二个边界值处理
	for i in range(2,n):
		dp[i]=max(dp[i-2]+nums[i],dp[i-1]) # 状态转移方程
	return dp[-1]
2.7問題213:強盗II
  • タイトル説明

あなたはプロの泥棒で、通り沿いに家を盗もうとしています。各部屋には一定量の現金が隠されています。盗難に影響する唯一の制約は、隣接する家に相互接続された盗難防止システムが装備されていることです。隣接する2つの家が同じ夜に泥棒に侵入された場合、システムは自動的に警告します。

  • 標準的な答え
def rob(self, nums: List[int]) -> int:
        if not nums:
            return 0
        elif len(nums)<=2:
            return max(nums)
        def helper(nums):
            if len(nums)<=2:
                return max(nums)
            dp=[0]*len(nums)
            dp[0]=nums[0]
            dp[1]=max(nums[0],nums[1])
            for i in range(2,len(nums)):
                dp[i]=max(dp[i-1],dp[i-2]+nums[i])
            return dp[-1]
        return max(helper(nums[1:]),helper(nums[:-1]))

おすすめ

転載: blog.csdn.net/xylbill97/article/details/108169009