带你深入了解双指针和前缀和的算法思想

前言:最近自己在备考蓝桥杯,如果有和我一样目标的同学,可以一起加油!双指针和前缀和算法都是对于编程竞赛很重要的思维,希望这篇文章能帮助到你!


目录

一、双指针算法

1.以暴力思维开始突破

首先分析题目

理清思路

画图验证思路是否可行

开始打代码

2.开始优化暴力算法代码

1.分析暴力算法的时间复杂度

2.想办法缩短时间

3.再去画图想另一种方法

4.重新打代码

二、前缀和算法

1.画图带大家了解一下前缀和思想

2.一道模板题深入前缀和思想

1.先带大家跑一遍暴力吧

2.前缀和解决该题

 3.二维前缀和

1.画图带你了解二维前缀和

2.前缀和法解题


一、双指针算法

以一道例题为突破口,来进行引入,循序渐进。

题目:最长连续不重复子序列(题目来自AcWing)

给定一个长度为n的整数序列,请找出最长的不包含重复的数的连续区间,输出它的长度

输入格式:第一行包含一个整数n,第二行包含n个整数(均在0--1e5的范围内),表示整数序列。

输出格式:共一行,包含一个整数,表示最长的不包含重复的数的连续区间的长度

数据范围:1<=n<=1e5

输入样例:

5

1 2 2 3 5

输出样例:

3

1.以暴力思维开始突破

对于编程竞赛中的题目,一般都是先根据自己的思路,写出一个暴力算法,再逐步分析,逐步优化代码,上面这道题也不例外。

首先分析题目

找出关键词:对于该题,要求是找出最长的、不包含重复数的、连续的区间

理清思路

大致思路:我们可以想到用两层for循环去遍历数组中每一个子序列,然后再对每一个子序列进行判断,判断每一个子序列中是否有相同元素。可以将判断部分分装成一个check函数

画图验证思路是否可行

判断main函数中所遍历的每一段子序列是否有相同元素的那一部分分装函数的思维是和遍历每一段序列的思维相同的,都是用两层for循环

开始打代码

大致思路理清之后就可以上手写代码了

#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1e5 + 10;
int arr[N];
int n;
bool check(int x, int y)
{
	for (int i = x; i <= y; i++)
	{
		for (int j = x + 1; j <= y; j++)
		{
			if (arr[i] == arr[j] && i != j)return false;//当i=j是也就是都指向最后一个元素arr[y]了
		}
	}
	return true;
}
int main(void)
{
	scanf_s("%d", &n);
	for (int i = 1; i <= n; i++)scanf_s("%d", &arr[i]);
	int res = 0;
	for (int i = 1; i <= n; i++)
	{
		for (int j = i; j <= n; j++)
		{
			if (check(i, j))
			{
				if (res < (j - i + 1))
				{
					res = j - i + 1;
				}
			}
		}
	}
	cout << res << endl;
	return 0;
}

其实可以发现,这个暴力算法中的i和j就相当于两个指针,只是还没进行优化。 

2.开始优化暴力算法代码

1.分析暴力算法的时间复杂度

main函数中两层for循环,然后两层for循环中又有check函数,check函数中也有两层for循环,也就是一共有四层for循环,时间复杂度是O(n的四次方),而n的最大值是1e5,所以该暴力算法的时间复杂度为O(1e20),很明显远远超过了1e9,时间远远超限。

2.想办法缩短时间

如果想通过所有测试用例,时间复杂度不能超过1e9,而两层for循环的时间复杂度就是1e10,说明优化之后的代码只能有1个for循环。这就和暴力的思路不太一样了,需要我们另辟蹊径了。

3.再去画图想另一种方法

注:题目中说是一段整数序列,我们要清楚,整数序列指的就是单调不减序列。

4.重新打代码

#include<iostream>
#include<stdio.h>
#include<algorithm>
using namespace std;
const int N = 1e5 + 10;
int arr[N];
int cnt[N];//记录每个数字出现的次数
int n;
int main(void)
{
	scanf_s("%d", &n);
	for (int i = 1; i <= n; i++)scanf_s("%d", &arr[i]);
	int res = 0;
	for (int i = 1,j = 1; i <= n; i++)
	{
		cnt[arr[i]]++;
		while (cnt[arr[i]] > 1)//while循环作用:利用j将重复数字之前的数字次数清空
		{
			cnt[arr[j]]--;j++;//这两条语句的顺序是不可以颠倒的 不懂可以画图分析一下
		}
		int tmp = i - j + 1;
		if (tmp > res)res = tmp;
	}
	cout << res << endl;
	return 0;
}

