当前位置: 首页 > news >正文

014枚举之指针尺取——算法备赛

枚举是数据结构与算法中基本的操作,常用于解决序列的区间问题。算法界将"双指针"视为其重要分支,类似地当然还有"三指针",“四指针”,最常见的还是“双指针”,我认为它们应统称为“指针尺取”。
双指针无疑就是定义两个变量记录内存空间位置(一般为线性数组下标)来进行枚举,常见的枚举策略有“反向扫描”,“同向扫描”。

  • 反向扫描,两指针的枚举方向不同,又可分为“两端向中间收缩”,“中间向两边扩散”等。
  • 正向扫描,两指针的枚举方向相同,起始位置或速度不同,常见的策略有“定右统左”。

定右统左

定右统左是双指针中的常用策略,即固定右端点,统计左端点,常用于区间统计。

子串简写

问题描述

规定长度大于等于k的字符串可以采用 用数值(中间字符数量)代替中间字符保留首尾字符的简写方法。

如k=4 “abjdjda” 可简写为 “a5a”

给定 k,字符串S,首尾字符c1,c2,问S有多少个首尾字符分别为c1,c2的子串可采用上述简写方法.

原题链接

思路分析

采用双指针(前指针i(初始为k-1),后指针j(初始为0)) 遍历子串长度为k的首尾字符,当j遍历到首字符为c1时,c1_sum++,c1_sum为一个统计数值,表示长度等于k且首字符为c1的子串个数(长度大于等于k且首字符为c1的左边界个数)。每次统计后,i++,j++,保证了两指针始终相距k。

前项指针i遍历到尾字符为c2时,sum+=cl_sum, 表示i位置为右边界,后面跟有c1_sum个左边界符合条件。

以后前项指针i每次遍历到c2时,子串长度会更长,前面统计的左边界肯定都符合条件,直接加cl_sum即可。

代码

#include <bits/stdc++.h> 
using namespace std;
int K;
long long ans=0,c1_sum=0;
string S;
char c1,c2;
int main(){cin>>K>>S>>c1>>c2;for(int i=K-1,j=0;i<S.size();i++,j++){if(S[j]==c1) c1_sum++;if(S[i]==c2) ans+=c1_sum;}cout<<ans;return 0;
}

每种字符至少取k个

问题描述

给你一个由字符 'a''b''c' 组成的字符串 s 和一个非负整数 k 。每分钟,你可以选择取走 s 最左侧 还是 最右侧 的那个字符。

你必须取走每种字符 至少 k 个,返回需要的 最少 分钟数;如果无法取到,则返回 -1

原题链接

思路分析

求最少分钟数就是求最少操作数,也就是最少取走的字符数。

由于每次只能取两侧的字符,最后保留的就是一个连续的子串。要使取走的字符数最少,相当于求最后满足条件的最长的子串

遍历r,寻找满足条件的最小的l。

代码

