一.算法题干
你的好友是一位健身爱好者。前段日子,他给自己制定了一份健身计划。现在想请你帮他评估一下这份计划是否合理。
他会有一份计划消耗的卡路里表,其中calories[i]
给出了你的这位好友在第i
天需要消耗的卡路里总量。
为了更好地评估这份计划,对于卡路里表中的每一天,你都需要计算他 「这一天以及之后的连续几天」 (共k
天)内消耗的总卡路里T
:
如果T < lower
,那么这份计划相对糟糕,并失去1分;
如果T > upper
,那么这份计划相对优秀,并获得1分;
否则,这份计划普普通通,分值不做变动。
请返回统计完所有calories.length
天后得到的总分作为评估结果。
注意:总分可能是负数。
二.解题思路
这道题我一开始的解题思路是:对数组中每一个元素,都取连续的k个数进行求和,然后进行剩余操作,若最后剩余不足k个数则结束循环。具体代码如下:
int dietPlanPerformance(vector<int>& c, int k, int lower, int upper) {
int sum=0;
for(int i=0;i<c.size()&&i+k-1<c.size();++i)
{
int ts=0;
for(int j=0;j<k;++j)
{
ts+=c[i+j];
}
if(ts<lower) --sum;
else if(ts>upper) ++sum;
}
return sum;
}
但是这个代码提交上去后,通过了26个测试样例,仅有一个样例没有通过,具体症状如下图所示。
可以看到,唯一没有通过的那个样子是专门卡时间的,这说明我的算法复杂度还不够低。我算了一次,针对我的算法,T(n)=O(n^2)
,在数组长度和k的数值给的较大时,时间表现并不好。所以我就考虑换了一种方法。
在新的方法中,我利用k这个不变量,维护了一个固定长度的滑动窗口,在数组上进行滑动,每次移动即“左减右加”,也就是将当前窗口的局部和减去左边“滑出”的值,加上右边“滑入”的值,以得到新的局部和。这种思路就避免了针对每个起点的遍历操作,降低了时间复杂度。
三.实现代码
int dietPlanPerformance(vector<int> &c,int k,int lower,int upper) {
int sum=0,ts=0;
for(int i=0;i<k;++i) tc+=c[i];
if(ts<lower) --sum;
else if(ts>upper) ++sum;
for(int i=1;i<c.size()&&i+k-1<c.size();++i)
{
ts=ts-c[i-1]+c[i+k-1];
if(ts<lower) --sum;
else if(ts>upper) ++sum;
}
return sum;
}
四.对比分析
int dietPlanPerformance(vector<int>& calories, int k, int lower, int upper) {
int n = calories.size();
vector<long long> sums(n + 1, 0);
for (int i = 0; i < n; i++) sums[i + 1] = sums[i] + calories[i];
long long points = 0;
for (int i = 0; i + k <= n; i++) {
long long sum = sums[i + k] - sums[i];
if (sum < lower) points--;
if (sum > upper) points++;
}
return points;
}
从这段代码中,我主要学习到了两点:
第一点是语法层面的,我知道了可以通过指定两个参数来构造一个具有固定长度的vector
。其中,第一个参数是初始元素的个数,第二个参数是初始元素值。
第二点是技巧层面的。该代码当中巧妙地使用到了前缀和的思想。所谓的前缀和,就是维护一个大小为n+1的数组,这个数组中第0个元素的值为0,第i个元素的值是原始数组从开头到第i-1个元素的累加和。这个数组的应用范围很广,只要是涉及到局部范围求和,且左右端点的变化是相对有规律的,那么就可以使用前缀和来尝试进行解决该类问题。