数学(三) -- LC[1010]&[1015] 可被 K 整除的最小整数

1 可被 K 整除的最小整数

1.1 题目描述

        题目链接:https://leetcode.cn/problems/smallest-integer-divisible-by-k/description/

1.2 思路分析

模运算

        如果让你计算 1234 ⋅ 6789 1234 \cdot 6789 12346789 的个位数,你会如何计算?

        由于只有个位数会影响到乘积的个位数,那么 4 ⋅ 9 = 36 4\cdot 9=36 49=36 的个位数 6 6 6 就是答案。

        对于 1234 + 6789 1234+6789 1234+6789 的个位数,同理, 4 + 9 = 13 4+9=13 4+9=13 的个位数 3 3 3 就是答案。

        你能把这个结论抽象成数学等式吗?

        一般地,涉及到取模的题目,通常会用到如下等式(上面计算的是 m = 10 m=10 m=10):

( a + b )   m o d   m = ( ( a   m o d   m ) + ( b   m o d   m ) )   m o d   m ( a ⋅ b )   m o d   m = ( ( a   m o d   m ) ⋅ ( b   m o d   m ) )   m o d   m (a+b)\bmod m = ((a\bmod m) + (b\bmod m)) \bmod m \\ (a\cdot b) \bmod m=((a\bmod m)\cdot (b\bmod m)) \bmod m (a+b)modm=((amodm)+(bmodm))modm(ab)modm=((amodm)(bmodm))modm

        证明:根据带余除法,任意整数 a a a 都可以表示为 a = k m + r a=km+r a=km+r,这里 r r r 相当于 a   m o d   m a mod m amodm。那么设 a = k 1 m + r 1 ,   b = k 2 m + r 2 a=k_1m+r_1,\ b=k_2m+r_2 a=k1m+r1, b=k2m+r2

第一个等式:
( a + b )   m o d   m = ( ( k 1 + k 2 ) m + r 1 + r 2 )   m o d   m = ( r 1 + r 2 )   m o d   m = ( ( a   m o d   m ) + ( b   m o d   m ) )   m o d   m \begin{aligned} &(a+b) \bmod m\\ =&((k_1+k_2) m+r_1+r_2)\bmod m\\ =&(r_1+r_2)\bmod m\\ =&((a\bmod m) + (b\bmod m)) \bmod m \end{aligned} ===(a+b)modm((k1+k2)m+r1+r2)modm(r1+r2)modm((amodm)+(bmodm))modm

        即:两个数相加对某个数求余等于两个数分别求余相加之后再求余。

第二个等式:
( a ⋅ b )   m o d   m = ( k 1 k 2 m 2 + ( k 1 r 2 + k 2 r 1 ) m + r 1 r 2 )   m o d   m = ( r 1 r 2 )   m o d   m = ( ( a   m o d   m ) ⋅ ( b   m o d   m ) )   m o d   m \begin{aligned} &(a\cdot b) \bmod m\\ =&(k_1k_2m^2+(k_1r_2+k_2r_1)m+r_1r_2)\bmod m\\ =&(r_1r_2)\bmod m\\ =&((a\bmod m)\cdot (b\bmod m)) \bmod m \end{aligned} ===(ab)modm(k1k2m2+(k1r2+k2r1)m+r1r2)modm(r1r2)modm((amodm)(bmodm))modm

举例一: k = 7 k = 7 k=7

        从小到大枚举 n n n,第一个能被 k k k 整除的数的长度即为答案。

1 → 11 → 111 → 1111 → 11111 → 111111 1 \rightarrow 11 \rightarrow 111 \rightarrow 1111 \rightarrow 11111 \rightarrow 111111 111111111111111111111

        根据前置知识,设 x x x 是上一次运算的结果(初始为1),则下一个 n n n k k k 的结果为 ( 10 x + 1 ) m o d k (10x + 1) mod k (10x+1)modk,看它是否为0。

1 → 4 ⟶ 6 ⟶ 5 ⟶ 2 ⟶ 0 1 \rightarrow 4 \longrightarrow 6 \longrightarrow 5 \longrightarrow 2 \longrightarrow 0 146520

举例二: k = 24 k=24 k=24

        如果计算结果和之前的某个数相同,由于计算规则不变,后面会无限重复下去,无法得到0。

