LeetCode解题笔记(动态规划)

1-10题

1.两数之和
穷举法
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。

你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。

你可以按任意顺序返回答案。

示例 1:
输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。

解决这题是比较容易的,只需要双层循环暴力解决即可。但是这样的时间复杂度就是O(n2)。因此,我们要考虑能否用空间换取时间。

查找表法
由题可知,我们可以遍历一遍数组,遍历的时候当前数为X,我们要寻找的另外一个数是target-X。
在这里插入图片描述

class Solution {
    public int[] twoSum(int[] nums, int target) {
        Map<Integer, Integer> hashtable = new HashMap<Integer, Integer>();
        for (int i = 0; i < nums.length; ++i) {
            if (hashtable.containsKey(target - nums[i])) {
                return new int[]{hashtable.get(target - nums[i]), i};
            }
            hashtable.put(nums[i], i);
        }
        return new int[0];
    }
}

2.两数相加
给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。

请你将两个数相加,并以相同形式返回一个表示和的链表。

你可以假设除了数字 0 之外,这两个数都不会以 0 开头。

示例 1:
在这里插入图片描述
输入:l1 = [2,4,3], l2 = [5,6,4]
输出:[7,0,8]
解释:342 + 465 = 807.

记得带上进位即可

    public static ListNode addTwoNumbers(ListNode l1, ListNode l2) {
        ListNode p1 = l1, p2 = l2;
        ListNode dummy = new ListNode(-1);
        ListNode p = dummy;
        int carry = 0, newVal = 0;
        while (p1 != null || p2 != null || carry > 0) {
            newVal = (p1 == null ? 0: p1.val) + (p2 == null ? 0: p2.val) + carry;
            carry = newVal / 10;
            newVal %= 10;
            p.next = new ListNode(newVal);
            p1 = p1 == null? null: p1.next;
            p2 = p2 == null? null: p2.next;
            p = p.next;
        }
        return dummy.next;
    }

3.二分查找

给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。

还是比较简单的一题,可以用递归实现或者循环实现,记住结束条件即可。

class Solution {
    public int search(int[] nums, int target) {
        int left = 0, right = nums.length -1;
        while(left<=right){
            int mid = left + (right - left)/2;
            if(nums[mid]==target){
                return mid;
            } else if(nums[mid]>target) {
                right = mid - 1;
            } else{
                left = mid + 1;
            }
        }
         return -1;
    }
}

4.罗马数字转整数
罗马数字包含以下七种字符: I, V, X, L,C,D 和 M。

字符 数值
I 1
V 5
X 10
L 50
C 100
D 500
M 1000
例如, 罗马数字 2 写做 II ,即为两个并列的 1 。12 写做 XII ,即为 X + II 。 27 写做 XXVII, 即为 XX + V + II 。

通常情况下,罗马数字中小的数字在大的数字的右边。但也存在特例,例如 4 不写做 IIII,而是 IV。数字 1 在数字 5 的左边,所表示的数等于大数 5 减小数 1 得到的数值 4 。同样地,数字 9 表示为 IX。这个特殊的规则只适用于以下六种情况:

I 可以放在 V (5) 和 X (10) 的左边,来表示 4 和 9。
X 可以放在 L (50) 和 C (100) 的左边,来表示 40 和 90。
C 可以放在 D (500) 和 M (1000) 的左边,来表示 400 和 900。
给定一个罗马数字,将其转换成整数。

这题不难,但是在一开始的时候,我并没有找到这个规律:若存在小的数字在大的数字的左边的情况,根据规则需要减去小的数字。对于这种情况,我们也可以将每个字符视作一个单独的值,若一个数字右侧的数字比它大,则将该数字的符号取反。
例如 XIV 可视作 X−I+V=10−1+5=14。

5. 最长回文子串
给你一个字符串 s,找到 s 中最长的回文子串。

示例 1:

输入:s = “babad”
输出:“bab”
解释:“aba” 同样是符合题意的答案。
示例 2:

输入:s = “cbbd”
输出:“bb”

这题看似简单,但是开始的时候笔者想不到好的解决方案。尝试使用暴力破解法来解决,很不幸。执行超时。
暴力破解法

