AcWing 1230. K倍区间

题目链接

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

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

输入格式

第一行包含两个整数 N 和 K。

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

输出格式

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

数据范围

1≤N,K≤1000001≤N,K≤100000,
1≤Ai≤100000

特别暴力做法O(n^3):

#include <cstdio>
#include <iostream>
#include <cstring>
#include <algorithm>
#include <cmath>

using namespace std ;
using ll = long long ; 
int n , mod ; 
const int N = 1e5 + 8 ; 
int sum[N] ; 
ll res ; 
ll cnt[N] ; 
int main(){
    std::ios::sync_with_stdio(false) ; 
    std::cin.tie(0) ; 
    cin >> n >> mod ;
    int x ; 
    for(int i = 1 ; i <= n ; i ++) {
        cin >> sum[i] ;         
    }
    int res = 0 ; 
    for(int r = 1 ; r <= n ; r ++) {
        for(int l = 1 ; l <= r ; l ++) {  //枚举所有区间
            int ans = 0 ; 
            for(int i = l ; i <= r ; i ++) {
                ans += sum[i] ; 
            }
            if(ans % mod == 0) res ++ ; 
        }
     }
    
    cout << res << '\n' ; 
    return 0 ; 
}

不是那么暴力但是也挺暴力的做法O(n^{2})

#include <cstdio>
#include <iostream>
#include <cstring>
#include <algorithm>
#include <cmath>

using namespace std ;
using ll = long long ; 
int n , mod ; 
const int N = 1e5 + 8 ; 
int sum[N] ; 
ll res ; 
ll cnt[N] ; 
int main(){
    std::ios::sync_with_stdio(false) ; 
    std::cin.tie(0) ; 
    cin >> n >> mod ; 
    int x ;
    for(int i = 1 ; i <= n ; i ++) {
        cin >> x ; 
        sum[i] = sum[i - 1] + x ;         //存个前缀和数组
    }
    int res = 0 ; 
    for(int r = 1 ; r <= n ; r ++) {
        for(int l = 1 ; l <= r ; l ++) {
            if( !((sum[r] - sum[l - 1]) % mod) ) res ++ ; 
        }
    }
    cout << res << '\n' ; 
    return 0 ; 
}

虽然用了前缀和优化,但是也会TLE,无法过掉所有的数据:

 那么如何再降低时间复杂度呢?基于前面前缀和的思想,我们在前缀和里面是为了找到一组满足这个公式的数据( sum[r] - sum[l - 1] ) % k == 0 ,那么根据同余公式我们可以直接化简为找一个sum[r] % k == sum[l - 1] % k 的左右端点。

那么我们在第一层循环右端点r的时候,可以用一个数组存sum[r] % k 的值出现的次数,然后加上即可,我们以样例为例:

/*
     5 2 
     1 2 3  4 5
     1 3 6 10 15        //前缀和
%k : 1 1 0 0  1        //%k以后的值
*/

1:%k后的值是1,之前没有出现过

2:%k后的值是1,之前出现过一次,res ++ ; 

3:%k后的值是0,之前没有出现过

4:%k后的值是0,之前出现过一次,res ++ ;

5:%k后的值是1,之前出现过两次,res += 2 ;

但是这样算,最后的res结果是4,似乎不符合题目要求。原来是因为,我们在计算%k后的值的时候,如果结果是0,那么就说明,从开始到目前为止就已经是一个复合条件的区间了,即[1 , r]就是一个符合条件的区间了,所以最后的结果要加上%k后值为0的所有次数,那么样例中%k后出现了两次结果为0的前缀和数组,那么最后的结果就是res + 2 ,复合答案要求。

优化代码O(n)

#include <cstdio>
#include <iostream>
#include <cstring>
#include <algorithm>
#include <cmath>

using namespace std ;
using ll = long long ; 

int n , mod ; 
const int N = 1e5 + 8 ; 
ll sum[N] ;          //记得要开long long ,不然会爆
ll res ;        
ll cnt[N] ;      
   
int main(){
    std::ios::sync_with_stdio(false) ; 
    std::cin.tie(0) ; 

    cin >> n >> mod ; 
    int x ; 
    for(int i = 1 ; i <= n ;i ++) {
        cin >> x ; 
        sum[i] = sum[i - 1] + x ;       //构造前缀和数组
    } 
    for(int r = 1 ; r <= n ; r ++) {
        res += cnt[sum[r] % mod] ;   //这里一定要先加,然后再更改cnt的值
        cnt[sum[r] % mod ] ++ ;        //用cnt来记录%k后的值出现了几次
    }

    cout << res + cnt[0] << '\n' ;  //+cnt[0]就是加上%k后的值为0的所有组合

    return 0 ; 
}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值