数学

2017/05/01 Algorithms 共 9350 字,约 27 分钟

乘法原理 list 里枚举右端点

求两两组合的数组,比如list里求(a,b)数对的数量。

  • 顺序枚举b,每次ans增加此时a的数量。即只计算左边的。避免重复。
  • 或者求子数组的数量,顺序枚举右边的。计算总数。

排列组合

参考

计算公式:

img

计算公式:

image-20211002195906842

image-20220904145610986

另外 C(0,0) = 1, 0 的为 1。

组合数性质

image-20220904145924154

大组合数求解边模

利用组合数性质. o(n2)

    def comb_mod(self, n, m, mod=10 ** 9 + 7):
        '''
        大组合数计算C(n,m),边求边mod, 利用组合数性质C(n,m) = 1*C(n-1,m-1) + C(n-1,m)
        '''
        dp = [[0 for _ in range(n+1)] for _ in range(n+1)]
        dp[0][0] = 1
        for i in range(1, n+1):
            dp[i][0] = 1
            for j in range(1, i+1):
                dp[i][j] = (dp[i-1][j-1] + dp[i-1][j]) % mod
        return dp[n][m]

组合数求和公式

image-20220904150115865

python中排列组合

    # 组合
    math.comb  # n! / (k! * (n - k)!)
    math.perm  # n! / (n - k)!

求组合数 小球放盒子

参考2

默认问题为 n 个小球放到 m 个盒子里,题型共有三项要求,球是否相同,盒子是否相同,能否有空盒。

  • 球相同,盒子不同,不能有空盒。 实质就是把 n 个小球分为 m 组。插板法。 C(n-1,m-1)
  • 球相同,盒子不同,可以有空盒。对于每个盒子,我们都给他一个球,那么上一个问题就和这问题一样了,所以我们可以看做自己有 n+m 个小球,然后我们在排列完之后在每一组都删去一个小球,这样就能枚举出有空盒的情况了。C(n+m-1,m-1)
  • 其他复杂情况看参考。

容斥原理

Inclusion-exclusion principle

参考

image-20220228201935551

定理:

image-20220228202026269

容斥原理在排列组合里常用。

比如100127. 给小朋友们分糖果 II 合法方案数 = 总方案数 - 不合法方案数。不合法方案就是A,B,C的并集,然后要A+B+C求和后还要多减去一些重叠的部分。

插板法求各方案数。 用容斥定理将重叠的不合法方案数求出。

这个题目也可以容斥原理: 100126. 重新排列后包含指定子字符串的字符串数目 容斥原理做法参考: https://leetcode.cn/circle/discuss/BLgiV1/

等差等比数列

参考

image-20210801124031771

image-20210801124109093

复杂的复杂度计算

幂函数和对数函数和指数函数对比

image-20220223194514512

####

  • 幂函数:x^n. 增长速度受限于 n 值,即便 n 很大,增长速度也是有限的。
  • 指数函数:n ^ x, 增长速度随 x 增加变得极其迅速。
  • 在 x 较小时,幂函数可能比指数函数大, 但x增大时指数函数的增长速度远快于幂函数,

调和级数

image-20220223193553568

这里有用到: link

二项式定理

在状态压缩中,枚举子集时可能就会用到。

image-20220223195451869

中位数

在一步操作中,你可以使数组中的一个元素加 1 或者减 1 ,所有数都靠近中位数可使所有数组元素相等时移动数最少。 相关题目: 最少移动次数使数组元素相等 II

即中位数到所有数距离和最小,如果是偶数,可以在中位数两侧的数据构成的区间内任意取值,对结果无影响证明:

image-20221023121438346

另一个相似题目:有cost,需转化拆分,然后再算中位数,6216. 使数组相等的最小开销

    # 在nums的[l,r]区间位置中,找中位数mid和一个切分位置,这个位置后面的元素的都>= mid,前面元素都<= mid
    # nums是有序的list,方法时间复杂度O(1)
    def findMid(self, nums: List[int], l=None, r=None):
        n = len(nums)
        if l is None and r is None:
            i = 0
            j = n - 1
        else:
            i = l, j = r
        if (j - i + 1) % 2 == 0:
            # 长度偶数时,中位数是中间两个数的平均数,这里取整数对求距离总和结果无影响,+0.5/-0.5而已。
            # 如果是用于求移动树,其实这里直接中间第一个元素就可以,因+1,-1都对结果无影响
            # 所以mid可以直接是nums[n//2], index是n//2
            index = (i + j + 1) // 2
            v = (nums[index] + nums[index - 1]) // 2
        else:
            index = (i + j) // 2
            v = nums[index]
        return [v, index]

    # 统计改nums为相等数组的最小sum
    # nums是有序的list,方法时间复杂度O(n)
    def sumDistancesToMid(self, nums: List[int], l=None, r=None):
        mid, index = self.findMid(nums)
        pre = Pre(nums)
        n = len(nums)
        if l is None and r is None:
            i = 0
            j = n - 1
        res = 0
        res += (index - i) * mid - pre.get_pre(i, index - 1)
        res += pre.get_pre(index, j) - (j - index + 1) * mid
        return res

