【ONE·基础算法 || 动态规划(二)】

在这里插入图片描述

总言

  主要内容:编程题举例,熟悉理解动态规划类题型(子数组、子序列问题)。
  
  
  
  
  

文章目录

  • 总言
  • 5、子数组问题(数组中连续的一段)
    • 5.1、最大子数组和(medium)
      • 5.1.1、暴力
      • 5.1.2、前缀和
      • 5.1.3、动态规划
    • 5.2、环形子数组的最大和(medium)
      • 5.2.1、题解
    • 5.3、乘积最大子数组(medium)
      • 5.3.1、题解
    • 5.4、乘积为正数的最长子数组长度(medium)
      • 5.4.1、题解
    • 5.5、等差数列划分(medium)
      • 5.5.1、题解
    • 5.6、最长湍流子数组(medium)
      • 5.6.1、题解
    • 5.7、单词拆分(medium)
      • 5.7.1、题解
    • 5.8、环绕字符串中唯一的子字符串(medium)
      • 5.8.1、题解
  • 6、子序列问题(数组中不连续的一段)
    • 6.0、概念区别:子序列和子数组
    • 6.1、最长递增子序列(medium)
      • 6.1.1、题解
    • 6.2、摆动序列(medium)
      • 6.2.1、题解
    • 6.3、最长递增子序列的个数(medium)
      • 6.3.1、记忆搜索化
      • 6.3.2、动态规划
    • 6.4、最长数对链(medium)
      • 6.4.1、题解
    • 6.5、最长定差子序列(medium)
      • 6.5.1、动态规划(不做优化版本)
      • 6.5.2、动态规划(使用hash优化版本)
    • 6.6、 最长的斐波那契子序列的长度(medium)
      • 6.6.1、题解
    • 6.7、 最长等差数列(medium)
      • 6.7.1、题解
    • 6.8、等差数列划分II-子序列(hard)
      • 6.8.1、题解
  • Fin、共勉。

  
  
  
  
  

5、子数组问题(数组中连续的一段)

5.1、最大子数组和(medium)

  题源:链接。

在这里插入图片描述

  
  

5.1.1、暴力

  两层for循环,统计每次求出的最大值。

class Solution {
public:int maxSubArray(vector<int>& nums) {int n = nums.size();int ret = -0x3f3f3f;// 用于获取所有层子数组和for(int i = 0; i < n; ++i){int sum = 0;int curmax = -0x3f3f3f; // 用于统计当前层的最大子数组和for(int j = i; j < n; ++j){sum += nums[j];curmax = max(sum,curmax);}ret = max(curmax,ret);}return ret;}
};

  
  
  
  

5.1.2、前缀和

  用一个变量记录历史的最小前缀和,在求第j个位置的前缀和时,与历史前缀和做差。

class Solution {
public:int maxSubArray(vector<int>& nums) {int min_sum = 0;//维护一个最小前缀和int sum = 0;//用于统计当前前缀和int ret = -0x3f3f3f;// 返回for(auto n: nums){sum += n;//获取当前前缀和ret = max(ret,sum - min_sum);//和历史前缀和比较min_sum = min(min_sum,sum);//更新历史最小前缀和}return ret;}
};

  
  
  
  
  

5.1.3、动态规划

  这里主要分析动态规划解题。

  1)、思路分析
  1、确定状态表示: 根据经验+题目要求,这里我们选择以 i 为结尾,则dp[i]表示,以 i 位置处的元素为结尾的所有连续子数组中,元素和的最大值。如下图所示,以 nums[i] 结尾的子数组可以有多个,而 dp[i] 存储的就是这些子数组中的最大和。

在这里插入图片描述

  2、确定状态转移方程: 我们可以将dp[i]的分为以下两种情况:

1、⼦数组的⻓度为1:此时dp[i] = nums[i]2、子数组的长度大于1:此时dp[i]应该等于以i-1做结尾的所有子数组中和的最大值,再加上nums[i], 也就是dp[i-1] +nums[i]

在这里插入图片描述
  上述两种情况中,我们需要的是最大值,因此dp[i] = max(nums[i],dp[i-1] +nums[i])
  

  3、初始化: 根据状态转移方程,为了防止填表越界,需要对 i=0 处位置进行初始化。这里,我们可以选择直接初始化,也可以引入虚拟节点。
  此处选择后者,那么需要注意两个事项:
  ①引入虚拟节点后,dp表和原先nums表中的映射关系。写状态转移方程时需要注意。
  ②虚拟节点处填写的值,需要保证后续填表时正确。
在这里插入图片描述

  

  4、填表顺序: 根据状态转移方程,这里填表顺序从左到右。
  

  5、返回值: 由于最大子数组和的结尾我们是不确定的,因此,这里需要返回整个dp表中的最大值。(可以在完成填表后遍历一遍dp表找最大值,也可以边填表边确定dp表中最大值。)
  
  

  2)、题解

class Solution {
public:int maxSubArray(vector<int>& nums) {int n = nums.size();// 1、创建dp表:dp[i]表示,以i位置元素为结尾的所有子数组的最大和vector<int> dp(n + 1); // 2、初始化:引入虚拟节点,初始化即dp[0] = 0int ret = -0x3f3f3f; // 用于返回// 3、填表:从左向右填for (int i = 1; i < n + 1; ++i) {dp[i] = max(nums[i - 1], dp[i - 1] + nums[i - 1]);ret = max(dp[i], ret); // 这里,在遍历的同时一并更新dp表中的最大和}// 4、返回return ret;}
};

  
  
  
  
  
  
  
  
  
  
  

5.2、环形子数组的最大和(medium)

  题源:链接。

在这里插入图片描述
  
  

5.2.1、题解

  1)、思路分析
  分析题目,可以发现此题和5.1很像,区别在于本题中数组成环,那么,和最大的子区间有以下两种存在形式:

1、和最大的子区间,就在原数组内连续的一段区域上(包括整个数组);
2、和最大的子区间,出现在原数组内首尾相连的两段上。

在这里插入图片描述
  如果是情况一,其思路就和5.1一致了。如果是情况二,其实我们可以稍微逆向思考一下。
在这里插入图片描述

  如上图分析,那么实际上我们只需要按照5.1中的思路,分别定义两个dp表,一个求最大元素和,一个求最小元素和即可(对于后者,用总的元素和减去当前求得的最小元素和,即可得到区域在首位两端的子区间的最大元素和)。
  
  

  1、确定状态表示: 根据经验+题目要求,这里以 i 为结尾。则有:

f[i]:以 i 为结尾的所有连续子数组中,元素和的最大值。
g[i]:以 i 为结尾的所有连续子数组中,元素和的最小值。

  

  2、确定状态转移方程: 5.1中分析过,此处不再重复。

f[i] = max(f[i-1] + nums[i], nums[i]);
g[i] = min(g[i-1] + nums[i], nums[i]);//注意这里求的是最小元素和
// PS:若填表时引入虚拟节点,则此处注意下标映射关系

  
  3、初始化: 这里我们选择引入虚拟节点进行初始化,为了避免下标越界,需要将 i=0 处的位置进行初始化。有 f[0] = 0g[0] = 0

  4、填表顺序: 从左往右。

  5、返回值: 注意特殊情况。

在这里插入图片描述

  
  
  2)、题解
  时间复杂度: O ( n ) O(n) O(n),空间复杂度: O ( n ) O(n) O(n)

class Solution {
public:int maxSubarraySumCircular(vector<int>& nums) {int n = nums.size();// 1、创建dp表,确定状态表示vector<int> f(n + 1); // f[i]:以i为结尾的所有连续子数组中的最大和。vector<int> g(n + 1); // g[i]:以i为结尾的所有连续子数组中的最小和。那么最大值即sum - g[i]// 2、初始化:其实也就是f[0] = g[0] = 0,不写默认就是0// 3、填表:从左向右int sum = 0;          // 记录nums数值元素总和int fmax = -0x3f3f3f; // 记录f表中的最大子数组和int gmin = 0x3f3f3f;  // 记录g表中的最小子数组和for (int i = 1; i < n + 1; ++i) // 一次更新所有:注意下标映射关系{sum += nums[i - 1];f[i] = max(f[i - 1] + nums[i - 1], nums[i - 1]);fmax = max(fmax, f[i]);g[i] = min(g[i - 1] + nums[i - 1], nums[i - 1]);gmin = min(gmin, g[i]);}// 4、返回return sum == gmin ? fmax : max(fmax, sum - gmin);}
};

  
  
  
  
  
  
  
  
  
  
  

5.3、乘积最大子数组(medium)

  题源:链接。

在这里插入图片描述
  
  

5.3.1、题解

  1)、思路分析

  1、确定状态表示: 根据经验+题目要求,这里以 i 为结尾分析。效仿之前 “5.1、最大子数组和” 的思路,我们很自然地想到:

dp[i]: 表示以 i 为结尾的所有连续子数组中,元素乘积的最大值。初步看来,我们可能会写出如下递推公式:
dp[i] = max(nums[i], dp[i-1]*nums[i])

  但这里需要仔细分析斟酌,这个递归公式在本题真的的适用吗?

  在求解最大子数组和时,dp[i-1] + nums[i] ,加减运算保持了子数组的最大和性质,因为对 i-1 每个的子数组加上 nums[i],其和的变化是单调的,不会改变原先最大和子数组的位置

  但在处理乘除运算时,情况就复杂多了。乘法运算具有不同于加法的性质:正负数相乘可能产生负数,负数相乘则可能变成正数。这意味着,当 nums[i] 为负数时,dp[i-1] * nums[i] 的结果可能不仅不是 dp[i] 的最大值,反而可能是最小值(如果 dp[i-1] 是正数)。同理,如果 dp[i-1] 是当前区间内的最小乘积(且为负数),而 nums[i] 也是负数,那么 dp[i-1] * nums[i] 可能会是一个很大的正数,从而成为 dp[i] 的最大值。

