数论的理论部分详见 math 专题,本部分仅讨论数论相关的核心算法,及其在实际问题中的应用。
整除性与素数
辗转相除法
- 欧几里得算法(Euclidean algorithm):也称为辗转相除法,通常用于计算两个整数的最大公约数,对$0\leqslant m<n$,EA用到以下递推式:
- 分析:时间复杂度为$O(lgn)$,具体证明参见拉梅定理
计算最大公约数
1 | def gcd(m,n): |
计算最小公倍数
最小公倍数和最大公约数有以下关系:
1 | def lcm(m,n): |
求解贝祖方程
扩展欧几里得算法(Extended Euclidean algorithm):在求得gcd(m,n)的同时,一定能找到整数x,y(其中一个可能是负数),使得它们满足贝祖等式:
由欧几里得算法递推过程可得:
整理化简得 a,b 的递推式 $xm+yn=(y_1-x_1\left \lfloor \frac{n}{m} \right \rfloor)m+x_1n$,即:
1 | def bz(m,n): |
贝祖定理(Bézout’s identity):整数不定方程 $mx+ny=c$ 有解(无穷多个解)的充要条件是$gcd(m,n)\mid c$。
如果贝祖方程有解,则一定有无穷解,设$(x_0,y_0)$是$mx+ny=gcd(m,n)$由辗转相除法得到的一个解,$d=gcd(m,n)$,则$mx+ny=c$的通解可表示为:
推论:$mx+ny=1$有解的充要条件为gcd(m,n)=1,即m,n互素。
[592] 分数加减运算
- 问题:给定一个表示分数加减运算表达式的字符串,你需要返回一个字符串形式的计算结果。 这个结果应该是不可约分的分数,即最简分数。 如果最终结果是一个整数,例如 2,你需要将它转换成分数形式,其分母为 1。所以在上述例子中, 2 应该被转换为 2/1。
1 | 输入:"-1/2+1/2" |
- 思路:通过最小公倍数通分,通过最大公约数约分
- 找到所有单元+-x/y,进而得到所有分子分母,
- 求分母最小公倍数
- 通分x*lcm/y求和,得到最终的分子、分母
- 最后再求最终分子分母的最大公约数
- 分子分母除以最大公约得到最简分数
- 代码:
1 | def fractionAddition(self, expression): |
[365] 水壶问题
- 问题:有两个容量分别为 x升 和 y升 的水壶以及无限多的水。请判断能否通过使用这两个水壶,从而可以得到恰好 z升 的水?如果可以,最后请用以上水壶中的一或两个来盛放取得的 z升 水。
- 思路:等价于ax+by=z有整数解(a,b),且z <= x+y
- 代码:
1 | def canMeasureWater(self, x, y, z): |
寻找 n 以内的素数
一般有”试除法“和”筛法“两种思路来寻找n以内的素数,这里介绍三种常用的方法:
方法 | 思路 | 空间 | 时间 |
---|---|---|---|
优化试除法 | 对[2,n]中的每一个数k,试除[2,sqrt(k)]中所有素数,如果能整除说明k不是素数 | O(1) | O(nlgn) |
埃拉托斯特尼筛法 | 假设所有数都是素数,然后[2,sqrt(n)]间的素数p,将[p*p::p]的整数标记为非素数 | O(n) | O(nlglgn) |
线性筛法 | 每个合数都能唯一分解为最小素因子和一个小于自己的数之积,只用合数的最小素因子筛将其筛去,避免重复的筛选 | O(n) | O(n) |
[204] 计算质数
1 | # 1. 试除法:4684 ms |
扩展:以上思路很容易用来求解一个整数是否为素数、整数因式分解等问题。
算术基本定理
每一个正整数都能唯一地表示为素数幂积的形式,p代表所有可能的素数:
[313] 超级丑数
- 问题:编写一段程序来查找第 n 个超级丑数。超级丑数是指其所有质因数都是长度为 k 的质数列表 primes 中的正整数。
1 | 输入: n = 12, primes = [2,7,13,19] |
- 思路:
1 | 后续丑数必然是由已有丑数与质数列表中的质数乘积的结果,为了实现有序输出,可进行k路归并,丑数排序可以看做是k个有序表的归并排序 |
- 代码:
1 | def nthSuperUglyNumber(self, n, primes): |
- 分析:时间复杂度O(kn)
模运算
定义模运算:n模m表示n除以m所得余数,记做$n\ mod\ m$或$n\%m$
- 模:mod后面的数称为模,至今还没有人给mod前面的数取名
- 余数:取模的结果,余数总是处于0和模之间
- 模运算可以推广到任意实数
模运算性质
mod运算的性质:以下性质都可以通过取模运算的定义来证明
- 四则运算法则:
- $(a+b)\%c=(a\%c+b\%c)\%c$
- $(a-b)\%c=(a\%c-b\%c)\%c$
- $(ab)\%c=(a\%cb)\%c=(a\%c*b\%c)\%c$
- $(a^b)\%c=((a\%c)^b)\%c$
- $\frac{a}{b}\%c = a\bar{b}\%c,\ if\ b \mid a, b\perp c$
- 分配律:$c(a\%b)=(ca)\%(cb)$,是mod运算最重要的代数性质
- 模模律:$a\%c\%c=a\%c$
[523] 连续子数组和为k的倍数
- 问题:给定一个包含非负数的数组和一个目标整数 k,编写一个函数来判断该数组是否含有连续的子数组,其大小至少为 2,总和为 k 的倍数,即总和为 n*k,其中 n 也是一个整数。
1 | 输入: [23,2,6,4,7], k = 6 |
- 思路:(a-b)%m=a%m-b%m,连续数组的和是k的倍数在k!=0时等价于能被k整除,等价于余数为0,等价于前i项和前j项和对k的模相等。记录模m第一次出现的位置,下次出现与首次出现隔了1个元素以上则返回True,初始没有元素设模为0,下标为-1.
- 代码:
1 | def checkSubarraySum(self, nums, k): |
大数取模
一般大数取模
对于某个大数,我们可以用字符串$s=a_1a_2…a_n$来存储,设f(n)代表前n个字符所组成的数值,则有:
利用取模四则运算法则来模拟手算竖式的方法:
代码:
1 | def get_mod(s,m): |
快速幂模
- 蒙哥马利(Montgomery)幂模运算是快速计算a^b%k的一种算法,是RSA加密算法的核心之一。设$f(c,e,n)=c^e\%n$,则有以下递推式:
代码:
1 | # 时间复杂度为lge |
[372] 超级次方——幂模
- 问题:你的任务是计算 a^b 对 1337 取模,a 是一个正整数,b 是一个非常大的正整数且会以数组形式给出。
- 思路:快速取模,取模的乘法法则(ab)%m=(a%mb%m)%m,f(a,b0+b110+…) = (a^b0%m f(a^10,b1+b2*10+…))%m
- 代码:
1 | def superPow(self, a, b): |
其他数论问题
勾股素数
- 问题:找到<=n的素勾股数 和 不能构成勾股数的边的长度个数。如果 (a, b, c) 是勾股数,它们的正整数倍数,也是勾股数,即 (na, nb, nc) 也是勾股数。若果 a, b, c 三者互质(它们的最大公因数是 1),它们就称为素勾股数。
- 思路:
1 | 以下的方法可用来找出勾股数。设 m > n 、 m 和 n 均是正整数, |
- 代码:
1 | #include<cstdio> |
整数-序列转化
这可能是最常用的操作了
1 | def n2s(n): |
[171] Excel列名转列号
- 问题:给定一个Excel表格中的列名称,返回其相应的列序号
1 | A -> 1 |
- 思路:26进制转化为10进制
- 代码:
1 | def titleToNumber(self, s): |
[168] Excel列号转列名
- 问题:给定一个正整数,返回它在 Excel 表中相对应的列名称
- 思路:难点在于A表示1而不是0,Ai26**i = ai26^i+26^i,a026^0+26^0 + a126^1+26^1 + … + =n,(n-1)/26 = a1*26^0 + 26^0+…转化为子问题,因此n = (n-1)/26,a0 = (n-1)%26 0~25对应A~Z
- 代码:
1 | def convertToTitle(self, n): |
[812] 三点构成最大面积
- 问题:给定包含多个点的集合,从其中取三个点组成三角形,返回能组成的最大三角形的面积
- 思路:遍历所有可能的三点组合,求出最大面积,核心在于已知三点如何求三点构成的三角形的面积,设A(x1,y1),B(x2,y2),C(x3,y3),则三角形面积可以表示为AB与AC叉乘绝对值的一半:
1 | i j |
- 代码:
1 | def largestTriangleArea(self, points): |
[593] 四点是否构成正方形
- 问题:给定二维空间中四点的坐标,返回四点是否可以构造一个正方形
- 思路:正方形充要条件:四条边相等,对角线相等>0;识别对角点的方法:如果是正方形,则排序后,第一个点代表左下点,最后一个点代表右上点,它们必是对角顶点
- 代码:
1 | def validSquare(self, p1, p2, p3, p4): |
[223] 矩形面积
- 问题:在二维平面上计算出两个由直线构成的矩形重叠后形成的总面积。每个矩形由其左下顶点和右上顶点坐标表示,如图所示。
- 思路:转化为求交集的面积sum() = max(0,min(D,H)-max(B,F)) * max(0,min(C,G)-max(A,E))
- 代码:
1 | def computeArea(self, A, B, C, D, E, F, G, H): |
[453] 最小移动次数使数组元素相等
- 问题:给定一个长度为 n 的非空整数数组,找到让数组所有元素相等的最小移动次数。每次移动可以使 n - 1 个元素增加 1
1 | 输入: |
- 思路::一次移动将n - 1个元素加1,等价于将剩下的1个元素减1。因此累加数组中各元素与最小值之差即可
- 代码:
1 | def minMoves(self, nums): |
[462] 最少移动次数使数组元素相等 II
- 问题:给定一个非空整数数组,找到使所有数组元素相等所需的最小移动数,其中每次移动可将选定的一个元素加1或减1
1 | 输入: |
- 思路:中位数定理:当a取x中的中位数时,绝对残差和sum(|x-a|)最小
- 代码:
1 |
|
[517] 超级洗衣机——均衡
- 问题:假设有 n 台超级洗衣机放在同一排上。开始的时候,每台洗衣机内可能有一定量的衣服,也可能是空的。在每一步操作中,你可以选择任意 m (1 ≤ m ≤ n) 台洗衣机,与此同时将每台洗衣机的一件衣服送到相邻的一台洗衣机。给定一个非负整数数组代表从左至右每台洗衣机中的衣物数量,请给出能让所有洗衣机中剩下的衣物的数量相等的最少的操作步数。如果不能使每台洗衣机中衣物的数量相等,则返回 -1。
- 思路:
1 | 讨论每个元素处的情形,记l,r代表i元素左右两侧和超出平均水平的值,讨论三种情形: |
- 代码:
1 | def findMinMoves(self, machines): |
[319] 灯泡开关
- 问题:初始时有 n 个灯泡关闭。 第 1 轮,你打开所有的灯泡。 第 2 轮,每两个灯泡你关闭一次。 第 3 轮,每三个灯泡切换一次开关(如果关闭则开启,如果开启则关闭)。第 i 轮,每 i 个灯泡切换一次开关。 对于第 n 轮,你只切换最后一个灯泡的开关。 找出 n 轮后有多少个亮着的灯泡。
- 思路:对于第i个灯泡,当i的因子个数为奇数时,最终会保持点亮状态,例如9的因子为1,3,9,当且仅当i为完全平方数时,其因子个数为奇数,另外1~n中完全平方数的个数为n**0.5
- 代码:
1 | def bulbSwitch(self, n): |
[172] 阶乘后的零
- 问题:给定一个整数 n,返回 n! 结果尾数中零的数量
- 思路:n!后缀0的个数 = n!质因子中5的个数 = floor(n/5) + floor(n/25) + floor(n/125) + ….
- 代码:
1 | def trailingZeroes(self, n): |
[343] 整数拆分
- 问题:给定一个正整数 n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。 返回你可以获得的最大乘积。例如,给定 n = 2,返回1(2 = 1 + 1);给定 n = 10,返回36(10 = 3 + 3 + 4)。注意:你可以假设 n 不小于2且不大于58。
- 思路:向下分解到3就不能再分了
- 如果刚好,返回3**(n/2)
- 如果余1,就把1加到3上构成4
- 如果余2,乘2
- 代码:
1 | def integerBreak(self, n): |
[279] 完全平方数——四平方和定理
- 问题:给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, …)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。
1 | 输入: n = 13 |
- 思路1:动态规划,状态转移方程dp[n] = min(dp[n-i^2]) + 1,if n不是完全平方数,如果是完全平方数返回1,时间复杂度O(n * sqrt n);
- 思路2:四平方和定理:任何自然数都可以用最多四个平方和数之和,且只有当$n=4^a(8b+7)$时才需要最少四个平方数之和;
- 代码:
1 | # 思路1 |
[368] 最大整除子集
- 问题:给出一个由无重复的正整数组成的集合, 找出其中最大的整除子集, 子集中任意一对 (Si, Sj) 都要满足: Si % Sj = 0 或 Sj % Si = 0。如果有多个目标子集,返回其中任何一个均可
1 | 集合: [1,2,3] |
- 思路:动态规划,dp[x] = max(dp[x], dp[y] + 1) 其中: 0 <= y < x 且 nums[x] % nums[y] == 0记录最大路径,只需每次转移时,记录x的父节点,最后即可从最大DP处追溯到整条最大的路径
- 代码:
1 | def largestDivisibleSubset(self, nums): |
[670] 最大交换
- 问题:给定一个非负整数,你至多可以交换一次数字中的任意两位。返回你能得到的最大值。
1 | 输入: 2736 |
- 思路:从后向前,记录当前后缀中最大元素的位置,相等算后面的;从前向后,如果当前位置元素小于后续最大元素,则交换二者;
- 代码:
1 | def maximumSwap(self, num): |
[754] 到达终点数字
- 问题:在一根无限长的数轴上,你站在0的位置。终点在target的位置。每次你可以选择向左或向右移动。第 n 次移动(从 1 开始),可以走 n 步。返回到达终点需要的最小移动次数。
1 | 输入: target = 2 |
- 思路:
1 | 分情况讨论: |
- 代码:
1 | import math |
- 分析:时间复杂度O(1)
[50] 快速幂
- 问题:实现 pow(x, n) ,即计算 x 的 n 次幂函数
- 思路:
1 | 快速计算幂:时间复杂度lgn |
- 代码:
1 | def myPow(self, x, n): |
[829] 连续整数求和
- 问题:给定一个正整数 N,试求有多少组连续正整数满足所有数字之和为 N?
1 | 输入: 9 |
- 思路:
1 | 思路2:假设连续数组长度为c,那么和为N的数组满足: |
- 代码:
1 | def consecutiveNumbersSum(self, N): |
[400] 第N个数字
- 问题:在无限的整数序列 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, …中找到第 n 个数字。
1 | 输入: |
- 思路:
1 | 1 1-9 |
- 代码:
1 | def findNthDigit(self, n): |
[233] 数字1的个数
- 问题:给定一个整数 n,计算所有小于等于 n 的非负整数中数字 1 出现的个数。
- 思路:
1 | 分类讨论1在各个位上出现的次数:if n = xyzdabc,现讨论千位上的1出现的次数 |
- 代码:
1 | def countDigitOne(self, n): |
[43] 字符串相乘
- 问题: 给定两个以字符串形式表示的非负整数 num1 和 num2,返回 num1 和 num2 的乘积,它们的乘积也表示为字符串形式。
1 | 输入: num1 = "123", num2 = "456" |
思路: 模拟手动乘法,从后向前,div,mod=divmod(xi*yj,10),mod存放在数组res的i+j位,div存放在i+j+1位,最后从后向前遍历res,div,mod=divmod(res[i],10),mod留到i位,div加到i+1位,结束时如果div不为0则创建新的位来存
代码:
1 | def multiply(self, num1, num2): |
不使用乘除的除法
- 问题:给定两个整数,被除数 dividend 和除数 divisor。将两数相除,要求不使用乘法、除法和 mod 运算符。
1 | 输入: dividend = 7, divisor = -3 |
- 思路:a/b=c,则:a = bc + m=(b2^k1 + b 2^k2 + …b2^0 + m),尝试用被除数减y的231…20倍数,如果减得动则把相应的倍数加到商中,如果减不动则尝试更小的倍数
- 代码:
1 | def divide(self, dividend, divisor): |
[166] 分数到小数
- 问题:给定两个整数,分别表示分数的分子 numerator 和分母 denominator,以字符串形式返回小数。如果小数部分为循环小数,则将循环的部分括在括号内。
1 | 输入: numerator = 2, denominator = 3 |
- 思路:
1 | 思路:模拟手除法 |
- 代码:
1 | def fractionToDecimal(self, numerator, denominator): |
[149] 直线上最多的点数——按起点分类
- 问题:给定一个二维平面,平面上有 n 个点,求最多有多少个点在同一条直线上。
1 | 输入: [[1,1],[3,2],[5,3],[4,1],[2,3],[1,4]] |
- 思路1:对于每个点统计其他点到它的斜率出现的次数,次数最大的斜率,对应共线点数最多的直线;需注意如果横坐标相同时记斜率为None;可能含有重复的点,重复的点可以算作任何斜率
- 思路2:RANSAC (Random sample consensus)算法,每次随机抽出一对点,找到所有和他们共线的点数,只需要重复几十次抽样就可以得到正确结果(但不保证每次都能成功)
- 代码:
1 | # 思路1: |
[810] 黑板异或游戏(位运算)
- 问题:一个黑板上写着一个非负整数数组 nums[i] 。小红和小明轮流从黑板上擦掉一个数字,小红先手。如果擦除一个数字后,剩余的所有数字按位异或运算得出的结果等于 0 的话,当前玩家游戏失败。 (另外,如果只剩一个数字,按位异或运算得到它本身;如果无数字剩余,按位异或运算结果为 0。)换种说法就是,轮到某个玩家时,如果当前黑板上所有数字按位异或运算结果等于 0,这个玩家获胜。假设两个玩家每步都使用最优解,当且仅当小红获胜时返回 true。
- 思路:
1 | bit : 3 2 1 0 |
- 代码:
1 | def xorGame(self, nums): |