方法一:哈希表

        用哈希表记录计算结果。如果在算出0之前就遇到了在哈希表中的数字,则返回-1。

class Solution:
    def smallestRepunitDivByK(self, k: int) -> int:
        x = 1 % k           #  x 为余数
        seen = set()        #  创建一个无序集合,用于存储余数
        while x and x not in seen:      # 当余数为0或者余数重复出现,退出循环
            seen.add(x)
            x = (10 * x + 1) % k
        return -1 if x else len(seen) + 1   # 余数不为0,返回-1,余数为0,返回len(seen)+1    

复杂度分析

  • 时间复杂度: O ( k ) \mathcal{O}(k) O(k)
  • 空间复杂度: O ( k ) \mathcal{O}(k) O(k)

方法二:抽屉原理

        循环 k k k 次,如果没有算出0,则返回-1。为什么?模 k k k 的结果在 [ 0 , k − 1 ] [0, k-1] [0,k1] 中,这有 k k k 个数字。如果循环 k k k 次还没有找到0,根据鸽巢原理(抽屉原理),必然有重复的数字。这也同时说明算法一的时间复杂度为 O ( k ) O(k) O(k)

class Solution:
    def smallestRepunitDivByK(self, k: int) -> int:
        if k % 2 == 0 or k % 5 == 0:
            return -1
        x = 1 % k
        for i in count(1):              # 一定有解
            if x == 0:
                return i
            x = (x * 10 + 1) % k

复杂度分析

  • 时间复杂度: O ( k ) \mathcal{O}(k) O(k)
  • 空间复杂度: O ( 1 ) \mathcal{O}(1) O(1),仅用到若干额外变量。

itertools.count(start=0, step=1)

        创建一个迭代器,它从 start 值开始,返回均匀间隔的值。常用于 map() 中的实参来生成连续的数据点。此外,还用于 zip() 来添加序列号。大致相当于:

def count(start=0, step=1):
    # count(10) --> 10 11 12 13 14 ...
    # count(2.5, 0.5) --> 2.5 3.0 3.5 ...
    n = start
    while True:
        yield n
        n += step

        当对浮点数计数时,替换为乘法代码有时精度会更好,例如:(start + step * i for i in count())

方法三:数学推导

        设 n n n 的长度为 x x x,那么 n = 1 0 x − 1 9 n=\frac{10^x-1}{9} n=910x1 n n n k k k 的倍数,等价于 1 0 x − 1 10^x-1 10x1 9 k 9k 9k 的倍数,即 1 0 x ≡ 1 ( m o d 9 k ) 10^x \equiv 1(mod 9k) 10x1(mod9k)

  • 结论:最小的 x x x 必然是 ϕ ( 9 k ) \phi(9k) ϕ(9k) 的因子。
  • 反证:如果 ϕ ( 9 k ) = p x + r ( 0 < r < x ) \phi(9k) = px + r (0 < r <x) ϕ(9k)=px+r(0<r<x),根据欧拉定理, 1 0 ϕ ( 9 k ) = ( 10 ) P ﹒ 10 ” = 10 ” = 1 ( m o d 9 k ) 10^{\phi(9k)}=(10)P﹒10”=10”= 1(mod 9k) 10ϕ(9k)=(10)P﹒10”=10”=1(mod9k),这说明有一个比 x x x 更小的 r r r,矛盾。

        那么计算 ϕ ( 9 k ) \phi(9k) ϕ(9k) 并枚举其因子 d d d,用快速幂判断 1 0 d m o d ( 9 k ) 10^d mod (9k) 10dmod(9k) 是否等于1。这一做法只需要 ( ( k ) l o g k ) (\sqrt(k)log k ) (( k)logk) 的时间。

一点优化

        由于 n n n 的个位数是1,所以必然不是 2 的倍数和 5 的倍数。如果 k k k 是 2 的倍数或 5 的倍数,那么必然无解,返回 —1。否则一定有解。

        证明:根据算法二,在计算过程中必然会出现两个数模 k k k 同余。设这两个数为 a = 1 0 x − 1 9 a=\frac{10^x-1}{9} a=910x1 b = 1 0 y − 1 9 b=\frac{10^y-1}{9} b=910y1,且 a > b a > b a>b。那么 a − b a-b ab k k k 的倍数。

        注意 a − b = 1 0 x − 1 0 y 9 = 1 0 y ⋅ 1 0 x − y − 1 9 a-b=\frac{10^x-10^y}{9}=10^y\cdot\frac{10^{x-y}-1}{9} ab=910x10y=10y910xy1 k k k在没有因子 2 和 5 的情况下,要想整除上式,必须要整除 1 0 x − y − 1 9 \frac{10^{x-y}-1}{9} 910xy1,这说明 n n n 的长度可以是 x − y x-y xy

