【算法竞赛】尺取法

news2024/11/20 12:25:28

尺取法(又称为双指针、Two Pointers)是算法竞赛中一个常)用的优化技巧,用来解决序列的区间问题,操作简单,容易编程。如果区间是单调的,也常常用二分法求解,所以很多问题用尺取法和二分法都行。另外,尺取法的操作过程和分治算法的步骤很相似,有时也用在分治中。

概念

什么是尺取法?为什么尺取法能用来优化?考虑下面的应用背景:
(1)给定一个序列,有时需要它是有序的,先排序;
(2)问题和序列的区间有关,且需要操作两个变量,可以用两个下标(指针)i和j扫描区间。
对于上面的应用,一般的做法是用i和j分别扫描区间,有二重循环,复杂度为O(n2)。
以反向扫描(即i与j的方向相反,后文有解释)为例,代码如下:


for(int i = 0; i<n; i++)                      //i从头扫到尾
	for(intj=n-1; n-1; j>= 0; j-- ){          //j从尾扫到头
			...
		}
	

下面用尺取法优化上述算法。
实际上,尺取法就是把二重循环变为一个循环,在这个循环不中同时处理i和j。复杂度也就从O(n2)变为O(n)。仍以上面的反向扫描为例,代码如下:

//用while实现
int i = 0, j = n - 1;
while (i<j){
	//i和j在中间相遇,这样做还能防止i和j越界
	i++;//i从头扫到尾
	j--;//j从尾扫到头
}
//用for实现
for(inti= 0, j=n-1;i<j;i++,j--){
		//满足题意的操作
	}

在尺取法中,i和j有以下两种扫描方向:
(1)反向扫描。i和j方向相反,让i从头到尾,j从尾到头,在中间相会。
(2)同向扫描。i和j方向相同,都从头到尾,速度不同,如让j跑在i前面。

把同向扫描的i、j指针称为"快慢指针",把反向扫描的i、j指针称为"左右指针",更加形象。其中,“快慢指针"在序列上产生了一个大小可变的"滑动窗口”,有灵活的应用,如寻找区间、数组去重、多指针问题。

反向扫描

用下面的几个应用说明反向扫描的编码方法。

一、找指定和的整数对
这个问题是尺取法中最经典,也最简单直接的应用。
在这里插入图片描述
为了说明尺取法的优势,下面给出4种解题方法。
(1)用二重循环暴力搜索,枚举所有的取数方法,复杂度为0(n2),超时。暴力法不需要排序。
(2)二分法。首先对数组从小到大排序,复杂度为O(nlog2n);然后从头到尾处理数组中的每个元素a[i],在大于a[i]的数中二分查找是否存在一个等于m-a[i]的数,复杂度也为O(nlog2n)。两部分相加,总复杂度仍然为O(nlog2n)。
(3)哈希。分配一个哈希空间s,把n个数放进去。逐个检查a[]中的几个数,如a[i],检查m-a[i]在s中是否有值,如果有,那么存在一个答案。复杂度为O(n)。哈希方法很快,但是需要一个额外的很大的哈希空间。
(4)尺取法。这是标准解法。首先对数组从小到大排序;然后设置两个变量i和j,分别指向头和尾,i初值为0,j初值为n-1,然后让i和j逐渐向中间]移动,检查a[i]+a[j],如果大于m,就让j减1;如果小于m,就让i加1,直至a[i]+a[j]=m。排序复杂度为O(nlog2n),检查的复杂度为O(n),总复杂度为O(nlog2n)。
尺取法代码如下,注意可能有多个答案。