class Solution {
public boolean isPalindromic(String s) {
		int len = s.length();
		for (int i = 0; i < len / 2; i++) {
			if (s.charAt(i) != s.charAt(len - i - 1)) {
				return false;
			}
		}
		return true;
	}

// 暴力解法
public String longestPalindrome(String s) {
    String ans = "";
    int max = 0;
    int len = s.length();
    for (int i = 0; i < len; i++){
        for (int j = i + 1; j <= len; j++) {
            String test = s.substring(i, j);
            if (isPalindromic(test) && test.length() > max) {
                ans = s.substring(i, j);
                max = Math.max(max, ans.length());
            }
        }
    }
    return ans;
}

}

拓展中心法
  在暴力破解法中,先用两层for循环截取字符串,再在里面使用for循环判断该字符串是否为回文字符串。所以时间复杂度为O(n3)。
  其实,在一开始的时候,也想到了这个方法。但是实现的过程中出了一点问题。该方法是将字符串从左到右,以当前字符为中心,分别向左向右来匹配字符。
在这里插入图片描述

class Solution {
    public int expandAroundCenter(int left, int right, String s) {
        int len = s.length();
        while (left >= 0 && right < s.length()) {
            if (s.charAt(left) == s.charAt(right)) {
                left--;
                right++;
            } else {
                break;
            }
        }
        return right - left - 1;
    }

    // 暴力解法
    public String longestPalindrome(String s) {
        String ans = "";
        int maxLength = 0;
        int len = s.length();
        if(len<2){
            return s;
        }
        for (int i = 0; i < len - 1; i++) {
            int begin = 0;
            int oddLen = expandAroundCenter(i, i, s);
            int evenLen = expandAroundCenter(i, i + 1, s);
            if (maxLength < Math.max(oddLen, evenLen)) {
                maxLength = Math.max(oddLen, evenLen);
                begin = i - (maxLength - 1) / 2;
                ans = s.substring(begin, begin + maxLength);
            }
        }
        return ans;
    }
}

动态规划法
在这里插入图片描述
动态规划最重要的是找到状态,并得出状态转移方程。核心是要将大问题拆解成小问题,回文串的最两侧一定是一样的字符,去掉两侧各一个字符以后,两侧还是相同的字符。这就是破题的关键。
在这里插入图片描述

我们用一个 boolean dp[l][r] 表示字符串从 i 到 j 这段是否为回文。试想如果 dp[l][r]=true,我们要判断 dp[l-1][r+1] 是否为回文。只需要判断字符串在(l-1)和(r+1)两个位置是否为相同的字符,是不是减少了很多重复计算。
进入正题,动态规划关键是找到初始状态和状态转移方程。
初始状态,l=r 时,此时 dp[l][r]=true。
状态转移方程,dp[l][r]=true 并且(l-1)和(r+1)两个位置为相同的字符,此时 dp[l-1][r+1]=true。

作者:reedfan
链接:https://leetcode.cn/problems/longest-palindromic-substring/solution/zhong-xin-kuo-san-fa-he-dong-tai-gui-hua-by-reedfa/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

6.最长公共前缀
编写一个函数来查找字符串数组中的最长公共前缀。

如果不存在公共前缀,返回空字符串 “”。

示例 1:
输入:strs = [“flower”,“flow”,“flight”]
输出:“fl”

示例 2:
输入:strs = [“dog”,“racecar”,“car”]
输出:“”
解释:输入不存在公共前缀。

最容易想到的就是横向扫描和纵向扫描,比较简单在此不赘述。还可以使用分冶和二分的算法。

7.杨辉三角
给定一个非负整数 numRows,生成「杨辉三角」的前 numRows 行。

在「杨辉三角」中,每个数是它左上方和右上方的数的和。

在这里插入图片描述

示例 1:

输入: numRows = 5
输出: [[1],[1,1],[1,2,1],[1,3,3,1],[1,4,6,4,1]]
示例 2:

输入: numRows = 1
输出: [[1]]

其实这题也是动态规划的简单应用,我们要找出状态的转换。可以知道的是,每行除了第一个和最后一个元素为1,其他元素都满足状态:

f[i][j] = f[i - 1][j] + f[i - 1][j - 1];
class Solution {
    public List<List<Integer>> generate(int numRows) {
        int f[][] = new int[numRows][numRows];
        f[0][0] = 1;
        List<List<Integer>> answerList = new ArrayList<>();
        ArrayList<Integer> initList = new ArrayList<>();
        initList.add(1);
        answerList.add(initList);
        for (int i = 1; i < numRows; ++i) {
            List<Integer> elementList = new ArrayList<>();
            for (int j = 0; j <= i; ++j) {
                if (j == 0 || j == numRows - 1) {
                    f[i][j] = 1;
                } else {
                    f[i][j] = f[i - 1][j] + f[i - 1][j - 1];
                }
                elementList.add(f[i][j]);
            }
            answerList.add(elementList);
        }
        return answerList;
    }
}

