洛谷题解P3131
[USACO16JAN]Subsequences Summing to Sevens S
题目描述
Farmer John’s N N N cows are standing in a row, as they have a tendency to do from time to time. Each cow is labeled with a distinct integer ID number so FJ can tell them apart. FJ would like to take a photo of a contiguous group of cows but, due to a traumatic childhood incident involving the numbers 1 … 6 1 \ldots 6 1…6, he only wants to take a picture of a group of cows if their IDs add up to a multiple of 7.
Please help FJ determine the size of the largest group he can photograph.
给你n个数,分别是a[1],a[2],…,a[n]。求一个最长的区间[x,y],使得区间中的数(a[x],a[x+1],a[x+2],…,a[y-1],a[y])的和能被7整除。输出区间长度。若没有符合要求的区间,输出0。
输入格式
The first line of input contains N N N ( 1 ≤ N ≤ 50 , 000 1 \leq N \leq 50,000 1≤N≤50,000). The next N N N
lines each contain the N N N integer IDs of the cows (all are in the range
0 … 1 , 000 , 000 0 \ldots 1,000,000 0…1,000,000).
输出格式
Please output the number of cows in the largest consecutive group whose IDs sum
to a multiple of 7. If no such group exists, output 0.
样例 #1
样例输入 #1
7
3
5
1
6
2
14
10
样例输出 #1
5
提示
In this example, 5+1+6+2+14 = 28.
以下为题解部分
题目要求,对于给定的数组a[n]
,要求出最长区间[x,y]
的和,使得这个区间内所有数的和是7的倍数。
由于要频繁地计算数组在某些区间的和,这里我们首先采用的处理是求这个数组对应的 前缀和 。这里可以先学习一下相关的知识:大佬的前缀和帖子。
我们如果仅仅利用求出的前缀和数组,然后暴力地遍历前缀和数组,发现复杂度过高!就像下面的代码片段:
int main(){
cin>>n;
for(i=1;i<=n;i++){
cin>>a[i];
sum[i]=sum[i-1]+a[i];
}
//然后遍历sum数组,暴力可能会超时的
for(i=0;i+1<n;i++){//这里为什么i=0?
//因为sum[0]=0,而sum[2]-sum[0]=a[1]+a[2],区间[1,2]的长度为2
for(j=(i+1)+1;j<=n;j++){//对于题目要求的区间[x,y],要满足 x<y,即i+1<j<=n
if((sum[j]-sum[i])%7==0){
length=max(j-i,length);
//注意要理解这里为什么区间长度是j-(i+1)+1=j-i,要紧扣前缀和的定义!
}
}
}
cout<<length<<endl;
return 0;
}
既然这样暴力会超时,那么我们有什么办法可以优化呢?
首先这个方法的困难之处在于求出sum[]
数组中的某个最大区间[x,y]
,使得(sum[y]-sum[x])%7==0
成立。要解决这个办法,我们要注意到,两数相减的结果为7的倍数,则这两个数mod7后的余数相同。这里简单介绍一下 同余定理。
同余定理:数论中的重要概念。给定一个正整数m,如果两个整数a和b满足a-b能够被m整除,即(a-b)/m得到一个整数,那么就称整数a与b对模m同余,记作a≡b(mod m)。对模m同余是整数的一个等价关系。
因此,我们可以把前缀和数组的每一项mod7。对sum[]
数组的每一项mod7以后,如果sum[x]==sum[y]
,这区间[x+1,y]
内的和恰好为7的倍数。紧接着,我们索要做的便是求出某个余数第一次出现到最后一次出现的最大区间。
在这里,我们借助两个数组 int first[7],last[7];
来记录这些余数第一次出现的位置和最后一次出现位置。
int main(){
cin>>n;
for(i=1;i<=n;i++){
cin>>a;
s[i]=(s[i-1]+a)%7;
}
for(i=0;i<=n;i++)
last[s[i]]=i;
for(i=n;i>=0;i++)
first[s[i]]=i;
for(i=0;i<=6;i++)
length=(last[i]-first[i])>length?(last[i]-first[i]):length;
cout<<length<<endl;
return 0;
}
这里求解某个余数第一次出现的位置和最后一次出现的位置的方法很有意思:
我们用i从前向后遍历,用s[i]
的值当作last[]
的下标索引,即为:last[s[i]=i;
由于i不断向后遍历推进,所以last[]数组的值不断被更新,所以当循环结束后last[]
数组的值便是余数最后一次出现的位置。同理,我们从后向前遍历,用first[]
数组记录余数第一次出现的位置,当循环结束后,first[]
的值即为余数第一次出现的位置。
对于上面的代码段,我们要注意在求解last数组和first数组的时候,要对端点特殊考虑。
数组的下标索引 | [0] | [1] | [2] | [3] |
---|---|---|---|---|
输入的原数组,a[] | 0 | 1 | 6 | 2 |
最初的前缀和,sum[] | 0 | 1 | 7 | 9 |
mod7后的数组,s[] | 0 | 1 | 0 | 2 |
注意到上表中s[2]==s[0]==0
,意味着 余数"0"第一次出现必定在s[0]的位置:0。
在理解了s[0]=0的基础上,我们可以继续优化一下上述代码
int last[7],first[7]={0,-1,-1,-1,-1,-1,-1};
int main(){
cin>>n;
for(i=1;i<=n;i++){
cin>>a;
s[i]=(s[i-1]+a)%7;
last[s[i]]=i;//这里,我们从前向后遍历,last[]的值不断更新
if(first[s[i]]==-1)first[s[i]]=i;//这一句想想问什么
}
first[0]=0;
for(i=0;i<7;i++)
if(first[i] != -1)
ans = (last[i]-first[i]>ans) ? (last[i]-first[i]) : ans;
cout << ans << endl;
return 0;
}
这里我们重新引入两个数组first[]和last[]。显然易知,求解last[]数组的值,我们要从前向后遍历,因此last[]的求解几乎没有变化。但是first[]数组的求解就麻烦了一些。
对与first[]数组的求解,要么采取从后向前遍历的方法,就如我们之前所讲。如果非得要从前向后遍历数组的同时求解first[]的值,我们需要判断一下,某一余数是不是第一次出现。如果某个余数s[i]是第一次出现,那么我们就把该余数s[i]对应的位置i存储到first[i]中。
因此,我们可以设置这样的数组:
int first[7]={-1,-1,-1,-1,-1,-1,-1};//first[0]待会特别讨论
我们在从前向后遍历的同时,判断first[i]==-1
是否成立。如果成立,则表明余数s[i]是第一次出现,那么我们就把余数s[i]的位置i更新到first数组里。
if(first[s[i]]==-1)first[s[i]]=i;
然后就是对first[0]的处理。
数组的下标索引 | [0] | [1] | [2] | [3] |
---|---|---|---|---|
输入的原数组,a[] | 0 | 1 | 6 | 2 |
最初的前缀和,sum[] | 0 | 1 | 7 | 9 |
同余后的前缀和,s[]%7 | 0 | 1 | 0 | 2 |
借助这个例子,我们再次重复一遍:余数0第一次出现的位置一定是0,即为first[0]==0恒成立。因此,我们可以用0初始化first[0]。
int first[7]={-1,-1,-1,-1,-1,-1,-1};//改进前
for(){...}
first[0]=0;
for(){ }//寻找最长区间
int first[7]={0,-1,-1,-1,-1,-1,-1};//改进后
for(){...}
for(){ }//寻找最长区间
因此,完整的代码为
AC代码:
#include<iostream>
using namespace std;
int main() {
int a, b[50001] = {0}, n, i, ans = 0;
int first[7] = {0, -1, -1, -1, -1, -1, -1}, last[7]={0};
cin >> n;
for (i = 1; i <= n; i++) {
cin >> a;
b[i] = (a + b[i-1]) % 7;
if (first[b[i]] == -1)first[b[i]] = i;
last[b[i]] = i;
}
for (i = 0; i < 7; i++)
if (first[i] != -1)
ans = (last[i] - first[i] > ans) ? (last[i] - first[i]) : ans;
cout << ans << endl;
return 0;
}
end!