// 函数用于找到数组中两数之和等于给定值 m 的两个数
void find_sum(int a[], int n, int m) {
    // 先对数组进行排序,时间复杂度为 O(n log₂ n)
    sort(a, a + n); 
    // i 指向数组头部,j 指向数组尾部
    int i = 0, j = n - 1;
    while (i < j) {
        int sum = a[i] + a[j];
        // 如果两数之和大于 m,j 指针向前移动
        if (sum > m) j--;
        // 如果两数之和小于 m,i 指针向后移动
        else if (sum < m) i++;
        // 如果两数之和等于 m,输出这两个数,然后 i 指针向后移动继续寻找可能的其他答案
        else {
            cout << a[i] << " " << a[j] << endl;
            i++;
        }
    }
}

在本题中,尺取法不仅效率高,而且不需要额外的空间。
把题目的条件改变一下,可以变化为类似的问题,如判断一个数是否为两个数的平方和。

二、判断回文串
给一个字符串,判断它是否为回文串。
在这里插入图片描述

#include <bits/stdc++.h>
using namespace std;

// 主函数
int main() {
    int n;
    cin >> n; // n 为测试用例个数
    while (n--) {
        string s;
        cin >> s; // 读一个字符串
        bool ans = true;
        int i = 0, j = s.size() - 1; // 双指针,i 指向字符串头部,j 指向字符串尾部
        while (i < j) {
            if (s[i]!= s[j]) { // 如果对应位置字符不相等
                ans = false;
                break;
            }
            i++;
            j--; // 移动双指针
        }
        if (ans)
            cout << "yes" << endl;
        else
            cout << "no" << endl;
    }
    return 0;
}

稍微改变条件,类似的题目如下。
(1)不区分大小写,忽略非英文字母,判断是否为回文串1。
(2)允许删除(或插入,本题只考虑删除)最多一个字符,判断是否能构成回文字符串
解题思路:设反向扫描双指针为i和j,如果s[i]和s[j]相同,则执行i++和j–;如果s[i]和s[j]不同,那么或者删除s[i],或者删除s[j],看剩下的字符串是否是回文串。

同向扫描

下面给出同向扫描的几个经典应用。

一、寻找区间和
这是用尺取法产生滑动窗口的典型例子。
在这里插入图片描述
指针i和j(i<j)都从头向尾扫描,判断区间[i,j]数组元素的利1是否等于s
如何寻找区间和等于5的区间?如果简单地对i和j做二重循环,复杂度为O(n2)
用尺取法,复杂度为O(n),操作步骤如下:
(1)初始值i=0,j=0,即开始都指向第1个元素a[0]。定义sum是区间[i,j]数组元素的和,初始值sum=a[0]。
(2)如果sum=s,输出一个解。继续,把sum减掉元素a[i],并把i向后移动一位。
(3)如果sum>s,让sum减掉元素a[i],并把i向后移动一位。
(4)如果sum<s,把j向后移动一位,并把sum的值加上这个新元素。

在上面的步骤中,有两个关键技巧:
(1)滑动窗口的实现。
窗口就是区间[i,j],随着i和j从头到尾移动,窗口就"滑动"扫描了整个序列,检索了所有数据。i和j并不是同步增加的,窗口像一只蚯蚓伸缩前进,它的长度是变化的,这个变化正对应了对区间和的计算。
(2)sum的使用。
如何计算区间和?暴力的方法是从a[i]到a[j]累加,但是这个累加的复杂度为O(n),超时。如果利用sum,每次移动i或j时,只需要把sum加或减一次,就得到了区间和,复杂度为O(1)。这是"前缀和"递推思想的应用。

// 函数用于在给定整数数组中找到连续子数组使得其和等于给定值 s
void findsum(int *a, int n, int s) {
    int i = 0, j = 0;
    int sum = a[0];
    while (j < n) { // 下面代码中保证 i <= j
        // 如果当前和大于等于给定值 s
        if (sum >= s) {
            // 如果当前和等于给定值 s,输出子数组的起始和结束位置
            if (sum == s)
                printf("%d %d\n", i, j);
            sum -= a[i];
            i++;
            // 防止 i 超过 j
            if (i > j) {
                sum = a[i];
                j++;
            }
        }
        // 如果当前和小于给定值 s
        if (sum < s) {
            j++;
            // 更新 sum
            sum += a[j];
        }
    }
}

