《算法竞赛从入门到国奖》算法基础:入门篇-双指针

💡Yupureki:个人主页

✨个人专栏:《C++》 《算法》


🌸Yupureki🌸的简介:


前言

双指针有时也被称作滑动窗口,是优化暴力枚举的一种有效方式。

当我们利用两层for循环时,发现可以用两个指针遍历一次,不用回退。即可用双指针来代替两层for循环

1. 唯一的雪花

题目链接:

UVA11572 唯一的雪花 Unique Snowflakes - 洛谷

算法原理

暴力求解:

两层for循环,第一层固定左边的left,第二层从left开始让right一直往右遍历判断。

但是n可能达到10^6的规模,直接超时

双指针:

我们注意到,每次固定好left后,right往后找,最终会找到一个重复的元素。那么在找到重复之前,left, right这个区间一定能保证元素不重复。那么找到之后,right也无需往后找了,毕竟这个区间现在非法了,我们让left往后遍历,此时left,right仍是非法的,如果left找到了那个与right下标重复的元素,left再++,就能使left,right合法了。

因此我们发现如下规律:

  1. right无需往后走,毕竟往后走也是非法的
  2. left往后走的时候,right无需回退,因为我们之前已经维护好left,right的信息,保证一定合法。

因此双指针的本质是两个指针无需回退

实操代码

cpp 复制代码
#include <iostream>
#include <vector>
#include <unordered_map>
using namespace std;

int main()
{
    int nums; cin >> nums;
    while (nums--)
    {
        int n; cin >> n;
        vector<long long> v;
        unordered_map<long long,int> m;
        while (n--)
        {
            long long num; cin >> num;
            v.push_back(num);
        }
        int left = 0, right = 0;
        int ret = 0;
        while (right != v.size())
        {
            m[v[right]]++;
            while (m[v[right]] > 1)//找到了重复的元素
                m[v[left++]]--;//left向后走
            if (right - left + 1 > ret)//[left,right]合法后,更新结果
                ret = right - left + 1;
            right++;
        }
        cout << ret << endl;
    }
    return 0;
}

2. 逛画展

题目链接:

P1638 逛画展 - 洛谷

算法原理

这题是要让left, right区间内存在1~m的数字

那么我们先让right往后走,每找到一个数字,就往哈希表里存储。当哈希表里面的大小正好为m时,说明left,right区间合法。由于我们要"瘦身"区间,即尽量减小left,right的长度且合法,那么我们再让left往后走,每找到一个元素,就让哈希表里统计的个数--。直到left所指的这个元素个数正好等于1,就不能走了,因为走了就要违法了。

实操代码

cpp 复制代码
#include <iostream>
#include <vector>
#include <unordered_map>
using namespace std;

int main()
{
    int n, k; cin >> n >> k;
    vector<int> v;
    unordered_map<int, int> m;
    while (n--)
    {
        int num; cin >> num;
        v.push_back(num);
    }
    int left = 0, right = 0, ret = 0xffffff;
    int prev1 = 0, prev2 = 0;
    while (right != v.size())
    {
        m[v[right]]++;
        while (m.size() == k && m[v[left]] > 1)//缩小区间
        {
            m[v[left]]--;
            if (m[v[left]] == 0)
                m.erase(v[left]);
            left++;
        }
        if (m.size() == k && right - left + 1 < ret)//更新结果
        {
            ret = right - left + 1;
            prev1 = left;
            prev2 = right;
        }
        right++;
    }
    cout << prev1+1 << " " << prev2+1;
    return 0;
}

3. 字符串

字符串

算法原理

这题跟上题差不多

  1. left, right区间保证存在26个小写英文字母
  2. left,right尽量小

实操代码

cpp 复制代码
#include <iostream>
#include <string>
#include <unordered_map>
using namespace std;

int main()
{
    string s;
    cin>>s;
    int left = 0,right = 0,ret = 0xffffff;
    unordered_map<char,int> m;
    while(right != s.size())
    {
        m[s[right]]++;
        while(m.size() == 26 && m[s[left]] > 1)
        {
            m[s[left]]--;
            if(m[s[left]] == 0)
                 m.erase(s[left]);
            left++;
        }
        if(m.size() == 26 && right - left + 1<ret)
                 ret = right - left + 1;
        right++;
    }
    cout<<ret;
    return 0;
}

4. 丢手绢

题目链接:

丢手绢

算法原理

这题的核心是 几个小朋友围成一个圈

那么在一个圆上,两个小朋友之间有两条路可以走,正好形成了一个圆(其实就是圆上两点)

那么对于这两条圆弧,我们走哪一条?那肯定是较短的那一条,不会有人傻到去走较长的那一条吧

因此,两个小朋友的距离是两条路的较短的那一条

假设从1到5,有从1-2-3-4-5和1-7-6-5两条路,哪条短走哪条

对于临界状态,即两条路正好相等,正好是圆长的一半。因此我们判断,那条路小于等于圆长的一半,就走哪条。

由于我们需要计算两个小朋友,即圆上两点的距离最大值。注意到如果固定left,right向右走,如果left, right之间的距离小于等于sum(圆周长)/2,这个是合法情况,找出这个距离的最大值即可。当left, right的距离大于sum/2时,我们向右移动left,直到left, right合法,更新结果。

因此left 和 right无需回退

实操代码

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

int main()
{
    int n; cin >> n;
    vector<long long> v;
    long long sum = 0;
    for (int i = 0; i < n; i++)
    {
        long long num; cin >> num;
        v.push_back(num);
        sum += num;
    }
    long long left = 0, right = 0, ret = 0;
    long long k = 0;[left, right]的长度
    while (right != v.size())//进窗口
    {
        k += v[right];
        while (2 * k >= sum)//长度大于sum/2,出窗口
        {
            if (sum - k > ret)
                ret = sum - k;
            k -= v[left++];
        }
        if (k > ret)//更新结果
            ret = k;
        right++;
    }
    cout << ret;
    return 0;
}
相关推荐
§ꦿCFོ༉11 分钟前
嵌入式八股文—C语言入门学习(四)
c语言·开发语言·学习
投图匠31 分钟前
十种不同风格的 CTP接口 框架
开发语言·python
Pniubi2 小时前
关于算法用现成数据结构和手撕
数据结构
唐墨1233 小时前
linux kernel 比较数据大小的宏定义
linux·算法
爱笑的k114 小时前
nano-vllm 源码剖析:千行 Python 手写离线推理引擎的调度、显存与并行时序
开发语言·python·vllm
拂拉氏4 小时前
【知识讲解-题目讲解】 统计满足K约束的子字符串数量2 (滑动窗口+前缀和+二分查找+分析)
算法·前缀和·二分查找·滑动窗口
X1A0RAN5 小时前
PPT 图片批量处理教程:导出全部幻灯片 · 提取全部素材
开发语言·javascript·powerpoint
熊猫钓鱼>_>6 小时前
普通用户视角的端侧智能体验手记【鸿蒙心迹】
开发语言·人工智能·华为·智能手机·性能优化·ai编程·harmonyos
瑞雪兆丰年兮6 小时前
JavaWeb-day13-SpringBootWeb事务管理&AOP
java·开发语言·springboot·javaweb·事务·web·aop