8.正则表达式匹配
给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 ‘.’ 和 ‘*’ 的正则表达式匹配。

‘.’ 匹配任意单个字符
‘*’ 匹配零个或多个前面的那一个元素
所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。

示例 1:

输入:s = “aa”, p = “a”
输出:false
解释:“a” 无法匹配 “aa” 整个字符串。
示例 2:

输入:s = “aa”, p = “a*”
输出:true
解释:因为 ‘*’ 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 ‘a’。因此,字符串 “aa” 可被视为 ‘a’ 重复了一次。
示例 3:

输入:s = “ab”, p = “."
输出:true
解释:".
” 表示可匹配零个或多个(‘*’)任意字符(‘.’)。

  这题难度为难,虽然题目是动态规划的题,但是笔者一直找不到状态转换的公式。主要难点是当第j个字符时,情况如下:在这里插入图片描述
  文字化的表述还是可以看懂的,但是转化成公式的时候,一直看不懂答案里,当j字符为’*'时,为什么f[i][j] = f[i-1][j]
在这里插入图片描述
  后面才知道这是化简后的结果。
在这里插入图片描述

    public boolean isMatch(String s, String p) {
        int m = s.length();
        int n = p.length();
        s = ' ' + s;
        p = ' ' + p;
        boolean f[][] = new boolean[m + 1][n + 1];
        f[0][0] = true;
        for (int i = 0; i <= m; ++i) {
            for (int j = 1; j <= n; ++j) {
                if (j + 1 <= n && p.charAt(j + 1) == '*') continue;
                if (p.charAt(j) != '*') {
                    f[i][j] = i > 0 && f[i - 1][j - 1] && (p.charAt(j) == s.charAt(i) || p.charAt(j) == '.');
                } else {
                    f[i][j] = (j >= 2 && f[i][j - 2]) ||
                            (i > 0 && f[i - 1][j] && (p.charAt(j-1) == s.charAt(i) || p.charAt(j-1) == '.'));
                }
            }
        }
        return f[m][n];
    }

9.打家劫舍
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

示例 1:

输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
示例 2:

输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
偷窃到的最高金额 = 2 + 9 + 1 = 12 。

解: 又是一道动态规划的题,我们需要找到状态转换的方程。当遇到第i家时,为了偷得最多则有:

 dp[i] = Math.max(dp[i - 1], nums[i] + dp[i - 2]);
    public int rob(int[] nums) {
        int[] dp = new int[nums.length];
        if (nums.length >= 2)
            dp[1] = Math.max(nums[0], nums[1]);
        dp[0] = nums[0];
        for (int i = 2; i < nums.length; ++i) {
            dp[i] = Math.max(dp[i - 1], nums[i] + dp[i - 2]);
        }
        return dp[nums.length - 1];
    }

10.删除并获得点数
给你一个整数数组 nums ,你可以对它进行一些操作。

每次操作中,选择任意一个 nums[i] ,删除它并获得 nums[i] 的点数。之后,你必须删除 所有 等于 nums[i] - 1 和 nums[i] + 1 的元素。

开始你拥有 0 个点数。返回你能通过这些操作获得的最大点数。

示例 1:

输入:nums = [3,4,2]
输出:6
解释:
删除 4 获得 4 个点数,因此 3 也被删除。
之后,删除 2 获得 2 个点数。总共获得 6 个点数。
示例 2:

输入:nums = [2,2,3,3,3,4]
输出:9
解释:
删除 3 获得 3 个点数,接着要删除两个 2 和 4 。
之后,再次删除 3 获得 3 个点数,再次删除 3 获得 3 个点数。
总共获得 9 个点数。

解: 难点还是在如何找到状态转换,题中说点数相邻的两个点数要去掉,其实就是打家劫舍的另一个版本。此外,我们还可以将数组中,同一点数进行累加。最后调用打家劫舍的算法来实现即可。

给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。

你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。

返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。

11-20题

11.买卖股票的最佳时机

示例 1:

输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
示例 2:

输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 没有交易完成, 所以最大利润为 0。

解: 我们知道的是

dp[i] = Max(nums[i] - 前i-1的最小值, dp[i-1]);
class Solution {
    public int maxProfit(int[] prices) {
        int minPrice = prices[0];
        int[] dp = new int[prices.length];
        for(int i=1;i<prices.length;++i){
            dp[i] = Math.max(dp[i-1], prices[i]-minPrice);
            minPrice = Math.min(minPrice, prices[i]);
        }
        return dp[prices.length-1];
    }
}

