前几天在力扣刷题时遇到了动态规划的问题,看了题解之后有些兴趣便自己下去多了解了一下,在此分享给大家(另外给的例题都是自己做过的,然后思考了下顺便分享下,希望大家喜欢~
本篇博客将分为两个部分:
那么接下来就让我们开始吧~
动态规划(Dynamic programming,简称 DP)中本阶段的状态往往是上一阶段状态和上一阶段决策的结果。换言之就是原问题可以拆解成若干个子问题,而这些子问题又可拆解……最后可以由初始问题的解来推出原问题的解。
而这之中拆解的过程又是耐人寻味的且有趣的。
也许到这你还是对动态规划不是很理解,那么就来谈谈这当中的经典问题:爬楼梯
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
初看觉得可以用排列组合来解,但那样的话n 越大越难解,效率也会很低
这时就要提到动态规划了:
n
阶台阶可以分为n-1
阶台阶走一阶台阶和n-2
阶台阶走两步,所以,n
阶台阶的走法就是n-1
阶台阶走法加上n-2
阶台阶走法。这时可得状态转移方程:F(n) = F(n-1) + F(n-2) (n >= 3)
(这里如果是n-2
阶台阶走1 1 两次的话,又会回到n-1
阶台阶上)
将原问题进行拆解,也就是随着n 的减少,到最后剩下1阶台阶和2阶台阶,而1阶台阶只能是走1步,2阶台阶可以是1 1 走两次和走2步到,所以可得F(1) = 1, F(2) = 2
,这就是问题的边界
可以看出,从最开始的问题解可以逐渐推出最终问题的解,妙啊~
所以动态规划问题的精髓就在于状态转移方程
初看完题目,可以确定的是一个状态所能达到的最长总时间与之前两个状态和本状态的分钟数有关
观察题目,因为是不能接收相邻的预约,而且预约的总时间要最长,所以第i
次、第i-1
次和第i-2
次预约就有了关系:
i
次预约,那么第i-1
次预约就休息,此时i-2
次预约所能达到的最长总时间 加上第i
次预约的时间nums[i]
就得大于第i-1
次预约所能达到的最长总时间i
次预约,那么第i-1
次预约的状态就不确定(由i-1
次预约前的总时间数确定,但i-1
次预约所能达到的最长总时间数 是可以确定的),此时i-2
次预约所能达到的最长总时间 加上第i
次预约的时间nums[i]
就得小于第i-1
次预约所能达到的最长总时间定义dp[i]
为[0,i]
区间内预约所能达到的最长总时间数
所以这时的状态转移方程就为dp[i] = max(dp[i-2] + nums[i], dp[i-1])
随着i 的减少,最后来到了边界上:i = 0
和i = 1
(i = 2
即可由这两个值所得出)
i = 0
时,因为之前没有总时间数所限制,此时dp[0] = nums[0]
i = 1
时,因为和i = 0
是相邻的,此时dp[1] = max(nums[0], nums[1])
到这里本题差不多就解完了,再贴出部分代码以帮助大家理解:
int len = nums.length;
int[] dp = new int[len];
dp[0] = nums[0];
dp[1] = Math.max(nums[0], nums[1]);
for(int i = 2;i < len;i++){
dp[i] = Math.max(dp[i-1], dp[i-2] + nums[i]);
}
这里先解释一下有效括号字符串:仅由 "("
和 ")"
构成的字符串,对于每个左括号,都能找到与之对应的右括号,反之亦然。换句话说,一个有效括号是由")"
结尾的。
要注意的是题目找的是最长的有效括号的子串,也就是说满足定义的子串如果不连续就只需要最长的,举个例子:"()))()())"
,这里应该输出4。
根据题目给的例子可以看到,遇到"......()"
这样的,最长有效括号长度就加2,另外可能还有"......(())"
这样的,也是满足有效括号的定义的,这里就直接加4。
可以发现,当前元素为"("
时,有效括号长度并不会发生变化,因此可以从")"
寻找出dp方程的一个关系,这里可以定义dp[i]
的值为以第i
个元素结尾时,最长有效括号子字符串的长度,这样的话就会出现两种情况(这里为了简化,dp[i]
为"("
时,其值就直接为 0):
当元素")"
前一个元素为"("
时,也就是形如"......()"
这样的子字符串,可以推出:
dp[i] = dp[i - 2] + 2
当元素")"
前一个元素为")"
时,也就是形如"......))"
这样的子字符串。这里就要稍微复杂些,因为还要考虑前一个")"
的情况。假设这样的子字符串是一个更长的有效子字符串的一部分,那么前面就有与之对应的"("
,那么再前面的呢,又怎么去找?
假设成立的情况下,这时的子字符串就类似于这样"...((...))"
,而中间一定会出现"...()..."
这样的子串,这时就回到了第一种情况,于是就可以通过"...()..."
中的")"
来推出下一个元素")"
的dp[i]
的值。仔细想想,由于是第一种情况推出来的,那么"......))"
这里前一个元素")"
的dp[i]
的值就意味着以它结尾的子串"...(...)"
的有效子字符串的长度。
想到这里,就可以去找出前面的有效子串的长度了:
dp[i] = dp[i - dp[i-1] - 2] + dp[i-1] + 2
这里举个例子方便去理解:
i |
0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|
s[i] |
( |
( |
) |
( |
( |
) |
) |
dp[i] |
0 | 0 | 2 | 0 | 0 | 2 | 6 |
如果你想问假设出现很多个"......))))))"
这样的情况的话,其实这都是第二种情况的递推,或者说第二种情况是该情况的子问题
到这里本题差不多就解完了,但要注意越界的问题,这里贴出部分代码以帮助大家理解:
class Solution {
public int longestValidParentheses(String s) {
int len = s.length(), max = 0;
int[] dp = new int[len];
for (int i = 1; i < len; 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] = ((i - dp[i-1]) >= 2 ? dp[i - dp[i-1] - 2] : 0) + dp[i-1] + 2;
}
max = Math.max(max, dp[i]);
}
}
return max;
}
}
初看完这道题,我自己的想法是直接嵌套循环去找不同,但那些显然不合适(也不说写得出来不,时间估计都超了)
这道题其实在于怎么去寻找子问题,从而将原问题进行拆解:这一状态可以看成是由上一状态经过插入、删除、替换操作转换过来的
另外可以发现使用一维的dp[]
不好去定义它,所以这里可以用二维的dp[][]
,那么这里dp[i][j]
就代表word1
中从头到i
位置的部分 转换成 word2
中从头到j
位置的部分 所需要的最少操作数
将原问题进行拆解,拆解成子问题,子问题再进行拆解…那么初始情况又怎么去找呢,也就是边界怎么去确定呢:考虑到word1
或者word2
可能为空字符的情况,这里放一张表方便大家去理解(这里借由实例1,给出部分数据,以下讨论也是建立在实例1的基础上)
'' |
r |
o |
s |
|
---|---|---|---|---|
'' |
0 | 1 | 2 | 3 |
h |
1 | ◆1 | ▼2 | |
o |
2 | ●2 | ||
r |
3 | 2 | ||
s |
4 | 3 | ||
e |
5 | 4 |
''
即为空字符,dp[i][j]
的值即为最少操作数(比如说表中那个5
即表示horse
至少经过5次删除操作后可以得到空字符''
)
这里还需要确定的是插入、删除、替换 操作怎么通过代码去实现(还请理解不到的朋友们仔细看表格)
回到给的表格中,可以看到第一个位置为0,意思是两者都为空字符,不需要进行操作,因此操作数为0,如果这时word1
和word2
的第一个字符相等的话,那么也不需要进行操作,所以这里需要分情况讨论:
当word1[i] == word2[j]
的时候,表示最新的一步不需要进行操作,也就是说此时word1[1 ~ i-1]
转换成word2[1 ~ j-1]
的最少操作数 与 word1[1 ~ i]
转换成word2[1 ~ j]
的最少操作数是一样的
所以可以得出:dp[i][j] = dp[i-1][j-1]
当word1[i] != word2[j]
的时候,表示最新的一步需要进行操作:
替换操作:比如说word1
第一个字符 与 word2
第一个字符 可以直接替换所得(对应表格中打◆的1)
这里可以得出:dp[i][j] = dp[i-1][j-1] + 1
删除操作:前面有提到,比如说word1
前两个字符ho
先是经过一次替换得到ro
,要再得到r
,则需要一次删除操作(对应表格中打●的2)
这里可以得到:dp[i][j] = dp[i-1][j] + 1
插入操作:比如说word1
前一个字符h
要得到ro
,先是h
要进行替换得到r
,再进行插入操作(对应表格中打▼的2)
这里可以得到:dp[i][j] = dp[i][j-1] + 1
因为要得到最少操作数,所以这里表示为
dp[i][j] = Math.min(Math.min(dp[i-1][j], dp[i][j-1]), dp[i-1][j-1]) + 1;
到这里本题也差不多解完了,这里再贴出部分代码帮助大家理解:
class Solution {
public int minDistance(String word1, String word2) {
int len1 = word1.length(), len2 = word2.length();
int[][] dp = new int[len1+1][len2+1];
for (int i = 0; i <= len1; i++) {
dp[i][0] = i;
}
for (int j = 0; j <= len2; j++) {
dp[0][j] = j;
}
char[] words1 = word1.toCharArray();
char[] words2 = word2.toCharArray();
for (int i = 1; i <= len1; i++) {
for (int j = 1; j <= len2; j++) {
if (words1[i-1] == words2[j-1]){
dp[i][j] = dp[i-1][j-1];
} else {
dp[i][j] = Math.min(Math.min(dp[i-1][j], dp[i][j-1]),
dp[i-1][j-1]) + 1;
}
}
}
return dp[len1][len2];
}
}
这篇文章自己是二十多天前开始写的,加上自己还不太会写博客,以及老师布置的作业实在是太多了… 最近几天也还是把它给码完了 QAQ
创作不易,如果大家喜欢的话可以点个赞