1590. 使数组和能被 P 整除
思路
我们先不考虑整除的问题,把数组里的每一个元素先模
p
p
p 再求和,得到一个结果
s
u
m
sum
sum ,如果
s
u
m
sum
sum 能被
p
p
p 整除,那就不用下一步操作了;如果不能被
p
p
p 整除,那相当于就是,在模过
p
p
p 的数组里面找一个最小连续子串,使得
s
u
m
sum
sum 刚好被
p
p
p 整除。
那怎么得到这个子串呢?最简单的就是枚举。但是一看数据量,悬。一试果然TLE。
TLE代码
class Solution {
public:
int minSubarray(vector<int>& nums, int p) {
int n=nums.size();
int res=n;
long long sum=0;
for(int i=0;i<n;i++){
nums[i]=nums[i]%p;
sum+=nums[i];
}
if(sum%p==0) return 0;//满足要求直接返回0
for(int i=0;i<n;i++){
long long ans=sum;//表示去掉这部分子串后的数组之和
for(int j=i;j<n;j++){
ans-=nums[j];
if(ans%p==0){
res=min(res,j-i+1);
}
}
}
return res==n?-1:res;//不允许全部移除,判断一下
}
};
优化AC
还是那句话,当你出现超时的时候,考虑换个方法或者存储中间值。
能不能存个中间值呢?这题显然不太能,因为循环并不是为了计算,而是为了枚举所有情况,就算提前计算一个前缀和,还是避免不了二层循环。既然如此,一定有方法快速找到适合的子串,而不是挨个枚举。考虑到题目引入了 “模” 这一因素,肯定要涉及我讨厌的数论了,看了题解果然如此。
我们引入一条定理,具体证明可看官解
(
y
−
z
)
m
o
d
p
=
x
等价于
z
m
o
d
p
=
(
y
−
x
)
m
o
d
p
(y-z)\,mod\,p=x\,等价于\,z\,mod\,p=(y-x)mod\,p
(y−z)modp=x等价于zmodp=(y−x)modp 仔细观察这个公式,再结合题意,我们不难发现,这里的
p
p
p 与题设对应,
x
x
x 是整个数组的和模
p
p
p 的余数,那么
y
y
y 和
z
z
z 分别对应什么呢?结合上面 “快速找到符合条件的子串” 的需求,是否这个就是快速找到子串的判断条件呢?对的,
y
y
y 和
z
z
z分别是两个不同位置的前缀和。假设当前位置的前缀和为
y
y
y ,如果存在一个位置的前缀和为
z
z
z ,使得
z
m
o
d
p
=
(
y
−
x
)
m
o
d
p
z\,mod\,p=(y-x)mod\,p
zmodp=(y−x)modp,那这两个位置之间的子数组就是满足条件的子数组。那么我们要怎么找到这个
z
z
z 呢?依然有等号,即相等的值,那肯定用哈希表就可以快速查找。哈希表以
z
m
o
d
p
z\,mod\,p
zmodp 为key,每次记录这个值出现的最靠后的位置,这样,当下一次这个值出现的时候,两个位置相减,得到的就是最短子串的长度。
class Solution {
public:
int minSubarray(vector<int>& nums, int p) {
int n=nums.size();
int res=n;
int x=0;
map<int,int> mp;
for(auto num:nums){//求整个数组模p的结果
x=(x+num)%p;
}
if(x==0) return 0;//满足条件直接返回
int sum=0;//前缀模p的值
mp[0]=0;//需要特殊处理0,不然当子集起点是0的时候会出错
for(int i=1;i<=n;i++){
sum=(sum+nums[i-1])%p;
int ans=(sum-x+p)%p;//等号右边部分,加p是为了防止负数
if(mp.count(ans)){//如果存在一个位置,使得等式成立,那二者之间的子集就是符合条件的
res=min(res,i-mp[ans]);//更新最短子集
cout<<ans<<" "<<i<<" "<<mp[ans]<<endl;
}
mp[sum]=i;
}
return res==n?-1:res;
}
};