动态规划
How-should-I-explain-dynamic-programming-to-a-4-year-old
*writes down "1+1+1+1+1+1+1+1 =" on a sheet of paper*
"What's that equal to?"
*counting* "Eight!"
*writes down another "1+" on the left*
"What about that?"
*quickly* "Nine!"
"How'd you know it was nine so fast?"
"You just added one more"
"So you didn't need to recount because you remembered there were eight!Dynamic Programmingis just a fancy way to say 'remembering stuff to save time later'"
- 要紧紧抓住回答中累加量这个概念,即动态规划就是通过记录状态从而减少了重复计算
- 那么为什么会减少了重复计算呢?因为有状态转移方程,通过记录状态可以就可以计算出新的状态。
- 一般leetcode题目不会太难,状态转移公式就是要求的结果公式,要注意一维不行要尝试二维,一般不会出到三维。
- 想不出公式的时候先蠢办法列出来再看哪里可以通过记录来减少计算量。
保持数组中的每个元素与其索引相互对应的最好方法是什么?
- 哈希表
构建哈希表往往还需要遍历执行剩余逻辑,两者是否能够合并在构建哈希表的时候就执行剩余逻辑
排序
- 如果你需要用到nums[i]==nums[i-1]的时候,就需要对数组进行排序
计算子数组数量
- 除了可以通过(n+1)*n/2
- 还可以直接通过一步步加和子数组长度,1+2+3+...n(省了去记录n,有效于分段多组子数组)
for (int x: A) {
cur = cur + 1;
ans += cur;
}
遍历数组
- 嵌套遍历数组O(n2)的时候,可以采用双指针的方式,注意l,r可以在i后面也可以在i前面两种要看情况而定
for(int i=1;i<nums.length;i++){
l = i+1;
r = nums.length-1;
while (l<r){
。。。
}
- 分段获取最大值
for(int i = ch.length-1;i>=0;i--){
if(ch[i]-'0'>max){
max = ch[i]-'0';
maxIndex = i;
}
help[i] = maxIndex;
}
矩阵
- 凡是碰到矩阵改变的问题,如旋转,可以先想一下矩阵转置,再思考。
matrix[i][j]与matrix[j][i]交换
for(int i = 0;i<n;i++){
for(int j = i;j<n;j++){
swap(matrix,i,j,j,i);
}
}
- 矩阵如果需要存储row+col的东西,可以选在放在矩阵的第一行和第一列
原地修改数组
- 原地修改数组考核的是双指针,指针来指定交换的两个位置
- 如果要求原地修改,可以直接赋值而不是交换,因为交换可能影响升序或者降序的顺序
- 坚持原则:要什么拿什么,其他的一律不考虑(值,位置,顺序都不考虑),即如何把正确挪到前面而不是如何把错误的挪到后面
public int removeDuplicates(int[] nums) {
int p = 1;
int q = 1;
int count = 1;
for(int i = 1;i<nums.length;i++){
if(nums[i]==nums[i-1]) count++;
else count = 1;
//考虑的是如何把正确挪到前面而不是如何把错误的挪到后面
if(count<=2){
nums[p] = nums[q];
p++;
}
q++;
}
return p;
}
多个小过程归并为一个大过程
这个思想还需要多锻炼,针对某个问题,按照直观思路遍历会发现有很多小过程时间复杂度不允许。此时就可以思考是否能通过数学公式将其归并为一个大过程,将每个过程量变成累积量。有点动态规划的味道
前缀和,紧紧把握住首位置始终算入加和中
- 前缀和可以和二分查找结合,因为它是单调递增,可以用来查找区间
- 对于寻找区间还是比较重要
B[0] = A[0];
for (int i = 1; i < N; i++) {
B[i] = B[i - 1] + A[i];
}
寻找区间,为了可以将A[0]区间减出来,可以往前设一个0元素
int[] B = new int[A.length]
for(i=0;i<A.length;i++){
B[i+1]=B[i]+A[i];
}
Kadane,紧紧把握住首位置不固定,求的是区间
ans = cur = None
for x in A:
cur = x + max(cur, 0)
ans = max(ans, cur)
return ans
- 945使数组唯一的最小增量:本来需要分别计算R-Q过程量再进行累加,转变为R-Q+S-P=(R+...S)-(P+....Q)。分别计算两次过程量即可
- 1109. 航班预订统计:本来需要嵌套遍历进行累加,但是可以先遍历一次计算出每个位置的累加量,再遍历一次累加量增加,避免了相同位置的重复遍历。
- 1292. 元素和小于等于阈值的正方形的最大边长:一维区域和转变为累加量,二维区域转变为二维的面积差。
二分查找
使用场景
- 单调递增的数组就多想想怎么用
模板
# 前提
# 数组递增
# 数组内必然存在值,否则最后需要对last进行检查
def lower_bound(arry,first,last,value):
while first<last:
mid = first+(last-first)//2
if array[mid]<value:first = mid+1
else:last = mid
return last
1.while (left <right) 不需要写等号,如果left=middle&&right=middle,那么此时left<right-1
2.middle = left + (right - left) / 2; 更好,有可能left+right越界
3.如果left=0,right=n-1,则区间为[0,n-1]
所以对应的二分数组为[left,middle-1],[middle+1,right]
如果left=0,right=n,则区间为[0,n)
所以对应的二分数组为[left,middle),[middle+1,right)
如果left=-1,right=n,则区间为(-1,n)
所以对应的二分数组为(left,middle),(middle,right)
4.一般判断逻辑为:
if (array[middle] > v)
{
right = middle;
}
else if (array[middle] < v)
{
left = middle;
}
else
{
return middle;
}
可以改写为两次判断,将其中一个改为等于,这样在最后跳出循环后再判断一次究竟是等于还是大于情况
这样可用来寻找第一次出现元素或最后一次出现元素,即在已出现的元素继续往做或右寻找。
!!!!因为right有可能等于v,所以必须要保证left小于right-1,否则当right=v时会陷入死循环
while (left < right-1)
{
middle = left + (right - left) / 2;
if (array[middle] < v)
left = middle;
else
right = middle;
}
if (right >= n || array[right] != v)
{
right = -1;
}
递归回溯+剪枝
递归的方法慎用,不能强用,因为它必须保证每个子过程是对等的很容易受题目限制,就想过于复杂了
- 能够采用回溯法,首先需要把算法解决方案用树的形式画出来,把树画出来就能很容易的按每个节点写出递归和结束条件。
- 回溯算法关键在于:往前走一步探探路,不对劲就缩回来换个方向。边界条件和回退条件
回溯法固定模板
/**
* dfs 模板.
* @param[in] input 输入数据指针
* @param[out] path 当前路径,也是中间结果
* @param[out] result 存放最终结果
* @param[inout] cur or gap 标记当前位置或距离目标的距离
* @return 路径长度,如果是求路径本身,则不需要返回长度
*/
//path:推荐使用 Deque<Integer> stack = new ArrayDeque<Integer>();直接addLast和removeLast即可
void dfs(type &input, type &path, type &result, int cur or gap,int len) {
if (数据非法) return 0; // 终止条件
if (cur == input.size()) { // 收敛条件
// if (gap == 0) {
将path 放入result
}
if (可以剪枝) return;
for(i=0/i=cur;i++;i<len) { // 执行所有可能的扩展动作
if(i>begin&&candidates[i-1]==candidates[i]...)
//去重
执行动作,修改path
dfs(input, i+ 1 or gap--, result);
恢复path
}
}
- 不要交换数据,因为会影响剪枝顺序,可使用外部数组记录已使用过的。(全排序中,213==需要使用到前面的数据)
- input.length这种固定的值可以传参而不需要每个递归都算
- 把树画出来之后,可以考虑加入剪枝条件,即是否存在某个条件就可以把该节点及以后的节点去掉,剪枝也就是加入终止条件(除了题目要求的,发现其他的内在关系可以终止)。
- 避免出现[123]和[132]这样的重复,每次递归只能选比自己大的数即可。
- 避免重复的技巧下方
具体示例如下:
40. 组合总和 II,这个递归避免重复的技巧很巧妙,if(i>begin)很重要!要先排序
public void dfs(int[]candidates, int residue,int begin, List<List<Integer>> res, LinkedList<Integer> path,int target){
if(residue==0){
res.add(new ArrayList<>(path));
return;
}
for(int i = begin;i<candidates.length;i++){
if(residue-candidates[i]<0)
break;
if(i>begin&&candidates[i-1]==candidates[i] ){
continue;
}
path.addLast(candidates[i]);
dfs(candidates,residue-candidates[i],i+1,res,path,target);
path.removeLast();
}
}
降低时间复杂度
- 数组尽量不要出现remove操作,新建一个数组来承载都尽量避免删除元素,很慢除非是链表数组删除头尾。
除法
- (a-b)%k==0 等价于 a%k==b%k
- 取余的题注意用桶去装余数,可以减少时间复杂度
乘法
- 多个数相乘,需要转为Log相加
解题思路注意点
- 测试用例一定要注意是否会出现相同元素,如果数组内两个位置的元素相同,那么应该取哪一个位置,决定了从头还是从尾开始遍历。
- 我最大的问题,没有把代码逻辑路径考虑完全,没有认认真真地在纸上把分支全部画出来,请不要贪快和懒就草草了事,请不要节约纸张,准备好大的A4白纸。
- 环形数组,注意分开讨论,先讨论非环形,再讨论环形。
- 当你想用linklist的时候,先试试双指针。
- 注意Integer.MaxValue,Integer.MinValue,不能用来作运算否则会溢出只能用来判断
- 先实现暴力的再实现优化的,有可能有些本身就是暴力的,循序渐进
- 要注意循环条件left<=right,这个等号需不需要,取决于你是否需要left==right的时候还要执行逻辑。
- 循环的判断条件如下
//注意判断变量和业务逻辑的关系,如果不希望left==right时,就应该把++left放在业务逻辑之后
while(left<right&&nums[left]==nums[++left]){
if(left<right)
ans+=(right-left);
}