在这里插入图片描述

  因此,我们不能仅凭 dp[i-1] * nums[i] 来确定 dp[i] 的最大值,还需要考虑 nums[i] 与之前子数组乘积的最小值之间的关系。所以,正确的状态转移需要同时记录到当前位置为止的最大乘积和最小乘积(因为最小乘积乘以负数可能成为最大乘积),从而更全面地处理所有可能的乘积情况。
  

f[i]: 表示以 i 为结尾的所有子区间中的最大乘积。
g[i]: 表示以 i 为结尾的所有子区间中的最小乘积。

  
  

  2、确定状态转移方程: 根据上述情况,对于f[i],也就是以 i 为结尾的所有子数组的最大乘积,可以分为三种形式:

1、nums[i]
2、f[i-1]*nums[i]
3、g[i-1]*nums[i]

  对于后两者最大、最小乘积可能发生转变,但不管如何变化。实则对于f[i],只需要求三者最大即可。对g[i],求三者最小。

f[i] = max( max(f[i-1]*nums[i], g[i-1]*nums[i]), nums[i]))
g[i] = min( min(f[i-1]*nums[i], g[i-1]*nums[i]), nums[i]))

  这里,我们展开细节来分析一下(虽然没必要,无非把各种情况组合一下)

最大 × 正数 = 仍旧等于最大
最大 × 负数 = 最小;最小 × 正数 = 仍旧等于最小
最小 × 负数 = 最大。

  1、对f[i],以i为结尾的所有子数组的最大乘积:

1、子数组的长度为1,也就是nums[i] ;
2、子数组的长度大于1,nums[i] > 0,此时i位置处的最大值为 nums[i] * f[i-1];
3、子数组的长度大于1,nums[i] < 0,此时i位置处的最大值为 nums[i] * g[i-1];综上所述,f[i] = max( max(f[i-1]*nums[i], g[i-1]*nums[i]), nums[i]))
(如果nums[i] = 0,所有子数组的乘积均为0,三种情况其实都包含了)

  2、对g[i],以i为结尾的所有子数组的最小乘积:

1、子数组的长度为1,也就是nums[i] ;
2、子数组的长度大于1,nums[i] > 0,此时i位置处的最小值为 nums[i] * g[i-1];
3、子数组的长度大于1,nums[i] < 0,此时i位置处的最小值为 nums[i] * f[i-1];(如果nums[i] = 0,所有子数组的乘积均为0,三种情况其实都包含了)
综上所述,g[i] = min( min(f[i-1]*nums[i], g[i-1]*nums[i]), nums[i]))

  
  
  
  3、初始化: 这里,采用引入虚拟节点的方式初始化。此时需要注意两点:
  ①下标的映射关系

// 引入虚拟节点后的状态转移方程:
f[i] = max( max(f[i-1]*nums[i-1], g[i-1]*nums[i-1]), nums[i-1]))
g[i] = min( min(f[i-1]*nums[i-1], g[i-1]*nums[i-1]), nums[i-1]))

  ②虚拟节点的初始值要保证后续填表时的结果正确。根据上述状态转移方程,为了填表时不发生越界,需要将 i = 0 处的位置进行初始化。由于这里是乘法,为了让max、min判断时只选到nums[i-1], f[0] = g[0] = 1即可。
在这里插入图片描述

  
  
  4、填表顺序: 从左往右,两个表⼀起填。

  5、返回值: 返回 f 表中的最⼤值。
  
  
  2)、题解

class Solution {
public:int maxProduct(vector<int>& nums) {int n = nums.size();// 1、创建dp表vector<int> f(n + 1), g(n + 1);// 2、初始化f[0] = g[0] = 1;// 3、填表int ret = INT_MIN;for (int i = 1; i < n + 1; ++i) {// 后续多个位置要用到相同数值int x = nums[i - 1];int y = f[i - 1] * x;int z = g[i - 1] * x;f[i] = max(x, max(y, z));g[i] = min(x, min(y, z));ret = max(ret, f[i]);}// 4、返回return ret;}
};

  
  
  
  
  
  
  
  
  

5.4、乘积为正数的最长子数组长度(medium)

  题源:链接。

在这里插入图片描述

  
  

5.4.1、题解

  1)、思路分析
  分析题目,我们可以提炼出以下关键信息:需要找到乘积为正数最长子数组,且这个子数组的长度要尽可能长(注意,这里的“最长”指的是子数组的长度,而非乘积的数值大小,长度最长不代表乘积最大)。

  
  1、确定状态表示: 根据经验+题目要求,这里以 i 为结尾,在5.3中我们分析过,由于乘法运算的性质,会导致同一个子数组,在后续累乘中,正负发生变化。 因此,我们需要定义两个状态数组。如下:

1、f[i]:表示以 i 为结尾的所有子数组中,乘积为正数的最长子数组的长度。
// 首先要是一个连续的子数组,其次要满足数组乘积为正数,最后,f[i]值记录的是这些子数组的最长长度。2、g[i]:表示以 i 为结尾的所有子数组中,乘积为负数的最长子数组的长度。

  
  
  2、确定状态转移方程: 来回顾一下乘法的基本性质。

正数 × 正数 = 正数
负数 × 负数 = 正数正数 × 负数 = 负数
负数 × 正数 = 负数

  
  1、对f[i],分析如下。

在这里插入图片描述

  最后将情况汇总一下:

当nums[i] > 0 时, f[i] = f[i-1] + 1;
当nums[i] < 0 时, f[i] = g[i-1] == 0 ? 0 : g[i-1] +1;

  

  2、对g[i],同理进行分析。
在这里插入图片描述

  最后将情况汇总一下:

当nums[i] > 0 时, g[i] = g[i-1]  == 0 ? 0 : g[i-1] +1;
当nums[i] < 0 时, g[i] = f[i-1];

  
  

  3、初始化: 这里,引入虚拟节点进行初始化。注意两个事项:
  ①下标映射关系。

当nums[i-1] > 0 时,f[i] = f[i-1] + 1;g[i] = g[i-1]  == 0 ? 0 : g[i-1] +1;当nums[i-1] < 0 时,f[i] = g[i-1] == 0 ? 0 : g[i-1] +1;g[i] = f[i-1];

  ②虚拟节点的初始化值,要保证后续填表正确。f[0] = g[0] = 0

在这里插入图片描述

  

  4、填表顺序: 从左往右,两个表一起填。
  

  5、返回值: 题目要求返回最长子数组长度,则其存储在 f 表中,返回 f 表中的最大值(不一定是最后一个位置的值)
  
  

  2)、题解

class Solution {
public:int getMaxLen(vector<int>& nums) {int n = nums.size();// 1、创建dp表并初始化vector<int> f(n + 1), g(n + 1);// 2、填表int ret = 0;for (int i = 1; i < n + 1; ++i) {if (nums[i - 1] > 0) {f[i] = f[i - 1] + 1;g[i] = g[i - 1] == 0 ? 0 : g[i - 1] + 1;} else if (nums[i - 1] < 0) {f[i] = g[i - 1] == 0 ? 0 : g[i - 1] + 1;g[i] = f[i - 1] + 1;}ret = max(ret, f[i]);}// 3、返回return ret;}
};

  
  
  
  
  
  
  
  
  

5.5、等差数列划分(medium)

  题源:链接。

在这里插入图片描述

  
  

5.5.1、题解

  1)、思路分析

  1、确定状态表示: 根据经验+题目要求,这里我们以 i 为结尾。则 dp[i] 表示 以 i 为结尾的子数组中,构成等差数列的个数。
  
  
  2、确定状态转移方程: 根据等差数列的定义和性质,首先能知道的是,如果length < 3 ,不满足构成等差数列的项数,可以直接排除。当length >=3时,我们只需要比较ii-1i-2三项是否满足等差数列即可。
在这里插入图片描述

  综上,这里状态转移方程为:

// 此处使用了三目运算符直接判断,当然也可以使用if语句分别讨论。
dp[i] = (nums[i - 2] + nums[i] == 2 * nums[i - 1]) ? dp[i-1] + 1 : 0;

  
  
  3、初始化: 这里,我们可以直接进行初始化。根据状态转移方程,这里需要用到前两个位置的元素,但是前两个位置的元素无法构成等差数列,因此dp[0] = dp[1] = 0 。这样一来,从 i =2 开始就是三项,即可代入方程获取值。

在这里插入图片描述

  
  
  4、填表顺序: 从左到右。
  
  
  5、返回值: 注意,题目要求返回所有构成等差数列的子数组的个数。而我们的状态表示中,dp[i] 仅存储 i 位置处构成等差数列的子数组个数,因此,这里需要我们返回的是dp表的和
  
  

  2)、题解

class Solution {
public:int numberOfArithmeticSlices(vector<int>& nums) {int n = nums.size();if(n <3) return 0;// 不满足构成等差数列的项数,可以直接排除// 1、创建dp表并初始化:dp[0] = dp[1] = 0 vector<int> dp(n);// 2、填表int sum = 0;for(int i = 2; i < n; ++i){dp[i] = nums[i] + nums[i-2] == nums[i-1]*2 ? dp[i-1] + 1 : 0;sum += dp[i];}// 3、返回return sum;}
};

  
  
  
  
  
  
  
  
  

5.6、最长湍流子数组(medium)

