lc 289. 生命游戏
Description
根据 百度百科 ,生命游戏,简称为生命,是英国数学家约翰·何顿·康威在 1970 年发明的细胞自动机。
给定一个包含 m × n 个格子的面板,每一个格子都可以看成是一个细胞。每个细胞都具有一个初始状态:1 即为活细胞(live),或 0 即为死细胞(dead)。每个细胞与其八个相邻位置(水平,垂直,对角线)的细胞都遵循以下四条生存定律:
如果活细胞周围八个位置的活细胞数少于两个,则该位置活细胞死亡;
如果活细胞周围八个位置有两个或三个活细胞,则该位置活细胞仍然存活;
如果活细胞周围八个位置有超过三个活细胞,则该位置活细胞死亡;
如果死细胞周围正好有三个活细胞,则该位置死细胞复活;
根据当前状态,写一个函数来计算面板上所有细胞的下一个(一次更新后的)状态。下一个状态是通过将上述规则同时应用于当前状态下的每个细胞所形成的,其中细胞的出生和死亡是同时发生的。
示例:
输入:
[
[0,1,0],
[0,0,1],
[1,1,1],
[0,0,0]
]
输出:
[
[0,0,0],
[1,0,1],
[0,1,1],
[0,1,0]
]
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/game-of-life
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
Solution
解题思路
分析:11 代表细胞活的, 00 代表细胞死的,那么这个位置就四种状态,用【下一个状态,当前状态】表示,最后需要用右移操作更新结果
状态 0: 00 ,死的,下一轮还是死的
状态 1: 01,活的,下一轮死了
状态 2: 10,死的,下一轮活了
状态 3: 11,活的,下一轮继续活着
进一步:下一轮活的可能有两种,也就是要把单元格变为 1
这个活细胞周围八个位置有两个或三个活细胞,下一轮继续活,属于 11
这个细胞本来死的,周围有三个活着的,下一轮复活了,属于 10
那遍历下每个格子看他周围细胞有多少个活细胞就行了,然后更改为状态,那么对于第一种可能,把 board[i][j]设置为 3,对于第二种可能状态设置为 2,设置个高位flag,遍历后面的格子,拿到与他相邻的格子中有多少个 alive 的,和 1 与一下即可,最后我们把 board[i][j]右移 1位,更新结果。
lc 56. 合并区间
Description
给出一个区间的集合,请合并所有重叠的区间。
示例 1:
输入: [[1,3],[2,6],[8,10],[15,18]]
输出: [[1,6],[8,10],[15,18]]
解释: 区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].
示例 2:
输入: [[1,4],[4,5]]
输出: [[1,5]]
解释: 区间 [1,4] 和 [4,5] 可被视为重叠区间。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/merge-intervals
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
Solution
经验:区间类的问题,一般而言是需要画图思考的。因为只有建立直观的感觉,才能更有效的去思考解决问题的方案。
还有需要画图思考的相关算法问题有(其实绝大部分都需要打草稿,大神除外):
和物理现象相关的:第 42 题:接雨水问题、第 11 题:盛最多水的容器、第 218 题:天际线问题;
本身问题描述就和图形相关的问题:第 84 题:柱状图中最大的矩形;
链表问题:穿针引线如果不画图容易把自己绕晕;
回溯算法问题:根据示例画图发现每一步的选择和剪枝的条件;
动态规划问题:画示意图发现最优子结构。
得出结论:可以被合并的区间一定是有交集的区间,前提是区间按照左端点排好序,这里的交集可以是一个点(例如例 2)。
至于为什么按照左端点升序排序,这里要靠一点直觉猜想,我没有办法说清楚是怎么想到的,有些问题的策略是按照右端点升序排序(也有可能是降序排序,具体问题具体分析)。
接着说,直觉上,只需要对所有的区间按照左端点升序排序,然后遍历。
如果当前遍历到的区间的左端点 > 结果集中最后一个区间的右端点,说明它们没有交集,此时把区间添加到结果集;
如果当前遍历到的区间的左端点 <= 结果集中最后一个区间的右端点,说明它们有交集,此时产生合并操作,即:对结果集中最后一个区间的右端点更新(取两个区间的最大值)。
这里用到的算法思想是:贪心算法。
在具体的算法描述中:
前提:区间按照左端点排序;
贪心策略:在右端点的选择中,如果产生交集,总是将右端点的数值更新成为最大的,这样就可以合并更多的区间,这种做法是符合题意的。
复杂度分析:
时间复杂度:O(NlogN),这里 N 是区间的长度;
空间复杂度:O(N),保存结果集需要的空间,这里计算的是最坏情况,也就是所有的区间都没有交点的时候。
class Solution {
public int[][] merge(int[][] intervals) {
int len = intervals.length;
if(len<2){
return intervals;
}
//按照区间左端排序
Arrays.sort(intervals,(v1,v2)->v1[0]-v2[0]);
List<int[]> res = new ArrayList<>();
res.add(intervals[0]);
for(int i=1;i<len;i++){
int[] curIntervals = intervals[i];
// 每次新遍历到的列表与当前结果集中的最后一个区间的末尾端点进行比较
int[] peek = res.get(res.size()-1);
if(curIntervals[0]>peek[1]){
res.add(curIntervals);
}else{
peek[1] = Math.max(curIntervals[1],peek[1]);
}
}
return res.toArray(new int[res.size()][]);
}
}
lc 122. 买卖股票的最佳时机 II
Description
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:
输入: [7,1,5,3,6,4]
输出: 7
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。
示例 2:
输入: [1,2,3,4,5]
输出: 4
解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。
因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。
示例 3:
输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。
提示:
1 <= prices.length <= 3 * 10 ^ 4
0 <= prices[i] <= 10 ^ 4
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-ii
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
Solution
贪心
算法流程:
遍历整个股票交易日价格列表 price,策略是所有上涨交易日都买卖(赚到所有利润),所有下降交易日都不买卖(永不亏钱)。
设 tmp 为第 i-1 日买入与第 i 日卖出赚取的利润,即 tmp = prices[i] - prices[i - 1] ;
当该天利润为正 tmp > 0,则将利润加入总利润 profit;当利润为 00 或为负,则直接跳过;
遍历完成后,返回总利润 profit。
复杂度分析:
时间复杂度 O(N) : 只需遍历一次price;
空间复杂度 O(1) : 变量使用常数额外空间。
这道题 “贪心” 的地方在于,对于 “今天的股价 - 昨天的股价”,得到的结果有 3 种可能:(1)正数(2)0(3)负数。
贪心算法的决策是:只加正数。
class Solution {
public int maxProfit(int[] prices) {
int res=0;
for(int i=1;i<prices.length;i++){
int temp = prices[i]-prices[i-1];
if(temp>0){
res = res+temp;
}
}
return res;
}
}
lc 300. 最长上升子序列
Description
给定一个无序的整数数组,找到其中最长上升子序列的长度。
示例:
输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/longest-increasing-subsequence
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
Solution
首先,需要对「子序列」和「子串」这两个概念进行区分;
子序列(subsequence)
子序列并不要求连续,例如:序列 [4, 6, 5] 是 [1, 2, 4, 3, 7, 6, 5] 的一个子序列。
子串(substring、subarray)
子串一定是连续的,例如:「力扣」第 3 题:“无重复字符的最长子串”,「力扣」第 53 题:“最大子序和”。
其次,题目中的「上升」的意思是「严格上升」,[1, 2, 2, 3] 都不能算作「上升子序列」;
第三,子序列中元素的相对顺序很重要。
它们必须保持在原始数组中的相对顺序。如果把这个限制去掉,将原始数组去重以后,元素的个数即为所求。
「动态规划」的基本思想是:
从一个小问题出发(「动态」这个词的意思的意思),通过「状态转移」,并且逐步记录求解问题的过程(「规划」这个词的意思,就是「打表格」,以「以空间换时间」),逐步得到所求规模的问题的解。
对于这道题来说,就是一个数一个数地去考虑(区别于递归的写法,直接面对问题求解)。
为了从一个较短的上升子序列得到一个较长的上升子序列,我们主要关心这个较短的上升子序列结尾的元素。由于要保证子序列的相对顺序,在程序读到一个新的数的时候,如果比已经得到的子序列的最后一个数还大,那么就可以放在这个子序列的最后,形成一个更长的子序列;
于是我们可以这样定义状态:
第 1 步:定义状态
由于一个子序列一定会以一个数结尾,于是将状态定义成:dp[i] 表示以 nums[i] 结尾的「上升子序列」的长度。注意:这个定义中 nums[i] 必须被选取,且必须是这个子序列的最后一个元素。
第 2 步:考虑状态转移方程
遍历到 nums[i] 时,需要把下标 i 之前的所有的数都看一遍;
只要 nums[i] 严格大于在它位置之前的某个数,那么 nums[i] 就可以接在这个数后面形成一个更长的上升子序列;
因此,dp[i] 就等于下标 i 之前严格小于 nums[i] 的状态值的最大者 +1。
语言描述:在下标 i 之前严格小于 nums[i] 的所有状态值中的最大者 + 1。dp[i]=max(dp[i],dp[j]+1)
第 3 步:考虑初始化
dp[i] = 1,1 个字符显然是长度为 11 的上升子序列。
第 4 步:考虑输出
这里要注意,不能返回最后一个状态值;
还是根据定义,最后一个状态值只是以 nums[len - 1] 结尾的「上升子序列」的长度;
状态数组 dp 的最大值才是最后要输出的值。
时间复杂度:O(N^2),这里 N 是数组的长度,我们写了两个 for 循环,每个 for 循环的时间复杂度都是线性的。
空间复杂度:O(N),要使用和输入数组长度相等的状态数组,因此空间复杂度是 O(N)。
class Solution {
public int lengthOfLIS(int[] nums) {
int len = nums.length;
if (len < 2) {
return len;
}
int[] dp = new int[len];
Arrays.fill(dp,1);
for(int i=1;i<len;i++){
for(int j=0;j<i;j++){
if(nums[j]<nums[i]){
dp[i] = Math.max(dp[i],dp[j]+1);
}
}
}
int res = 0;
for(int i=0;i<dp.length;i++){
res = Math.max(res,dp[i]);
}
return res;
}
}
lc 3. 无重复字符的最长子串
Description
给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。
示例 1:
输入: "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
示例 2:
输入: "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
示例 3:
输入: "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/longest-substring-without-repeating-characters
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
Solution
这道题主要用到思路是:滑动窗口
定义一个 map 数据结构存储 (k, v),其中 key 值为字符,value 值为字符位置 +1,加 1 表示从字符位置后一个才开始不重复
我们定义不重复子串的开始位置为 start,结束位置为 end
随着 end 不断遍历向后,会遇到与 [start, end] 区间内字符相同的情况,此时将字符作为 key 值,获取其 value 值,并更新 start,此时 [start, end] 区间内不存在重复字符
无论是否更新 start,都会更新其 map 数据结构和结果 ans。
时间复杂度:O(n)
class Solution {
public int lengthOfLongestSubstring(String s) {
int len = s.length(),ans=0;
HashMap<Character,Integer> map = new HashMap<>();
int end = 0, start = 0;
for(;end<len;end++){
char alpha = s.charAt(end);
if(map.containsKey(alpha)){
start = Math.max(start,map.get(alpha));
}
ans = Math.max(end-start+1,ans);
map.put(s.charAt(end),end+1);
}
return ans;
}
}
lc 53. 最大子序和
Description
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例:
输入: [-2,1,-3,4,-1,2,1,-5,4],
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/maximum-subarray
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
Solution
动态规划的是首先对数组进行遍历,当前最大连续子序列和为 sum,结果为 ans
如果 sum > 0,则说明 sum 对结果有增益效果,则 sum 保留并加上当前遍历数字
如果 sum <= 0,则说明 sum 对结果无增益效果,需要舍弃,则 sum 直接更新为当前遍历数字
每次比较 sum 和 ans的大小,将最大值置为ans,遍历结束返回结果
时间复杂度:O(n)
class Solution {
public int maxSubArray(int[] nums) {
int ans=nums[0];
int sum=0;
for(int i=0;i<nums.length;i++){
if(sum>0)sum=sum+nums[i];
if(sum<=0)sum = nums[i];
ans = Math.max(ans, sum);
}
return ans;
}
}