"滑动窗口"的例子还有:
(1)给定一个序列以及一个整数M,在序列中找M个连续读递增的元素,使它们的区间和最大;
(2)给定一个序列以及一个整数K,求一个最短的连续子序列,其中包含至少K个不同的元素。

二、数组去重
数组去重是很常见的操作,方法也很多,尺取法是其中优秀的算法。
问题描述:给定数组a ,长度为n,把数组中重复的数去掉。
下面给出两种解法:哈希和尺取法。
(1)哈希
哈希函数的特点是有冲突,利用这个特点去重。把所有数插入哈希表,用冲突过滤重复
的数,就能得到不同的数。缺点是哈希把数据的值本身看作地址,如果数据值过大,需要的
空间也非常大。
(2)尺取法
(1)将数组排序,排序后重复的整数就会挤在一起。
(2)定义双指针i和j,初始值都指向a[0]。i和j都从头到尾扫描数组a[]。i指针走得快,逐个遍历整个数组;j指针走得慢,它始终指向当前不重复部分的最后一个数。也就是说,j用于获得不重复的数。
(3)扫描数组。快指针执行i++,如果此时a[i]不等于慢指针j指向的a[j],就执行j++,并且把a[i]复制到慢指针j的当前位置a[j]。
(4)i扫描结束后,a[0]~a[j]就是不重复数组。

三、多指针
有时两个窗口指针不够用,需要更多的指针。例2.4是三指针的例子。
在这里插入图片描述
如果用暴力法统计数对,复杂度为O(n2),超时。下面试试尺取法。
对输入样例排序后得{445778),其中第1个4和后面两个7是两对,第2个4和后面两个7也是两对,共4对。如果仅使用i和j两个指针,无法实现.
如何解决?可以把后面两个7看作一个整体,一起统计数对。用两个指针j和k指示这种区间,[j,k]区间内每个数都相同,这个区间可以产生,一j个数对。细节见下面的代码。使用3个指针:i是主指针,从头到尾遍历几个数;j和k是辅助指针,用于查找数字相同的区间[j,k]。
第10行的尺取法代码只有一个for循环,且j和k随着i递增,所以复杂度为O(n)。
另外,第8行的排序复杂度为O(nlog2n)。

#include <bits/stdc++.h>
using namespace std;

const int N = 2e5 + 5;
int a[N];

// 主函数
int main() {
    int n, c;
    cin >> n >> c;
    for (int i = 1; i <= n; i++)
        cin >> a[i];
    sort(a + 1, a + 1 + n);
    long long ans = 0;
    // 使用三个指针 i、j、k 进行遍历和查找
    for (int i = 1, j = 1, k = 1; i <= n; i++) {
        // 用 j 指针查找第一个与 a[i]之差大于等于 c 的位置
        while (j <= n && a[j] - a[i] < c)
            j++;
        // 用 k 指针查找第一个与 a[i]之差大于 c 的位置
        while (k <= n && a[k] - a[i] <= c)
            k++;
        // 如果 a[j]-a[i]==c 且 a[k - 1]-a[i]==c 且 k - 1 >= 1,则更新答案
        if (a[j] - a[i] == c && a[k - 1] - a[i] == c && k - 1 >= 1)
            ans += k - j;
    }
    cout << ans;
    return 0;
}

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

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

相关文章

位运算(4)_丢失的数字

个人主页&#xff1a;C忠实粉丝 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 C忠实粉丝 原创 位运算(4)_丢失的数字 收录于专栏【经典算法练习】 本专栏旨在分享学习算法的一点学习笔记&#xff0c;欢迎大家在评论区交流讨论&#x1f48c; 目录 温馨提示:…

学习VTK的目的和方法

