【第八届蓝桥杯省赛C++B组】K倍区间【所用算法:前缀和】

给定一个长度为 N 的数列,A1,A2,…AN,如果其中一段连续的子序列 Ai,Ai+1,…Aj 之和是 K 的倍数,我们就称这个区间 [i,j] 是 K 倍区间。

你能求出数列中总共有多少个 K 倍区间吗?

输入格式
第一行包含两个整数 N 和 K。

以下 N 行每行包含一个整数 Ai。

输出格式
输出一个整数,代表 K 倍区间的数目。

数据范围
1≤N,K≤100000,
1≤Ai≤100000
输入样例:
5 2
1
2
3
4
5
输出样例:
6

因为这个题的做法大家基本上都明白是利用的前列和,所以我在此就不对前列和这个知识点多加赘述

因为我们发现单纯的利用前缀和,已经不能解决我们的问题,因为时间会超限
所以我们必须将它的时间复杂度控制在(On)
所以我们直接步入正题,怎么优化前列和
我会先给出我的代码,然后再给出我的解释
首先给出我的代码

#include <iostream>

using namespace std;

typedef long long ll;

const int N = 100010;

int n, k;
ll s[N], cnt[N];

int main(){
    cin >> n >> k;
    for (int i = 1; i <= n; i ++ ) scanf("%d", &s[i]), s[i] = (s[i] + s[i -1]) % k;

    ll res = 0;
    cnt[0] = 1;
    for (int i = 1; i <= n; i ++ ){
        res += cnt[s[i]];
        cnt[s[i]] ++;

    }

    cout << res << endl;

    return 0;
}

下面时我对代码的分析

知识点前列和大家都学的很好,所以我这里从前列和后开始说起
先给出我的样例:
n = 9,k = 4, a[1~9]:1, 2, 3, 4, 5, 6, 7, 8, 9//这是输入的数据
s[10] = {0, 1, 3, 6, 10, 15, 21, 28, 36, 45};//这是前列和
s1[10] = {0, 1, 3, 2, 2, 3, 1, 0, 0, 1};//这是每一项前列和对k = 4取模运算的结果


我们最开始的解题思路是这样的:

for (int R = 1; R <= 9; R ++ )//我用R代表区间右端点,(用i不好看*……*)
    for (int L = 1; L <= R; L ++ )//L表示区间的左端点
        if ((s[R] - s[L - 1]) % k == 0)//这一步就是判断这个区间段元素之和是否是k的倍数 
            res ++;//如果是就+1

//这一个代码大家都了解,就是最朴素的解题思想


//然后我们开始进行第二步:转化1
我们用r替换R,l替换L - 1(等价于L = l + 1)

for (int r = 1; r <= 9; r ++ )
    for (int l = 0; l <= r - 1; l ++ )
        if ((s[r] - s[l]) % k == 0)
            res ++;

//这个应该是高中的换元思想,这是一个等价的变形


//然后我们开始进行第三步:转化2
因为if ((s[r] - s[l]) % k == 0)这一步是判断区间[l+1, r]这个区间的和是否是k的倍数
则,if ((s[r] - s[l]) % k == 0)与if(s[r] % k == s[l] % k)是等价的
我们可以再次进行变形:
给每个s[] 去模上k,这就有了我们的s1[],记录s[]取模后的值。


那么我们的公式就可以在进行变形了
【变形开始】

for (int r = 1; r <= 9; r ++ )
    for (int l = 0; l <= r - 1; l ++ )
        if ((s1[r] == s1[l]))
            res ++;

但是当代码简化到这里的时候,我们突然会有一种代码变得很罗嗦的感觉,这是为什么呢
啊!我们找了一圈,就是在找这个(s1[r] == s1[l]),就是找让这两个东东,他们的值相等
法外狂徒张三表示很淦!!! 张三都看不下去了(&^&)
为了守护我们张三同志,我们必须把它进行优化.