12.买卖股票的最佳时机2
给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。

在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。

返回 你能获得的 最大 利润 。

示例 1:

输入:prices = [7,1,5,3,6,4]
输出:7
解释:在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4 。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6 - 3 = 3 。
总利润为 4 + 3 = 7 。
示例 2:

输入:prices = [1,2,3,4,5]
输出:4
解释:在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4 。
总利润为 4 。
示例 3:

输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 交易无法获得正利润,所以不参与交易可以获得最大利润,最大利润为 0 。

解: 这题可以用累加法解决,但是这里讨论一下动态规划的做法。

定义状态dp[i][0] 表示第 i 天交易完后手里没有股票的最大利润,dp[i][1] 表示第 i 天交易完后手里持有一支股票的最大利润(i 从 0 开始)。

dp[i][0] = Max(dp[i-1][1] + prices[i], dp[i-1][0]);
dp[i][1] = Max(dp[i-1][1], dp[i-1][0] - prices[i]);

然而为了使利益最大,最后一天肯定要卖出。

    public int maxProfit(int[] prices) {
        int minPrice = prices[0];
        int len = prices.length;
        int[][] dp = new int[len][2];
        dp[0][1] = -prices[0];
        dp[0][0] = 0;
        for(int i=1; i<len; ++i){
            dp[i][0] = Math.max(dp[i-1][1] + prices[i], dp[i-1][0]);
            dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] - prices[i]);
        }
        return dp[len-1][0];  
    }

12.买卖股票的最佳时机3
给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。

注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

示例 1:

输入:prices = [3,3,5,0,0,3,1,4]
输出:6
解释:在第 4 天(股票价格 = 0)的时候买入,在第 6 天(股票价格 = 3)的时候卖出,这笔交易所能获得利润 = 3-0 = 3 。
随后,在第 7 天(股票价格 = 1)的时候买入,在第 8 天 (股票价格 = 4)的时候卖出,这笔交易所能获得利润 = 4-1 = 3 。
示例 2:

输入:prices = [1,2,3,4,5]
输出:4
解释:在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。
因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。
示例 3:

输入:prices = [7,6,4,3,1]
输出:0
解释:在这个情况下, 没有交易完成, 所以最大利润为 0。
示例 4:

输入:prices = [1]
输出:0

解: 多了一个买卖的情况,相当于买、卖分别多了一种状态,相当于多了两种,总共有四种状态。

class Solution {
    public int maxProfit(int[] prices) {
        int len = prices.length;
        int[][] dp = new int[len][4];
        dp[0][0] = -prices[0];
        dp[0][1] = 0;
        dp[0][2] = -prices[0];
        dp[0][3] = 0;
        for(int i=1; i<len; ++i){
            dp[i][0] = Math.max(-prices[i], dp[i-1][0]);
            dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] + prices[i]);
            dp[i][2] = Math.max(dp[i-1][1] - prices[i], dp[i-1][2]);
            dp[i][3] = Math.max(dp[i-1][2] + prices[i], dp[i-1][3]);
        }
        return Math.max(dp[len - 1][1], dp[len - 1][3]);
    }
}

13.最佳观光组合
给你一个正整数数组 values,其中 values[i] 表示第 i 个观光景点的评分,并且两个景点 i 和 j 之间的 距离 为 j - i。

一对景点(i < j)组成的观光组合的得分为 values[i] + values[j] + i - j ,也就是景点的评分之和 减去 它们两者之间的距离。

返回一对观光景点能取得的最高分。

示例 1:

输入:values = [8,1,5,2,6]
输出:11
解释:i = 0, j = 2, values[i] + values[j] + i - j = 8 + 5 + 0 - 2 = 11
示例 2:

输入:values = [1,2]
输出:2

解: 找到状态转换的方程,其他都迎刃而解

class Solution {
    public int maxScoreSightseeingPair(int[] values) {
        int len = values.length;
        int[] dp = new int[len];
        dp[0] = 0;
        dp[1] = values[0] + 0 + values[1] - 1;
        int temp = Math.max(values[0] + 0, values[1] + 1 );
        for(int i=2;i<len;++i){
            dp[i] = Math.max(temp + values[i]-i, dp[i-1]);
            temp = values[i] + i;
        }
        return dp[len-1];
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值