欢迎来到我的频道[【点击跳转专栏】]
作者说:我想说 基础 不等于 简单 ;算法能力不是一蹴而就的,而是来自日积月累的积累和练习!积小流终成江海,诸君 加油!!
文章目录
- [1. 双指针](#1. 双指针)
- [1.1 唯⼀的雪花(模版题)](#1.1 唯⼀的雪花(模版题))
- [1.2 逛画展(练习题)](#1.2 逛画展(练习题))
- [1.3 字符串(练习题)](#1.3 字符串(练习题))
- [1.4 丢手绢(创新题)](#1.4 丢手绢(创新题))
- [2. 知识点补充](#2. 知识点补充)
1. 双指针
双指针算法有时候也叫尺取法或者滑动窗口,是一种优化暴力枚举策略的手段:
- 当我们发现在两层 for 循环的暴力枚举过程中,
两个指针是可以不回退的,此时我们就可以利用两个指针不回退的性质来优化时间复杂度。 - 因为双指针算法中,两个指针是朝着同一个方向移动的,因此也叫做同向双指针。
注意:在学习该算法的时候,不要只是去记忆模板,一定要学会如何从暴力解法优化成双指针算法。不然往后遇到类似题目,你可能压根都想不到用双指针去解决。
具体算法 请看例题部分!
1.1 唯⼀的雪花(模版题)
https://www.luogu.com.cn/problem/UVA11572
⚠️:这道题的重点不是双指针 而是怎么从暴力枚举中 想到要用双指针进行优化!解法一:暴力枚举-> 枚举出所有符合要求的子数组
1.如何枚举?
两层
for循环就行!
- 那么判断枚举的子数组中,所有元素全都不相同
借助哈希表就行!
在算法题中
C++的运算速度为1e8次每秒也就说 2秒级别的运算力是2e8次左右你O(N^2)=1e12的时间复杂度绝对会死翘翘的 所以必须优化!
解法二: 利用
单调性,使用同向双指针来优化!当我们「暴力枚举」的过程中,固定一个起点位置 l e f t left left,然后 r i g h t right right 之后向后遍历时。当 r i g h t right right
第一次扫描到一个位置,使 [ l e f t , r i g h t ] [left, right] [left,right] 这个区间「出现重复字符」,此时我们会发现:
- r i g h t right right 无需再向后遍历,因为继续向后走也是「不合法」的;
(此时right 无法继续往后走!)- l e f t left left 向后移动一格之后, r i g h t right right 指针也不用回退,因为我们已经维护出来 [ l e f t , r i g h t ] [left, right] [left,right] 区间的信息,并且以 l e f t + 1 left + 1 left+1 为起点的最优解一定不会比 l e f t left left 为起点的好。(
left+1 指向2 时候 结果不如原本指向1的时候!)当我们发现暴力枚举的「
两个指针不回退」这一规律时,就可以用「同向双指针」优化:
- 进窗口: r i g h t right right 位置元素记录到统计次数的哈希表中;
- 判断:当哈希表中 r i g h t right right 位置的值出现超过 1 次之后,窗口内子串不合法;
- 出窗口:让 l e f t left left 所指位置的元素在哈希表中的次数减一;
- 更新结果:判断结束之后,窗口合法,此时更新窗口的大小。
那么怎么写代码呢?
- 初始化
定义
left=1,right=1
- 用什么结构来维护窗口的信息!
创建一个
unordered_map<int,int> mp;来进行优化 第一个int 表示元素 第二个int 表示出现的次数!
- 进窗口怎么弄
让right 所指的元素进窗口即可
mp[a[right]++
- 如何判断窗口是否合法和怎么出窗口
只需要判断
mp[a[right]]是否大于1即可当不合法的时候 只需要让
left所指向元素出窗口到合法为止! mp[a[left]]--
- 更新结果
ret=max(ret,right-left+1)
该方法 时间复杂度最大为
O(2N)=2e6!
cpp
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
int a[N];
int main()
{
int T;cin>>T;
while(T--)
{
int n;
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>a[i];
}
//双指针
int ret=0;
int left=1,right=1;
unordered_map<int, int> mp; // 维护窗⼝内所有元素出现的次数
while(right<=n)
{
//进窗口
mp[a[right]]++;
//判断是否合法
while(mp[a[right]]>1)
{
//不合法则需要出窗口
mp[a[left]]--;
left++;
}
//窗口合法 更新结果
ret=max(ret,right-left+1);
right++;
}
cout<<ret<<endl;
}
}
1.2 逛画展(练习题)
练手题 如果1.1你搞懂了 那么这道题是非常简单的! 结合1.1的分析方法 发现遍历指针是不回退的 所有可以优化成双指针法!需要注意的是 用
int kind来标记目前区间的画家数量(通过kind是否)哈希表直接可以用
int mp[](因为所有画家注定都被标记 不要像1.1那样(不是所有雪花都会标记)节省空间用unordered_map)
- 进窗口 :将
right位置元素记录到统计次数的哈希表中,如果次数从 0 变为 1,说明窗口内多了一种字符,记录字符种类数;- 判断 :当窗口内字符种类等于 (m) 时,窗口合法,
right停止右移,接下来执行出窗口操作;- 出窗口 :让
left所指位置的元素在哈希表中的次数减一,如果次数从 1 变为 0,说明窗口内少了一种字符,更新字符种类数;- 更新结果:在窗口合法时,更新当前窗口的大小(如记录最小长度)。
⚠️:
其实你自己画画立刻就知道怎么写了 信我 主播就是模拟画了下 就得到了细节该怎么处理了 别光想 动笔!
cpp
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
int mp[N],a[N];
int kind=0;
int main()
{
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++)
{
cin>>a[i];
}
int left=1,right=1,ret=0x3f3f3f3f;
int x,y;//记录结果
while(right<=n)
{
mp[a[right]]++;
if(mp[a[right]]==1)
{
kind++;
}
while(kind==m)
{
//更新结果
if(right-left+1<ret)
{
x=left;
y=right;
ret=right-left+1;
}
mp[a[left]]--;
if(mp[a[left]]==0)
{
kind--;
}
left++;
}
right++;
}
cout<<x<<" "<<y;
}
1.3 字符串(练习题)
https://ac.nowcoder.com/acm/problem/18386
练手题:解法完全和1.2 相同 做一做来巩固下算法还是那句话
双指针算法套路相同 具体细节需要自己画一下模拟后再写!
cpp
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
int mp[26];
int kind,ret=0x3f3f3f3f;
string s;
int main()
{
cin>>s;
int n=s.size();
for(int left=0,right=0;right<n;right++)
{
mp[s[right]-'a']++;
if(mp[s[right]-'a']==1)
{
kind++;
}
while(kind==26)
{
ret=min(ret,right-left+1);
mp[s[left]-'a']--;
if(mp[s[left]-'a']==0)
{
kind--;
}
left++;
}
}
cout<<ret;
}
1.4 丢手绢(创新题)
https://ac.nowcoder.com/acm/problem/207040
处理环的⼀个技巧,把环分成两部分分析:针对第
i个人,如何找出距离他最远的那个人离他的距离是多少?(这道题最关键,最难的部分!)首先 关于最远的那个人,会有两种情况:一个是顺时针最远,另一个则是逆时针最远。
如图:
假设顺时针开始 当首次
a[1]+a[2]+a[3]>=sum/2的时候!此时4就是逆时针最远,3就是顺时针最远(即当首次出现2*k>=sum的位置时 此时k就是逆时针最远,k-1就是顺时针最远 )
当我们「暴力枚举」的过程中,固定一个起点位置 left,然后 right 之后向后遍历时,记 k 为 [left,right] 之间的距离。当 right 第一次扫描到 k × 2 ≥ sum 时,此时我们会发现:
- right 无需再向后遍历,因为继续向后走的结果一定不是最优的;(
此时 sum-k 只会越来越小)- left 向后移动一格之后,right 指针也不用回退,因为我们已经维护出来 [left,right] 区间的信息,right 回退也不是最优解。(
这一块画一下很容易找到规律!)当我们发现暴力枚举的「两个指针不回退」时,就可以用「
滑动窗口」优化:
- 进窗口:right 位置与前一个位置的距离累加到 k 中;
- 判断:k × 2 ≥ sum 时,此时 right 指针不用前进,应该让 left 所指的元素出窗口
- 出窗口:让 left 所指位置与前一个位置的距离累减到 k 中;
- 更新结果:需要在两个地方更新: a. 判断结束之后,此时 [left,right] 之间可能是最优解,用 k 更新结果; b. 判断成立的时候,此时 [right,left] 之间可能是最优解,用 sum − k 更新结果。
⚠️:很多人疑惑为什么这道题循环终止条件还是(right<=n) 其实比如说
1号位顺时针最远是3号逆时针最远是4号;那么反过来3号位逆时针最远是1号4号位顺时针最远是1号
cpp
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=1e5+10;
LL a[N];
int main()
{
int n;
cin>>n;
int sum=0;
for(int i=1;i<=n;i++)
{
cin>>a[i];
sum+=a[i];
}
LL ret=0;
LL k=0;
for(int left=1,right=1;right<=n;right++)
{
k+=a[right];
//此时 sum-k 可能会是最终解
while(2*k>=sum)
{
ret=max(ret,sum-k);
k-=a[left++];
}
//当 2*k<sum 最终解可能为k
ret=max(k,ret);
}
cout<<ret;
}
2. 知识点补充
| 变量类型 | 存储位置 | 是否自动清零 | 原因 |
|---|---|---|---|
| 全局变量 | BSS 段 / 数据段 | 是 | 操作系统加载时强制清零,且为了节省磁盘空间。 |
| 静态变量 | BSS 段 / 数据段 | 是 | 同上。 |
| 局部变量 | 栈 | 否 | 为了性能,且使用的是上一轮函数留下的脏数据。 |
因为局部变量存放在栈(Stack)上,这块内存是重复利用的。当你定义一个局部变量时,系统只是给你分配了一块"别人刚用完还没来得及打扫"的房间。
所以局部变量一定要记得初始化!







