CS3K.com 九章算法强化班
Posted jzsf
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了CS3K.com 九章算法强化班相关的知识,希望对你有一定的参考价值。
Advanced Data Structure -- Union Find
Number of Islands
思路I:BFS 避免相同位置的元素重复入列,访问过的元素都要标记为已访问。BFS为了得到下一个点的坐标,所以需要建立一个表示位置的坐标类。
思路II:并查集 等价于求集合中连通块的个数。
Number of Islands II
思路:并查集,把二维数组转化为一维father数组。
LeetCode 547. Friend Circles
思路:并查集的典型应用。题目是求朋友圈的个数,求将这n个人看成n个节点,等价于求连通块的个数。
Graph Valid Tree
思路I:BFS 最后判断连通的节点个数是否为n。
思路II:并查集 判断n - 1条边是否构成环,即判断是否有两个点的father一样。
Surrounded Reigions
思路I:BFS BFS二位矩阵的四周,讲从外向内的字符O都标记为字符V代表已访问过的字符O,然后依次便利整个二位矩阵,字符为O的都是除了四周的里面的O,将它置为字符X,为V的字符还原成原来的字符O。
思路II:并查集 建立一个n * m的dummy node,遍历二维数组,边上的为字符O的直接合并到dummy node, 在里面的字符为O的根据情况合并,如果该字符与相邻字符O的parent一样就不合并,如果不一样分两种情况,如果该字符的parent为dummy node就将相邻字符O合并到该字符的parent,否则将该字符的parent合并到相邻字符O的parent。最后再遍历一次二维数组,字符为O并且该字符的parent不是dummy node就将该字符置为字符X。也就是说,不能被包围的字符O的parent都是dummy node,能被包围的字符O的parent都不是dummy node。
Advanced Data Structure -- Trie
Implement Trie
思路:trie树的插入,查找,前最匹配。需要注意一点的就是存储子节点的HashMap不能再构造方法里初始化,而是要在定义的时候直接初始化,这是为了覆盖dummy root的HashMap子节点情况。
Add and Search Word
思路:类似上题,trie树的添加可以更加简化。查找单词要递归。
Word Search
思路:DFS 记得dfs的过程中把访问过的字符标记称某一特殊字符来表示访问过这一字符,避免重复访问。
time:O(n*m*4^len) space:O(len) + O(n*m)
Word Search II
思路:1. 把字典建成Trie树。2. 用dfs的方法遍历矩阵,同时在Trie上搜索前缀是否存在。 3. 查询所有Trie里面有可能出现的字符。 注意找到string之后还要继续往下低轨而不是return,因为下面也可能有要找的string。同时TrieNode中多了一个字符串s来代表从根走到当前位置的字符串,也就是插入的字符串。
Word Squares
思路:Trie + DFS。先构建trie,新增一个List<String> startWith属性表示包含以当前字符结尾的前缀的所有单词的集合。然后DFS遍历,注意下一个可能构成word squares的单词的前缀是由前面的单词严格限制的,也就是说下一个单子的前缀是固定的,在trie中找到包含这个前缀的所有可能的单词(startWith),对这些单词进行下一轮DFS。
Boggle Game
思路:trie + DFS 不得不说这道题真的难,搞了半天。
139. Word Break
思路:DP 两种DP方法,第一种是j从0遍历到i- 1,时间复杂度O(len^2),len为字符串s的长度。适用于字典大,s短的情况。
第二种是遍历字典中的每个单词的长度,如果canSegment[i - word.length()] == true && s.substring(i - word.length(), i).equals(当前遍历的单词),那么canSegment[i]为true。时间复杂度O(len * size),len为字符串s的长度,size为字典的大小。适用于字典小,s大的情况。而且本题中字典为list而不是set,判断list是否含有某个元素的contains方法时间复杂度是O(size)的不是O(1),所以建议使用第二种方法。
140. Word Break II
思路:和上一题不同的是,这道题要返回所有可能的组合。所以现在dp[i]里面应该存可以使长度为i所有可能的String里的最后一个word。dp有两种写法,一个就是直接写成数组:List[]的形式,不能形成的dp[i] = null。还有一个是用个hashmap:Map<Integer, List>,用hashmap的好处是如果s很长而且用dict能组合成的长度不是很多的话,map用的空间相对少。dp结束之后,第二步就是通过dp里面保存的word,一步一步回溯找到所有结果。
472. Concatenated Words
思路I:DP 常规思路是遍历数组中的每一个单词,然后对该单词,查看该单词是否能右数组中剩余的元素组成,这样做时间复杂度过高。需要进行优化,一个单词要想被其他词组成,其他词的长度必然小于这个词的长度,于是先对数组按各单词长度进行从小到大排序,然后每次遍历过程中,用一个set来存储比当前单词长度小的单词,也就是说set存储的是能组成当前单词的候选单词,在遍历过程中如果当前单词能由set里的单词组成(这个过程借用Word Break这道题的DP思路),就把当前单词加入到结果中。最后循环结束就可以返回结果了。
思路II:trie Trie 先将各个单词插入到Trie树,然后对每个单词,判断它是否能由其他单词组成。
Find the Weak Connected Component in the Directed Graph
思路:并查集 由于节点的值不确定,所以用HashMap来存储一个节点和它的父节点的映射关系而不是用数组存储,注意用HashMap来存储的时候并查集中的find函数的写法!
Sweep Line
Building Outline
思路I:使用PriorityQueue
我的解法:用一个带最大堆的扫描线遍历数组,每出现一个拐点则记录一次区间。新加入一个元素后若堆顶元素和之前不同,则表明出现拐点。
-
首先处理数组中元素。将每一个点用一个point来保存,保存time(开始写错了,应该是位置,但是要改的太多了),flag(起点为1,终点为0),height。用一个HashMap来记录每一对终点和起点(终点为key,起点为value)。
-
将所有point保存在一个list中并排序,首先根据时间从小到大排序,若时间相等则根据flag排序(先起点后终点),若flag也相等则根据height排序(若同为起点则从大到小排序,若同为终点则从小到大排序)。这样可以避免重复解。
-
再构建一个最大堆,用于记录加入的元素的最大值。
-
开始遍历list中每个point,起点元素加入堆,终点元素删去堆中对应的起点元素。
-
当遇到一个起点元素时,先记录加入前堆顶元素,然后将该起点元素加入,再看加入后堆顶元素,1)若没有变化,则继续下一个point;2)若有变化,则说明出现拐点,将之前堆顶元素时间作为起点,当前堆顶元素时间作为终点,之前堆顶元素高度作为高度。注意:就算堆顶元素变化,但是如果之前堆顶元素和当前堆顶元素时间相同,说明是在这个时间连续有几个起点被加入,宽度为0,不能算一个区间。
-
当遇到一个终点元素时,将其对应的起点元素从堆中删除。若此时堆为空,则和5中一样记录一个区间,并继续下一个point。若堆不为空,则需要看此时堆顶元素是否改变。若不变则继续,否则说明出现“拐点”。此处“拐点”要分两种情况讨论: 1)若新的堆顶元素高度和之前堆顶元素高度相同,则说明相同高度的两段区间有重叠,题目要求若发生这种情况要合并这两段区间,所以我们要保留之前的堆顶元素(两段同高度相同重叠区间的最左边),删去新的堆顶元素(即代替原堆顶元素被删除,因为每遇到一个终点必须删去一个起点),具体做法可以是先删去新堆顶元素,再加入原堆顶元素,或者直接将新堆顶元素时间改为原堆顶元素时间。 2)若新堆顶和原堆顶元素高度不同,则像5中那样记录一个区间,但是要将现在的堆顶元素时间改为遇到的终点元素的时间。
-
遍历完整个list结束
思路II:自定义HashHeap堆可以将PriorityQueue中O(n)的remove操作优化到O(lgn)的remove操作。注意点已经在代码中标出。
Advanced Data Structure -- Segment Tree
Segment Tree Build
思路:很简单,类似写建立二叉树的程序。
Segment Tree Build II
思路:多了求每个区间的最大值,navie做法是每次建立某一区间节点,就调用findMax()函数在数组中的该区间内找最大值,这个没必要,我们可以从左右区间子树的max属性来更新当前区间节点的max。
Segment Tree Query
思路:查询给定区间中的最大值,根据给定区间与当前区间节点的三种位置关系来执行相应的操作,注意要包含给定区间超过原有区间范围的情况。
Segment Tree Query II
思路:查询给定区间中的元素个数,根据给定区间与当前区间节点的三种位置关系来执行相应的操作,注意要包含给定区间超过原有区间范围的情况。
Segment Tree Modify
思路:改变某一元素的值从而更新线段树。根据要改变值的索引index与当前区间的中点mid的大小关系来选择相应的操作,记得从小到大更新遍历过的区间的max属性。
Interval Sum
思路:线段树build + query的结合,注意query函数中是start ~ end与当前线段树节点的root.start ~ root.end相比,所以query中mid = root.start + (root.end - root.start) / 2,是当前线段树节点的区间中点,不要误写成start + (end - start) / 2,错弄成待查询的区间中点,这个错误非常容易犯。
Interval Sum II
思路:线段树build + modify + query的结合。
Interval Minimum Number
思路:线段树build + query结合,只要熟悉线段树的模版,写起来很容易。
Count of Smaller Number
思路I:数组中最小值min,最大值max,map存储每个元素出现的次数,以这三个来构建线段树。注意这一题的边界情况,queries数组len >= 1时,对每一个询问必须有返回值(0或者非0),当A数组为空或者len == 0时不能直接返回空的list,而是返回长度为queries.length,元素都为0的list。
思路II:考虑可扩展性,同Count of Smaller Number before itself。
Count of Smaller Number before Itself
思路:将数组A中的元素作为索引,对应的值为该元素个数,建立线段树。每次遍历A中的元素时,先在线段树中query,再modify,更新线段树中节点的count值。
Min Stack
思路I:两个栈,一个栈stack保存元素,另一个栈minStack保存当前stack中的最小值,minStack元素个数等于stack中的元素个数。
优化:minStack只存储小于等于当前minStack栈顶元素的值。但是空间复杂度不会改变。
Largest Rectangle in Histogram
思路:单调栈 注意循环次数为length + 1次!!!栈中存储的是索引。
Maximal Rectangle
思路:单调栈 二维数组从上到下累计直方图的高度,每一行看作是直方图的宽度。然后转化为求每一行的直方图能构成的最大面积即转化成Largest Rectangle in Histogram问题。
Max Tree
思路I:分治法 但是时间复杂度最坏可达到O(n^2),栈深过大会溢出,如升序序列。
思路II:单调递减栈,找出每个数往左数第一个比他大的数和往右数第一个比他大的数,两者中较小的数即为该数的父亲节点。每个数往左数第一个比他大的数用单调递减栈来维护,当遇到往右数第一个比他大的数就把栈顶元素pop出来,补充该节点与父亲节点的关系。循环次数为length(数组长度) + 1。为了完善所有节点的关系,虚拟出一个节点,值为Integer.MAX_VALUE,确保所有节点的关系。
Expression Tree Build
思路:首先定义一个Node
,对于每一个符号或者数字,附上一个val,以便于之后evaluate它,来建立树
如果expression[i]
等于"("或者")"调整base然后continue
跳过后面的步骤
维护一个单调递增stack,建立最小树,类似题[Max Tree]
Expression Evaluation
思路:两个栈,一个栈存数字,一个栈存操作符。
入栈操作:
1. 遇到"(",放入操作符栈
2. 遇到")",取出数字栈中两个数与操作符栈中一个操作符运算,将结果存入数字栈,循环该过程直到遇到"(",然后从操作符栈中pop出"("。
3. 遇到操作符,如果当前操作符栈中的优先级等于或者高于(等于也要运算,防止类似"999/3/3/3"表达式执行错误的运算顺序)该操作符,取出数字栈中两 个数与操作符栈中一个操作符运算,将结果存入数字栈,循环该过程,直到当前操作符栈中的优先级小于该操作符或者遇到"("。
4. 遇到数字,直接存入数字栈。
出栈操作:
然后对数字栈和操作符栈中剩余的元素进行:取出数字栈中两个数与操作符栈中一个操作符运算,将结果存入数字栈,循环该过程。最后操作符栈应该为空, 数字栈只剩下一个数,返回这个数。注意这里需要判断数字栈是否为空,比如表达式"((()))"执行程序后数字栈为空,返回0;不为空才返回这个数。
Convert Expression to Reverse Polish Notation
思路:先Expression Tree Build,然后后序遍历该树就能求出逆波兰表示法。逆波兰表示法实际上就是二叉树的后序遍历。
Convert Expression to Polish Notation
思路:先Expression Tree Build,然后前序遍历该树就能求出波兰表示法。波兰表示法实际上就是二叉树的前序遍历。
Find Peak Element
思路:根据A[mid]与A[mid - 1](单调性)来二分
Find Peak Element II
思路I:根据列取最大值,根据上一行对应位置或者下一行对应位置是否比该位置的值大来进行二分。时间复杂度O(nlgn)
思路II:采用思路I,但是交替按照行和列进行二分。时间复杂度O(n)
Sqrt(x)
思路:从0 ~ x二分答案。易错点:mid * mid很容易超过整数表示范围,所以check的时候记得转为long比较。
Sqrt(x) II
思路:从0 ~ x二分答案。易错点:由于二分的是double数,当这个二分数<1时你永远也不可能二分到想要的答案,最后的二分结果就是这个数本身,所以这种情况下end的取值应该置为1才行。还有注意浮点数二分的二分模版上与整数二分的不同,while是end - start是大于某个精度(比题目给定的精度还要小以保证结果的正确性)循环。
Find the Duplicate Number
思路:从可能的答案范围1 ~ n进行二分,当在数组中小于等于mid的元素个数等于mid说明小于等于mid的元素都不是重复元素,反之小于等于mid的元素中含有重复元素。
Wood Cut
思路:二分这些木头所能切割成的相等木头的长度,注意二分的区间,既然是长度,范围是1 ~ max(这些木头的长度),不能从0开始,因为所得到的长度不可能是0,0代表不存在这样的长度。Last/Biggest length that can get >= k pieces。
Copy Books
思路:Smallest time that k people can copy all books。二分抄书所需要的时间,范围是max(抄每本书的时间)~sum(抄每本书的时间),而且答案一定在这个范围内!对每一个二分的抄书时间,判断在这个时间下k个人能不能抄完所有书,这个判断可以转化为在这个时间下所需要的最小抄书人数是不是小于等于k。
Maximum Average Subarray
思路:涉及子数组问题用到prefix_sum,sum[i] = sum[i - 1] + num[i - 1]。最大平均和转化为原数组中每一个元素减去二分的mid所得到的数组中是否有一段长度大于等于k的subArray的和大于等于0.
第5周 & 第6周 ---- Dynamic Programming
Maximal Square
思路:1.状态 dp[i][j] 表示以i和j作为正方形右下角可以拓展的最大边长
2.方程 if matrix[i][j] == 1 dp[i][j] = min(left[i][j-1], up[i-1][j], dp[i-1][j-1]) + 1
(upper[i][j] 表示以i, j 作为全为1的向上长条有多长,left[i][j] 表示以i, j 作为全为1的向左长条有多长,)
if matrix[i][j] == 0 dp[i][j] = 0
然而我们可以将上面这个状态转移方程简化如下面这个:
if matrix[i][j] == 1 dp[i][j] = min(dp[i - 1][j], dp[i][j-1], dp[i-1][j-1]) + 1;
if matrix[i][j] == 0 dp[i][j] = 0
3.初始化 f[i][0] = matrix[i][0];
f[0][j] = matrix[0][j]
4.答案 max{dp[i][j]} * max{dp[i][j]}
Maximal Square II
思路:1.状态 dp[i][j] 表示以i和j作为正方形右下角可以拓展的最大边长
upper[i][j] 表示以i, j 作为全为0的向上长条有多长
left[i][j] 表示以i, j 作为全为0的向左长条有多长
2.方程 if matrix[i][j] == 1 dp[i][j] = 1 + Math.min(dp[i - 1][j - 1], Math.min(up[i - 1][j], left[i][j - 1]))
up[i][j] = 0
left[i][j] = 0
if matrix[i][j] == 0 dp[i][j] = 0
up[i][j] = 1 + up[i - 1][j]
left[i][j] = 1 + left[i][j - 1]
3.初始化 不需要
4.答案 max{dp[i][j]} * max{dp[i][j]}
区间类DP
特点:1. 求一段区间的解max/min/count
2. 转移方程通过区间更新
3. 从大到小的更新
Stone Game
思路:1.状态 dp[i][j] 表示把第i到第j个石子合并到一起的最小花费
2.方程 dp[i][j] = min(dp[i][k]+dp[k+1][j]+sum[i,j]) (记得提前初始化sum[i][j]) for k: i ~ j - 1
3.初始化 dp[i][i] = 0
4.答案 dp[0][n-1]
Stone Game II
思路:数组变成循环数组。解决循环方式有三种:1.拆环 2.复制 3.取反
这里采用复制数组的方式解决循环问题。复制前len - 1个元素到原数组末尾,对得到的新数组用Stone Game中一样的方法DP,最后在得到的dp数组中找到长度为len并且score最小子数组。
1.状态 dp[i][j] 表示把第i到第j个石子合并到一起的最小花费 (dp[2 * n - 1][2 * n - 1])
2.方程 dp[i][j] = min(dp[i][k]+dp[k+1][j]+sum[i,j]) (记得提前初始化sum[i][j]) for k: i ~ j - 1
3.初始化 dp[i][i] = 0
4.答案 dp[i][i + n - 1] for i : 0 ~ n - 1
Copy Books
思路I:二分法。二分答案,即二分抄完书所花费的最短时间。初始范围max ~ sum。如果这些人在二分的时间mid内抄完书所用的最少人数(贪心来求该人数)<=k,说明能完成任务,时间end=mid;否则start = mid。最后Double Check。
思路II:动态规划。思路同Copy Books II。需要预先求得一个人抄完前i本书所花费的时间sum[i]数组,为状态方程做准备。
1.状态 dp[i][j] 表示前i+1个人,抄前j本书的最小完成时间。
2.方程 dp[i][j] = min{max{dp[i - 1][j - k], sum[j] - sum[j - k]}} for k:0 ~ j
3.初始化 dp[0][j] = sum[j] for j:0 ~ n; dp[i][0] = 0 for i:0 ~ len - 1
4.答案 dp[len - 1][n]
Copy Books II
思路:1.状态 dp[i][j] 表示前i+1个人,抄前j本书的最小完成时间。
2.方程 dp[i][j] = min{max{dp[i - 1][j - k], k * times[i]}} for k:0 ~ j
3.初始化 dp[0][j] = j * times[0] for j:0 ~ n;dp[i][0] = 0 for i:0 ~ len - 1
4.答案 dp[len-1][n]
注意:这道题卡时间复杂度和空间复杂度,代码中要注意的地方已经标记。
Post Office Problem
思路:
dp[i][j]表示在前i个村庄中建j个post的最短距离,l为分隔点,可以将问题转化为在前l个村庄建j-1个post的最短距离+在第l+1到第i个村庄建1个post的最短距离。其中有个性质,如元素是单调排列的,则在中间位置到各个元素的距离和最小。
-
初始化dis矩阵,枚举不同开头和结尾的村庄之间建1个post的最小距离,即求出开头和结尾村庄的中间点,然后计算开头到结尾的所有点到中间点的距离。记得要对原矩阵排序,这样才能用中间点距离最小性质。
-
初始化dp矩阵,即初始化dp[i][1],求前i个村庄建1个post的最小距离(可根据dis求出)。
-
post数l从2枚举到k,开始村庄i从j枚举到结尾(因为要建j个post至少需要j个村庄,否则没有意义,其实可以从j + 1开始枚举),然后根据状态函数求dp[i][j],分割点l从j-1枚举到i-1(前l个村庄建j-1个post则至少需要j-1个村庄),在这些分隔点的情况下求dp[i][j]的最小值。
4.返回dp[n][k]即可。
1.状态 dp[i][j] 表示前i个村庄中建j个邮局的最短距离
2.方程 dp[i][j] = min{dp[l][j - 1] + dis[l + 1][i] for j - 1 <= l < i}
3.初始化 dp[i][1] = dis[1][i] for 1 <= i <= n
4.答案 dp[n][k]
双序列DP
1. state: f[i][j]代表了第一个sequence的前i个数字/字符,配上第二个sequence的前j个...
2. function: f[i][j] = 研究第i个和第j个的匹配关系
3. initialize: f[i][0] 和 f[0][i]
4. answer: f[n][m] min/max/数目/存在关系
5. n = s1.length() m = s2.length()
解题技巧画矩阵,填写矩阵
Edit Distance
思路:1.状态 dp[i][j]表示A的前i个字符最少要用几次编辑可以变成B的前j个字符
2.方程 A[i - 1] == B[j - 1],dp[i][i] = dp[i - 1][j - 1];A[i - 1] != B[j - 1],dp[i][j] = 1 + Math.min(Math.min(dp[i - 1][j], dp[i][j - 1]), dp[i - 1][j - 1])。
3.初始化 dp[i][0] = i dp[0][j] = j
4.答案 dp[n][m]
5. 可以用滚动数组优化空间复杂度
K Edit Distance
思路:暴力法,按Edit Distance思路对每个word计算最短编辑距离然后判断该距离是否小于等于k,不可取。考虑到这些word有很多重复,因此这些word可以用trie树来存储,从而减少重复字母的最小编辑距离的计算。也就是在trie树上跑动态规划。
动态规划 & 贪心
121. Best Time to Buy and Sell Stock
思路I:动态规划 动态规划的一般方式是利用一个数组dp[]用来存储如果以今天结尾, 能得到的结果。 对于我们这个问题来说, 就是用来存储到第i天的为止, 如果只能买卖一次股票, 所能获得的最大收益。 这个收益和截止到前一天为止的最大收益有关 — 或者说是昨天的收益有关, 我们将这个收益称为A。 它还和今天的股价有关, 确切的说, 是和今天的股价和截止到今天为止的最小股价的差有关, 这个差即为如果今天卖出, 可以得到 的最大收益, 我们将这个收益称为B。 今天的最终最大收益则为A和B中大的那个。
思路II:贪心 如果用贪心的思想, 我们不需要保存每天的状态, 而只需要保存截至到目前为止最大的收益即可, 则最后一天的最大的收益即为最后的收益。
122. Best Time to Buy and Sell Stock II
这道题与前一道题的区别在于, 我们可以买卖股票很多次(as many as you want), 求最大收益。
这道题相对简单,不要把自己绕进去,其实只要比较是不是今天比昨天贵, 不用管之前的情况。举个简单的栗子,比如考虑第三天的情况的时候, 因为day3 - day1 = day3 - day2 + day2 - day1, 而day2 - day1 又已经包含在dp[day2]中,所以,只要day3比day2大,则,dp[day3] = dp[day2] + day3 - day2
思路I:动态规划
思路II:贪心
309. Best Time to Buy and Sell Stock with Cooldown
思路:动态规划
很好的一道题, 很容易把自己绕乎进去。
每天都有两种可能性,要么卖,要么啥都不干。 当然这天的最大收益则是这两个方案中大的那个。 按照DP的思想,不如开两个数组,一个表示今天要卖,另一个表示今天啥也不干。
profitDoNothing[i]比较容易想,因为今天啥也不干,所以今天就继承昨天的结果,昨天的结果又有两种可能 --- 卖、啥也不干,当然要继承它俩中大的那个,所以profitDoNothing[i] = Math.max(profitSell[i-1], profitDoNothing[i-1])。
重点来了,profitSell[i]怎么算。其实也有两种情况,如果day[i-1]我买入了(或者卖了,因为卖完了可以再买回来),则day[i]可以直接卖,收益是:profitSell[i] = profitSell[i-1] + prices[i] - prices[i-1]。但是还有一种情况,就是day[i-1]是啥也没干的,所以day[i]需要把自己先买回来,再卖,收益是:profitSell[i] = profitDoNothing[i-1] + prices[i] - prices[i] = profitDoNothing[i-1]。最终取这两个大的就行了。
思路II:贪心
仔细看你的动态规划的状态转移方程,你会发现其实你不需要一个数组,你只需要两个数: profitSell & profitDoNothing 然后每次比较的时候创建一个中间变量即可。
123. Best Time to Buy and Sell Stock III
思路:动态规划
使用”局部最优解和全局最优解”的思想, 和Cooldown那道题的思路很像, 每一天将有两个状态 – “卖出”和”不卖出”。 利用两个动态规划数组local和global分别来存储局部最优解和全局最优解, 难点在于 这道题的状态更复杂, 因为要在两个维度计算, 第一个维度是天数的维度, 第二个维度是交易次数的维度, 所以local和global都是二维数组。 其中, global[i][j]表示到第i天最多进行j次交易可以达到的最大利润,而 local[i][j]表示到第i天最多进行j次交易,并且在第i天卖出可以达到的最大利润。
则: global[i][j] = Math.max(local[i][j], global[i-1][j]) 也就是到今天为止的全局最好,是到今天为止的局部最好与到昨天的全局最好中大的那个。因为今天的最大利润有两种情况,第一是几天卖出了(既local[i][j]),另一种是今天什么都没做,那今天的最好的情况就是昨天的全局最好情况
而: local[i][j] = Math.max(global[i-1][j-1] + prices[i] - prices[i-1], local[i-1][j] + prices[i] - prices[i-1]) — 这里是local[i-1][j]因为就算第i-1天卖了, 可以再买回来在第i天再卖,所以并不增加transaction。 这里就不太好理解了, 因为今天需要卖出,所以需 要加prices[i] - prices[i-1], 两种加的可能性,(1) 和全局比,(2)和局部比。
思路:贪心
如果用贪心的做法, 则不需要维护天数的维度, 因为我们只需要记录每天的local和global即可, 后一天的根据前一天的结果进行计算。 如果只是两次操作,相当于j = 2。但是贪心的时候有一个小技巧需要注意一下, 就是我们需要倒序的算, 因为我们其实需要和前一天第2次操作的比, 所以我们不希望这个值被覆盖掉。
这里倒序的原因是不想让global[k]被覆盖掉 --- 这里也是难理解的一个点, 因为我们简化了二维数组,而选择只用一维数组
所以相当于这个数组只维护了前一天的信息,而今天的量也只和前一天有关,而我们只care最后一天k次交易的情况,这个情况跟前一天的k-1次交易有关,所以为了不让这个值被覆盖掉,我们选择倒叙
如果用二维数组则没有这个问题
188. Best Time to Buy and Sell Stock IV
思路I:动态规划
第四题是第三题的推广, 由第三题中只能卖两次, 推广到现在可以卖k次。 思路和第三题是一样的, 我们在这里不再赘述。
但是需要注意的一点技巧是, 需要注意一个细节, 当k比prices.length的一半还要大的时候, 说明我们无法买卖够k次, 那么这道题就转变为了第二题。 这一点需要提前考虑到, 否则会得到一个TLE。不考虑这一点的话,当k很大的时候时间复杂度就比较高了!O(n * k)
思路II:贪心
第7周 ---- Follow Up Question
子数组和
Subarray Sum
思路:map记录子数组的和与子数组最后一个元素索引的映射,注意一开始map.put(0, 1)。
Subarray Sum II
思路:这里由于是求范围,一边求的前缀和数组prefixSum[i],一边判断前缀和数组中的前面的元素是否有prefixSum[i] - end ~ prefixSum[i] - start这个范围的元素出现,这个判断过程如果直接依次遍历前面的元素是O(n)的,为了加快查找速度,用二分查找,转化为查找prefixSum[i] - end和prefixSum[i] - start + 1(注意:一定要+1,否则会出现错误!!!【0,1,3】查找3~3和3~4的区别)这两个元素的索引,然后这两个索引的差值就是当前满足条件的子数组的个数。遍历整个i即可求的总共的子数组个数。注意这一题中的二分查找的写法,最后返回索引,这个索引是不能随便返回的,按照模版写法最后要进行详细的判断过程。
Submatrix Sum
思路:先前缀和预处理,prefixSum[i][j] = sum of submatrix [(0,0), (i - 1,j - 1)].然后对于行数枚举,low:0 ~ n - 1; high:low + 1 ~ n。这样固定了矩阵的宽度,再建立一个map,对高度j进行枚举,只要有两个同高不等宽的子矩阵的和相同,说明这两个子矩阵的非重叠部分的子矩阵的和为0,这样转化为Subarray Sum这道一维的题目。
Sliding Window Maximum
思路:用到双端队列deque在左边删除,右边插入和删除,每次一个元素入列,把列中小于该元素的元素全部出列,列中只保存可能是当前最大值的元素。
Sliding Window Matrix Maximum
思路:先前缀和预处理,prefixSum[i][j] = sum of submatrix [(0,0), (i - 1,j - 1)].然后对于行数和列数枚举,i:0 ~ n - k j : 0 ~ m - k 然后依次遍历每个k * k矩阵,求出每个k * k矩阵的最大值,最后返回这些值中的最大值即可。
循环连续子序列
Continuous Subarray Sum
思路:求连续子数组最大和/最小和的模版,记下来。是一个叫***算法。
Continuous Subarray Sum II
思路:这道题数组变成循环数组了。分两步,第一步跟Continuous Subarray Sum一样求0 ~ len - 1之间的连续子数组的最大和及索引对;第二步考虑尾->首的数组情况,将问题等价于求1 ~ len - 2之间的连续子数组的最小和及索引对,然后用数组和total减去这个最小和就是尾->首的子数组元素和。返回这两步中的最大和及其索引对。
Partition Follow UP
Kth Largest问题
PriorityQueue
? 时间复杂度O(nlogk) ? 更适合Topk
? QuickSelect
? 时间复杂度O(n) ? 更适合第k大
Wiggle Sort
思路:很简单。对索引分奇偶考虑,如果索引奇且当前值小于前一个值或者索引偶且当前值大于前一个值,则交换。
Wiggle Sort II
思路I:先排序,然后排好序的数组分一半,依次从前一半的末尾和后一半的末尾取数放在原数组中。时间复杂度O(nlgn)。
思路II:用QuickSelect模版选择第(length + 1) / 2大的数mid,新建temp数组,初始化为mid。然后遍历nums数组,将大于mid的元素从前往后放入temp,将小于mid的元素从后往前(注意初始化的位置由nums数组长度的奇偶来决定是nums.length - 1还是nums.length - 2)放入temp,来分割重复元素,最后将temp中的元素复制到nums中。
Nuts & Bolts Problem
思路:先从bolts中选一个pivot,对nuts进行partition,然后返回与该pivot对应的nuts中的索引,再以此相对应的pivot对bolts进行partition,这样nuts和bolts就都有一个元素相对应了。然后递归处理左右两边即可。注意这道题由于要nuts的pivot与bolts的pivot的位置对齐,所以不能用令狐冲老师讲的partition模版,这个模版只能将数组分成左右两半部分,但是pivot的索引不定,很可能不会再最终的位置上。所以这里用到另一种确保pivot在最终位置上的partiton写法,记住这个写法,考研的时候学过,具体看程序即可。
迭代器相关:
Flatten List
思路:遍历每个元素,如果元素是Integer,加入到结果中,否则递归调用该函数。
Flatten Nested List Iterator
思路:1. List转Stack 2. 主函数放在hasNext()中,next只做一次pop处理。
Flatten 2D Vector
思路:同Flatten Nested List Iterator
Binary Search Tree Iterator
思路:同Flatten Nested List Iterator。注意当中序非递归把元素压栈之后,要及时把cur指针置为null,否则cur指针还是指向原来的位置而不改变!!!
Zigzag Iterator
思路:用计数器count控制取两个中的哪一个。
Zigzag Iterator II
思路:用计数器count控制取k个中的哪一个,当count对应的list没有元素时count++,直到找到含有元素的list。
Maximum Gap
思路:桶排序(bucket sort)
假设有N个元素A到B。
那么最大差值不会小于ceiling[(B - A) / (N - 1)] (ceiling为向上取整)
令bucket(桶)的大小len = ceiling[(B - A) / (N - 1)],则最多会有(B - A) / len + 1个桶
对于数组中的任意整数K,很容易通过算式loc = (K - A) / len找出其桶的位置,然后维护每一个桶的最大值和最小值
由于同一个桶内的元素之间的差值至多为len - 1,因此最终答案不会从同一个桶中选择。
对于每一个非空的桶p,找出下一个非空的桶q,则q.min - p.max可能就是备选答案。返回所有这些可能值中的最大值。
Bomb Enemy
思路:扫描矩阵,用rows表示该点所在行内有多少敌人可见。用cols[i]表示该点所在列i内有多少敌人可见。然后该点可见的敌人为两者之和。
初始时两值为0.
当我们位于行首或者该行上一格为墙,那么在该行向右扫描直到行尾或者遇见一堵墙,更新rows。
当我们位于列首或者该列上一格为墙,那么在该列向下扫描直到列尾或者遇见一堵墙,更新cols[i]。
时间复杂度O(mn)(这个复杂度我还没想明白),空间复杂度O(N)。
以上是关于CS3K.com 九章算法强化班的主要内容,如果未能解决你的问题,请参考以下文章