714 Best Time to Buy and Sell Stock with Transaction Fee
思路
首先是暴力枚举。考虑在第idx天能做的操作:买?卖?不操作?
/**
* 暴力枚举
*
* @param prices
* @param fee
* @return
*/
public int maxProfitV99(int[] prices, int fee) {
return dfs(prices, 0, 0, fee, 0);
}
/**
** 考虑在第idx天是买?卖?不操作?
**/
private int dfs(int[] prices, int idx, int buyed, int fee, int profit) {
if (idx >= prices.length) {
return profit;
}
int max = Integer.MIN_VALUE;
if (buyed == 0) {
// 买
max = Math.max(max, dfs(prices, idx + 1, 1, fee, profit - prices[idx]));
} else {
// 卖
max = Math.max(max, dfs(prices, idx + 1, 0, fee, profit + prices[idx] - fee));
}
// 不操作
max = Math.max(max, dfs(prices, idx + 1, buyed, fee, profit));
return max;
}
接着:用动态规划的思路来解决
关键是找到递归方程。定义:hold[i]是在第i天持有股票的状态下的最大收益;sold[i]是在第i天卖掉股票的状态下的最大收益; 方程式:hold[i] = Math.max(hold[i-1],sold[i-1]-prices[i])//第i-1天就持有股票,或者第i天购买股票。sold[i] = Math.max(hold[i-1]+prices[i]-fee,sold[i-1])。大家能看到上面的暴力搜索的思路并不完全浪费。在找递归方程中起了作用:hold[i]的计算可能是前一天已经hold,或者是前一天卖了,今天买入。sold[i]的计算可能是前一天已经hold今天卖,或者是前一天已经sold。初始化:hold[0] = -prices[0];sold[0] = 0。
public int maxProfit(int[] prices, int fee) {
int n = prices.length;
if(n<2) return 0;
int[] hold = new int[prices.length];
int[] sold = new int[prices.length];
hold[0] = -prices[0];
sold[0] = 0;
for(int i=1;i<prices.length;i++){
hold[i] = Math.max(hold[i-1],sold[i-1]-prices[i]);
sold[i] = Math.max(hold[i-1]+prices[i]-fee,sold[i-1]);
}
return Math.max(hold[n-1], sold[n-1]);
}
还可以用贪心
关键是选择一个能不能卖掉股票的点,重新开始寻找买入机会。例如序列 1 3 2 8 ,如果发现2<3,就完成交易买1卖3,此时,由于fee=2,那么收益=(3-1-fee)+(8-2-fee) < (8-1-fee),说明卖早了。令maxP是当前最大的price,当(maxP-prices[i]>=fee),时则可以在maxP处卖出,且不会存在早卖的情况。
证明:不明白。
public int maxProfitV3(int[] prices, int fee) {
int n = prices.length;
if (n < 2)
return 0;
int maxP = prices[0];
int minP = prices[0];
int curProfit = 0;
int profit = 0;
for (int i = 1; i < prices.length; i++) {
maxP = Math.max(maxP, prices[i]);
minP = Math.min(minP, prices[i]);
curProfit = Math.max(curProfit, prices[i] - minP - fee);
if(maxP - prices[i]>=fee){
profit += curProfit;//在maxP处交易
maxP = prices[i];
minP = prices[i];
curProfit = 0 ;
}
}
return profit+curProfit;
}
376 Wiggle Subsequence
思路:用贪心或者动态规划可以解决。
代码
84 Largest Rectangle in Histogram
思路:首先是暴力枚举。 枚举矩形的左右边界。
学习:接着还是暴力枚举,只是这次枚举的是矩形的最低高度。对于一个heights[i],算一下能够到达的最左边的下标和最右边的下标。复杂度还是没降低。在这次枚举过程中会发现可以执行跳跃,省掉一些计算。例如找左边最远:如果当前设置变量j = i;如果heights[i]<=heights[j-1],那直接跳到left[j+1];j=left[j-1],循环再继续判断。例如找右边最远:如果当前设置变量j = i;如果heights[i]<=heights[j+1],那直接跳到right[j+1];j=right[j+1],循环再继续判断。这种跳跃的思想,是学习到的。
代码
621 Task Scheduler
思路:每次应该选择符合条件的剩余总数多的那个任务。符合条件是指符合间隔条件。需要一个map,计算每个任务的数量;需要第二个map,记录当前状态下还有多少个时间间隔。就形成了第一版本的代码。在这个过程中,我没有考虑n=0,没有考虑 每个任务数量不同的情况。是后来才加上的代码。当然代码是超时的。
学习:计算每个不同任务的数量。每次小循环i从1到n,优先选择数量多的不同任务。这里很关键的地方是:只与不同任务的数量有关系,而与任务名称没有关系。用一个一个的数字表示不同的任务。这是我之前一直转不过弯的地方。例如 3个A 1个B 1个C 1个D 1个E ,n=2,最好的顺序是A->B->C->A->D->E->A。第一个小循环用数字表示就是 3-1,1-1,1-1;第二个小循环用数字表示就是2-1,1-1,1-1,第3个小循环是:1-1,因为数组为空,退出。
该思路的难点是处理小循环,注意小循环退出的条件。该思路可以用数组、优先队列两种方法实现。
代码
斐波那契数列
斐波那契数列的计算可以用动态规划计算,也可以用矩阵计算,有
O(logn)
O
(
l
o
g
n
)
的时间复杂度。
算法去冗余有两种角度:时间冗余和空间冗余。去时间冗余,就是利用空间,将本次计算结果存下来,下次用的时候直接取。
F(n)=F(n−1)+F(n−2)
F
(
n
)
=
F
(
n
−
1
)
+
F
(
n
−
2
)
,在这个式子中就有重复计算,可以用数组f[i]保存是i时候的结果。空间冗余,就看本次计算相关因素,只需要记录相关因素。还是
F(n)=F(n−1)+F(n−2)
F
(
n
)
=
F
(
n
−
1
)
+
F
(
n
−
2
)
,不需要记录f[0],f[1],只需要记录f[n-1],f[n-2],两个变量就可以。
动态规划时间复杂度:状态个数*(每个状态计算的时间复杂度);空间复杂度:数组大小。
动态规划解题套路:
1 设计暴力算法,找到冗余;
2 设计并存储状态(一维、二维、三维数组)
3 递归式(状态转移方程)
4 自底向上计算
48 Rotate Image
思路:不多写了。感叹一下自己没想到可以通过多个步骤得到旋转后的矩阵。
代码
153 Find Minimum in Rotated Sorted Array
学习:主要还是要观察所求值的特征。自己总结特征。最小元素的特点有两个:1 如果是旋转元素,那么
nums[min]<nums[min−1]
n
u
m
s
[
m
i
n
]
<
n
u
m
s
[
m
i
n
−
1
]
;2 如果不是旋转元素,那么它应该是nums[0]。所以可以使用二分查找法:如果
nums[mid]<nums[mid−1]
n
u
m
s
[
m
i
d
]
<
n
u
m
s
[
m
i
d
−
1
]
,则就是最小元素;否则如果nums[mid]>nums[start] && nums[mid]>nums[end]
nums[mid]>nums[start] && nums[mid]>nums[end]
,那最小元素在右侧;否则在左侧。
代码
560 Subarray Sum Equals K
思路:最近看了动态规划的视频,知道一言不合就暴力搜索。子数组,那可能的枚举就是从0到1,2,3,….从1到2,3,…..。计算这些子数组的和是不是等于k。不过我遇到的几个问题是:1,开始是用递归写的,出现栈溢出,用循环解决。2 没有考虑负数的情况,在sum>=k的时候就break。3 没有考虑[0,0,0,0,0] 这种情况,在以i为开始,找到和为k的情况就break。时间复杂度O(
n2
n
2
)。
学习:子数组的和就是指从[i,j]的和。sum[i,j] = sum[0,j]-sum[0,i-1]。
代码
795 Number of Subarrays with Bounded Maximum
思路:分析题意要求每个子数组的最大值maxVal必须满足
maxVal>=L
m
a
x
V
a
l
>=
L
and
maxVal<=R
m
a
x
V
a
l
<=
R
。
maxVal<=R
m
a
x
V
a
l
<=
R
可以推出子数组中每个元素
<=R
<=
R
<script type="math/tex" id="MathJax-Element-11"><=R</script>。
子数组的暴力枚举:以i为起始,可以行程[i][i,i+1],[i,i+2]….[i,n-1]个子数组。在枚举子数组的时候把不符合条件的去掉,计数有效子数组即可。时间复杂度O(n^2)。
学习:改为O(n)。为什么不是O(nlogn)。因为一般logn需要有二分,而二分一般涉及到排序。本题是不能排序的。所以应该是O(n)。要想O(n)就应该是一个一个元素遍历,或者根据需要多遍历几次。
遍历每个元素i,会发现,如果
A[i]>=L并且A[i]<=R
A
[
i
]
>=
L
并
且
A
[
i
]
<=
R
。
数组[2,1,4,3] L=2,R=3
下标0:
A[i]>=L并且A[i]<=R
A
[
i
]
>=
L
并
且
A
[
i
]
<=
R
增加个数1
下标1:
A[i]<L
A
[
i
]
<
L
自己不能单独成为子数组,可以追加在前面的子数组后面。所以前面有几个子数组这里就增加几。
下标2:
A[i]>R
A
[
i
]
>
R
不增加
下标3:
A[i]>=L并且A[i]<=R
A
[
i
]
>=
L
并
且
A
[
i
]
<=
R
,增加1个。因为前一个元素超出范围了,只能从下标3开始计算。
代码
162 Find Peak Element
思路:找极值点。之前在贪心里面有一道题比这个还要难。思路就是找到一个元素i 如果 (nums[i]-nums[i-1])*(nums[i+1]-nums[i])<0,就找到了极值点的下标i。
学习:二分查找。这道题目居然也可以用二分。一直以为不排序的数组没法用二分查找。原因是这样的。只要找到一个极值点即可。在达到极值点前或者过了极值点之后是有序的。
ifnums[i−1]<nums[i]<nums[i+1]
i
f
n
u
m
s
[
i
−
1
]
<
n
u
m
s
[
i
]
<
n
u
m
s
[
i
+
1
]
,那么极值点一定在i+1,i+2…n-1中。
ifnums[i−1]>nums[i]>nums[i+1]
i
f
n
u
m
s
[
i
−
1
]
>
n
u
m
s
[
i
]
>
n
u
m
s
[
i
+
1
]
,那么极值点一定在[0,1,…i-1]中
ifnums[i−1]<nums[i]>nums[i+1]
i
f
n
u
m
s
[
i
−
1
]
<
n
u
m
s
[
i
]
>
n
u
m
s
[
i
+
1
]
,那么极值点就是i
所以学习到局部有序,也可以用二分查找。
代码