素数

判断是否素数 o(sqrt(N))

# 判断某数是否是素数 o(sqrt(n))
def is_prime(a):
    if a <= 1: return False
    import math
    for i in range(2, int(math.sqrt(a) + 1)):
        if a % i == 0: return False
    return True


求 1-n 的所有素数 筛法 o(N)

埃氏筛法

# 求1-n每个数的素数,以下时间复杂度O(nloglogn) 接近o(n)
# 如果我们从小到大考虑每个数,然后同时把当前这个数的所有(比自己大的)倍数记为合数,那么运行结束的时候没有被标记的数就是素数了。
#  Eratosthenes 筛法(埃拉托斯特尼筛法,简称埃氏筛法)
def get_all_prime(n):
    a = [False] * n
    res = []
    for i in range(2, n):
        if a[i]: continue
        # a[i]是素数
        res.append(i)
        for j in range(2 * i, n, i):
            a[j] = True
    return res

线性筛法 也称为 Euler 筛法(欧拉筛法)

埃氏筛法仍有优化空间,它会将一个合数重复多次标记。有没有什么办法省掉无意义的步骤呢?答案是肯定的。

如果能让每个合数都只被标记一次,那么时间复杂度就可以降到 o(n)了。

def get_all_prime(n):
    a = [False] * n
    res = []
    for i in range(2, n):
        if a[i]: continue
        # a[i]是素数
        res.append(i)
        for j in range(2 * i, n, i):
						if a[j]: break # 多了个这行
            a[j] = True
    return res

平方数

先看 Cyc2018

生成所有平方数

平方序列:1,4,9,16,..

间隔:3,5,7,…

间隔为等差数列,使用这个特性可以得到从 1 开始的平方序列。

3 的 n 次方

Power of Three (Easy)

public boolean isPowerOfThree(int n) {
    return n > 0 && (1162261467 % n == 0);
}

除法

    # b % a == 0
    # 表示b能被a整除,a可以整除b, 被除数永远都是有这个『被』

表示b能被a整除,a可以整除b, 被除数永远都是有这个『被』

大整数除法,除法中取模。

有负数的除法求模 不同语言可能不同

摘自网络, 待考证

比如a 除以 b,求对应商和余数。不同语言可能不同

计算-7 Mod 4

那么:a = -7;b = 4;

第一步:求整数商c,c应该是-1.75,如进行求模运算c = -2(向负无穷方向舍入),求余运算则c = -1(向0方向舍入);

第二步:计算模和余数的公式相同,但因c的值不同,求模时r = 1,求余时r = -3。

当符号不一致时,结果不一样。求模运算结果的符号和b一致,求余运算结果的符号和a一致。

在C/C++, C#, JAVA, PHP这几门主流语言中,%运算符都是做取余运算,而在python中的%是做取模运算。

python求商是向负无穷靠近,而其他是靠近0.

本地最新实验,其实是 python 会把结果变成正数。 Mac m1 OS.

-7 % 4 = 1. -3 % 4 = 1

因数理论

素数分解

每一个数都可以分解成素数的乘积,例如 84 = 22 _ 31 _ 50 _ 71 _ 110 _ 130 _ 170 * …

整除

令 x = 2m0 _ 3m1 _ 5m2 _ 7m3 _ 11m4 * …

令 y = 2n0 _ 3n1 _ 5n2 _ 7n3 _ 11n4 * …

如果 x 整除 y(y mod x == 0),则对于所有 i,mi <= ni。

最大公约数最小公倍数的素数表示

每个质因数的乘积

x 和 y 的最大公约数为:gcd(x,y) = 2min(m0,n0) _ 3min(m1,n1) _ 5min(m2,n2) * …

x 和 y 的最小公倍数为:lcm(x,y) = 2max(m0,n0) _ 3max(m1,n1) _ 5max(m2,n2) * …

约数个数和约数之和

