模运算规则 避免数值溢出
leetcode1018. 可被 5 整除的二进制前缀
给定由若干 0 和 1 组成的数组 A。我们定义 N_i:从 A[0] 到 A[i] 的第 i 个子数组被解释为一个二进制数(从最高有效位到最低有效位)。返回布尔值列表 answer,只有当 N_i 可以被 5 整除时,答案 answer[i] 为 true,否则为 false。
解题思路
tmp = (tmp << 1) + A[i]; 考虑到数组A可能很长,如果每次都保留 tmp
的值,则可能导致溢出。由于只需要知道每个 tmp 是否可以被5整除,因此在计算过程中只需要保留余数即可。
根据下面公式1可推出,判断tmp%5相当于判断
(上一个tmp%5的余数+A[i])%5,所以每次保存上一个数模上5的余数即可.
这题其实涉及比较基础的数学知识——模运算法则,即除了除法外,几乎常见的运算都符合某种类似分配律的运算律:
- (a + b) % p = (a % p + b % p) % p
- (a - b) % p = (a % p - b % p) % p
- (a * b) % p = (a % p * b % p) % p
- (a^b) % p = ((a % p)^b) % p
题解已经隐含了1和3的证明,剩下两个的证明也十分类似。本题的推演实际涉及加法和乘法(1和3),我们简化一下,假设我们想求2^n对5的模(即2 n% 5),如果n很大,我们是无法通过计算出2n,再去取模的。那么依律3,我们可以先算r = (2n-1) % 5,再将结果r带入(r * 2) % 5;那么如何计算r呢,已经发现了吧——递归(递归(递归)…)。甚至依律3,我们还可以对2^n进行 2(n/2) * 2(n/2) 这样的拆分,以此实现类似快速幂的求模方式。
类似的求模技巧虽然很简单,但是作为某些难题的组成部分,经常是容易被忽视的,比如律1律3经常用在字符串匹配的rolling-hash算法当中,想计算一个整形的hash值,而小写文字有26种可能取值,指数幂26n很容易就会超过值域,即便是长整形也需要不断取模才能使得hash值有意义——当我们忽略碰撞,模空间内的值就会成为可靠的校验依据。
class Solution {
public:
//考虑到数组 A 可能很长,如果每次都保留 N_i的值,则可能导致溢出。
//由于只需要知道每个 N_i是否可以被5整除,因此在计算过程中只需要保留余数即可。
vector<bool> prefixesDivBy5(vector<int>& A) {
int n = A.size();
vector<bool> ans;
int prefix = 0;
for(int i = 0; i < n; i++){
prefix = ((prefix << 1) + A[i]) % 5; //(a + b) % p = (a % p + b % p) % p,只存余数来判断就不会数值溢出
ans.push_back(prefix == 0);
}
return ans;
}
};
补充
模运算与基本四则运算有些相似,但是除法例外。其规则如下:
(a + b) % p = (a % p + b % p) % p (1)
(a – b) % p = (a % p – b % p) % p (2)
(a * b) % p = (a % p * b % p) % p (3)
(a^b) % p = ((a % p)^b) % p (4)
结合律:
((a+b) % p + c) % p = (a + (b+c) % p) % p (5)
((ab) % p * c)% p = (a * (bc) % p) % p (6)
交换律:
(a + b) % p = (b+a) % p (7)
(a * b) % p = (b * a) % p (8)
分配律:
((a +b)% p * c) % p = ((a * c) % p + (b * c) % p) % p (9)
重要定理:
若a≡b (% p),则对于任意的c,都有(a + c) ≡ (b + c) (%p);(10)
若a≡b (% p),则对于任意的c,都有(a * c) ≡ (b * c) (%p);(11)
若a≡b (% p),c≡d (% p),则 (a + c) ≡ (b + d) (%p),(a – c) ≡ (b – d) (%p),
(a * c) ≡ (b * d) (%p),(a / c) ≡ (b / d) (%p); (12)
leetcode1128. 等价多米诺骨牌对的数量
给你一个由一些多米诺骨牌组成的列表 dominoes。
如果其中某一张多米诺骨牌可以通过旋转 0 度或 180 度得到另一张多米诺骨牌,我们就认为这两张牌是等价的。
形式上,dominoes[i] = [a, b] 和 dominoes[j] = [c, d] 等价的前提是 a == c 且 b == d,或是 a == d 且 b == c。
在 0 <= i < j < dominoes.length 的前提下,找出满足 dominoes[i] 和 dominoes[j] 等价的骨牌对 (i, j) 的数量。
法1. 数组
由于转换出来的数的范围固定为 [11,99],我们可以直接使用等长数组来进行计数。
并且 每个两位数都按照 小的在前,大的在后 合并
对于数量为 n 的骨牌,其对数等于 1 + 2 + 3+ 4 + … + (n - 1) 因此可以采用边遍历边累加的方式。
class Solution {
public:
//二元组+(排序后)计数
int numEquivDominoPairs(vector<vector<int>>& dominoes) {
vector<int> nums(100, 0);//两个数合并成两位数(11~99)不会超过89个
int cnt = 0;
for(auto d : dominoes){
int val = d[0] > d[1] ? 10 * d[1] + d[0] : 10 * d[0] + d[1];//小的数在前面,下标为算出的两位数
cnt += nums[val];//对于数量为 n 的骨牌,其对数等于 1 + 2 + 3 + 4 + ... + (n - 1)
nums[val]++;
}
return cnt;
}
};
法2. 哈希表+组合数
1.哈希表存 组成的两位数 : 出现次数;
2.组成两位数(统一小的在前,大的在后)
3.找到重复次数>=2的value,计算组合数(相当于n个里取两个,求组合数)
class Solution {
public:
//哈希表+组合数
int numEquivDominoPairs(vector<vector<int>>& dominoes) {
unordered_map<int, int> hashmap;//组成的两位数:出现的次数
int cnt = 0;
for(auto d : dominoes){
int val = d[0] < d[1] ? 10*d[0]+d[1] : 10*d[1]+d[0];//小的在前,组成两位数
if(hashmap.count(val)){
hashmap[val]++;
}else{
hashmap.insert(pair<int, int>(val, 1));
}
}
//取出个数>=2的值,按组合数求有多少对
for(auto [k, v] : hashmap){
if(v >= 2){
cnt += v * (v - 1) / 2;
}
}
return cnt;
}
};
补充知识
- 排列:
从n个不bai同元素中取出dum(m≤n)个元素的zhi所有排列的个dao数,叫做从n个不同zhuan元素中取出m个元素的排列数,用符号
A(n,m)表示。
- 组合:
从n个不同元素中,任取m(m≤n)个元素并成一组,叫做从n个不同元素中取出m个元素的一个组合;从n个不同元素中取出m(m≤n)个元素的所有组合的个数,叫做从n个不同元素中取出m个元素的组合数。用符号 C(n,m) 表示。
注意:
排列就是指从给定个数的元素中取出指定个数的元素进行排序。组合则是指从给定个数的元素中仅仅取出指定个数的元素,不考虑排序。
两数之和
leetcode989. 数组形式的整数加法
对于非负整数 X 而言,X 的数组形式是每位数字按从左到右的顺序形成的数组。例如,如果 X = 1231,那么其数组形为[1,2,3,1]。
给定非负整数 X 的数组形式 A,返回整数 X+K 的数组形式。
思路
这道题和 2. 两数相加 一样,换汤不换药。
只要记住这个公式,不管两个数是列表形式,还是数组形式,都不会写错!
- <公式>
当前位 = (A 的当前位 + B 的当前位 + 进位carry) % 10 注意,AB两数都加完后,最后判断一下进位 carry,
进位不为 0 的话加在前面。
- <加法模板>
while ( A 没完 || B 没完){
A 的当前位
B 的当前位
和 = A 的当前位 + B 的当前位 + 进位carry
当前位 = 和 % 10;
进位 = 和 / 10;
}
判断还有进位吗
顺便附上字符串比较的模板。比如这道谷歌高频题:809. 情感丰富的文字
- <比较模板>
while( A 没完 && B 没完){
A 的当前字符
B 的当前字符
A 的当前字符长度
B 的当前字符长度
判读符合比较条件吗
}
判断 A B 都走完了吗
代码
class Solution {
public:
vector<int> addToArrayForm(vector<int>& A, int K) {
int n = A.size();
vector<int> ans;
int i = n - 1, sum = 0, carry = 0;//进位
while(i >= 0 || K != 0){//两个数的终止条件
//取出当前位的数,两个数可能不一样长,故需要判断
int a = i >= 0 ? A[i] : 0;
int k = K != 0 ? K%10 : 0;
sum = a + k + carry;
carry = sum / 10;//计算进位
//两个数移到下一位
K = K / 10;
i--;
ans.insert(ans.begin(), sum%10);
}
//勿忘判断最高位是否进位
if(carry != 0) ans.insert(ans.begin(), carry);
return ans;
}
};