其实看了我画的图然后再自己试着画图分析一下,可以发现,i和j相当于快慢指针,另外还有对撞指针,都是双指针的种类。

总之:双指针的核心思想:优化暴力!(利用了某些单调的性质/其他性质)

二、前缀和算法

前缀和----数组前n项和

作用:快速求出数组中一段区间和

int a[N];

S[i]=a[1]+a[2]+...+a[i-1]+a[i];

1.画图带大家了解一下前缀和思想

2.一道模板题深入前缀和思想

题目:前缀和

输入一个长度为n的序列

接下来再输入m个询问,每个询问输入一对l,r

对于每个询问,输出原序列中从第l个数到第r个数的和

输入格式:第一行包含两个整数n和m,第二行包含n个整数,表示整数数列,接下来m行,每行包含两个整数l和r,表示一个询问的区间范围。

输出格式:共m行,每行输出一个询问的结果

数据范围:1<=l<=r<=n;1<=n,m<=100000;-1000<=数列中元素的值<=1000

输入样例:

5 3

2 1 3 6 4

1 2 

1 3

2 4

输出样例:

3

6

10

1.先带大家跑一遍暴力吧

#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1e5 + 10;
int arr[N];
int main(void)
{
	int n, m;
	scanf_s("%d%d", &n, &m);
	for (int i = 1; i <= n; i++)
	{
		scanf_s("%d", &arr[i]);
	}
	while (m--)
	{
		int l, r;
		scanf_s("%d%d", &l, &r);
		int s = 0;
		for (int i = l; i <= r; i++)
		{
			s += arr[i];
		}
		cout << s << endl;
	}
	return 0;
}

 放到官网中跑一下,很明显就知道超时了。

还是简单的分析一下时间复杂度:m的最大值是1e5,l的最小值是1,r的最大值是n,所以for循环时间复杂度也是1e5,所以该程序的时间复杂度为1e10,很显然超时了。

2.前缀和解决该题

#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1e5 + 10;
int arr[N];
int sum[N];
int main(void)
{
	int n, m;
	scanf_s("%d%d", &n, &m);
	for (int i = 1; i <= n; i++)
	{
		scanf_s("%d", &arr[i]);
		sum[i] = sum[i - 1] + arr[i];
	}
	while (m--)
	{
		int l, r;
		scanf_s("%d%d", &l, &r);
		int end = sum[r] - sum[l - 1];
		cout << end << endl;
	}
	return 0;
}

再分析这段已经优化好的代码:时间复杂度最大就是1e5,里面的for循环就直接省略了。这就是优化的魅力,也是算法的魅力所在。

 3.二维前缀和

我们还是拿一道题开始说起

题目:子矩阵的和

输入一个n行m列的整数矩阵,再输入q个询问,每个询问包含四个整数x1,y1,x2,y2,表示一个子矩阵的左上角和右下角的坐标。对于每个询问输出子矩阵中所有数的和

输入格式:第一行包含三个整数n,m,q。接下来n行,每行包含m个整数,表示整数矩阵。接下来q行,每行包含四个整数x1,y1,x2,y2,表示一组询问

输出格式:共q行,每行输出一个询问的结果

数据范围:1<=n,m<=1000;1<=q<=200000;1<=x1<=x2<=n;1<=y1<=y2<=m;-1000<=矩阵内元素的值<=1000

1.画图带你了解二维前缀和

 最后的公式必须记住,理解了之后就不难记了!!!

2.前缀和法解题

#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1e4 + 10;
int n, m, k;
int arr[N][N], s[N][N];
int main(void)
{
	cin >> n >> m >> k;
	for (int i = 1; i <= n; i++)
	{
		for (int j = 1; j <= m; j++)
		{
			scanf_s("%d", &arr[i][j]);
			s[i][j] = arr[i][j] + s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1];
		}
	}
	while (k--)
	{
		int x1, x2, y1, y2;
		scanf_s("%d%d%d%d", &x1, &x2, &y1, &y2);
		printf("%d\n", s[x2][y2] - s[x2][y1 - 1] - s[x1 - 1][y2] + s[x1 - 1][y1 - 1]);
	}
	return 0;
}

 

 下一篇文章带你们刷题,关于双指针和前缀和算法的题,感兴趣的可以看一看!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

写不出bug的小李

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值