1 VTK&#xff08;Visualization Toolkit&#xff09;是一个开源的跨平台软件系统&#xff0c;用于三维计算机图形学、图像处理和可视化。学习VTK的主要目的有&#xff1a; 3D可视化&#xff1a; VTK提供了丰富的工具和算法&#xff0c;可以用来可视化各种科学数据&#xff0c…

VBA数据库解决方案第十五讲:Recordset集合中单个数据的精确处理

《VBA数据库解决方案》教程&#xff08;版权10090845&#xff09;是我推出的第二套教程&#xff0c;目前已经是第二版修订了。这套教程定位于中级&#xff0c;是学完字典后的另一个专题讲解。数据库是数据处理的利器&#xff0c;教程中详细介绍了利用ADO连接ACCDB和EXCEL的方法…

windows下安装nginx和基本配置

1. 下载 Nginx 从 Nginx 官方网站下载 Windows 版本的 Nginx。访问 Nginx 官网 并选择适合 Windows 的版本。通常应选择稳定版&#xff08;Stable version&#xff09; 2. 安装 Nginx 安装 Nginx 实际上是解压下载的文件。可以选择一个适合的位置来存放 Nginx 的文件夹 例如…

【数据结构初阶】排序算法(下)冒泡排序与归并排序

文章目录 4. 交换排序4. 1 冒泡排序 5. 归并排序6. 非比较排序6. 1 计数排序 5. 排序性能分析6. 排序算法复杂度及稳定度分析 4. 交换排序 交换排序基本思想: 所谓交换**&#xff0c;就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置**。 交换排序的特点是…

下一代性能怪兽RTX 5090最新规格更新与Blackwell架构解析

据悉&#xff0c;目前各家AIC厂商已经陆续收到NVIDIA的相关资料&#xff0c;RTX 5090、RTX 5080已经正式进入开案阶段&#xff0c;也就是厂商们开始设计各自的产品方案了。不出意外&#xff0c;年初的CES 2025上会看到RTX 5090/5080的发布。 作为NVIDIA的新一代GPU&#xff0c…

2024年健康经济与大数据研讨会(HEBD 2024)2024 Seminar on Health Economics and Big Data

在线投稿&#xff1a;学术会议-学术交流征稿-学术会议在线-艾思科蓝 2024年经济决策与人工智能国际学术会议 &#xff08;EDAI 2024&#xff09;将在2024年11月08-10日在广东省广州市隆重举行。大会邀请来自国内外高等院校、科学研究所、企事业单位的专家、教授、学者、工程师…

理解互联网链路:从本地ISP到Tier 1 ISP运营商

1. 互联网服务提供商&#xff08;ISP&#xff09; 互联网服务提供商&#xff08;ISP&#xff09;是指提供互联网接入服务的公司或组织。它们负责将用户连接到互联网&#xff0c;并提供相关的服务&#xff0c;如电子邮件、网站托管和其他在线服务。ISP可以分为不同的层级&#…

告别转换顾虑,来试试这四款pdf转换器~

各位小伙伴们&#xff0c;大家好&#xff01;今天我来给大家分享几款超级好用的PDF转换工具&#xff0c;无论是工作还是学习&#xff0c;相信这些工具都会给你带来极大的便利&#xff1b;别看PDF文件看似难搞&#xff0c;其实有了这些神器&#xff0c;一切都变得轻松又愉快&…

在线css像素Px到百分比(%)换算器

具体请前往&#xff1a;在线Px转百分比(%)工具--将绝对像素(px)长度单位转换为相对父级元素内尺寸的相对长度单位百分比(%)

PCL GridMinimum获取栅格最低点

目录 一、概述 1.1原理 1.2实现步骤 1.3应用场景 二、代码实现 2.1关键函数 2.1.1 GridMinimum 栅格最低点提取 2.1.2 可视化函数 2.2完整代码 三、实现效果 PCL点云算法汇总及实战案例汇总的目录地址链接&#xff1a; PCL点云算法与项目实战案例汇总&#xff08;长…