  题源:链接。

在这里插入图片描述
  
  

5.6.1、题解

  1)、思路分析
  先来分析题目中湍流的含义:
在这里插入图片描述

  
  1、确定状态表示: 根据经验+题目要求,以 i 为结尾,则 dp[i] 表示,以 i 为结尾的所有子数组中,构成湍流的最长子数组长度。
  根据上述的分析,以 i 为结尾的湍流,可以是上升趋势,可以是下降趋势。我们并不知道前⼀个最长湍流数组的结尾处是递增的,还是递减的。因此,这里的状态表示需要再细化:

f[i]: 以 i 位置元素为结尾的所有⼦数组中,最后呈现"上升状态"下的最⻓湍流数组的⻓度;
g[i]: 以 i 位置元素为结尾的所有⼦数组中,最后呈现"下降状态"下的最⻓湍流数组的⻓度;

  
  

  2、确定状态转移方程:
在这里插入图片描述

  
  
  3、初始化: 为了防止越界,需要将 i = 0处的位置初始化。根据题目要求,即使是单个结点自身,也构成湍流。因此可以将表内所有元素初始化为1。 我们填表从 i=1位置开始填。
  
  
  4、填表顺序: 从左往右,两个表⼀起填。
  
  
  5、返回值: 由于不确定最终湍流是上升还是下降趋势,因此,这里应该取两表中的最大长度值。
  
  
  2)、题解

class Solution {
public:int maxTurbulenceSize(vector<int>& arr) {int n = arr.size();// 1、创建dp表并初始化vector<int> f(n,1),g(n,1);// 2、填表int fmax = 1;// 自身一个结点也算做湍流,故不存在为0的情况,最少长度为1int gmax = 1;for(int i = 1; i < n; ++i){if(arr[i-1] > arr[i])// 最后一次呈现下降趋势g[i] = f[i-1] + 1;else if(arr[i-1] < arr[i])// 最后一次呈现上升趋势f[i] = g[i-1] + 1;// 其它情况:不满足,每次都以i位置作为新的子数组// 更新fmax = max(fmax,f[i]);gmax = max(gmax,g[i]);}// 3、返回return max(fmax,gmax);}
};

  
  
  
  
  
  
  
  
  
  
  
  

5.7、单词拆分(medium)

  题源:链接。

在这里插入图片描述

  
  

5.7.1、题解

  1)、思路分析
  先来确定题目需求,给定的字符串 s ,能否被多个字典单词拼接而成。这意味着我们需要将字符串拆分为一个一个的单词。

在这里插入图片描述

  
  1、确定状态表示: 根据经验+题目要求,这里我们以 i 为结尾,则dp[i]表示,从[0,i]区间内的字符串,能否被字典中的单词拼接而成。即,这是一个bool类型的状态表。

dp[i] = true, 表示[0,i]区间段内的字符串,存在至少一种拆分方式,能被字典拼接出。
dp[i] = false, 表示[0,i]区间段内的字符串无论如何拆分,都不能被字典拼接出。

在这里插入图片描述

  
  
  2、确定状态转移方程: 根据上述的状态表示,[0,i]区间实则可以拆分为两部分。一部分是以 i 结尾的最后一个单词。另一部分是其它单词组合。这里,我们设最后一个单词的首下标在 j 位置( 0<= j <=i),则[0,i]区间段内分为如下两部分:
在这里插入图片描述
  因此,对dp[i],要确定[0,i]区间内的字符串是否能够被字典单词拼接,只需要确定:

1[j,i]组成的最后一个单词,是否出现在字典中?
2[0.j-1]区间段内的字符串是否能被字典单词拼接?

  对情况1,我们需要提取子串。substr(j,i-j+1),对比字典单词进行判断;
  对情况2,其描述正好是dp[j-1],直接读取dp表状态即可。

所以,dp[i] = (dp[j-1]==true) &&  (substr(j,i-j+1)是否在字典中)
如果二者均为true,则 dp[i] = true

  
  
  需要注意,最后一个单词的长度,我们是不确定的,即substr(j,i-j+1) && dp[j-1] 存在多种组合方式,这就需要我们遍历寻找。
在这里插入图片描述

  
  3、初始化: 根据上述状态方程,为了防止越界,这里需要对i = 0位置处进行初始化。
  在这种字符串、子串问题中,一般我们选择引入虚拟节点进行初始化。同时,为了方便处理下标映射关系,可以给原字符串虚拟结点对应的位置插入一个占位字符(一般选择空串)。这样一来,下标的映射关系就得到了统一,我们只需要考虑虚拟节点的填值问题。
在这里插入图片描述

  
  
  4、填表顺序: 从左到右
  
  
  5、返回值: 返回dp[n-1]处的bool值。
  
  

  2)、题解

class Solution {
public:bool wordBreak(string s, vector<string>& wordDict) {unordered_set<string> hash(wordDict.begin(), wordDict.end()); // 哈希表,方便找字典单词s = ' ' + s; // 为了方便dp表中引入虚拟节点初始化,下标映射能对得上int n = s.size();// 1、创建dp表并初始化vector<bool> dp(n,false);dp[0] = true;// 2、填表:注意起始位置for (int i = 1; i < n; ++i) {// 不断找最后一个单词,以此将[0,i]区间分为两部分进行判断for (int j = i; j >= 1; --j) {string endstr = s.substr(j, i - j + 1); // 提取最后一个单词if (dp[j - 1] && hash.count(endstr))    // 如果都满足条件{dp[i] = true; // 说明存在至少一种分割方式,能被字典拼接break; // 这里我们只用判断是否存在即可,不需要找全所有方式}}}// 3、返回return dp[n - 1];}
};

  
  
  
  
  
  
  
  
  
  
  
  

5.8、环绕字符串中唯一的子字符串(medium)

  题源:链接。

在这里插入图片描述

  
  

5.8.1、题解

  1)、思路分析
  先来分析题目,题目问的是,找出 s 中有多少 不同非空子串 也在 base 中出现。
  由示例2,s = "cac"可以看出,相同的子串只能被计数一次
  由示例3,s = "zab"可以看出,由于base是无限环绕的,需要考虑……za……此类跨越首尾的情况。

  
  如何判断 s 中的某个子串,是否会出现在 base 中?
  仔细观察 base 这个连续字符串,我们会发现,它的顺序都是固定的,从a→z无限循环。而我们知道a~z这26个字母,相邻两个字符之间,ASCII码相差为1。这就意味着,base中随机挑选出的子串,也要保持这一连续的顺序性。即:

base[i-1]+1 == base[i]

  那么对于za这种跨越首尾的情况呢?我们可以单独对这个衔接部分做处理。也可以做取模运算让z折回到a处。

base[i-1] == 'z' && base[i] == 'a';
(base[i-1] + 1 - 'a') % 26) == (base[i]- 'a');

  

  上述是对题目性质的分析,下述是对解题方法的分析。这里我们以动态规划解题。动态规划只是给我们一种解题的方法,而这些方法是需要套用在具体题目中的。

  

  1、确定状态表示: 根据经验+题目要求,这里,以 i 为结尾。那么dp[i]表示,s中,以 i 位置为结尾的所有子串,出现在 base 中的个数。

  2、确定状态转移方程: 有了上述对题目性质的分析,这里状态转移理解起来会顺畅很多。对dp[i],要找其中的子串,我们可以根据长度划分为两部分:

1、⼦串的长度为 1 :单独的s[i],根据题目,这一个字符一定会出现在base中
2、⼦串的长度 > 1 :此时,只需要判断s[i-1]和s[i]组合是否出现在base中。

在这里插入图片描述
  综上:dp[i] = 1 + dp[i - 1](后者需要判断)
  
  
  3、初始化: 实际上,因为每个单独的字符都出现在 base 中,我们可以直接将 dp 表初始化为1。那么填表时,只需要判断长度大于1的情况:

if(s[i] - 1 == s[i - 1] || (s[i - 1] == 'z' && s[i] == 'a'))dp[i] += dp[i - 1];// 填dp[i] = dp[i - 1] + 1 也行,+1是指单独的s[i]本身。

  4、填表顺序: 从左到右。

  5、返回值: 这里就需要仔细琢磨。题目要求返回所有出现在base中的子串,若我们直接遍历dp表进行统计,根据示例2,s = "cac"可知,这会导致重复计数。因此,我们需要进行“去重”。
  这里,去重仍旧接用了base字符串的连续性。对某一字符ch,以它为结尾的子串在base中形成的组合是相对固定的。因此我们只需要找长度最长的一个即可。在这里插入图片描述

  对相同字符结尾的dp值,我们仅需保留最大值。因为其余dp 值对应的子串都可以在最大的里面找到。为了方便记录,可以创建一个大小为26 的 hash 数组,遍历dp表,统计出所有字符结尾的最大dp值。
  最后返回该 hash数组统计出的总和。

  
  
  
  2)、题解
  时间复杂度O(n),空间复杂度O(n)

class Solution {
public:int findSubstringInWraproundString(string s) {// 1、创建dp表并初始化int n = s.size();vector<int> dp(n,1);// 2、填表for(int i = 1; i < n; ++i){// 判断自身与之前的字符串组合if( ((s[i-1] + 1 - 'a') % 26) == (s[i]- 'a'))//利用子串的连续性dp[i] += dp[i-1] ;}// 3、返回结果// 3.1、去重int hash[26] = {0};for(int i = 0; i < n; ++i){hash[s[i] - 'a'] = max(hash[s[i]-'a'], dp[i]);}// 3.2、统计int sum = 0;for(auto ch : hash)sum+= ch;return sum;}
};

  
  
  
  
  
  
  
  
  
  
  

6、子序列问题(数组中不连续的一段)

6.0、概念区别:子序列和子数组