如果 N = p1^c1 _ p2^c2 _ … _pk^ck 约数个数: (c1 + 1) _ (c2 + 1) _ … _ (ck + 1) 约数之和: (p1^0 + p1^1 + … + p1^c1) _ … _ (pk^0 + pk^1 + … + pk^ck)

因数相关问题

试除法求所有约数:

求一个数的因子列表 o(sqrt(n))

o(sqrt(n))

divisors = []
d = 1
while d * d <= k:  # 预处理 k 的所有因子
	if k % d == 0:
		divisors.append(d)
		if d * d < k: # 避免 d= k/d的情况,此时如果append会重复
				divisors.append(k / d)
				d += 1

统计 1-n 每个数的因子列表 o(nlogn)

o(nlogn)

MX = 100001
divisors = [[] for _ in range(MX)] # 每个value就是key的因子列表,乘积肯定>value,因为因子之间可能有倍数
for i in range(1, MX):  # 预处理每个数的所有因子,时间复杂度 O(MlogM),M=1e5
    for j in range(i, MX, i):
        divisors[j].append(i)

分解质因数

求某数质因数列表 o(sqrt(N))

o(sqrt(N))

求某数的质因数列表,比如 8,是[2,2,2]

枚举[2, sqrt(n)+1), 如果是质因数,就接着除,最大大于 1,它本身就是质数。

也叫 求欧拉函数

# 求质因数列表
def breakdown(N):
    result = []
    from math import sqrt
    for i in range(2, int(sqrt(N)) + 1):
        if N % i == 0:  # 如果 i 能够整除 N,说明 i 为 N 的一个质因子。
            while N % i == 0:
                N = N // i
                result.append(i)
    if N != 1:  # 说明再经过操作之后 N 留下了一个素数
        result.append(N)
    return result

统计 1-n 每个数的质因数列表

筛法求欧拉函数

类似,暂时不写。

gcd 求最大公约数和最小公倍数 欧几里得算法

欧几里得算法

class Math1:
    def gcd(self, a, b):
        if b == 0:
            return a
        return self.gcd(b, a % b)

最小公倍数就是 a*b/gcd(a,b)

gcd性质

如果ax+by=1, 则gcd(x,y) = 1

多个数求最大公约数和最小公倍数

多个数的最大公约数:

def gcd_list(self, nums):
    import math
    cur = nums[0]
    for i in range(1, len(nums)):
        cur = math.gcd(cur, nums[i])
    return cur

多个数的最小公倍数:

注意这里不是直接多个数乘积除以他们的gcd呀,而且不断一个一个加计算的。

def lcm_list(self, nums):
    import math
    prod = 1
    for v in nums:
        prod = prod * v / math.gcd(prod, v)
    return prod

1到n里有多少个数可整除a?

n//a

1到n里有多少个a的因数?

参考另一个博客

    def calcu(x,a):
        '''
        1-x中有多少个因数a, 比如1-26里有5,10, 15, 20, 各自1个5, 5*5两个5,一共6个
        '''
        r = 0
        while x:
            # 贡献5的数量,贡献5*5的数量,贡献5*5*5的数量
            r += x //a
            x //= a
        return r
    print(calcu(26, 5))

取模问题

大数相乘取模

    def prod(d=[], mod=10 ** 9 + 7):
        r = d[0] % mod
        for v in d[1:]:
            r *= v
            r %= mod
        return r

大数相除取模 费马小定理

如果a/b,然后a和b都是大数要取模,这时不能相乘取模来计算

(a/b)%c=(a%c)/(b%c)是不成立的

除法取模


# 原: i//j%MOD 
# 现: i*modInverse(j)%MOD
MOD = int(1e9 + 7)

class BigIntDivide:
    def mod_inverse(sefl, i):
        # 调用取模的乘幂运算, pow复杂度是log(exp)即log(MOD)
        return pow(i, MOD - 2, MOD)

    def divide_mod(self, a, b):
        '''
        计算(a/b) % MOD, 除法变乘法,前提是b和MOD互为质数
        '''
        # 如果有多个b,比如a/b1/b2, 那就可以递归。a*mod_inverse(b1) % mod * mod_inverse(b2) % mod这样
        return a * self.mod_inverse(b) % MOD

    def divide_mods(self, a, b=[]):
        '''
        计算a /(b1*b2*b3) % MOD等形式,前提是b和MOD互为质数
        '''
        r = a % MOD
        for i, v in enumerate(b):
            r *= self.mod_inverse(v)
            r %= MOD
        return r


