一、前言
关于本人最近的低沉表现,经过一些思考过后,发现了一些问题,包括但不限与对知识的理解只停留在外面,在低效的刷题中渐渐失去了乐趣,一味地变成代码的搬运工。因此打算写一点东西总结自己所学到的东西,或许在理解层面会有些欠缺,在实现层面会有些繁琐,欢迎任何不足之处的指出。但仅以这些文字作一点微弱的干草,请见谅。
二、初见
题目:
倘若用暴力遍历累加的方法,时间复杂度为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;
}
四、后言
此篇文章本来是由于个人无法从理解算法模板中获得乐趣,想根据一种普通的逻辑思维,逐渐推导出算法的面貌,但是由于个人条件的限制,越想越觉得无望,终是南辕北辙。很抱歉匆匆结尾。