  子序列和子数组是处理序列数据时常见的两个概念。
  
  1)、子数组
  子数组(Subarray):子数组是指从原数组中连续选取的一部分元素组成的数组。

  特点:
  ①连续性: 子数组中的元素在原数组中必须是连续的。
  ②顺序性: 子数组保持原数组中元素的相对顺序。

  示例: 假设有一个数组 arr = [1, 2, 3, 4, 5],那么 [1, 2, 3]、[3, 4] 和 [5] 都是它的子数组,而 [1, 3, 4] 不是(因为 1 和 3 之间不连续)。
  

在这里插入图片描述
  
  2)、子序列
  子序列(Subsequence):子序列是指从原数组中选取的一部分元素(可以不连续),并且保持它们在原数组中的相对顺序。

  特点:
  ①非连续性: 子序列中的元素在原数组中不必是连续的。
  ②顺序性: 子序列保持原数组中元素的相对顺序。
  ③空序列: 空序列(不包含任何元素的序列)是任何序列的子序列。(但具体是否包含要看题目要求)

  示例: 假设有一个数组 arr = [1, 2, 3, 4, 5],那么 [1, 3, 5]、[1, 2, 4]、[2] 和 [](空序列)都是它的子序列,而 [1, 4, 2] 不是(因为 4 和 2 的相对顺序被改变了)。
  
  
  

  3)、加深理解
  实际上,从集合的角度,子序列的范围更大,包含了子数组。
在这里插入图片描述

  对于一个包含 n n n 个元素的集合(或数组):

  子序列的数量: 对于包含 n n n 个元素的集合,每个元素都有两种可能的状态:要么在子序列中(用 1 表示),要么不在子序列中(用 0 表示)。因此,对于 n n n 个元素,总共有 2 n 2^n 2n 种不同的组合方式,即 2 n 2^n 2n 个不同的子序列。这包括了空序列(所有元素都不在子序列中)和原集合本身(所有元素都在子序列中)作为特殊情况。
在这里插入图片描述

  
  子数组的数量: 对于包含 n n n 个元素的数组,其子数组的数量可以通过考虑所有可能的起始点和结束点来计算。数组中的每个元素都可以作为子数组的起始点,而对于每个起始点,子数组可以一直延伸到数组的末尾。因此,对于第 i i i 个元素( i i i 1 1 1 n n n),有 n − i + 1 n−i+1 ni+1 个可能的子数组以该元素为起始点(包括只包含该元素本身的子数组和一直延伸到数组末尾的子数组)。

将这些数量相加,我们得到子数组的总数: ( n − 0 + 1 ) + ( n − 1 + 1 ) + ⋯ + ( n − ( n − 1 ) + 1 ) = 1 + 2 + ⋯ + n = n ( n + 1 ) 2 ≈ n 2 (n−0+1)+(n−1+1)+⋯+(n−(n−1)+1)=1+2+⋯+n= \frac{n(n+1)}{2} ≈ n^2 (n0+1)+(n1+1)++(n(n1)+1)=1+2++n=2n(n+1)n2
在这里插入图片描述

  
  对比一下这两个函数:

在这里插入图片描述

  
  
  
  

6.1、最长递增子序列(medium)

  题源:链接。

在这里插入图片描述

  
  

6.1.1、题解

  关于本题的其它解法,见:记忆搜索化。
  

  1)、思路分析

  1、确定状态表示: 根据经验+题目要求,以 i 为结尾,则dp[i]表示:以 i 位置元素为结尾的所有子序列中,最长递增子序列的长度。

  2、确定状态转移方程: 为了找到以 nums[i] 结尾的最长递增子序列,需要考虑所有可能的子序列,并从中选择最长的那个。根据 6.0 中对子序列的分析,可以分为以下两种构建方式:

1、子序列长度为1:在这种情况下,单独的 nums[i] 自身构成一个子序列。因此,dp[i] 至少为1,即 dp[i] = 12、子序列长度大于1:对于 j 在 [0, i-1] 区间中的每一个位置,如果满足 nums[i] > nums[j],则 nums[i] 可以跟在 nums[j] 后面形成一个更长的递增子序列。此时,dp[i] 可以更新为 dp[j] + 1 (这里的+1是因为长度在原先的基础上,增加了1)。但是,我们需要确保选择的是所有可能 j 值中使得 dp[j] + 1 最大的那个,因此需要遍历 [0, i-1] 区间内的所有 dp[j] 值。因此,dp[i] = max(dp[i], dp[j]+1),其中,0 <= j <= i - 1 && nums[j]< nums[i]

在这里插入图片描述

  
  
  3、初始化: 由于单独1个元素也可以构造子序列,因此,可以直接将dp表初始化为1.

  4、填表顺序: 根据这里定义的状态方程,从左往右填表。

  5、返回值: dp表记录的是以每一个位置为结尾的最长递增子序列,我们要返回其中的最大值。
  

  2)、题解
  时间按复杂度为 O ( n 2 ) O(n^2) O(n2),空间复杂度为 O ( n ) O(n) O(n)。这种解法不算是最优解(贪心+二分)。

class Solution {
public:int lengthOfLIS(vector<int>& nums) {int n = nums.size();// 1、创建dp表并初始化vector<int> dp(n,1);// 2、填表int ret = 1;//因为我们从i=1开始填表,i=0处只存在一种子序列,即nums[0]for(int i = 1; i < n; ++i){//在[0,i-1]区间内找对应下标的最长递增子序列,判断加上nums[i]后,是否仍旧满足题目要求for(int j = i-1; j >= 0;--j){if(nums[j] < nums[i])dp[i] = max(dp[i], dp[j] + 1);//由于建表时直接初始化为1,相当于这里也直接对单独一个元素的子序列做了比较}ret = max(ret,dp[i]);//填表时顺带求最大值}// 3、返回return ret;}
};

  
  
  
  
  
  
  
  
  
  

6.2、摆动序列(medium)

  题源:链接。

在这里插入图片描述

  
  

6.2.1、题解

  1)、思路分析
  此题的思想和 5.6中的湍流问题基本一致,只是那里求的是最长子数组,这里求的是最长子序列。
  
在这里插入图片描述

  

  1、确定状态表示: 如上图,要构成摆动序列,则要求两两元素之间“上升”(递增)、“下降”(递减)趋势交替进行
  以 i 为结尾进行状态分析。由于 i 位置的状态趋势需要依赖 [0,i-1] 区间内任意 j 位置的状态趋势,如果仅用一个状态来表示当前位置的最长摆动序列长度,这样无法区分序列是以递增还是递减结尾。因此,我们需要状态表示能表示多一点的信息:要能让我们知道这个最长摆动序列的结尾是递增的还是递减的。
在这里插入图片描述

  因此,对于以 i 位置元素为结尾dp表,我们将其定义为两种状态:

1、f[i]: 以 i 位置元素为结尾的所有⼦序列中,最后呈现"上升趋势"时,此时最⻓摆动序列⻓度;
2、g[i]: 以 i 位置元素为结尾的所有⼦序列中,最后呈现"下降趋势"时,此时最⻓摆动序列⻓度;

  
  

  2、确定状态转移方程: 在5.6湍流中,对于长度大于 1 的子数组,因为连续性的需求,i 的前一个位置必须是 i-1,我们只用分析 i 与 i-1所呈现的趋势即可。但子序列的形成条件无需元素连续性,以 i 位置为结尾的子序列,前一个位置可以是[0,i-1] 区间内的任意位置。因此,我们不妨设 [0,i-1]区间内的任意位置为 j 。
  根据子序列的构成方式,可以分类讨论:
  ①长度为1时: 单独的 i 位置可以看作是一个长度为1的摆动子序列(题目给定条件)。
  ②长度大于1时: 我们需要找到 j(0 <= j < i)使得 nums[i] 可以接在以 nums[j] 结尾的摆动子序列后面,形成新的摆动趋势。
  
  对f[i]

1、长度为1时:f[i] = 12、长度大于1时:因为以i结尾要呈现"上升"趋势,则需要 nums[j] < nums[i]。 在满足这个条件下,j 结尾需要呈现"下降"趋势。因此,最长的摆动序列就是g[j] + 1。由于 0 <= j <= i-1,因此我们要找出所有满足条件下的最大的g[j]+1。综上,f[i] = max(g[j] + 1, f[i])

  
  对g[i]

1、长度为1时:g[i] = 12、长度大于1时:因为以i结尾要呈现"下降"趋势,则需要 nums[j] > nums[i]。 在满足这个条件下,j 结尾需要呈现"上升"趋势。因此,最长的摆动序列就是f[j] + 1。由于 0 <= j <= i-1,因此我们要找出所有满足条件下的最大的f[j]+1。综上,g[i] = max(f[j] + 1, g[i])

  
  3、初始化: 题目表示所有的元素单独自身也能构成一个摆动序列,因此可以将 dp 表内所有元素初始化为 1 。

  4、填表顺序: 从左往右,两表一起填。

  5、返回值: 由于并不清楚最长摆动序列最后呈上升还是下降趋势,因此这里应该返回两个dp表里面的最大值。

  
  

  2)、题解
  时间按复杂度为 O ( n 2 ) O(n^2) O(n2),空间复杂度为 O ( n ) O(n) O(n)

