OpenJudge 刷题思路汇总

文章目录

  • 模拟
  • 双指针
  • 动态规划
  • 二分
  • 数学
  • 分治
  • 编程竞赛记录

模拟

  • lc 7. 整数反转:从右往左计算每个数字,然后逆序累加到一个整数中。res 用 long long 来存,C++ 负数取模仍是负,所以不需要对负数进行额外处理。若 res 超过[INT_MIN, INT_MAX]范围,则返回 0

  • lc 螺旋矩阵系列 54.I 59.II 885.II:关键点是设一个方向数组dx[] = {0, 1, 0, -1}, dy[] = {1, 0, -1, 0}; 来简化代码,然后用 ( a , b ) (a,b) (a,b) 来确定 ( x , y ) (x,y) (x,y) 的下一个点是否可走,如果不能走,就改变个方向(因为由起点位置以及矩阵的特性,改变一次方向后一定能走了,而且一定能把所有点走完),然后让 ( x , y ) (x,y) (x,y) 变为可走的那个点,继续下一轮循环

  • lc 498. 对角线遍历:斜线遍历模版 用 row 和 col 来记录斜线起点的位置,每次改变 row 和 col,同时利用 sign,判断 temp 中的元素是否需要逆序存到 res 中。 螺旋矩阵法 遍历过程中只有四个方向,而且是按顺序选择方向的,每次循环先把当前可走的点加到 r e s res res中,然后利用 ( a , b ) (a,b) (a,b)找到下一个可以走的点,再让 ( x , y ) (x,y) (x,y)变成下一个可走的点,然后进入下一个循环

  • lc1424. 对角线遍历 II:如果按照这个顺序模拟一遍,遇到是空的话就跳过,但这样会超时。其实对角线上横纵坐标和是一个定值,利用这个性质,可以正常遍历 nums,然后把每个元素加到一个二维数组中(第一维是 x + y),因为加进去的时候是倒序的,所以最后再倒序遍历一下即可

双指针

双指针的核心思想:暴力做法通常需要枚举 [ 0 , n − 1 ] [0,n-1] [0,n1] 内的所有区间,而双指针 j , i j, i j,i 通过一次线性扫描,就能包含 [ 0 , n − 1 ] [0,n-1] [0,n1] 内的所有可能是答案的区间,那些一定不是答案的区间会被直接过滤掉。

之所以能直接过滤掉一系列区间,是因为题目会有某种性质,让指针 i i i 先动到一定位置,满足某个条件后,没有必要继续往后动了,后面的 i i i 一定不能和当前 j j j 构成答案区间,所以这时要开始动指针 j j j 了。

存字符的键值对可以用 int 数组( 操作 O ( 1 ) O(1) O(1) ,推荐)或用 unordered_map( 操作 O ( l o g n ) O(logn) O(logn))

在子串和字符串匹配问题上,通常会用变量 value 记录当前有多少个键值对和 need 匹配了,这样可以节省用循环判断当前窗口是否和 need 完全匹配

  • lc 76. 最小覆盖子串:滑动窗口万能模版 核心:找到缩小窗口的时机。用 value 表示窗口中满足 need 条件(匹配键值对)的字符个数。need 记录 T 中相应字符的出现次数 {A:1, B:2, C:1}。window 记录“窗口”中相应字符的出现次数。本题缩小窗口的时机是 valid == need.size() ,因为这时 i 再动, [ j , i ] [j,i] [j,i] 这个窗口一定不是最终答案(子串不是最短,或 value 变大),因为必须动 j 了,并更新一系列数据 滑动过程动图演示。注意存字符的键值对用 int 数组,这样复杂度可以降低很多。

  • lc 567. 字符串的排列: 滑动窗口 关键点是找到缩小窗口的时机,此题缩小窗口的时机是:i - j + 1 == s1.size(),因为是排列,所以必须长度相等,如 i 再往后移只会让答案不符,所以应该动 j 了。另外注意在改变指针时,都要更新一系列数据,同时用变量 value 记录当前有多少个键值对和 need 匹配了,这样可以节省用循环判断当前窗口是否和 need 完全匹配

  • lc 438. 找到字符串中所有字母异位词: 滑动窗口 跟567. 字符串的排列几乎一样,只要返回多个答案即可

  • lc 209. 长度最小的子数组:滑动窗口 这题缩小窗口的时机是 sum >= s,因为这时再动 i,得到的子数组虽然满足 sum >= s,但是长度一定不是最小的了

  • lc 面试题57. 和为s的连续正数序列:滑动窗口 滑动窗口 i = 1 , j = 1 i = 1, j = 1 i=1,j=1, i i i 先动,如果 s u m > t a r g e t sum > target sum>target了,那当前的 j 与后面的 i i i 之间的区间和也一定超过 s u m sum sum,所以 j j j 可以必须往后走了,直到 s u m < = t a r g e t sum <= target sum<=target 才停下来,接着是具体问题的逻辑,最后再动 i i i。时间复杂度: O ( n ) O(n) O(n) 数学公式推导 也可以用类似 lc 829. 连续整数求和 的方法去求数学公式 时间复杂度: O ( n ) O(\sqrt n) O(n )

  • lc 3. 无重复字符的最长子串:滑动窗口 跟“和为s的连续正数序列”这题思路完全一样,都是用两个指针去维护一个滑动窗口

  • ac 800. 数组元素的目标和:两个序列上的双指针 思路:对于一个固定 j j j,找到最小的 i i i 满足 A i + B j ≥ x A_i + B_j \geq x Ai+Bjx 。即 i i i 先动,当 A i + B j ≥ x A_i + B_j \geq x Ai+Bjx 时,再动 i i i ,一定不可能是答案,所以到了动指针 j j j 的时候了

