数学技巧---leetcode中遇到的数学相关解题技巧(持续更新中~)

模运算规则 避免数值溢出

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的余数即可.
这题其实涉及比较基础的数学知识——模运算法则,即除了除法外,几乎常见的运算都符合某种类似分配律的运算律:

  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) % p
  4. (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;
    }
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值