class Solution {
public:int wiggleMaxLength(vector<int>& nums) {int n = nums.size();//if(n <=2) return n;// 1、创建dp表并初始化vector<int> f(n,1);vector<int> g(n,1);// 2、填表int maxf = 1;// 从 i=1开始遍历,最初i=0时,一个元素也构成摆动序列int maxg = 1;for(int i = 1; i < n; ++i){//j可以是[0,i-1]中任意一个位置for(int j = i-1; j>=0; --j){if(nums[j] > nums[i])// 最后一次呈现下降趋势g[i] = max(f[j] + 1,g[i]);else if(nums[j] < nums[i])// 最后一次呈现上升趋势f[i] = max(g[j] + 1, f[i]);}maxf = max(maxf,f[i]);maxg = max(maxg,g[i]);}// 3、返回return max(maxf,maxg);}
};

  这里,也可以直接用一个变量记录两个表中的最大值:

int ret = 1;
ret = max(ret, max(f[i], g[i]));
return ret;

  
  
  
  
  
  
  
  
  

6.3、最长递增子序列的个数(medium)

  题源:链接。

在这里插入图片描述

  
  

6.3.1、记忆搜索化

  实则会发现这题和力扣第300题,最长递增子序列(medium)相同,只是区别在于那里返回的是最长递增子序列的长度,此处返回的是最长递增子序列的个数。
  
  对300题我们曾使用过记忆搜索化的解法(指路),仿照该题,这里仍旧可以使用记忆搜索化。图方便的话,直接把备忘录修改为键值对pair,同时记录最大长度和最大长度的个数即可。
  需要注意如何更新数据。
  时间按复杂度为 O ( n 2 ) O(n^2) O(n2),空间复杂度为 O ( n ) O(n) O(n)

class Solution {vector<pair<int,int>> demo;// 备忘录:最大长度,最大长度的个数
public:int findNumberOfLIS(vector<int>& nums) {int n = nums.size();demo.resize(n,{0,0});//对备忘录初始化int maxlen = 0;int maxlensize = 0;for(int i = 0; i < n; ++i){pair<int,int> ret = DFS(nums,i);if(ret.first > maxlen)// 如果长度更大{maxlen = ret.first;// 更新最长长度和对应的数量maxlensize = ret.second;}else if(ret.first == maxlen) maxlensize += ret.second;// 如果长度相同,则累加数量}return maxlensize;}pair<int,int> DFS(vector<int>& nums, int pos){if(demo[pos].first != 0) return demo[pos];// 如果已经计算过,则直接返回结果pair<int,int> maxsize = {1,1};// 初始化为长度为1,数量为1:当前pos自身长度for(int i = pos + 1; i <nums.size(); ++i){if(nums[i] > nums[pos]){pair<int,int> ret = DFS(nums,i);// 递归找最大路径if(ret.first + 1 > maxsize.first) // 如果长度更大,更新最长长度和对应的数量{maxsize.first = ret.first + 1;// 注意需要ret.first + 1,才等于当前pos位置的长度maxsize.second = ret.second;}else if(ret.first+1 == maxsize.first) maxsize.second += ret.second; // 如果长度相同,则累加数量(路径长度不变)}}demo[pos] = maxsize;// 将结果存入备忘录return demo[pos];}
};

  
  
  
  

6.3.2、动态规划

  1)、扩展:一个简单贪心策略
  这里,我们补充一点小知识。给定一个数组,如何只遍历一遍,获得其中最大值的个数?

  首先,要明确的是,在寻找最大值及其个数的过程中,最大值与其个数之间是一一对应的。我们不能仅记录个数而不记录对应的值,因为不记录值就无法判断后续出现的元素是否是新的最大值。常规思路是:先遍历一遍数组,求出最大值;再遍历一次数组,根据最大值统计其出现的次数。

  但这里要求我们只遍历一次数组。实则这种思想我们在上述记忆搜索化的解法中就运用过,这里只是将其提炼出来。

  为了满足这个要求,我们可以使用两个变量:maxvalvalcount

maxval:用于记录遍历过程中遇到的数组的最大值
valcount:用于记录该最大值出现的次数

  在遍历数组时,会遇到以下三种情况:

1、当前值 i < maxval:当前位置的值不是当前已知的最大值,不满足要求。忽视,继续遍历下一个元素。2、当前值 i == maxval:如果当前位置的值等于当前已知的最大值,那么说明我们找到了另一个与最大值相等的元素。此时,需要更新 valcount 的值(valcount+1)3、当前值 i > maxval:这意味着找到了一个新的更大的值。此时,需要将 maxval 更新为当前值 i,并且重置 valcount 为1。因为从这一刻起,i 成为了新的最大值,并且,我们需要重新统计这个新最大值出现的次数。

  下述,我们在使用动态规划解题时,也需要用到上面这个思想。
  
  

  2)、思路分析

  1、确定状态表示: 先前已经说明,如果不知道递增子序列的长度,是无法统计出该长度下的递增子序列的个数的。因此,解决这个问题需要两个状态,我们可以使用两个dp表分别表示长度、个数。

len[i] :以 i 位置元素结尾的子序列中,最长递增子序列的"长度"
count[i]:以 i 位置元素结尾的子序列中,具有最长长度的递增子序列的"个数"

  
  2、确定状态转移方程: 思路与6.1题基本一致。
在这里插入图片描述
  要求 i 处的最长递增子序列的长度和个数 ,可将其分为两类情况讨论:
  ①、子序列长度为1 时,即仅包含 nums[i] 自身,此时有 len[i] = count[i] = 1
  ②、子序列长度大于1时,需要在 [0,i-1] 区间内,找任意位置 j 处的最长递增子序列,并判断 nums[i] 加其后是否能仍旧构成递增子序列,即是否满足 nums[j] < nums[i] 。

  若满足上述条件,由于 j 位置可以是 [0, i-1] 中的任意值,且 i 位置记录的是最长长度及其个数,故可能存在多个满足条件的 j 位置。对此,需利用前述策略进行记录:

1、当 len[j]+1 < len[i], 表示 j 处递增子序列加 nums[i] 后形成的长度,非当前已知的最长长度,直接忽视。2、当 len[j]+1 == len[i], 表示 j 处递增子序列加 nums[i] 后,与当前最长长度相同,此时需累加 count[i]。即 count[i] += count[j]( 注意这里不是count[i]++,因为 j 位置处满足该长度的子序列可能不止一个!)3、当 len[j]+1 > len[i], 表示发现了更长的递增子序列,需要更新len[i]并重新记录count[i]。即 len[i] = len[j] + 1 , count[i] = count[j]

  

  3、初始化: 自身一个元素的情况,也构成递增子序列,此时的长度、个数为1。因此可将两表直接初始化为1。
  

  4、填表顺序: 从左往右
  

  5、返回值: 这里,由于并不清楚最大长度出现的个数具体在dp表中的哪一个位置。因此我们需要遍历dp表,找最大值的个数。
  听起来是不是很熟悉?就是1)中方法策略的再次使用。我们可以使用此方法,在完成填表后,再遍历一次求最长子序列的个数,也可以在填表时一并完成该操作。

  
  
  3)、题解
  时间按复杂度为 O ( n 2 ) O(n^2) O(n2),空间复杂度为 O ( n ) O(n) O(n)

class Solution {
public:int findNumberOfLIS(vector<int>& nums) {int n = nums.size();// 1、创建dp表并初始化vector<int> len(n,1);vector<int> count(n,1);// 2、填表int maxlen = 1;// 用于记录最终结果,这里初始化为1是因为遍历填表时从i=1开始。 i = 0时,自身长度、个数为1。int maxlensize = 1;for(int i = 1; i < n; ++i){//在[0,i-1]区间中找前一个递增子序列for(int j = 0; j < i; ++j){if(nums[j] < nums[i])// 满足严格递增的条件下{if(len[i] == len[j]+1) // a、长度相同时count[i] += count[j];// 累加个数else if(len[i] < len[j]+1) // b、长度更长了{len[i] = len[j] + 1;// 更新当前i位置处的最长递增子序列长度count[i] = count[j];// 重新计数}}}// 更新最终值if(maxlen == len[i]) maxlensize += count[i];// 累加个数else if(len[i] > maxlen) {maxlen = len[i];// 更新长度maxlensize = count[i];// 重新计数}}// 3、返回值return maxlensize;}
};

  
  
  
  
  
  
  
  
  
  

6.4、最长数对链(medium)

  题源:链接。

在这里插入图片描述

  
  

6.4.1、题解

  1)、思路分析
  先分析题目,对于从数对数组 pairs 中挑选出的数对 p1 = [a, b] 、 p2 = [c, d] ,只有当 b < c 时,才满足构造数对链的要求。
  题目要求我们求最长的数对链,如果把每一个数对看作一个独立的元素,能够发现这题的思想和 6.1中求最长递增子序列 很像。如此一来我们就可以仿照6.1中的思想,使用动态规划解题。

在这里插入图片描述

  但由示例2可以发现,题目给定的数组对是乱序的,即挑选数对时没有严格从左往右,或从右往左进行选数。我们可以随机挑选。

输入:pairs = [[1,2],[7,8],[4,5]]
输出:3
解释:最长的数对链是 [1,2] -> [4,5] -> [7,8]

  这就限制了我们使用动态规划的思想。因为动态规划要求 以 i 为结尾思考时,只能依赖 [0,i-1]之间的数据,以 i 为起始思考时,只能依赖 [i+1,n-1]之间的数据。即,填表顺序具有单向性。
