算法基础详解(4)双指针算法

欢迎来到我的频道[【点击跳转专栏】]

作者说:我想说 基础 不等于 简单 ;算法能力不是一蹴而就的,而是来自日积月累的积累和练习!积小流终成江海,诸君 加油!!

文章目录

  • [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循环就行!


  1. 那么判断枚举的子数组中,所有元素全都不相同

借助哈希表就行!


在算法题中 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 所指位置的元素在哈希表中的次数减一;
  • 更新结果:判断结束之后,窗口合法,此时更新窗口的大小。

那么怎么写代码呢?

  1. 初始化

定义left=1,right=1

  1. 用什么结构来维护窗口的信息!

创建一个unordered_map<int,int> mp;来进行优化 第一个int 表示元素 第二个int 表示出现的次数!

  1. 进窗口怎么弄

让right 所指的元素进窗口即可 mp[a[right]++

  1. 如何判断窗口是否合法和怎么出窗口

只需要判断mp[a[right]]是否大于1即可

当不合法的时候 只需要让left所指向元素出窗口到合法为止! mp[a[left]]--

  1. 更新结果

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号位逆时针最远是14号位顺时针最远是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)上,这块内存是重复利用的。当你定义一个局部变量时,系统只是给你分配了一块"别人刚用完还没来得及打扫"的房间。

所以局部变量一定要记得初始化

相关推荐
zk_ken1 小时前
优化图像拼接算法思路
算法
golang学习记1 小时前
VS Code官宣:全面支持Rust!
开发语言·vscode·后端·rust
xwz小王子2 小时前
Nature Communications从结构到功能:基于Kresling折纸的多模态微型机器人设计
人工智能·算法·机器人
luj_17682 小时前
从R语言想起的,。。。
服务器·c语言·开发语言·经验分享·算法
三道渊2 小时前
C语言:二级指针及void与void*的区别
c语言·开发语言
杜子不疼.2 小时前
Python + Ollama 本地跑大模型:零成本打造私有 AI 助手
开发语言·c++·人工智能·python
小此方2 小时前
Re:思考·重建·记录 现代C++ C++11篇 (一) 列表初始化&Initializer_List
开发语言·c++·stl·c++11·现代c++
计算机安禾2 小时前
【数据结构与算法】第29篇:红黑树原理与C语言模拟
c语言·开发语言·数据结构·c++·算法·visual studio
叹一曲当时只道是寻常2 小时前
Tauri v2 + Rust 实现 MCP Inspector 桌面应用:进程管理、Token 捕获与跨平台踩坑全记录
开发语言·后端·rust