好不就是用了来来回回的找(s1[r] == s1[l])吗,那我就打表找规律。
因为我们看的是(s1[r] == s1[l]),我们就将数据抽出来专门看这个
已知s1[10] = {0, 1, 3, 2, 2, 3, 1, 0, 0, 1};,我们的代码进行的就是将他循环两边,并且在找
(s1[r] == s1[l])。那我们可以这样淦!
进行枚举的时候每次都判断一次太麻烦了,我们把if条件成立的数据给拿出来,单看看!
比如我们就拿s1[] = 1来看, 一共有三个1
好!

int ss[4] = {0,1,1,1}
for (int r = 1; r <= 3; r ++ )
    for (int l = 0; l <= 3 - 1; l ++ )
        if (ss[r] == ss[l])
            res ++;

我们就得到了一个公式,令number为s1中的某个元素的个数,则
res = number * (number - 1) / 2

那么我们只要将每个元素的个数都给找出来,然后套用一下这个公式就OK了
但是一定要注意,找元素0的个数时,千万别忘记+1,为啥,因为s[0] = 0;
千万不要忽略,想想看我们用了多少次s[L == 1],及s[l == 0]运算,可不能提了裤子不认人啦!


这里我们简化的太厉害了,简直就是直击本质呀,但是简化的太厉害了,都过了头了,我们要是
还要再求一遍每个元素的个数,那就舍近求远了

所以下面我就分析一下中间那块代码

LL res = 0;//res用来计数
    cnt[0] = 1;//就是我们前面讲的,一定要先给cnt[0] 加上一个1,因为cnt时来计数的
                //s[0] = 0就是一个
    for (int i = 1; i <= n; i ++ )
    {
        res += cnt[s[i] % k];
        cnt[s[i] % k] ++ ;
        /*
        这里像不像我们上面的res = number * (number - 1) / 2
        只不过它是一个大杂烩,将所有的数都给放到一起加了。
        其实和我们上面的解释差不多
        如果我们将相同元素抽出来看(res就是相当于给cnt[]重新排个序加起来。
        然后再全部加起来。
        res负责将数据全部加起来
        而cnt[],你看像不像是把我们分析里的res的乘法变成了最先给出的代码里的加法
        我们重新拍个顺序看一下
        当s[i] % k == 0 时, cnt[s[i] % k]的变化为:1->2->3
        当s[i] % k == 1 时, cnt[s[i] % k]的变化为:0->1->2
        当s[i] % k == 2 时, cnt[s[i] % k]的变化为:0->1
        当s[i] % k == 2 时, cnt[s[i] % k]的变化为:0->1
        除了顺序变了以外,cnt的值的变化就是这个样子,没毛病
        然后我们交给res都给加起来,
        不就是我们分析的里面的:
        res1 = 3*(3 - 1) / 2 = 3;
        res2 = 3*(3 - 1) / 2 = 3;
        res3 = 2*(2 - 1) / 2 = 1;
        res4 = 2*(2 - 1) / 2 = 1;
        res = res1 + res2 + res3 + res4 = 3 + 3 + 1 + 1 = 8
        */
        
        /*
        如果质疑这个结论的正确定,或者还是没有理解,我们再来模拟一次,
        模拟一次例题给的样例
        n = 5, k = 2;
        a[6] ={1, 2, 3, 4, 5};
        s[6] =  {0, 1, 3, 6, 10, 15};
        s1[6] = {0, 1, 1, 0, 0, 1}
        
        再次验证一下我的结论
        当s[i] % k == 0 时, cnt[s[i] % k]的变化为:0->1->2
        当s[i] % k == 0 时, cnt[s[i] % k]的变化为:0->1->2
        res1 = 3*(3 - 1) / 2 = 3;
        res2 = 3*(3 - 1) / 2 = 3;
        res = res1 + res2 = 3 + 3 = 6;
        
        完美!!!
        */
    }
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值