在这里插入图片描述

  为了解决这个问题,我们可以先对原数组对进行排序,这样就能使得我们在填表时,选数只会依赖 i 一侧的数据。
  为什么排序就能满足条件?
  假如按照升序排序, 设排序后 i = [a,b],i+1 = [c,d],那么 a <=c 一定恒成立。又因为题目说明 airs[i] = [left, right] 中, left < right,则有 c < d恒成立,一定有 a < d。这样就能保证 我们不会挑选 i + 1处的数对,让 i 排在其后。
在这里插入图片描述
  
  
  以动态规划解题, 剩余思考方式就和 6.1中一致。

  1、确定状态表示: 这里,我们以 i 为结尾,则dp[i]表示,以 i 位置的元素对为结尾的所有数对链中,最长的数对链长度。

  2、确定状态转移方程: 对于dp[i],遍历所有[0,i-1]区间内数对(用j表示下标),找出所有满足pairs[j][1] < pairs[i][0]的位置。更新 dp[i]dp[j] + 1(表示将 pairs[i] 添加到以 pairs[j] 结尾的数对链后面,形成一条更长的数对链)。因为满足条件的 j 处数对可能有很多个,因此 dp[i] 应取其中较大者。

  3、初始化: 初始化时,所有 dp[i] 都为 1,因为每个数对本身都可以看作一条长度为 1 的数对链。

  4、填表顺序: 根据状态转移方程,从左往右填表。
  
  5、返回值: 根据状态表示,返回dp表中的最大值。
  

  2)、题解

class Solution {
public:int findLongestChain(vector<vector<int>>& pairs) {// 0、预处理:先排个序(默认就是升序,这里练习一下lambda表达式的用法)sort(pairs.begin(), pairs.end(),[](vector<int>& a, vector<int>& b) -> bool {if (a[0] == b[0])return a[1] < b[1];return a[0] < b[0];});// 1、创建dp表并初始化int n = pairs.size();vector<int> dp(n, 1);// 2、填表int ret = 1;for (int i = 1; i < n; ++i) {for (int j = i - 1; j >= 0; --j) {if (pairs[j][1] < pairs[i][0]) // 注意这里比较的方式dp[i] = max(dp[j] + 1, dp[i]);}ret = max(ret, dp[i]);}// 3、返回return ret;}
};

  
  
  
  
  
  
  
  
  
  
  
  

6.5、最长定差子序列(medium)

  题源:链接。

在这里插入图片描述

  
  

6.5.1、动态规划(不做优化版本)

  分析此题,我们能想到的第一思路就是循规蹈矩根据之前的动态规划思路解题。

dp[i]表示:以 i 位置的元素为结尾所有的子序列中,最长的等差子序列的长度。
class Solution {
public:int longestSubsequence(vector<int>& arr, int difference) {int n = arr.size();// 1、建表,初始化vector<int> dp(n, 1);// 2、填表int ret = 1; // 用于返回for (int i = 1; i < n; ++i) {for (int j = i - 1; j >= 0; --j) {if (arr[j] + difference == arr[i]) // 题目给定的性质条件dp[i] = max(dp[j] + 1, dp[i]);}ret = max(ret, dp[i]);}// 3、返回return ret;}
};

  但如果用这种方法提交后,可以看到超时。因为题目给定 1 <= arr.length <= 10^5。这种版本的动态规划时间复杂度为 O ( n 2 ) O(n^2) O(n2)
  
  
  

6.5.2、动态规划(使用hash优化版本)

  1)、问题说明
  之前解决最长递增子序列(LIS)问题时,我们只知道前后两个元素之间是递增的关系,并不知道前一个元素的具体值应该是多少。
  但在本题中,由于明确给定了等差 difference,我们是可以计算出前一个数准确的数值的

设 i-1 ≥ j ≥ 0, arr[j] + difference = arr[i]
则有:arr[j] = arr[i] - difference 

  因此我们只需要在 [0,i-1] 区间中找到满足上述关系的下标位置即可。

  继续分析,满足该关系式的 j 的位置可能有很多个,我们需要遍历[0,i-1]把它们全都找出来后,获取长度吗?

在这里插入图片描述
  
  经过上述分析,至少,我们能够确定,不需要遍历[0,i-1]所有位置,只需要找到[0,i-1]区间中,离 i 处元素最近的、满足 arr[j] = arr[i] - difference 关系的下标 j 处元素即可。那么,我们可以让 j = i-1,从后往前遍历。
  
  但即使是这样,我们只是在找 j 时,缩短了一定的遍历数量,并没有优化两层遍历本身。时间复杂度仍旧为 O ( n 2 ) O(n^2) O(n2)
  
  

  2)、使用hash表
  实际上,由于前一个位置arr[j]的数值是确定的,在解决本题时,无需遍历寻找 i 的前一个值,我们可以利用哈希表来动态地存储这些状态信息

  具体做法是:将「元素值,该元素值对应的最长等差子序列长度」作为一个键值对存入哈希表中。这样,每当我们遍历到一个新的元素 arr[i] 时,我们都可以在哈希表中快速地查找是否存在一个元素 arr[j](j < i)满足 arr[j] = arr[i] - difference 这样就省略了内层的循环,使得查询 j 位置的时间复杂度为 O(1)。

  
  状态表示: hash[arr[i],dp[i]]:hash表中,存的是arr[i]具体元素值,以及对应的dp[i],以 i 位置的元素为结尾所有的子序列中,最长的等差子序列的长度。

  分析状态转移方程: 在哈希表中直接定位寻找arr[j] = arr[i] - difference
  ①、如果找到了这样的 arr[j],说明我们可以将 arr[j] 所在的最长等差子序列“延长”到 arr[i],从而形成一个更长的等差子序列。此时,我们更新哈希表中 arr[i] 对应的长度为 哈希表中arr[j]对应的长度 + 1。

  ②、如果哈希表中不存在这样的 arr[j],说明 arr[i] 本身就是一个新的等差子序列的起点。此时,我们在哈希表中为 arr[i] 创建一个新的键值对,并将其长度初始化为 1。
  
  初始化: hash[arr[0]] = 1,只有一个元素时,该元素的最长序等差列长度为1。
  
  

  3)、题解

class Solution {
public:int longestSubsequence(vector<int>& arr, int difference) {// 1、创建hash表用于进行动态规划。unordered_map<int,int> hash;// key:记录arr[i]的值,value:对应dp[i],即以i结尾的最长定差子序列长度// 2、初始化hash[arr[0]] = 1;// 刚开始的时候,需要把第一个元素放进哈希表中// 3、填表int ret = 1;// 记录返回值for(int i = 1; i < arr.size(); ++i){hash[arr[i]] = hash[arr[i] - difference] + 1;// 注意理解.这里直接把找到j、找不到j两种情况一并处理了ret = max(ret, hash[arr[i]]);}// 4、返回:hash表里的最大值return ret;}
};

  分析理解这里的hash[arr[i]] = hash[arr[i] - difference] + 1
  1)、当arr[j],也就是 arr[i] - difference 不存在时,此时有 hash[arr[i]] = 1
  2)、当arr[j]存在时,此时有 hash[arr[i]] = hash[arr[i] - difference] + 1 ,即 dp[i] = dp[j] + 1
  3)、先前我们说过,满足条件的 j 下标位置可能有很多个,我们只取 [0,i-1] 中离 j 最近的一个。那么这一条语句,能保证这一要求吗?回答:能保证。在我们从左到右填表的过程中,会多次遍历到 arr[j],即多次填表 j,hash表中始终保存的是最新一次的 hash[arr[j]]值,也就是离 i 最近的一次 j 。
  (以下举了一个相对极端的例子进行理解)

在这里插入图片描述

  
  
  
  
  
  
  
  
  
  

6.6、 最长的斐波那契子序列的长度(medium)

  题源:链接。

在这里插入图片描述

  
  

6.6.1、题解

  1)、思路分析

  1、确定状态表示:i 为结尾,通常,我们容易想到dp[i] 表示以 i 位置元素为结尾的所有子序列中,最长的斐波那契子数列的长度。
  但是这样有一个问题,由于斐波那契数的定义X_i + X_{i+1} = X_{i+2},如果我们想找到一个斐波那契子序列,就需要能够“追溯”到它的前两个元素。而dp[i] 只能告诉我们以第 i 个元素结尾的最长斐波那契子序列的长度,它无法告诉我们这个子序列的前两个元素是什么。这会导致我们不能很好的推导出状态转移方程,因为以最长长度到达arr[i]位置处的子序列存在多个。
在这里插入图片描述

  因此,单独的dp[i]无法满足状态表示。

  分析斐波那契数列的性质,对 a + b = c,实则一个序列中,我们只需要知道序列里的最后两个元素/最前两个元素,就可以确定这个序列的样子。
在这里插入图片描述
  因此,我们可以以序列中最后两个元素来确定一个斐波那契数子序列。

dp[i][j]:以 i 和 j 位置为结尾的子序列中,最长斐波那契子序列的长度。这里,规定 i < j。

  
  
  
  2、确定状态转移方程: 对于每个 (i, j) 对,我们可以检查是否存在一个 k(k < i)使得 arr[k] + arr[i] = arr[j]。如果这样的 k 存在,那么我们可以将 dp[i][j] 更新为 dp[k][i] + 1(因为我们可以将 arr[k], arr[i], arr[j] 作为一个新的更长斐波那契子序列的结尾)。