# 计算欧拉函数(n 以内的与 n 互质的数的个数)
def phi(n: int) -> int:
    res = n
    i = 2
    while i * i <= n:
        if n % i == 0:
            res = res // i * (i - 1)
            while n % i == 0:
                n //= i
        i += 1
    if n > 1:
        res = res // n * (n - 1)
    return res

class Solution:
    def smallestRepunitDivByK(self, k: int) -> int:
        if k % 2 == 0 or k % 5 == 0:
            return -1
        m = phi(k * 9)
        # 从小到大枚举不超过 sqrt(m) 的因子
        i = 1
        while i * i <= m:
            if m % i == 0 and pow(10, i, k * 9) == 1:
                return i
            i += 1
        # 从小到大枚举不低于 sqrt(m) 的因子
        i -= 1
        while True:
            if m % i == 0 and pow(10, m // i, k * 9) == 1:
                return m // i
            i -= 1

复杂度分析

  • 时间复杂度: O ( k log ⁡ k ) \mathcal{O}(\sqrt{k}\log k) O(k logk)。计算 ϕ ( 9 k ) \phi(9k) ϕ(9k) 和枚举 ϕ ( 9 k ) \phi(9k) ϕ(9k) 的因子都需要 O ( k ) \mathcal{O}(\sqrt{k}) O(k ) 的时间,对每个因子计算快速幂需要 O ( log ⁡ k ) \mathcal{O}(\log k) O(logk) 的时间,所以时间复杂度为 O ( k log ⁡ k ) \mathcal{O}(\sqrt{k}\log k) O(k logk)
  • 空间复杂度: O ( 1 ) \mathcal{O}(1) O(1)。仅用到若干额外变量。

2 总持续时间可被 60 整除的歌曲

2.1 题目描述

        题目链接:https://leetcode.cn/problems/pairs-of-songs-with-total-durations-divisible-by-60/description/

2.2 思路分析

1. 组合数学

        遍历数组的同时用一个哈希表(或者数组)记录元素的出现次数。

        遍历 time \textit{time} time

  • 举例,如果 time [ i ] = 1 \textit{time}[i]=1 time[i]=1,那么需要知道左边有多少个模 60 余数是 59 的数。
  • 举例,如果 time [ i ] = 62 \textit{time}[i]=62 time[i]=62,那么需要知道左边有多少个模 60 余数是 58 的数。
  • 一般地,对于 time [ i ] \textit{time}[i] time[i],需要知道左边有多少个模 60 余数是 60 − time [ i ] m o d 60 60-\textit{time}[i] mod 60 60time[i]mod60 的数。
    特别地,如果 time [ i ] \textit{time}[i] time[i] 模 60 的余数是 0,那么需要知道左边有多少个模 60 余数也是 0 的数。
    这两种情况可以合并为:累加左边 ( 60 − time [ i ] m o d 60 ) m o d 60 (60-\textit{time}[i] mod 60) mod 60 (60time[i]mod60)mod60 的出现次数。

        代码实现时,用一个长为 60 的数组 cnt \textit{cnt} cnt 维护 time [ i ] m o d 60 \textit{time}[i] mod 60 time[i]mod60 的出现次数。

class Solution:
    def numPairsDivisibleBy60(self, time: List[int]) -> int:
        ans = 0
        cnt = [0] * 60
        for t in time:
            # 先查询 cnt,再更新 cnt,因为题目要求 i<j
            # 如果先更新,再查询,就把 i=j 的情况也考虑进去了
            ans += cnt[(60 - t % 60) % 60]
            cnt[t % 60] += 1
        return ans

复杂度分析

  • 时间复杂度: O ( n + U ) \mathcal{O}(n+U) O(n+U),其中 n n n nums \textit{nums} nums 的长度, U = 60 U=60 U=60
  • 空间复杂度: O ( U ) \mathcal{O}(U) O(U)

参考

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

长路漫漫2021

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值