大数幂取模

r = pow(base=2, exp=3, mod=3)
print(r)

(i-j)%k == 0的转换

(i-j)%k == 0, 可等价于i % k == j % k. 这样可以用 map 来记录,比如前缀和常见问题。

阶乘

import math
math.factorial(3)

分配问题

a能否分成满足条件的两份,其中只能分给指定一份

2513. 最小化两个数组中的最大值 给你两个数组 arr1 和 arr2 ,它们一开始都是空的。你需要往它们中添加正整数,使它们满足以下条件:

arr1 包含 uniqueCnt1 个 互不相同 的正整数,每个整数都 不能 被 divisor1 整除 。 arr2 包含 uniqueCnt2 个 互不相同 的正整数,每个整数都 不能 被 divisor2 整除 。 arr1 和 arr2 中的元素 互不相同 。 给你 divisor1 ,divisor2 ,uniqueCnt1 和 uniqueCnt2 ,请你返回两个数组中 最大元素 的 最小值 。

参考, link

Key: 判断过程中分为三类:保证在范围内有充足的数不是第一个数的倍数;不是第二个数的倍数;不为公倍数的数总数不少于总共要取的数。根据这三个条件即得到结果。

将 s 个物品分成大小为 k 或 (k+1) 的的最少组数

将 s 个物品分成大小为 k 或 (k+1) 的的最少组数:

  1. 情况1:首先尝试将 s 个物品分成大小是 (k+1) 的组,这样会分出 d1 = ⌊ (s/(k+1)) ⌋ 组,并且还剩下 r1 = s mod (k+1) 的物品。
    • 若 r1 = 0,则直接分成 d1 组即可。
    • 否则,如果 d1 + r1 ≥ k,则可以组成新的一组,总共可以分成 (d1+1) 组。
  2. 情况2:接下来尝试将 s 个物品分成大小是 k 的组,这样会分出 d2 = ⌊ (s/k) ⌋ 组,并且还剩下 r2 = s mod k 的物品。
    • 因为 d2 组的大小都是最小值 k,所以不能再拿出物品和 r2 凑成新的一组,而是反过来要把 r2 塞进每一组里。由于 d2 组的每一组最多再加入一个物品。因此如果 r2 ≤ d2,那么就可以把所有物品分成 d2 组。
  3. 如果所有尝试都失败,那么这些物品不能被分成大小是 k 和 (k+1) 的组。
        def min_groups(s, k):
            divisor = s // (k + 1)
            remainder = s % (k + 1)
            cnt = 0
            if remainder == 0:
                cnt += divisor
            elif divisor + remainder >= k:
                cnt += divisor + 1
            else:
                divisor = s // k
                remainder = s % k
                # cnt == -1 表示无法分组
                if remainder > divisor:
                    cnt = -1
                else:
                    cnt += divisor
            # print(s, k, cnt)
            return cnt

位运算

a xor b ==c 则 a = b xor c.

进制转换

    def baseX(self, n, x):
        '''
        进制转换,10进制变x进制
        :return: 数组,每个数是该位上数
        '''
        ans = []
        while n != 0:
            n, b = divmod(n, x)
            ans.append(b)
        ans.reverse()
        return ans

    def base10(self, n=[], x=2):
        '''
        从x进制转成10进制
        '''
        ans = 0
        for v in n:
            ans *= x
            ans += v
        return ans

负进制的转换

负二进制转换

个人blog 1017. 负二进制转换 可以任何负进制转换

class Solution:
    def baseNeg2(self, n: int) -> str:
        if n == 0:
            return '0'
        ans = []
        b = -2
        while n != 0:
            x = n % b
            ans.append(str(abs(x)))
            if x < 0:
                x += abs(b)  # 变成正数
            n = (n - x) // b
        ans = ans[::-1]
        return "".join(ans)

小数的进制转换

可以参考

十进制小数转二进制数:“乘以 2 取整,顺序排列”(乘 2 取整法)

例: (0.625)10= (0.101)2

image-20220603233141181

幂

快速幂 O(logk)

求 m^k mod p,时间复杂度 O(logk)。

int qmi(int m, int k, int p)
{
    int res = 1 % p, t = m;
    while (k)
    {
        if (k&1) res = res * t % p;
        t = t * t % p;
        k >>= 1;
    }
    return res;
}

求根号

题目

牛顿迭代法

求根号变种 小数精度

while l<=r不用变,只需要变步长即可。eps 可以比期望精度再小一个量级。