在这里插入图片描述
  一个优化: 如上图,在填表 dp[i][j] 时,我们需要频繁确定 arr[k] 在数组中是否存在。如果我们不做处理,在实际填表时,这需要我们遍历 [0,i-1] 区间,找寻 k 下标位置。
  为了省去这一层遍历查询,在最初阶段,我们可以使用一个哈希表,记录对应 arr[k] 的值,以及其下标位置。

unordered_map<int, int> hash;
// key值:arr[j] - arr[i],也就是arr[k]的值
// value值: arr[k]的下标位置 k
// PS : 题目明确说明 arr 是严格递增的正整数数组,因此不存在多个 arr[k] 的情况。

  
  
  
  3、初始化: 根据dp状态表示,最次情况下,子序列为 { arr[i]、arr[j] },也就是长度为2。因此,我们可以将dp表内的值全都初始化为 2 。
在这里插入图片描述

  
  
  4、填表顺序: 要知道dp[i][j] 的值,需要先知道dp[k][i]的值,由于 k < i ,因此填表顺序从上往下。(每一行的填表顺序不做要求)
  
  5、返回值: 因为不知道最终结果以谁为结尾,因此返回dp表中的最大值ret。
  
  

  2)、题解
  两层循环,时间复杂度为 O ( n 2 ) O(n^2) O(n2)。二维dp表,空间复杂度为 O ( n 2 ) O(n^2) O(n2)

class Solution {
public:int lenLongestFibSubseq(vector<int>& arr) {int n = arr.size();unordered_map<int,int> hash;// 创建hash表:用于辅助快速定位数组中的元素及其下标位置for(int i = 0; i < n; ++i)hash[arr[i]] = i;// 1、建表并初始化vector<vector<int>> dp(n,vector<int>(n,2));// 2、填表int ret = 0;for(int j = 2; j < n; ++j)// 确定序列中倒数第一个元素位置{for(int i = 1; i < n; ++i)// 确定序列中倒数第二个元素位置{int a = arr[j] - arr[i];// 我们要找寻的元素arr[k]if(hash.count(a) && hash[a] < i)// 该元素存在,且满足 k < i < j。如果想要判断快点,由于题目表示严格递增,不存在重复元素,可改为 a < arr[i] && hash.count(a)dp[i][j] = dp[hash[a]][i]+1;ret = max(ret, dp[i][j]);}}// 3、返回return ret > 2 ? ret : 0;}
};

  
  
  
  
  
  
  
  

  

6.7、 最长等差数列(medium)

  题源:链接。

在这里插入图片描述

  
  

6.7.1、题解

  1)、思路分析
  1、确定状态表示: 分析此题,根据经验+题目要求,若我们以 i 为结尾, dp[i] 表示以 arr[i] 为结尾的最长等差数列的长度,是否可行? 显而易见,如果只知道长度,我们无法确定以 i 结尾的等差序列的样子,即无法推导出状态转移方程。
在这里插入图片描述

  因此我们定义的状态表示需要能够确定一个等差序列。而根据等差数列的性质,我们仅需知道序列中相邻的两个元素,就可以获取到公差,从而推导出整个等差序列的样子。因此,状态表示为:

dp[i][j]表示:以 i、j 位置处的元素(arr[i]、arr[j])为结尾的所有子序列中,最长的等差序列的长度。规定 i < j。

  

  2、确定状态转移方程: 对于每个 (i, j) 对,我们可以检查是否存在一个 k(k < i)使得 arr[i] - arr[k] = arr[j] - arr[i],转换一下形式,有 arr[k] = 2*arr[i] - arr[j] 。如果这样的 k 存在,那么我们可以将 dp[i][j] 更新为 dp[k][i] + 1(即,在原先{ ……,arr[k], arr[i] } 序列的基础上,新增一个元素 arr[j], 作为一个新的更长的等差序列的结尾)。

在这里插入图片描述

  可以看到此题和6.6中的思路很相似,但与之区别的是,本题并没有特殊说明元素唯一。这意味着在数组中,我们进行运算后得到的arr[k] ,可能会在多个下标位置处。这里,我们只用选择[0,i-1]区间中,离 i 最近的一个即可。

在这里插入图片描述
  如果我们不做处理,则需要我们遍历寻找 k 位置下标,而在此前需要先定位 i、j 位置,相当于需要 3 层嵌套循环,时间复杂度会很高。
  因此,对于如何寻找 arr[k] ,需要进行优化处理。 正好,此类题由于固定计算公式中,当我们固定好 i 、j 位置时,arr[k] 的值实则就已经确定好了。因此,我们可以使用哈希表,将 {元素值,下标} 映射存放,这样,当我们用公式计算出 arr[k]时,就能快速得出其对应的 dp[i]。
  
  这里有一个问题,在上述我们提到过,arr[k] 对应的元素下标有多个,由于 i 、j 位置也是通过遍历进行固定的,因二者的变动性,导致合法的 arr[k] 的下标也是变动的。

在这里插入图片描述
  
  1)、一种方法是,创建一个 hash{元素值,元素值对应下标集合} ,在填表前之前,将存放相同元素值的下标形成一个数组,放进哈希表中。那么在填表时,我们找到对应的 arr[k],就找到该元素值所在的下标集合。减轻了一定程度的遍历,但仍旧需要遍历下标集合找到合法下标位置。如果题目给定的某一元素 arr[k] 在 超级多的下标位置处 均存在,这就相当于遍历 n 个位置,时间复杂度没得到改善。

  2)、另一种方法是,一遍填表,一遍进行 hash 存值。这样我们只用创建一个 hash{元素值,最新一次下标位置} ,保存最近的元素下标即可。但这种情况下,需要注意遍历填表时,i , j 位置固定谁而枚举谁。
  
  此处顺带把填表顺序分析清楚:
  方法①:固定等差子序列倒数第一个元素 arr[j],在 [0,j-1] 中,枚举倒数第二个元素 arr[i]。
  方法②:固定等差子序列倒数第二个元素 arr[i],在 [i+1,n-1] 中,枚举倒数第一个元素 arr[j]。

在这里插入图片描述

  
  
  3、初始化: 根据实际情况,可以将所有位置初始化为2。
  

  4、填表顺序: 固定倒数第二个元素 arr[j] ,枚举倒数第一个元素arr[j]。
  

  5、返回值: 由于不知道最长子序列的结尾元素在哪,因此需要返回dp表中最大值。

  
  

  2)、题解
  

class Solution {
public:int longestArithSeqLength(vector<int>& nums) {int n = nums.size();// 1、创建dp表并初始化vector<vector<int>> dp(n, vector<int>(n, 2));unordered_map<int, int> hash; // 用于辅助查值的哈希表。key:元素, value:元素最新一次下标位置hash[nums[0]] = 0;//已知的位置,先存到hash表中。// 2、填表:规定dp[i][j]中,i < jint ret = 2;// 用于记录返回值:题目保证了最少有2个元素for (int i = 1; i < n; ++i) // 固定倒数第二个元素{for (int j = i + 1; j < n; ++j) // 枚举倒数第一个元素{int a = 2 * nums[i] - nums[j]; // 我们要找的arr[k]if (hash.count(a))             // 这里就不用判断 k < i < j了,因为这种填表顺序保证该条件满足。dp[i][j] = dp[hash[a]][i] + 1;ret = max(ret, dp[i][j]);}hash[nums[i]] = i; // 在hash中存入/更新当前位置的元素及其下标}// 3、返回return ret;}
};

  
  
  
  

  
  
  
  
  

6.8、等差数列划分II-子序列(hard)

  题源:链接。

在这里插入图片描述

  
  

6.8.1、题解

  1)、思路分析
  1、确定状态表示: 根据经验+题目要求,这里以 i 为结尾,则 dp[i] 表示,以 i 位置的元素 arr[i] 为结尾的所有子序列中,等差数列的个数。
  在经过上述几题的经验,能知道的是,如上述这样定义状态表示,由于无法确定 i 位置处所构成的各个等差数列,在只知道个数的情况下,无法推导得出状态转移方程。
在这里插入图片描述
  由于等差数列的性质,知道两个相邻元素,就能得出公差,从而推导出整个等差数列集合。因此,这里我们以序列中最后两个元素为结尾,定位状态表示:

dp[i][j]:以 i 和 j 位置的元素为结尾(arr[i]、arr[j])的所有子序列中,等差数列的个数。这里,规定 i < j。

  
  
  2、确定状态转移方程: 根据等差数列的公式,对于每个 (i, j) 对,检查数组中是否存在一个 k(k < i)使得arr[k] = 2*arr[i] - arr[j]
在这里插入图片描述

  优化:为了方便我们找寻arr[k]值,可以使用hash表,将{元素值,元素值对应下标集合}存放入hash表中。这里,value值之所以是下标集合/下标数组,我们在上图也分析过,我们要统计个数,那么每个满足条件的arr[k],都要进行统计。
  
  
  3、初始化: 刚开始是没有等差数列的,因此初始化dp 表为0

  
  4、填表顺序: 可以先固定倒数第一个元素,枚举倒数第二个元素。
  
  5、返回值: 此返回dp表中,所有元素的和。
  

  2)、题解
  虽然嵌套了三层遍历,但for(auto k : hash[a])取下标集合时,相对外层两个循环可忽略不计,因此时间复杂度总体而言为 O ( n 2 ) O(n^2) O(n2)。n×n的dp表,以及hash表,空间复杂度为 O ( n 2 ) O(n^2) O(n2)

