科目一总结一

导语

本文无意介绍各类具体算法内容,重点在于在线编程题目当中的分析技巧和基础算法的剖析与运用。

各类算法的经典题目可以通过标签查找,我就不一一罗列,会详细讲一些我认为比较开拓思路的题目,再罗列一些我新看到的有意思的题目,有兴趣可以看看。

通用技巧

数据范围的信息

算法时间10^9几乎铁定超时,10^8可能超时,可能卡常数

所以,见到n<10^4的题还可以试试n^2带剪枝的算法,10^3的题最多用n^2lgn的算法。n<100的,甚至可以考虑n^4的算法.如果数据范围<32或更小,则可以考虑状态压缩等方法。

空间的检测方法

对于空间的要求,可以估算最坏情况直接预申请内存提交一次进行检测。

算法可行性分析

思维容易有定势,所以一定要估算清楚方法可行性再去写,对于贪心方法最好能给出证明。一旦出错,调试非常花时间,要争取一遍通过。

算法复杂度估算

常用数据结构复杂度:

数组:查询O1,插入On。

链表:插入O1,查询On。数组和链表作为线性表在这里是矛盾的。

Treemap:查询n,插入lgn,有序。

HashMap:插入查询都可以当作o1。

易错点:

Python的in(),insert()都是on操作。

ArrayList.add(index,item)是On, LinkedList.get(index)是On

二分查找

什么时候用

二分查找本质是对于单调函数,对定义域进行压缩,通过舍弃区间去缩小选择空间,有单调性,有定义域,就可以用二分查找。

怎么用

考虑这样一个问题,对于数组[1,3,5,5,7]来说,如果我要二分查找4,我应该返回3的下标1呢还是5的下标2?

如果我要查找5,我应该返回第一个5的下标2还是第二个5的下标3?

可以看出,虽然都是简单的二分查找,但是诉求会随着问题改变,所以我们就希望把问题尽量归一话,这样就减少了出现失误的可能性。

这里我们定义两个二分查找函数,理解他们的使用场景即可应付各类二分查找问题:

Lower_bound:查找第一个大于等于目标数的位置。

Upper_bound:查找第一个大于目标数的位置。

其余位置可以用这两个函数结果加减1得到。

例如:寻找比目标数小的最大位置是low_bound – 1

这两个函数的定义是在C的STL里的,python里有个bisect模块,里边的方法也是对应的。Bisect_left 返回目标值的插入位置,如果重复就返回第一个元素的位置(等价于lower_bound)。Bisect_right返回最右边元素的位置(等价于upper_bound-1)。Java也有Arrays. binarySearch,但是行为不一样,详见官方文档。

怎么写

我个人常用的写法,比较稳定,唯一区别在于>号和>=

贴个STL内的源码解析:https://www.cnblogs.com/cobbliu/archive/2012/05/21/2512249.html

//第一个大于等于x的位置
static int low_bounder(int[] nums, int x) {
   
int l = 0, r = nums.length;
   
while (l < r) {
       
int m = l + (r - l) / 2;
        if (x > nums[m]) {
            l = m +
1;
        }
else {
            r = m;
        }
    }
   
return l;
}

//第一个大于x的位置
static int up_bounder(int[] nums, int x) {
   
int l = 0, r = nums.length;
   
while (l < r) {
       
int m = l + (r - l) / 2;
        if (x >= nums[m]) {
            l = m +
1;
        }
else {
            r = m;
        }
    }
   
return l;
}

下一小节会具体分析代码行为,没有兴趣的同学只需要记住这两个函数的实现,或者熟悉应用各类语言的二分查找方法即可,下一节可以自行跳过。

代码行为分析:

前文已提,二分查找的是在对单调函数的定义域进行压缩,我们详细剖析一下代码行为:

在上图的场景中,递增函数f(x)定义域是[l,r),对于某一长度为n的数组来说,就可以让初始的 l = 0,r=n ,因为最大下标是n-1,所以初始下标范围[l,r)对应从0到n-1。

那么在如图所示的一次迭代中,行为如下:

1.初始范围[l,r)//l=0,r=n

2.因为f(m)<target

3.所以target必然处于[m+1,r)

所以若干轮迭代后,实际我们得到的是一个 [lend,rend)的范围,只是因为数组的定义域是离散的整数,所以我们最后可以根据需要得到结果。

upper_bound,lower_bound这两个名字也是对应 [ ) 这种区间定义得来的。

同样道理,我们可以用三分法求极值,例如:

1.已知函数先递减再递增

2.初始范围[l,r)

3.两个三等分点m1<m2

4.因为f(m2)<=f(m1),所以从m1左边必然还是递减

5.所以极值点存在范围必然是[m1,r)之间,最后当我们将范围压缩在[l,r=l+1)时,整个函数的最小值就是min(f(l),f(l+1))

例题:

/**
 *
某雪场共有 N 座雪山,数组 altitude中存储了各雪山海拔(精确到整数)。雪场出售新手票与老手票,新手区票价较高。
 * 若该雪场内最高海拔与最低海拔的差值大于 difference,则为老手区;否则为新手区。现在是滑雪活动旺季,雪场经营者希望获得更大收益,

想要将整个雪场打造成新手雪场。改造某座雪山海拔高度的成本为:变更高度的平方。注意:
 * 变更高度仅可为整数;
 * 变更工程可增加雪山海拔,也可降低雪山海拔;
 * 请问雪场经营者改造需要投入的最少成本是多少(即:所有改造雪山的成本之和)?
 * 答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。
 *
 * 示例 1:
 * 输入:altitude = [5,1,4,3,8], difference = 3
 * 输出:8
 * 解释:将 1 变成 3,8 变成 6 ,这时最高是 6,最小是 3,相差不超过 3。需要成本为:2^2 + 2^2 = 8
 * 示例 2:
 * 输入:altitude = [1,2,99999,3,100000], difference = 3
 * 输出:998679962
 * 解释:将 1,2 和 3 分别变为 40000,将 99999 和 100000 分别变为 40003,此时最高为 40003,最低为 40000,相差不超过 3,此时需要成本为 11998680039,为最小值,取模后为 998679962
 * 提示:
 *
 * 1 <= altitude.length <= 10^5
 * 1 <= altitude[i],difference <= 10^5
 */

PS:这道题我没在平台上找到,所以我也没有AC代码,主要分析解题思路。

我们定义一个函数f(x), x表示最终雪场内的最低山峰,f(x)表示最后成本。

F(x) = i=0nx<altitudeialtitude[i]-x|^2  x>altitudei+difference |altitudei-x ^2

我们可以直观地感觉,f(x)是一个先递减再递增的函数。证明省略。

容易看出这个函数的时间复杂度是 O(n),而原题数据范围是10^5,那么对于一个具备单调性的函数,我们求取极小值的时间复杂度就是O(lgn*n),肯定是够的。

所以最终解法就可以如上节所列三分法求极小值去找到最后的答案。

其他题目:

//1862. 向下取整数对和  

//1855. 下标对中的最大距离

好用的数据结构

绝大多数经典的数据结构不需要我们实现,我第一章所列的在各类语言中都有,做题时应灵活使用。如果已经很熟悉的本章不用看。

堆&优先级队列

优先级队列是一个非常非常非常常用的数据结构,可以动态维护最大值,Java里是PriorityQueue,python 是heapq。值得注意的是,红黑树也可以提供堆的功能,两者复杂度也相同,红黑树系数高一点,所以编码时可以用treeMap替代PriorityQueue。Python中没有类似TreeMap的数据结构。

    使用堆时就是对于需要动态获取最优值的场景,除了直接使用,还常常可以搭配贪心的思想使用,最典型的例子就是迪杰斯特拉算法。

//5774. 使用服务器处理任务,这是近期的一个周赛题目。

//295 数据流中的中位数,这是一个经典问题。

红黑树

红黑树没啥好说的,太常用了,所以单列在这里,常用方法如字面意思:

TreeMap<Integer, Integer> tee = new TreeMap<>();
tee.pollLastEntry();
tee.lastEntry();
tee.firstKey();
tee.higherKey(
1);
tee.lowerEntry(
2);
tee.pollFirstEntry();//
这个就可以替代前文说的poll

搜索

搜索是指BFS和DFS,其实包括动态规划内的大量问题最终都可以抽象为是对状态的搜索问题。这里不再具体解释,仅针对特性说明。

  • 对于一个E,V的图问题来说,两者都可以用于遍历,复杂度相同,即所有点(边)遍历一次。
  • 对于带环的图中,例如走迷宫,注意标记visited
  • BFS可以确保层数最短。
  • DFS可以记录临时max,不断压缩深度。

BFS

BFS可以记录每一层的节点数,适用于对层数敏感的问题。

此外,BFS使用的队列可以使用优先级队列,例如:迪杰斯特拉算法,或者数轴上进行搜索时进行优化

