推荐一下大佬的文章,受益匪浅一份给算法新人们的「动态规划」讲解
什么是动态规划,也就是核心?也可以是做题思路
- 问题能划分为几个小问题
- 问题的解只依赖前面问题的解
什么题适合 动态规划?
- 这种找数组找满足条件的最优解的问题
类型1
leetcode 42 接雨水
- 问题能划分为几个小问题?可以,从0到1时的雨水量、从0到2时的雨水量、从0到3时的雨水量......
- 问题的解只依赖前面问题的解?是的
核心思路:
- 对于下标 i,下雨后水能到达的最大高度等于下标 i 两边的最大高度的最小值,下标 i 处能接的雨水量等于下标 i 处的水能到达的最大高度减去height[i]。
- 也就是俩dp数组
public int trap(int[] height) {
int n = height.length;
// 显而易见,从0至少有 3 根柱子才能存住水
if(n < 3) return 0;
int[] leftMax = new int[n];
leftMax[0] = height[0];
for(int i = 1; i < n; i++) {
leftMax[i] = Math.max(leftMax[i - 1], height[i]);
}
int[] rightMax = new int[n];
rightMax[n - 1] = height[n - 1];
for(int i = n - 2; i >= 0; i--) {
rightMax[i] = Math.max(rightMax[i + 1], height[i]);
}
int res = 0;
for(int i = 0; i < n; i++) {
// 核心逻辑
res += Math.min(leftMax[i], rightMax[i]) - height[i];
}
return res;
}
leetcode 5 最长回文字符串
- 问题能划分为几个小问题?可以,字符串从i到j时是回文,那么去掉头尾,i+1到j-1一定也是回文
- 问题的解只依赖前面问题的解?是的
核心思路:
- 动态规划,什么时候可以用动态规划呢?数组的某种情况下的最优解,
- 如果用dp[i][j]=1表示字符串从i到j是否是回文
- 该题,明显就是回文,回文去掉头尾,他一定也是回文,也就是dp[i+1][j-1]=1,
- 也就是如果dp[i][j]=1,且s[i-1]=s[j+1],dp[i-1][j+1]=1
- 这很明显就要有条件,j-1 >= i+1,也就是j-i >=2, 也就是有两种情况,
- 1.j-i=2,也就是3个字符判断回文,只要s[j]=s[i],即可
- 2.j-i > 2,递归找dp[i+1][j-1]
- 如果j-i<2呢,
- 很明显有j=i,j=i+1俩种情况,
- 1.j=i,他一定是回文,
- 2.j=i+1且 s[j]=s[i],他是回文
public String longestPalindrome(String s) {
int len = s.length();
if (len < 2) {
return s;
}
int maxLen = 1;
int begin = 0;
// dp[i][j] 表示 s[i..j] 是否是回文串
int[][] dp = new int[len][len];
// 初始化:所有长度为 1 的子串都是回文串
for (int i = 0; i < len; i++) { dp[i][i] = 1; }
char[] charArray = s.toCharArray();
// 递推开始
// 先枚举子串长度
for (int L = 2; L <= len; L++) {
// 枚举左边界,左边界的上限设置可以宽松一些
for (int i = 0; i < len; i++) {
// 由 L 和 i 可以确定右边界,即 j - i + 1 = L 得
int j = L + i - 1;
// 如果右边界越界,就可以退出当前循环
if (j >= len) { break;}
if (charArray[i] == charArray[j]) {
if (j - i < 3) { dp[i][j] = 1; }
else { dp[i][j] = dp[i + 1][j - 1]; }
}
// 只要 dp[i][L] == true 成立,就表示子串 s[i..L] 是回文,此时记录回文长度和起始位置
if (dp[i][j] ==1 && j - i + 1 > maxLen) {
maxLen = j - i + 1;
begin = i;
}
}
}
return s.substring(begin, begin + maxLen);
}
leetcode 22. 括号生成
- 问题能划分为几个小问题?可以,i=0时的括号生成方案、i=1时的括号生成方案、i=2时的括号生成方案......
- 问题的解只依赖前面问题的解?是的,长度n时的解只依赖<n时的解,因此可以用动态规划
核心思路:
- n时的括号生成方案
- ( + p时括号生成的全部方案 + ) + q时括号生成的全部方案,其中p+q = n-1
- 也就是说,第n个括号的位置方案
- 不能重复,p和q的组合不能重复出现
java版本代码
// 动态规划
public static List<String> generateParenthesis(int n) {
if (n==0) return new ArrayList<>();
List<String> dp0 = new ArrayList<>();
dp0.add("");
List<String> dp1 = new ArrayList<>();
dp1.add("()");
if (n==1) return dp1;
List<List<String>> dp = new ArrayList<>();
//dp 0
dp.add(dp0);
//dp 1
dp.add(dp1);
for (int i =2;i<=n;i++){
List<String> dpn = new ArrayList<>();
dp.add(dpn);
int p=0,q=i-p-1;
while(q >=0){
List<String> dpp = dp.get(p);
List<String> dpq = dp.get(q);
for (int x = 0;x < dpp.size();x++){
for (int y=0;y<dpq.size();y++){
String s = "(" + dpp.get(x) + ")" + dpq.get(y);
dpn.add(s);
}
}
p++;
q=i-p-1;
}
}
return dp.get(n);
}
leetcode 32. 最长有效括号
暴力解法:
滑动窗口,判断窗口内子串是否是连续的有效括号
// 暴力滑动窗口,从大到小 on3,超时
public static int longestValidParentheses1(String s) {
if (s == null || s.length() == 0 || s.length() == 1) return 0;
int maxLen = 2*(s.length()/2);
while(maxLen > 0){
int i = maxLen;
for (int j =0;j + i - 1< s.length();j++){
String str = s.substring(j,j+i);
if(isValid(str)) return i;
}
maxLen -=2;
}
return maxLen;
}
private static boolean isValid(String str){
Stack<Character> stack = new Stack<Character>();
if (str.charAt(str.length()-1) == '(') return false;
for(int i =0;i<str.length();i++){
char c = str.charAt(i);
if ('(' == c){
stack.push(c);
}else {
if (stack.isEmpty()) return false;
stack.pop();
}
}
return stack.isEmpty();
}
动态规划
- 问题能划分为几个小问题?可以,我们可以从s头到尾依次判断最长连续有效括号字符串长度
- 问题的解只依赖前面问题的解?可以,长度n时的解只依赖<n时的解,因此可以用动态规划
- 核心思路:
- i=n且是)时,找到对应的前面的(下标,减dp[n-1],因为要去掉前面的连续有效括号字符串长度, 也就是n - dp[n-1] - 1位, 如果是(,dp[n] +2;
- 然后加上内部的连续有效括号字符串长度,也就是这对 ()内部的值,dp[n] + dp[n-1]
- 然后看看这对()前面是否有?也要加,dp[n] + dp[n - dp[n-1] - 1]
public int longestValidParentheses(String s){
// 动态规划数组,判断下标i(包括i)前最长连续有效括号字符串
// 如果是左括号,不改变其默认值0
int[] dp = new int[s.length()];
// 找最长,留一个标志
int maxLen = 0;
for(int i = 0;i < s.length();i++){
char c = s.charAt(i);
if (c=='(') continue;
//右括号计算dp值
// 1.找该右括号对应的左括号下标
// 1.1.要减去前一位的dp值,因为如果前一位有dp值,说明前面有一些有效的括号了,要去再前一位找,也就是下标 i-dp[i-1] -1
// 1.2 0的话,说明是左括号,匹配上了, 不可能是其他值,因为是其他值的话,前一位的dp早就加上了,所以这就只能是该有括号对应的左括号下标,
// 如果他不是左括号,该右括号没有对应的,也是0
if (i-1 >= 0 && i-dp[i-1] -1 >=0){
if (s.charAt(i-dp[i-1] -1) == ')') continue;
dp[i] = 2;
// 2.找该[i - dp[i-1] -1,i]子串内是否有连续有效括号字符串,也就是判断前一位是否有dp值?
// 2.1 dp[i-1]=0,说明没有,+0
// 2.2 dp[i-1]!=0,说明内部也有,+dp[i-1]
dp[i] += dp[i-1];
//3. 连续么,找前面一位是否能连上呢?
// 也就是判断该子串左下标i - dp[i-1] -1前一位的dp值是否为0,
// 如果是0,说明连不上,+0, 如果连上了,+dp值
if (i-dp[i-1] -1 -1 >=0){
dp[i] += dp[i-dp[i-1] -1 -1 ];
}
// 判断是否是最大长度
if (dp[i] > maxLen) maxLen = dp[i];
}
}
return maxLen;
}