class Solution {
public:int numberOfArithmeticSlices(vector<int>& nums) {int n = nums.size();if(n < 3) return 0;// 1、创建dp表并初始化vector<vector<int>> dp(n,vector<int>(n,0));unordered_map<long long ,vector<int>> hash;// 优化:用于辅助快速查找元素的哈希表。key:元素,value:下标集for(int i = 0; i < n; ++i)hash[nums[i]].push_back(i);// 2、填表:int sum = 0;//统计返回值for(int j = 2; j < n; ++j)// 固定倒数第⼀个数{for(int i = 1; i < j; ++i)// 枚举倒数第⼆个数{long long a = (long long )2*nums[i] - nums[j];// 获取nums[k].使用long long是因为题目规定的nums[i]取值,这里做运算时可能溢出if(hash.count(a))// 对应的值存在{for(auto k : hash[a])// 取下标{if(k < i) // 注意,因为是下标集合,这里需要判断取出的下标 k 是否合法dp[i][j] += dp[k][i] + 1;}}sum += dp[i][j];}}// 3、返回return sum;}
};

  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  

Fin、共勉。

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/61996.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

数据库相关学习杂记-事务

ARIES&#xff08;基于语义的恢复与隔离算法&#xff09;是现代数据库理论的基础。提供了解决ACID中A、I、D重要的解决思路。 基础知识 这里先复习一下关于ACID的含义以及数据库隔离级别&#xff1a; ACID的含义 原子性&#xff08;Atomicity&#xff09;: 一个事务中被视为…

2024 java大厂面试复习总结(一)(持续更新)

10年java程序员&#xff0c;2024年正好35岁&#xff0c;2024年11月公司裁员&#xff0c;记录自己找工作时候复习的一些要点。 java基础 hashCode()与equals()的相关规定 如果两个对象相等&#xff0c;则hashcode一定也是相同的两个对象相等&#xff0c;对两个对象分别调用eq…

Python绘制太极八卦

文章目录 系列目录写在前面技术需求1. 图形绘制库的支持2. 图形绘制功能3. 参数化设计4. 绘制控制5. 数据处理6. 用户界面 完整代码代码分析1. rset() 函数2. offset() 函数3. taiji() 函数4. bagua() 函数5. 绘制过程6. 技术亮点 写在后面 系列目录 序号直达链接爱心系列1Pyth…

mfc100u.dll是什么?分享几种mfc100u.dll丢失的解决方法

mfc100u.dll 是一个动态链接库&#xff08;DLL&#xff09;文件&#xff0c;属于 Microsoft Foundation Classes (MFC) 库的一部分。MFC 是微软公司开发的一套用于快速开发 Windows 应用程序的 C 类库。mfc100u.dll 文件包含了 MFC 库中一些常用的函数和类的定义&#xff0c;这…

【JavaEE】Servlet:表白墙

文章目录 一、前端二、前置知识三、代码1、后端2、前端3、总结 四、存入数据库1、引入 mysql 的依赖&#xff0c;mysql 驱动包2、创建数据库数据表3、调整上述后端代码3.1 封装数据库操作&#xff0c;和数据库建立连接3.2 调整后端代码 一、前端 <!DOCTYPE html> <ht…

WebRTC音视频同步原理与实现详解(上)

第一章、RTP时间戳与NTP时间戳 1.1 RTP时间戳 时间戳&#xff0c;用来定义媒体负载数据的采样时刻&#xff0c;从单调线性递增的时钟中获取&#xff0c;时钟的精度由 RTP 负载数据的采样频率决定。 音频和视频的采样频率是不一样的&#xff0c;一般音频的采样频率有 8KHz、…

蓝桥杯每日真题 - 第21天

题目&#xff1a;(空间) 题目描述&#xff08;12届 C&C B组A题&#xff09; 解题思路&#xff1a; 转换单位&#xff1a; 内存总大小为 256MB&#xff0c;换算为字节&#xff1a; 25610241024268,435,456字节 计算每个整数占用空间&#xff1a; 每个 32 位整数占用…

利用Python爬虫获得1688按关键字搜索商品:技术解析

在电商领域&#xff0c;1688作为中国领先的B2B电商平台&#xff0c;其商品搜索功能对于商家来说具有极高的价值。通过获取搜索结果&#xff0c;商家可以更好地了解市场趋势&#xff0c;优化产品标题&#xff0c;提高搜索排名。本文将介绍如何使用Python编写爬虫&#xff0c;以获…

三、计算机视觉_05MTCNN人脸检测

0、人脸识别流程概述 人脸识别流程包括两个主要步骤&#xff1a; Step1&#xff1a;人脸检测&#xff0c;确保我们处理的是正确的人脸区域 Step2&#xff1a;身份识别&#xff0c;确定该人脸的身份 0.1 人脸检测 人脸检测是从图像中定位人脸并抠出人脸区域的过程&#xff…

「Chromeg谷歌浏览器/Edge浏览器」篡改猴Tempermongkey插件的安装与使用

1. 谷歌浏览器安装及使用流程 1.1 准备篡改猴扩展程序包。 因为谷歌浏览器的扩展商城打不开&#xff0c;所以需要准备一个篡改猴压缩包。 其他浏览器只需打开扩展商城搜索篡改猴即可。 没有压缩包的可以进我主页下载。 也可直接点击下载&#xff1a;Chrome浏览器篡改猴(油猴…

STM32F103C8T6实时时钟RTC

目录 前言 一、RTC基本硬件结构 二、Unix时间戳 2.1 unix时间戳定义 2.2 时间戳与日历日期时间的转换 2.3 指针函数使用注意事项 ​三、RTC和BKP硬件结构 四、驱动代码解析 前言 STM32F103C8T6外部低速时钟LSE&#xff08;一般为32.768KHz&#xff09;用的引脚是PC14和PC…

【JavaEE初阶】多线程初阶下部

文章目录 前言一、volatile关键字volatile 能保证内存可见性 二、wait 和 notify2.1 wait()方法2.2 notify()方法2.3 notifyAll()方法2.4 wait 和 sleep 的对比&#xff08;面试题&#xff09; 三、多线程案例单例模式 四、总结-保证线程安全的思路五、对比线程和进程总结 前言…

【人工智能】Python在机器学习与人工智能中的应用

Python因其简洁易用、丰富的库支持以及强大的社区&#xff0c;被广泛应用于机器学习与人工智能&#xff08;AI&#xff09;领域。本教程通过实用的代码示例和讲解&#xff0c;带你从零开始掌握Python在机器学习与人工智能中的基本用法。 1. 机器学习与AI的Python生态系统 Pyth…

“iOS profile文件与私钥证书文件不匹配”总结打ipa包出现的问题

目录 文件和证书未加载或特殊字符问题 证书过期或Profile文件错误 确认开发者证书和私钥是否匹配 创建证书选择错误问题 申请苹果 AppId时勾选服务不全问题 ​总结 在上线ios平台的时候&#xff0c;在Hbuilder中打包遇见了问题&#xff0c;生成ipa文件时候&#xff0c;一…

element-ui 中el-calendar 日历插件获取显示的第一天和最后一天【原创】

需要获取el-calendar 日历组件上的第1天和最后一天。可以通过document.querySelector()方法进行获取dom元素中的值&#xff0c;这样避免计算问题。 获取的过程中主要有两个难点&#xff0c;第1个是处理上1月和下1月的数据&#xff0c;第2个是跨年的数据。 直接贴代码&#xff…

JavaScript的基础数据类型

一、JavaScript中的数组 定义 数组是一种特殊的对象&#xff0c;用于存储多个值。在JavaScript中&#xff0c;数组可以包含不同的数据类型&#xff0c;如数字、字符串、对象、甚至其他数组。数组的创建有两种常见方式&#xff1a; 字面量表示法&#xff1a;let fruits [apple…

5.5 W5500 TCP服务端与客户端

文章目录 1、TCP介绍2、W5500简介2.1 关键函数socketlistensendgetSn_RX_RSRrecv自动心跳包检测getSn_SR 1、TCP介绍 TCP 服务端&#xff1a; 创建套接字[socket]&#xff1a;服务器首先创建一个套接字&#xff0c;这是网络通信的端点。绑定套接字[bind]&#xff1a;服务器将…

Android 15 版本更新及功能介绍

Android 15版本时间戳 Android 15,代号Vanilla Ice Cream(香草冰淇淋),是当下 Android 移动操作系统的最新主要版本。 开发者预览阶段:2024年2月,谷歌发布了Android 15的第一个开发者预览版本(DP1),这标志着新系统开发的正式启动。随后,在3月和4月,谷歌又相继推出了D…

第02章_MySQL环境搭建(基础)

1. MySQL 的卸载 1.1 步骤1&#xff1a;停止 MySQL 服务 在卸载之前&#xff0c;先停止 MySQL8.0 的服务。按键盘上的 “Ctrl Alt Delete” 组合键&#xff0c;打开“任务管理器”对话 框&#xff0c;可以在“服务”列表找到“MySQL8.0” 的服务&#xff0c;如果现在“正在…

红队笔记--W1R3S、JARBAS、SickOS、Prime打靶练习记录

W1R3S(思路为主) 信息收集 首先使用nmap探测主机&#xff0c;得到192.168.190.147 接下来扫描端口&#xff0c;可以看到ports文件保存了三种格式 其中.nmap和屏幕输出的一样&#xff1b;xml这种的适合机器 nmap -sT --min-rate 10000 -p- 192.168.190.147 -oA nmapscan/ports…