void bfs(node root) {
   
//这个队列可以换成优先队列
   
LinkedList<node> queue = new LinkedList<>();
    queue.addFirst(root);
   
while (!queue.isEmpty()) {
       
int num = queue.size();//这里可以记录每一层节点数目
       
for(int i =0;i<num;i++){
            node cur = queue.getLast();
            queue.removeLast();
           
//to do
           
for(node next:cur.children){ //这里可以增加一个判断是否访问过
                queue.addFirst(next);
            }
        }
    }
}

DFS

Dfs的特点是,如果对节点进出时标记上时间戳,则时间戳的包涵关系一定符合父子关系。所以对于祖先关系敏感的问题可以用dfs解决。

其他应用例如:tarjian算法

对于dfs中,寻找子节点的顺序,也可以根据某种优先级队列决定,例如:A星算法。地图问题中,使用A星算法加剪枝往往比bfs还要快。

void dfs(node root){
   
//do sth for start
   
root.start =time++;
   
for(node next:root.children){
        bfs(next);
    }
    root.
end =time++;
   
//do sth for end
}

一个例子:

如上图,进行dfs搜索时,每个节点进出都标记时间戳,最后形成的时间戳数记录如下:

那么我们就可以清楚滴看出来谁是谁的祖先,也可以根据差值计算某个节点的字节点数量,当然,这个时间戳还可以染色,加维度,进行各种骚操作。

单点&区间 VS 查询&修改

区间和的查询与修改

注:此处区间修改指对某一区间增加相同的值。

此处区间查询仅指查询区间和(或积等符合结合律并具有逆运算的运算)

单点查询

单点修改

区间查询

区间修改

原数组

o1

o1

on

on

前缀和数组

o1

on

o1

-

差分数组

on

o1

-

o1

RMQ问题

RMQ指区间最值查询问题,也是区间查询的一种。前缀和数组仅能解决带有逆运算的区间查询,如加法乘法,RMQ问题中运算方式是min()或max(),这个是不带逆运算的,所以不能用前缀和。

常用的有ST表,作为静态数据结构仅支持查询。

线段树可以支持查询和修改。

线段树和树状数组属于比较高级的数据结构,考试应该不会考,但是如果掌握了基本可以应付一切的区间查询及修改问题,可以参考这个视频。

https://www.bilibili.com/video/BV1f7411i7Nb

动态规划 & 贪心算法

把这两个放在一起写是因为(lan)无论动态规划还是贪心算法,都仅仅是一种思想,而且并没有合适的算法模板。

可信考试大纲说了没有动态规划,但是可能会有贪心算法。这两个东西某种程度要靠灵光一闪(yun)和见多识广(qi)。我列出一些自己做过的很巧妙的动态规划,具体解法见leetcode题解。贪心算法其实也是动态规划的一种特殊情况。具体原理介绍可以参考《算法导论》15、16章。

410. 分割数组的最大值

546. 移除盒子

1397. 找到所有好字符串

1463. 摘樱桃 II

1531. 压缩字符串 II

1547. 切棍子的最小成本

1659. 最大化网格幸福感

LCP 13. 寻宝  状态压缩+动态规划

887. 鸡蛋掉落  前谷歌面试题,非常经典

滑动窗口:

在这个话题下边增加一个小节介绍滑窗,是因为这个方法容易被误用,如果做题时一开始使用了错误的方法太浪费时间了,所以特殊介绍。

滑窗的本质是动态规划,假设当前窗口为 [ I,j] 那接下来的操作一定是变为[i+1,j] 或者[I,j+1]。例如二维动态规划如下,如果是三个指针就相当于3维的动态规划空间,但是因为每个指针最多移动n次,所以复杂度很低。

如果遍历过程中,指针需要逆向运行,那这个算法就没有任何意义,因为复杂度又跌落回到O(n^)2了。

此外,还需要满足单调性才可以使用,如上图需要保障向右向下的解永远更优,这样在规划的路径里才一定能记录到最优解。而且滑窗只记录一个状态,是非常有限的动态规划。

例如:规划路径为(0,0)->(0,1)->(1,1)->(1,2)时,发现(0,2)是比(1,2)更优的解,此时左指针是不能走回去的,这种就不能用滑窗。

例题:

53 最大字序和 虽然可以用滑窗,但实际是利用了一个很特殊的单调性,这题便于理解滑窗的动态规划本质。

523. 连续的子数组和  虽然也是连续子数组问题,但是用不了滑窗,同时也是一个使用了前缀和的题目。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值