int takeCharacters(string s, int k) {if(k==0)return 0;int n=s.size();int num[3]={0};for(int i=0;i<n;i++){++num[s[i]-'a'];  //频数统计,全部取走}if(num[0]<k||num[1]<k||num[2]<k) return -1;int l=-1;int maxL=0;  //求r,l的最大距离for(int r=0;r<n;r++){num[s[r]-'a']--;  //放回//求最小的满足条件的l,当r加一时,小于l的左边界肯定不符和条件,所以l不用从0开始重新遍历.//此法相当于两次遍历s,时间复杂度为O(n)while(l<r&&(num[0]<k||num[1]<k||num[2]<k)){l++;  num[s[l]-'a']++;  //取走}maxL=max(maxL,r-l);}return n-maxL;
}

统计定界子数组的数目

问题描述

给你一个整数数组 nums 和两个整数 minK 以及 maxK

nums 的定界子数组是满足下述条件的一个子数组:

  • 子数组中的 最小值 等于 minK
  • 子数组中的 最大值 等于 maxK

返回定界子数组的数目。

子数组是数组中的一个连续部分。

原题链接

思路分析

首先考虑一个简单的情况,nums 的所有元素都在 [minK,maxK] 范围内。

在这种情况下,问题相当于:

同时包含 minKmaxK 的子数组的个数。
核心思路:枚举子数组的右端点i,统计有多少个合法的左端点。

遍历 nums,记录 minK 离 i 最近的位置 minI,以及 maxK 离 i 最近的位置 maxI,当遍历到 nums[i] 时,如果 minK 和 maxK 都遇到过,则左端点在 [0,min(minI,maxI)]范围中的子数组,包含 minK 和 maxK,最小值一定是 minK,最大值一定是 maxK。

i 为右端点的合法子数组的个数为:min(minI,maxI)+1

回到原问题,由于子数组不能包含在 [minK,maxK] 范围之外的元素,我们需要额外记录在 [minK,maxK] 范围之外的离 i 最近的位置,记作 i0,则左端点在 [i0+1,min(minI,maxI)] 中的子数组都是合法的。

以 i 为右端点的合法子数组的个数为min(minI,maxI)−i0

代码

long long countSubarrays(vector<int>& nums, int minK, int maxK) {long long ans = 0;int min_i = -1, max_i = -1, i0 = -1;for (int i = 0; i < nums.size(); i++) {int x = nums[i];if (x == minK) {min_i = i; // 最近的 minK 位置}if (x == maxK) {max_i = i; // 最近的 maxK 位置}if (x < minK || x > maxK) {i0 = i; // 子数组不能包含 nums[i0]}ans += max(min(min_i, max_i) - i0, 0);  //若i0大于min(min_i, max_i),则取不到合法子数组}return ans;
}
作者:灵茶山艾府

最高频元素的频数

问题描述

元素的 频数 是该元素在一个数组中出现的次数。

给你一个整数数组 nums 和一个整数 k 。在一步操作中,你可以选择 nums 的一个下标,并将该下标对应元素的值增加 1

执行最多 k 次操作后,返回数组中最高频元素的 最大可能频数

原题链接

思路分析

首先直接一点思考,枚举每个数作为最高频的那个数x,看看通过最多k次操作最多能有多少个数变成x,更新最大频数ans,最后的ans就是答案。因为操作是对选取的数加1,所以只需考虑操作比x小的数,选取的数应该小于x且尽量大,这样便可以尽可能操作更多的数。

对于以上分析,我们可以先对数组按升序排序,从左到右遍历,选取当前枚举数为最高频的数,再从当前数开始向左反向遍历确定操作后的最大频数,最后维护更新的ans就是答案。

代码

int maxFrequency(vector<int>& nums, int k) {sort(nums.begin(),nums.end());int ans=1,n=nums.size();long long sum=0;int j=0;for(int i=1;i<n;i++){int t=k;for(int j=i-1;j>=0;j--){t-=nums[i]-nums[j];if(t>=0) ans=max(ans,i-j+1);else break;}}return ans;}

上述代码两次循环,最大复杂度还是来到了O(n^2),数据规模大,最后超时了,需要优化.

上述代码中,每次枚举一个nums[i]都向左遍历,其实要计算以当前nums[i]kans值,可以借助前一个计算的结果。

定义j指针为前一个nums[i]往左遍历的截止点,求nums[i+1]对应的j指针时,因为nums[i+1]大于等于nums[i],其对应的j指针不可能更小,只会更大(请读者自行分析),因此只需在原有的j指针上往右移动即可。算法相当于对数组最多两次遍历,时间复杂度为O(n)。

代码

int maxFrequency(vector<int>& nums, int k) {sort(nums.begin(),nums.end());int ans=1,n=nums.size();long long sum=0;int j=0;for(int i=1;i<n;i++){sum+=(long long)(nums[i]-nums[i-1])*(i-j);while(sum>k){sum-=nums[i]-nums[j];j++;}ans=max(i-j+1,ans);}return ans;}

两端向中间收缩

接雨水

接雨水问题可以称的上指针尺取法中反向扫描的经典例题

问题描述

给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。

例图

在这里插入图片描述

原题链接

思路分析

定义左右指针l,r,初始l=0,r=height.size()-1;

每次判断height[l]<height[r]l++,否则 r++,直到l==r;

  1. height[l]<height[r]时可以确定 l从左到右遍历过程中子数组[0,l]的最大值lmax<height[r]

此时对于下标l处表示的雨水量是 lmax(子数组[0,l]的最大值)-height[l]

  1. 反之 当height[l]>=height[r]时可以确定 r从 右到左遍历过程中子数组[r,n-1]的最大值rmax<=height[l]

此时对于下标r处表示的雨水量是 rmax(子数组[r,n-1]的最大值)-height[r]

代码

int trap(vector<int>& height) {int ans=0;int l=0,r=height.size()-1;int lmax=0,rmax=0;while(l<r){lmax=max(height[l],lmax);rmax=max(height[r],rmax);if(height[l]<height[r]){ans+=lmax-height[l];l++;}else{ans+=rmax-height[r];r--;}}return ans;}

中间向两端扩散

包含k下标元素的子数组的最大容量

问题描述

给你一个整数数组 nums **(下标从 0 开始)**和一个整数 k

一个子数组 (i, j)容量 定义为 min(nums[i], nums[i+1], ..., nums[j]) * (j - i + 1)

请你返回子数组(i,j)(i<=k<=j) 的最大可能 容量

  • 1 <= nums.length <= 10^5
  • 1 <= nums[i] <= 2 * 10^4
  • 0 <= k < nums.length

原题链接

思路分析

由于子数组必须包含nums[k],可以定义两个指针left,right,初始时left=k-1right=k+1ans=k;

所有目标子数组中最大的min值为nums[k],最小的min值为数组nums的最小值,

从[1,nums[k]]范围中每次枚举最小值i,初始枚举nums[i],然后依次减1;

向左移动left,直到nums[left]<i或left<0

向右移动right,直到nums[right]<i或ritgh>=n(n为nums数组大小)

此时的子数组[left+1,right-1]是最小值为i的最长目标子数组,记录其容量(right-left-1)*i,用它更新历史最大容量。

当i继续减少,而指针没有移动时,此时(right-left-1)不变,i减少,ans=(right-left-1)*i比上阶段所求更小,所以历史最大容量不会改变。

直到枚举到left<0且ritgh>=n,此时的历史最大容量值即为正确答案。

时间复杂度O(n+C),C为nums中元素的取值范围。

代码

class Solution {
public:int maximumScore(vector<int>& nums, int k) {int n = nums.size();int left = k - 1, right = k + 1;int ans = 0;for (int i = nums[k];; --i) {while (left >= 0 && nums[left] >= i) {--left;}while (right < n && nums[right] >= i) {++right;}ans = max(ans, (right - left - 1) * i);if (left == -1 && right == n) {break;}}return ans;}
};

优化

上述代码效率较低的原因是在 i 比 (left,right) 中的最小值更小,但指针没有移动时,计算出的分数是没有意义的。

指针没有移动的原因是 nums[left] 和 nums[right] 都小于 i。

此时我们应当直接把 i 减小至二者的较大值,而不是每次减少 1,这样就可以保证每一次循环中都至少会移动一次指针,就可以将 C 从时间复杂度中移除,总时间复杂度为O(n)

细节

在减少 i 时,需要注意指针已经超出数组边界的情况。

代码

int maximumScore(vector<int>& nums, int k) {int n = nums.size();int left = k - 1, right = k + 1;int ans = 0;for (int i = nums[k];;) {while (left >= 0 && nums[left] >= i) {--left;}while (right < n && nums[right] >= i) {++right;}ans = max(ans, (right - left - 1) * i);if (left == -1 && right == n) {break;}i = max((left == -1 ? -1 : nums[left]), (right == n ? -1 : nums[right]));}return ans;}

指向不同容器的指针尺取

寻找右区间

问题描述

给你一个区间数组 intervals ,其中 intervals[i] = [starti, endi] ,且每个 starti不同

区间 i右侧区间 是满足 startj >= endi,且 startj 最小 的区间 j。注意 i 可能等于 j

返回一个由每个区间 i 对应的 右侧区间 下标组成的数组。如果某个区间 i 不存在对应的 右侧区间 ,则下标 i 处的值设为 -1

原题链接

代码

vector<int> findRightInterval(vector<vector<int>>& intervals) {vector<pair<int, int>> start;  //将intervals拆分为两个数组vector<pair<int, int>> end;int n = intervals.size();for(int i=0; i<n; i++){start.emplace_back(intervals[i][0], i);end.emplace_back(intervals[i][1], i);}sort(start.begin(), start.end());sort(end.begin(), end.end());vector<int> res(n, -1);auto e_iter = end.begin(), s_iter = start.begin();  //两个指针while(e_iter != end.end()){while(s_iter != start.end() && s_iter->first < e_iter->first){s_iter++;}if(s_iter == start.end()) break;res[e_iter->second] = s_iter->second;e_iter++;}return res;}

多指针

统计好三元组的数目

问题描述

给你一个整数数组 arr ,以及 abc 三个整数。请你统计其中好三元组的数量。

如果三元组 (arr[i], arr[j], arr[k]) 满足下列全部条件,则认为它是一个 好三元组

  • 0 <= i < j < k < arr.length
  • |arr[i] - arr[j]| <= a
  • |arr[j] - arr[k]| <= b
  • |arr[i] - arr[k]| <= c

其中 |x| 表示 x 的绝对值。

返回 好三元组的数量

原题链接

代码

int countGoodTriplets(vector<int>& arr, int a, int b, int c) {int n=arr.size(),ans=0;vector<int>ids(n);ranges::iota(ids,0);ranges::sort(ids,{},[&](int i){return arr[i];});for(int j:ids){vector<int>left,right;for(int i:ids){if(i<j&&abs(arr[i]-arr[j])<=a) left.push_back(arr[i]);}for(int k:ids){if(k>j&&abs(arr[k]-arr[j])<=b) right.push_back(arr[k]);}int s1=0,s2=0;for(int value:left){while(s2<right.size()&&right[s2]-value<=c) s2++;while(s1<right.size()&&right[s1]-value<-c) s1++;ans+=s2-s1;}}return ans;
}

相关文章:

  • 【Linux网络#17】TCP全连接队列与tcpdump抓包
  • LeetCode 热题 100 78. 子集
  • 洛谷 P9007 [入门赛 #9] 最澄澈的空与海 (Hard Version)
  • N-Gram 模型
  • 基于Vue3开发:打造高性能个人博客与在线投票平台
  • Java---Object和内部类
  • 协程补充---viewModelScope 相关知识点
  • 蓝桥杯 19. 植树
  • 事务隔离(MySQL)
  • 5.4 - 5.5Web基础+c语言拓展功能函数
  • sqli-labs靶场11-17关(POST型)
  • 深度解析:从 GPT-4o“谄媚”到 Deepseek“物理腔”,透视大模型行为模式的底层逻辑与挑战
  • ns-3仿真_pcap抓取时间太长问题_log打印时间显示5s结束,pcap抓包抓到了10s
  • Kubernetes控制平面组件:Controller Manager详解
  • ByteArrayInputStream 类详解
  • 什么是“系统调用”
  • JS DAY3
  • STM32 PulseSensor心跳传感器驱动代码
  • 【实战教程】React Native项目集成Google ML Kit实现离线水表OCR识别
  • unity TMP字体使用出现乱码方框
  • 两个灵魂,一支画笔,意大利艺术伴侣的上海灵感之旅
  • 巴菲特股东大会4.5万字问答实录:股神60年穿越牛熊的最新心得和人生思考
  • 多地政府机关食堂五一“开门迎客”:怎么看这场“宠粉”大戏
  • 澎湃读报丨央媒头版五四青年节集中刊文:以青春之我,赴时代之约
  • 华尔兹转岗与鲁比奥集权:特朗普政府人事震荡背后的深层危机
  • A股2024年年报披露收官,四分之三公司盈利