def f(x, eps):
    l, r = 0, max(x, 1)
    while l <= r:
        mid = (l + r) / 2
        if int(mid) * int(mid) == x:
            return int(mid)
        if mid * mid > x:
            r = mid - eps
        else:
            l = mid + eps
    return r


if __name__ == '__main__':
    eps = 1e-3
    # print(f(4, eps))
    # print(f(8, eps))
    # print(f(9, eps))
    print(f(0.04, 1e-10))

数字与中文英文转换

阿拉伯数字转中文

200001010200 -> 二千亿零一百零一万零二百

  • 倒序,结果放到stack里,最后reverse.
  • 一个单位,一个数字。遇到0就没单位了。0还会省略
// 1 最后一位是 0 的情况下,需要忽略,
// 2 中间某一位是 0 的情况下,不要加上 十百千万 这种
// 3 中间如果有多个 0 ,只需要保留一位 0 

let chinese = [
    '零',
    '一',
    '二',
    '三',
    '四',
    '五',
    '六',
    '七',
    '八',
    '九'
]

let units = [
    '十',
    '百',
    '千',
    '万'
]

// 解法:
// 1. 判断当前位数匹配到 Chinese 中,然后获取到对应的中文
// 2. 然后判断当前是多少位数,然后看是十百千万这种
// 尝试使用 reduce 写一下
// 注意的点:
// 1 最后一位是 0 的情况下,需要忽略,
// 2 中间某一位是 0 的情况下,不要加上 十百千万 这种
// 3 中间如果有多个 0 ,只需要保留一位 0 
function toChinese(num) {
    let str = '' + num;
    let res = '';
    // 需要使用倒序而不是正序
    for (let len = str.length, i = len - 1; i >= 0; i--) {
        // 末尾如果是 0,则忽略
        if (i == len - 1 && str[i] == 0) {
            continue;
        }
        // 如果某位数是 0 ,则不要加上 十百千万 这种
        if (i == len - 1 || str[i] == 0) {
            // 如果下一位还是 0,那么当前这位就可以忽略
            // 为什么这里不是 i - 1 而是 i + 1呢
            if (str[i + 1] == 0) {
                continue;
            }
            res += chinese[str[i]];
        }
        else {
            // 单位的索引 和 数字的索引 不一样
            res += units[len - 1 - i - 1] + chinese[str[i]];
        }
    }
    return res.split('').reverse().join('');
}

中文转阿拉伯数字

顺序处理

  • 临时数字变量
  • 遇到单位就乘以临时数字变量加到结果并重置,数字变0,单位变1.
  • 最后将数字加到结果。
def chinese_to_digits(chinese_num):
    chinese_digits = {'零': 0, '一': 1, '二': 2, '三': 3, '四': 4, '五': 5, '六': 6, '七': 7, '八': 8, '九': 9}
    chinese_units = {'十': 10, '百': 100, '千': 1000, '万': 10000, '亿': 100000000}

    result = 0
    temp_num = 0
    temp_unit = 1  # 当前数字的倍数单位

    for char in chinese_num:
        if char in chinese_digits:  # 如果是数字
            temp_num = chinese_digits[char]
        elif char in chinese_units:  # 如果是单位
            unit_value = chinese_units[char]
            if unit_value >= 10000:  # 处理 "万" 或 "亿"
                result += (temp_num * temp_unit)  # 将当前级别的值加到结果中
                result *= unit_value  # 将整个结果乘以单位(万或亿)
                temp_num = 0  # 清零当前累积值
                temp_unit = 1  # 重置单位
            else:
                temp_unit = unit_value  # 对于"十"、"百"、"千",更新当前倍数单位
                result += (temp_num * temp_unit)  # 将当前数字乘以倍数单位并加入结果
                temp_num = 0  # 清零当前累积的数字
                temp_unit = 1  # 重置单位

    # 最后将剩余的数字加到结果
    result += temp_num * temp_unit

    return result

# 测试
chinese_num = "二百零五"
result = chinese_to_digits(chinese_num)
print(result)  # 输出: 205

chinese_num = "十"
result = chinese_to_digits(chinese_num)
print(result)  # 输出: 10

chinese_num = "二十万三千四百五十六"
result = chinese_to_digits(chinese_num)
print(result)  # 输出: 203456

chinese_num = "八十九万零三十"
result = chinese_to_digits(chinese_num)
print(result)  # 输出: 890030

中英文互转

三位一组。来处理。

参考 Acwing 数学

参考 本文只涉及部分

Search

    Table of Contents