新农人的求索:既要种菜,也要种钱

澎湃新闻记者 何惠子 灯下立着一个玻璃瓶&#xff0c;内里空无一物&#xff0c;清晰透亮。 一只手握住瓶身。“就像这个瓶子。前途一片光明&#xff0c;但其实都在瓶子里。” 解晓巍说的是音乐——他曾梦想以此维生。事实上&#xff0c;这也适合描述农业。 在没有任何收入的202…

计算机网络:计算机网络概述 —— 初识计算机网络

文章目录 计算机网络组成部分网络架构协议与标准网络设备网络类型作用实际应用案例 计算机网络 计算机网络是指将多台计算机通过通信设备和通信链路连接起来&#xff0c;以实现数据和信息的交换和共享的技术和系统。它是现代信息社会的基础设施之一&#xff0c;也是互联网的基…

工业现场干扰问题及处理方法

目前&#xff0c;各种干扰在各类工业现场中均存在&#xff0c;所以仪表及控制系统的可靠性直接影响到现代化工业生产装置安全、稳定运行&#xff0c;系统的抗干扰能力是关系到整个系统可靠运行的关键。随着DCS、现场总线技术的应用&#xff0c;被控对象和被测信号往往分布在各个…

Ubuntu启动后第一次需要很久才能启动GTK应用问题

Ubuntu启动后第一次需要很久才能启动GTK应用问题 自从升级了 Ubuntu 之后&#xff0c;设备重启&#xff0c;发现打开 Terminal 、Nautilus 以及其他的GTK 应用都很慢&#xff0c;需要至少一分钟的时间启动。 刚开始也是拿着 journalctl 的异常日志去寻找答案&#xff0c;但是没…

cheese安卓版纯本地离线文字识别插件

目的 cheese自动化平台是一款可以模拟鼠标和键盘操作的自动化工具。它可以帮助用户自动完成一些重复的、繁琐的任务&#xff0c;节省大量人工操作的时间。可以采用Vscode、IDEA编写&#xff0c;支持Java、Python、nodejs、GO、Rust、Lua。cheese也包含图色功能&#xff0c;识别…

山东大学操作系统学习笔记:第3.1讲程序的结构-简单的程序

第3.1讲&#xff1a;程序的结构-简单的程序 可执行文件 & 程序的装入 .rwdata(读写数据段): 存放程序中的含初值常量。这些常量在程序运行可以修改。 零初始化数据段&#xff08;.zidata/.bss - Block Started by Symbol&#xff09;&#xff1a;存放程序中的不含初值&am…

《自控原理》最小相位系统

在复平面右半平面既没有零点&#xff0c;也没有极点的系统&#xff0c;称为最小相位系统&#xff0c;其余均为非最小相位系统。 从知乎看了一篇答案&#xff1a; https://www.zhihu.com/question/24163919 证明过程大概率比较难&#xff0c;我翻了两本自控的教材&#xff0c;…

【中间件】fastDFS的相关知识

一、分布式文件系统 1.1 传统的文件系统 我们在Linux中学习的文件系统就是传统的文件系统&#xff1a; 传统的文件系统格式&#xff1a; ntfs/fat32/ext3/ext4 可以被挂载和卸载&#xff0c;就是一般一个盘可以分成多个盘&#xff0c;每一盘都可以挂载到不同的目录路径中。…

实时语音交互,打造更加智能便捷的应用

随着人工智能和自然语言处理技术的进步&#xff0c;用户对智能化和便捷化应用的需求不断增加。语音交互技术以其直观的语音指令&#xff0c;革新了传统的手动输入方式&#xff0c;简化了用户操作&#xff0c;让应用变得更加易用和高效。 通过语音交互&#xff0c;用户可以在不…