算法————前缀和

一、前言

      关于本人最近的低沉表现,经过一些思考过后,发现了一些问题,包括但不限与对知识的理解只停留在外面,在低效的刷题中渐渐失去了乐趣,一味地变成代码的搬运工。因此打算写一点东西总结自己所学到的东西,或许在理解层面会有些欠缺,在实现层面会有些繁琐,欢迎任何不足之处的指出。但仅以这些文字作一点微弱的干草,请见谅。

二、初见

        

题目:

      倘若用暴力遍历累加的方法,时间复杂度为m*每次的(r-l),最大值为10的10次方,而时间

限制是2s,必然是

      因此需要寻找一种方式去减少时间复杂度。

      作为一个不了解前缀和的人来说,要如何去解决这个问题,首先惯性分析之前暴力遍历的方法。

#include <iostream>

using namespace std;

int n, m;
int a[100010];

int main()
{
	cin >> n >> m;
	for (int i = 1; i <= n; i++) cin >> a[i];
	while (m--)
	{
		int l, r;
		cin >> l >> r;
		int sum = 0;
		for (int i = l; i <= r; i++) sum += a[i];
		cout << sum<<"\n";
	}
	return 0;
}

       

       正确性是必然的,而如何简化成为了问题。

       开销最大,最可能被简化的是这一部分

		int sum = 0;
		for (int i = l; i <= r; i++) sum += a[i];

     

       若每次的l,r之间有重复部分的话,再次的从零开始加造成了额外的计算,因此记录每一步的计算结果,每一步的l,r值,再开始前判断区间是否有重合部分,将重合部分直接加入,于是写出了以下的代码

#include <iostream>

using namespace std;

typedef pair<int, int> PII;

const int N = 100010;
int n, m;
int s[N];
int j[N];
PII point[100010];

int main()
{
	cin >> n >> m;
	for (int i = 1; i <= n; i++) cin >> s[i];
	int cnt = 0;
	while (m--)
	{
		int l, r,sum=0;
		cin >> l >> r;
		int flag = 0,fl,fr,fi;
		for (int i = 0; i < cnt; i++)
		{
			if (l <= point[i].first && r >= point[i].second)
			{
				flag = 1,fl=point[i].first,fr=point[i].second,fi=i;
				break;
			}
		}
		if (flag)
		{
			for (int i = l; i <= fl; i++) sum += s[i];
			for (int i = fr; i <= r; i++) sum += s[i];
			sum += j[fi];
			j[fi] = sum;
		}
		else
		{
			for (int i = l; i <= r; i++) sum += s[i];
			j[cnt++] = sum;
		}
		cout << sum << "\n";
	}
	return 0;
}

      在这段代码中不能简化时间的因素好像有很多,但是我认为最核心的原因究其只有一个,记录的数组不可拆解,无法做到上述所说的“判断区间是否有重合部分,将重合部分直接加入”,只有在一个区间覆盖住之前记录的数组时才能使用记录的数组,而对于这个问题我们有两个选择:1.继续沿着之前的思路去研究如何去拆分数组。(沉没成本思考)         2.利用以获得的“数组无法被拆分,只有覆盖才能被使用”的性质去思考其他解决方案。(经验思考)

       我并不是一个懂得变通的人,如果要我做出选择的话,我只会死脑筋地走下去,在一次又一次的失败下,带着巨大的失望离去。因此我也希望自己能学会第二种思考方法,在尝试之后,并不是一无所获,而是获得对这个事件更深的认识,以便策略的调整,是一种不断以经验填补自己的智慧。

      说了一些题外话,回到正题。在第一种选择无果后,我们转到第二种选择,这个“数组无法被拆分”的性质,又该如何去利用呢?从最好的角度考虑,如果每一次的计算都能用到上一个数组,这样这个l,r的范围是不是越来越大,最后才达到边界,而刚开始则是一个较小的范围,这像是一个数组在成长,或者换一个词-------读入

      我们惊奇的发现,读入其实就是一个数组固定的左边界,而右边界不断向外拓展的过程,在这个过程中,每一步都能使用上一步的数组,在思考再三后,我们写出了

	for (int i = 1; i <= n; i++)
	{
		cin >> s[i];
		s[i] += s[i - 1];
	}

      这四,五行的距离,真的走了很久啊。

三、它像换了个人出现,好久不见

题目:

      

      在这道题目中好像根本就用不上前缀和,题目里根本没提到和或者段处理之类的字眼,如果只根据算法标签思考,或许只会一直在前缀和的胡同里打转(我)。因此我们从题目本身入手,正确性很容易想到,逐个遍历即可。

#include <iostream>

using namespace std;

const int N = 100010;

int n;
int a[N], b[N], c[N],db[N],dc[N];

int main()
{
	cin >> n;
	for (int i = 0; i < n; i++) cin >> a[i];
	for (int i = 0; i < n; i++) cin >> b[i];
	for (int i = 0; i < n; i++) cin >> c[i];
	int cnt = 0;
	for (int i = 0; i < n; i++)
	{
		for (int j = 0; j < n; j++)
		{
			for (int h = 0; h < n; h++)
			{
				if (c[h] > b[j] && b[j] > a[i]) cnt++;
			}
		}
	}
	cout << cnt;
	return 0;
}

         问题在与如何简化,而此段代码重复的部分在于c数组的重复遍历,如果每个数组有序的话,可以从上次遍历结束的位置开始,减少计算量,但是这样浪费了a,b数组的有序性,而我们从c数组的处理上理解到以b[i]基准点的话,右段的连续性减少了计算量,而进而想到左段的连续性也可以利用,就形成了新的

       以b[i]为基准点,再判断a数组和c数组具体小于大于的位置,而数组的有序性,缩减性也让我们想到了可以使用二分和双指针。

      但在此处还有前缀和的方法处理,但是我思考了很久,都没有想出一个合适的方法能够顺其自然地将其引出,于是这里只给出题解,供读者自己理解,如果您有什么思路的话,可以与我分享。

#include <iostream>
#include <cstring>

using namespace std;

const int N = 100010;

int n;
int a[N], b[N], c[N];
int as[N];
int cs[N];
int cnt[N], s[N];

int main()
{
	cin >> n;
	for (int i = 0; i < n; i++) cin >> a[i],a[i]++;
	for (int i = 0; i < n; i++) cin >> b[i],b[i]++;
	for (int i = 0; i < n; i++) cin >> c[i],c[i]++;
	//计算小于b[i]的a数量
	for (int i = 0; i < n; i++) cnt[a[i]]++;
	for (int i = 1; i < N; i++) s[i] += s[i - 1] + cnt[i];
	for (int i = 0; i < n; i++) as[i] = s[b[i] - 1];
	//计算大于b[i]的c数量
	memset(cnt, 0, sizeof cnt);
	memset(s, 0, sizeof s);
	for (int i = 0; i < n; i++) cnt[c[i]]++;
	for (int i = 1; i < N; i++) s[i] += s[i - 1] + cnt[i];
	for (int i = 0; i < n; i++) cs[i] = s[N-1] - s[b[i]];

	long long res = 0;
	for (int i = 0; i < n; i++) {
		res += (long long)as[i] * cs[i];
	}
	cout << res;
	return 0;
}

四、后言

      此篇文章本来是由于个人无法从理解算法模板中获得乐趣,想根据一种普通的逻辑思维,逐渐推导出算法的面貌,但是由于个人条件的限制,越想越觉得无望,终是南辕北辙。很抱歉匆匆结尾。

  • 21
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值