动态规划

  • lc 300.最长上升子序列:动态规划 O ( n 2 ) O(n^2) O(n2) f [ i ] f[i] f[i] 表示以第 i 个数为结尾的上升子序列的集合,属性(最大值) 。贪心+二分 O ( n l o g n ) O(nlogn) O(nlogn) 对于一个上升子序列,其结尾元素越小,越有利于在后面接其他的元素,也就越可能变得更长。 t a i l [ i ] tail[i] tail[i] 表示长度为 i 的所有最长上升子序列结尾的最小值。具体地,每次来一个新的数 num,利用二分找到最后一个小于 num 的位置 l,然后让 t a i l [ l + 1 ] = n u m tail[ l + 1] = num tail[l+1]=num,这样新的数有更多可能性接在 t a i l [ l + 1 ] tail[l+1] tail[l+1]的后面

  • ac1024. 装箱问题:01背包问题 把物品的体积同时看做体积和价值,问题转化为在不超过背包最大容量的情况下,最大价值是多少。由于 N = 2 e 4 N = 2e4 N=2e4,所以必须对代码进行等价变形,降低空间复杂度

  • ac900. 整数划分:将 n 划分为若干个正整数之和的划分方案数 把1,2,3, … n分别看做n个物体的体积,这n个物体均无使用次数限制,问恰好能装满总体积为n的背包的总方案数。由推导得 f [ i ] [ j ] = f [ i − 1 ] [ j ] + f [ i ] [ j − i ] f[i][j] = f[i - 1][j] + f[i][j - i] f[i][j]=f[i1][j]+f[i][ji],注意初值 f [ i ] [ 0 ] = 1 f[i][0] = 1 f[i][0]=1(全不选也是一种方案),再对这公式进行等价变形。拓展: 将 n 划分为最大数不超过 k 的划分方案数(完全背包返回前 k 种物品的方案数 f [ k ] [ n ] f[k][n] f[k][n])

  • 百炼 4119:复杂的整数划分问题

  1. N划分成K个正整数之和的划分数(状态转移难)
    状态表示: f [ i ] [ j ] f[i][j] f[i][j]表示用 i 个物品(可重复),且恰好拼成j的方案数
    状态计算:此题状态转移比较难, f [ i ] [ j − i ] f[i][j - i] f[i][ji]表示用i个物品(可重复)且每个物品都大于1(同时减1), f [ i − 1 ] [ j − 1 ] f[i - 1][j - 1] f[i1][j1]表示用i个物品(可重复)且至少有一个物品是1(减去这个1)
    边界设置:只要设置 f [ 0 ] [ 0 ] = 1 f[0][0] = 1 f[0][0]=1,则所有状态都能被算出来
  2. N划分成不同正整数之和的划分数(01背包)
    状态表示: f [ i ] [ j ] f[i][j] f[i][j]表示只用前i种物品(最多一次),恰好拼成j的方案数
    状态计算: f [ i − 1 ] [ j ] + f [ i − 1 ] [ j − i ] f[i - 1][j] + f[i - 1][j - i] f[i1][j]+f[i1][ji](这两个集合不一定存在)
    边界设置:只要设置 f [ 0 ] [ 0 ] = 1 f[0][0] = 1 f[0][0]=1,则所有状态都能被算出来
  3. N划分成若干奇正整数之和的划分数 (完全背包)
    状态表示: f [ i ] [ j ] f[i][j] f[i][j]表示只用前i种物品(只选奇数),恰好拼成j的方案数
    状态计算: f [ i − 2 ] [ j ] + f [ i ] [ j − i ] f[i - 2][j] + f[i][j - i] f[i2][j]+f[i][ji] (这两个集合不一定存在)
    边界设置:只要设置 f [ 1 ] [ 0 ] = 1 f[1][0] = 1 f[1][0]=1,则所有状态都能被算出来
  • ac 1307. 牡牛和牝牛:状态表示:f[i]表示符合题目要求的 i 头牛的排列方式,状态计算:根据第 i 头牛是"0"还是"1"去划分,边界设置:设置f[0] = 1后,f[1]~f[n]都能被计算出

  • lc 1218. 最长定差子序列:这题用DP四部曲分析,思路会很清晰,但在状态表示时用 f [ i ] f[i] f[i] 表示所有以第 i 个数结尾,公差为 d 的子序列,复杂度 O ( n 2 ) O(n^2) O(n2) 会超时,而状态表示用 f [ i ] f[i] f[i] 表示以 i 结尾,公差为 d 的所有子序列就可以优化时间复杂度,另外注意 i 可能是负数,所以要用 map

  • ac282. 石子合并 区间DP专题 核心:最后一次合并一定是左边连续的一部分和右边连续的一部分进行合并。
    状态表示: f [ i ] [ j ] f[i][j] f[i][j] 表示将 i i i j j j 合并成一堆的方案的集合,属性 Min。
    状态计算:
    i < j i < j i<j 时, f [ i ] [ j ] = min ⁡ i ≤ k ≤ j − 1 { f [ i ] [ k ] + f [ k + 1 ] [ j ] + s [ j ] − s [ i − 1 ] } f[i][j] = \min\limits_{i\leq k \leq {j - 1}}\{f[i][k]+f[k+1][j] + s[j] -s[i - 1]\} f[i][j]=ikj1min{f[i][k]+f[k+1][j]+s[j]s[i1]}
    i = j i = j i=j 时, f [ i ] [ i ] = 0 f[i][i] = 0 f[i][i]=0 (合并一堆石子代价为 0)
    问题答案: f [ 1 ] [ n ] f[1][n] f[1][n]
    注意:公式中是 f [ k + 1 ] [ j ] f[k + 1][j] f[k+1][j] ,不是 f [ k ] [ j ] f[k][j] f[k][j]
    所有的区间dp问题,第一维都是枚举区间长度,一般 len = 1 用来初始化,枚举从 len = 2 开始,第二维枚举起点 i (右端点 j 自动获得,j = i + len - 1)。其实也可以按左端点倒着枚举,因为只要保证状态转移所依赖的状态被提前计算出来即可 参考这里

  • lc 312. 戳气球:区间DP 状态表示: f [ i ] [ j ] f[i][j] f[i][j] 表示戳破 ( i , j ) (i,j) (i,j) 之间所有气球的集合,属性(最大值)。这题的状态表示很巧妙,用到了开区间。状态计算:设 i 和 j 之间最后一个被戳破的气球为 k,那此时 ( i , k ) (i,k) (i,k) ( k , j ) (k,j) (k,j) 之间的气球已被戳破,最后 ( i , j ) (i,j) (i,j) 之间只剩下气球 k ,相邻的就是气球 i 和 j
    j − i > = 2 j - i >= 2 ji>=2时, f [ i ] [ j ] = f [ i ] [ k ] + f [ k ] [ j ] + p [ i ] ∗ p [ k ] ∗ p [ j ] f[i][j] = f[i][k] + f[k][j] + p[i]*p[k]*p[j] f[i][j]=f[i][k]+f[k][j]+p[i]p[k]p[j]
    状态穷举,最重要的一点是状态转移时用到的状态都已被提前计算出来,这题含有图解


  • ac 897. 最长公共子序列:分析动态规划的万能方法 DP 四部曲 具体请看这里
  1. 状态表示(由经验所得): f [ i ] [ j ] f[i][j] f[i][j]表示所有 A [ 1 , . . . , i ] A[1,...,i] A[1,...,i] B [ 1 , . . . , j ] B[1,...,j] B[1,...,j]的公共子序列的集合,属性(最大值)
  2. 状态计算:划分依据 a [ i ] a[i] a[i] b [ j ] b[j] b[j] 是否相同
    f [ i ] [ j ] = { f [ i − 1 ] [ j − 1 ] + 1 , if  ( a [ i ] = = b [ j ] ) m a x ( f [ i − 1 ] [ j ] , f [ i ] [ j − 1 ] ) , if  ( a [ i ] ! = b [ j ] ) f[i][j] = \left\{ \begin{aligned} &f[i-1][j-1] + 1,&\text{if } (a[i] == b[j]) \\ &max(f[i-1][j],f[i][j-1]),&\text{if } (a[i]!=b[j]) \end{aligned} \right. f[i][j]={f[i1][j1]+1max(f[i1][j],f[i][j1])if (a[i]==b[j])if (a[i]!=b[j])
    说明:上面的 a[i] 表示 A 的第 i 个数
  3. 边界设置:只要设置 f [ i ] [ 0 ] = 0 f[i][0] = 0 f[i][0]=0 f [ 0 ] [ j ] = 0 f[0][j] = 0 f[0][j]=0,则循环中所有状态都可以计算
  4. 问题答案 f [ 1 ] [ n ] f[1][n] f[1][n] (由状态表示的实际含义推得)
    说明:DP 问题求最大值的时候只要保证不遗漏就行了,可以重复,求最长公共子序列时, f [ i − 1 ] [ j ] f[i-1][j] f[i1][j]包含00和01,而 f [ i ] [ j − 1 ] f[i][j-1] f[i][j1]包含00和10,所以他们是有交集的

  • lc 718. 最长公共子串(子数组):DP 四部曲
  1. 状态表示 f [ i ] [ j ] f[i][j] f[i][j]表示 A [ 1 , . . , i ] A[1,..,i] A[1,..,i] B [ 1 , . . , j ] B[1,..,j] B[1,..,j]的公共子串,且以 A [ i ] A[i] A[i] B [ j ] B[j] B[j]结尾的结合,属性(最大值)

  2. 状态计算:划分依据 a [ i ] a[i] a[i] b [ j ] b[j] b[j] 是否相同
    f [ i ] [ j ] = { f [ i − 1 ] [ j − 1 ] + 1 , if  ( a [ i ] = = b [ j ] ) 0 , if  ( a [ i ] ! = b [ j ] ) f[i][j] = \left\{ \begin{aligned} &f[i-1][j-1] + 1,&\text{if } (a[i] == b[j]) \\ &0,&\text{if } (a[i]!=b[j]) \end{aligned} \right. f[i][j]={f[i1][j1]+10if (a[i]==b[j])if (a[i]!=b[j])

  3. 边界设置:只要设置 f [ i ] [ 0 ] = 0 f[i][0] = 0 f[i][0]=0 f [ 0 ] [ j ] = 0 f[0][j] = 0 f[0][j]=0,则循环中所有状态都可以计算

  4. 问题答案 m a x 1 ≤ i ≤ n , 1 ≤ j ≤ m f [ i ] [ j ] \mathop{max} \limits _{1\leq i \leq n,1\leq j \leq m}f[i][j] 1in,1jmmaxf[i][j] (由状态表示的实际含义推得)

  • 笔试题. 输出一个最长公共子串:按斜线遍历 时间复杂度 O ( n m ) O(nm) O(nm),空间复杂度 O ( 1 ) O(1) O(1) 。 因为计算每一个 f [ i ] [ j ] f[i][j] f[i][j] 的时候只需要计算 f [ i − 1 ] [ j − 1 ] f[i-1][j-1] f[i1][j1],所以只要按照斜线方向(遍历 n + m - 1条斜线)计算所有的值,用一个变量维护最大值即可。核心:知道如何遍历 n + m - 1 条斜线(这里从最右上角的斜线开始)对角线遍历模版题
  • lc 813. 最大平均值和的分组:状态表示: f [ i ] [ j ] f[i][j] f[i][j]表示前 i 个元素分成 j 组的方案集合,属性(最大值),状态计算:把前面的 j - 1 组用一个状态表示,但要注意有些集合不一定存在,画一个分析图会很清楚!

二分

  • lc374. 猜数字大小 + KickStart NumberGuessing + eoj3342. 经典的猜数字游戏:交互题 猜数字游戏的本质就是个二分,题目会有一个数 pick,然后你在一个区间 [ 1 , n ] [1, n] [1,n] 中二分查找这个数。由于你实现不知道 pick 是多少,所以需要出题人告诉你这个数大了还是小了,还是猜中了(guess 函数的功能),这部分相当于二分中的 check 函数。注意:虽然范围是 [ 1 , n ] [1,n] [1,n],但用这范围如果出题人的数是最后一个,那最后一次会还没有猜就退出循环,所以保险起见最好用 w h i l e ( t r u e ) while(true) while(true)

  • lc LCP 12. 小张刷题计划:最大值最小化,二分答案常用套路

数学

  • lc204. 计数质数:线性筛素数 核心:每个数只会被最小的质因数筛掉

  • lc372. 超级次方:快速幂 a [ 1 , 5 , 2 , 6 ] = ( a [ 1 , 5 , 2 ] ) 10 a 6 a^{[1,5,2,6]} = (a^{[1,5,2]})^{10} a^6 a[1,5,2,6]=(a[1,5,2])10a6 可以看到问题的规模减小了,所以可以用递归来解决

  • lc96. 不同的二叉搜索树:卡特兰数 模版题

  • lc 829. 连续整数求和:数学公式推导 起点 i i i,长度 n n n,求和公式 ( 2 ∗ i + n − 1 ) n / 2 = N (2*i+n-1)n/2 = N (2i+n1)n/2=N,解出 i = ( N − n ( n − 1 ) / 2 ) / n i = (N-n(n-1)/2)/n i=(Nn(n1)/2)/n。我们从 1 1 1开始遍历长度 n n n i i i 一开始会很大),如果 i < 1 即 N < n ( n + 1 ) / 2 i < 1即N < n(n+1)/2 i<1N<n(n+1)/2退出循环;如果 ( N − n ∗ ( n − 1 ) / 2 ) % n = = 0 (N - n * (n - 1)/2) \% n == 0 (Nn(n1)/2)%n==0 说明起点 i i i 存在。时间复杂度: O ( N ) O(\sqrt N) O(N ) 另一种思路:3 个连续整数(a,b,c)时,b 比 a 大 1,c 比 a 大 2,如果 N - 1 - 2 能整除 3,则商、商+1 与商+2 构成 N,其余同理

  • lc 78. 找子集 二进制组合 从子集的特点出发,n 个元素,每个元素选或不选,共有 2 n 2^n 2n 种可能。遍历 n 位二进制数 i i i,其取值范围为 [ 0 , 2 n ) [0,2^n) [0,2n) ,每次判断一下 i i i 上哪几位是 1 就表示选择了这几个数。 DFS搜索 用搜索也是可以做的

  • AcWing 890.能被整除的数 容斥原理 二进制组合 方法类似lc 78. 找子集,只需用二进制的思想去枚举选法即可,第 k 位是 1 则表示选择了第 k 个质数

分治

  • 剑指offer. 丑数: 三路归并 { a i } \{a_i\} {ai} 表示至少包含一个因子 2 的丑数集合…,借助 { a i / 2 } , { b i / 3 } , { c i / 5 } \{a_i / 2\},\{b_i/3\},\{c_i / 5\} {ai/2},{bi/3},{ci/5} 去还原 { a i } , { b i } , { c i } \{a_i \},\{b_i\},\{c_i \} {ai},{bi},{ci}。当我们先把 1 加到答案集合后,每当我需要用 a i a_i ai 这个值时,可以发现在一步步把丑数添加到答案集合的过程中 a i / 2 a_i / 2 ai/2 这个值一定已经被求出,所以乘 2 后就得到了 a i a_i ai,同理可知 b j b_j bj c k c_k ck 这样就可以进行三路合并了。
  • lc 313. 超级丑数:K路合并 和求第 n 个丑数类似,只不过那里是三路合并,这里是 K 路合并

编程竞赛记录

算法编程竞赛的一些经验和教训
算法编程学习过程中的技巧积累
算法编程学习过程中的踩坑记录
手把手撕LeetCode题目,扒各种算法套路的裤子

  • 2020 年 “联想杯”全国高校程序设计在线邀请赛
  • 2020 年 “游族杯” 全国高校程序设计网络挑战赛
  • 2019 年 “游族杯” 全国高校程序设计网络挑战赛
  • 2020 力扣杯!Code Your Future 春季全国编程大赛 个人赛
  • 2020 力扣杯!Code Your Future 春季全国编程大赛 战队赛

你可能感兴趣的:(LeetCode,Algorithm)