【推荐算法工程师技术栈系列】数据结构与算法--数学建模与解题方法

常见符号

复杂度函数

  1. \(O\) 符号:当且仅当存在正实数 \(M\) 和实数 \(x_0\) ,使得 \(\forall x\geq x_0,\ |f(x)|\leq M|g(x)|\) ,我们就可以认为, \(f(x)=O(g(x))\)
  2. \(\Omega\) 符号:当且仅当存在正实数 \(M\) 和实数 \(x_0\) ,使得 \(\forall x\geq x_0,\ f(x)\geq Mg(x)\) ,我们就可以认为, \(f(x)=\Omega (g(x))\) . 大 \(O\) 与大 \(\Omega\) 恰好相反,即 \(f(x)=O(g(x))\Leftrightarrow g(x)=\Omega(f(x))\)
  3. \(\Theta\) 符号:大 \(\Theta\) 符号是大 \(\text{O}\) 和大 \(\Omega\) 的结合,即 \(f(x)=O(g(x))\wedge f(x)=\Omega(g(x))\ \Rightarrow f(x)=\Theta(g(x))\)

整除/同余理论常见符号

  1. 整除符号: \(x\mid y\) ,表示 \(x\) 整除 \(y\) ,即 \(x\)\(y\) 的因数。
  2. 取模符号: \(x\bmod y\) ,表示 \(x\) 除以 \(y\) 得到的余数。
  3. 互质符号: \(x\perp y\) ,表示 \(x\) , \(y\) 互质。
  4. 最大公约数: \(\gcd(x,y)\) ,在无混淆意义的时侯可以写作 \((x,y)\)
  5. 最小公倍数: \(\operatorname{lcm}(x,y)\) ,在无混淆意义的时侯可以写作 \([x,y]\)

数论函数常见符号

求和符号: \(\sum\) 符号,表示满足特定条件的数的和。举几个例子:

  • \(\sum_{i=1}^n i\) 表示 \(1+2+\dotsb+n\) 的和。其中 \(i\) 是一个变量,在求和符号的意义下 \(i\) 通常是 正整数或者非负整数 (除非特殊说明)。这个式子的含义可以理解为, \(i\)\(1\) 循环到 \(n\) ,所有 \(i\) 的和。这个式子用代码的形式很容易表达。当然,学过简单的组合数学的同学都知道 \(\sum_{i=1}^n i=\frac{n(n+1)}{2}\)
  • \(\sum_{S\subseteq T}|S|\) 表示所有被 \(T\) 包含的集合的大小的和。
  • \(\sum_{p\le n,p\perp n}1\) 表示的是 \(n\) 以内有多少个与 \(n\) 互质的数,即 \(\varphi(n)\)\(\varphi\) 是欧拉函数。

求积符号: \(\prod\) 符号,表示满足特定条件的数的积。举几个例子:

  • \(\prod_{i=1}^ni\) 表示 \(n\) 的阶乘,即 \(n!\) 。在组合数学常见符号中会讲到。
  • \(\prod_{i=1}^na_i\) 表示 \(a_1\times a_2\times a_3\times \dotsb\times a_n\)
  • \(\prod_{x|d}x\) 表示 \(d\) 的所有因数的乘积。

在行间公式中,求和符号与求积符号的上下条件会放到符号的上面和下面,这一点要注意。

其他常见符号

  1. 阶乘符号 \(!\)\(n!\) 表示 \(1\times 2\times 3\times \dotsb \times n\)
  2. 向下取整符号: \(\lfloor x\rfloor\) ,表示小于等于 \(x\) 的最大的整数。常用于分数,比如分数的向下取整 \(\left\lfloor\frac{x}{y}\right\rfloor\)
  3. 向上取整符号: \(\lceil x\rceil\) ,与向下取整符号相对,表示大于等于 \(x\) 的最小的整数。

最优化初步

优化问题及其性质

最优化,是应用数学的一个分支,主要研究以下形式的问题:
给定一个函数\(f : A -> R\),寻找一个元素\(x^0 \in A\)使得对于所有A中的x,\(f(x^0)\leq f(x)\)(最小化);或者\(f(x^0)\geq f(x)\)(最大化)。
这类定式有时还称为“数学规划”(譬如,线性规划)。许多现实和理论问题都可以建模成这样的一般性框架。
典型的,A一般为欧几里得空间\(R^n\)中的子集,通常由一个A必须满足的约束等式或者不等式来规定。 A的元素被称为是可行解。函数f被称为目标函数,或者代价函数。一个最小化(或者最大化)目标函数的可行解被称为最优解。
一般情况下,会存在若干个局部的极小值或者极大值。局部极小值\(x^*\)定义为对于一些\(\delta >0\),以及所有的x满足\(||f(x) - f(x^*)|| \leq \delta\);
公式\(f(x^*) \leq f(x)\) 成立。这就是说,在$f(x^*) $周围的一些闭球上,所有的函数值都大于或者等于在该点的函数值。一般的,求局部极小值是容易的,但是要确保其为全域性的最小值,则需要一些附加性的条件,例如,该函数必须是凸函数。

主要分支

  • 线性规划:当目标函数f是线性函数而且集合A是由线性等式函数和线性不等式函数来确定的, 我们称这一类问题为线性规划
  • 整数规划:当线性规划问题的部分或所有的变量局限于整数值时, 我们称这一类问题為整数规划问题
  • 二次规划:目标函数是二次函数,而且集合A必须是由线性等式函数和线性不等式函数来确定的。
  • 分数规划:研究的是如何优化两个非线性函数的比例。
  • 非线性规划:研究的是目标函数或是限制函数中含有非线性函数的问题。
  • 随机规划:研究的是某些变量是随机变量的问题。
  • 动态规划:研究的是最优策略基于将问题分解成若干个较小的子问题的优化问题。
  • 组合最优化:研究的是可行解是离散或是可转化为离散的问题。
  • 无限维最优化:研究的是可行解的集合是无限维空间的子集的问题,一个无限维空间的例子是函数空间。

无约束优化问题

对于无约束的优化问题, 如果函数是二次可微的话,可以通过找到目标函数梯度为0(也就是拐点)的那些点来解决此优化问题。我们需要用黑塞矩阵来确定此点的类型。如果黑塞矩阵是正定的话,该点是一个局部最小解, 如果是负定的话,该点是一个局部最大解,如果黑塞矩阵是不定的话,该点是某种鞍点
要找到那些拐点,我们可以通过猜测一个初始点,然后用比如以下的迭代的方法来找到。

如果目标函数在我们所关心的区域中是凸函数的话,那么任何局部最小解也是全局最优解。现在已经有稳定,快速的数值计算方法来求二次可微地凸函数的最小值。

有约束优化问题

有約束條件的约束问题常常可以通过拉格朗日乘数转化为非约束问题。
其他一些流行的方法有:

概率统计

  • 概率论(英语:Probability theory)是集中研究概率及随机现象的数学分支,是研究随机性或不确定性等现象的数学。概率论主要研究对象为随机事件、随机变量以及随机过程

  • 统计学是在数据分析的基础上,研究测定、收集、整理、归纳和分析反映数据数据,以便给出正确消息的科学。譬如自一组数据中,可以摘要并且描述这份数据的集中和离散情形,这个用法称作为描述统计学。另外,观察者以数据的形态,创建出一个用以解释其随机性和不确定性的数学模型,以之来推论研究中的步骤及总体,这种用法被称做推论统计学。这两种用法都可以被称作为应用统计学数理统计学则是讨论背后的理论基础的学科。
    描述统计学处理有关叙述的问题:是否可以摘要的说明数据的情形,不论是以数学或是图片表现,以用来代表总体的性质?基础的数学描述包括了平均数和标准差等。图像的摘要则包含了许多种的表和图。主要是就说明数据的集中和离散情形。
    推论统计学被用来将数据中的数据模型化,计算它的概率并且做出对于总体的推论。这个推论可能以对/错问题的答案所呈现(假设检定),对于数字特征量的估计(估计),对于未来观察的预测,关系性的预测(相关性),或是将关系模型化(回归)。其他的模型化技术包括方差分析,时间序列,以及数据挖掘

样本及抽样分布

抽样(Sampling)是一种推论统计方法,它是指从目标总体(Population,或称为母体)中抽取一部分个体作为样本(Sample),通过观察样本的某一或某些属性,依据所获得的数据对总体的数量
特征得出具有一定可靠性的估计判断,从而达到对总体的认识。常用的抽样方法简单随机抽样,分层抽样,系统抽样和整群抽样。

参数:描述总体或概率分布的数量值,是未知的;如:正态分布的均值, 正态分布的标准差等

样本统计量(statistic):样本数据特征值的数量描述;如:样本均值, 样本比例,样本方差等

抽样分布:在重复选取容量为n的样本时,由该统计量的所有可能取值形成的相对频数分布。三大抽样分布:卡方分布,F分布,和t分布

概率分布

常见概率分布图表总结
Zipf分布以及对数正态分布
概率论杂记

参数估计

假设检验

(1) 假设检验问题: 在总体的分布函数完全未知,或只知其形式但不知其参数的情况,提出某些关于总体分布函数或对于其参数的假设,然后抽取样本,构造合适的统计量,在根据样本对所提的假设作出是接受还是拒绝的决策,这样的问题称为假设检验问题。
(2) 检验法: 借助于样本值来判断接受假设还是拒绝假设的法则,称为检验法。
(3) 原假设与备择假设: 称需要着重观察的假设为原假设(试验者想收集证据予以反对的假设 ,又称“零假设”),原假设常记为H0。与原假设相对立的假设称为备择假设或者对立假设,备择假设通常记为H1。
(4) 检验统计量: 如果根据某一统计量的观测值来决定接受H0还是拒绝H0,这一统计量称为检验统计量。
(5) 拒绝域和临界点: 当检验统计量的观测值落在某个区域时就拒绝H0,这一区域称为拒绝域,拒绝域的边界点称为临界点。
(6) 显著性水平: 在做检验时要求犯第一类错误的概率不大于α,α称为检验的显著性水平。
(7) 显著性检验: 对于给定的样本容量,只控制犯第一种错误的概率,而不考虑犯第二种错误的概率的检验法,称为显著性检验。

边界值
临界值是在原假设下,检验统计量在分布图上的点,这些点定义一组要求否定原假设的值。这组值称为临界或否定区域。通常,单侧检验有一个临界值,双侧检验有两个临界值。在临界值处,当原假设为真时,检验统计量在检验的否定区域中有值的概率等于显著性水平(用 α 或 alpha 表示)

P值
即概率,反映某一事件发生的可能性大小。统计学根据显著性检验方法所得到的P值,一般以P < 0.05 为有统计学差异, P<0.01 为有显著统计学差异,P<0.001为有极其显著的统计学差异
显著性水平p是指在原假设为真的条件下,样本数据拒绝原假设这样一个事件发生的概率。例如,我们根据某次假设检验的样本数据计算得出显著性水平 p = 0.04;这个值意味着如果原假设为真,我们通过抽样得到这样一个样本数据的可能性只有 4%。
那么,0.04 这个概率或者说显著性水平到底是大还是小,够还是不够用来拒绝原假设呢?这就需要把 p 和我们采用的第 I 类错误的小概率标准 α 来比较确定。假设检验的决策规则:
若 p ≤ α,那么拒绝原假设;
若 p > α,那么不能拒绝原假设。

如果 α 取 0.05 而 p = 0.04,说明如果原假设为真,则此次试验发生了小概率事件。根据小概率事件不会发生的判断依据,我们可以反证认为原假设不成立。

置信区间
置信区间是从样本统计量派生的值范围,可能包含未知总体参数的值。由于置信区间具有随机性,因此来自特定总体的两个样本将不可能生成相同的置信区间。但是,如果您将样本重复多次,则在所生成的置信区间中有特定百分比的置信区间将包含未知总体参数。
如果置信区间同为正或同为负,说明试验结果是统计显著的。如果置信区间为一正一负,说明试验结果是非统计显著的。

方差分析及回归分析

回归分析(regression analysis) 是确定两种或两种以上变量间相互依赖的定量关系的一种统计分析方法。运用十分广泛,回归分析按照涉及的变量的多少,分为一元回归和多元回归分析;按照因变量的多少,可分为简单回归分析和多重回归分析;按照自变量和因变量之间的关系类型,可分为线性回归分析和非线性回归分析。

方差分析 (Analysis of Variance,简称 ANOVA),又称 “变异数分析”,用于两个及两个以上样本均数差别的显著性检验。方差分析是从观测变量的方差入手,研究诸多控制变量中哪些变量是对观测变量有显著影响的变量。其分为单因素方差分析,无交互作用的双因素方差分析,有交互作用的双因素方差分析,多因素正交表设计及方差分析。

方差分析考察的是自变量对因变量的影响是否显著,而回归分析考察的是自变量与因变量之间的可以用数学表达式来表示的线性相关关系。

随机过程

在概率论概念中,随机过程是随机变量的集合。若一随机系统的样本点是随机函数,则称此函数为样本函数,这一随机系统全部样本函数的集合是一个随机过程。实际应用中,样本函数的一般定义在时间域或者空间域。随机过程的实例如股票和汇率的波动、语音信号、视频信号、体温的变化,随机运动如布朗运动、随机徘徊等等

随机过程的一些例子:伯努利过程,正弦波过程(\(x(t) = v\sin (\omega t + \phi)\))

随机过程是一个统称,根据T是否连续可以分为离散随机过程和连续随机过程。离散的随机过程也叫作随机序列或者时间序列

随机算法

随机化算法(randomized algorithm),是这样一种算法,在算法中使用了随机函数,且随机函数的返回值直接或者间接的影响了算法的执行流程或执行结果。就是将算法的某一步或某几步置于运气的控制之下,即该算法在运行的过程中的某一步或某几步涉及一个随机决策,或者说其中的一个决策依赖于某种随机事件。随机算法(概率算法)大致分为四类:数值概率算法,蒙特卡罗(Monte Carlo)算法,拉斯维加斯(Las Vegas)算法和舍伍德(Sherwood)算法

数值概率算法

数值概率算法常用于数值问题的求解。这类算法所得到的往往是近似解。而且近似解的精度随计算时间的增加不断提高。在许多情况下,要计算出问题的精确解是不可能或没有必要的,因此用数值概率算法可得到相当满意的解。

蒙特卡罗(Monte Carlo)算法

蒙特卡罗(Monte Carlo)算法用于求问题的准确解。用蒙特卡罗算法能求得问题的一个解,但这个解未必是正确的。求得正确解的概率依赖于算法所用的时间。算法所用的时间越多,得到正确解的概率就越高。蒙特卡罗算法的主要缺点就在于此。一般情况下,无法有效判断得到的解是否肯定正确。
蒙特卡罗方法的解题过程可以归结为三个主要步骤:
1)构造或描述概率过程;
2)实现从已知概率分布抽样:使用伪随机数序列模拟概率分布采样
3)建立各种估计量。

蒙特卡罗(Monte Carlo)算法实例

拉斯维加斯(Las Vegas)算法

拉斯维加斯(Las Vegas)算法不会得到不正确的解,一旦用拉斯维加斯算法找到一个解,那么这个解肯定是正确的。但是有时候用拉斯维加斯算法可能找不到解。与蒙特卡罗算法类似。拉斯维加斯算法得到正确解的概率随着它用的计算时间的增加而提高。对于所求解问题的任一实例,用同一拉斯维加斯算法反复对该实例求解足够多次,可使求解失效的概率任意小。

舍伍德(Sherwood)算法

舍伍德(Sherwood)算法总能求得问题的一个解,且所求得的解总是正确的。当一个确定性算法在最坏情况下的计算复杂性与其在平均情况下的计算复杂性有较大差别时,可以在这个确定算法中引入随机性将它改造成一个舍伍德算法,消除或减少问题的好坏实例间的这种差别。舍伍德算法精髓不是避免算法的最坏情况行为,而是设法消除这种最坏行为与特定实例之间的关联性。这意味着不存在坏的输入,只有坏的随机数。

数论

整除及其性质

最大公约数

最大公约数即为 Greatest Common Divisor,常缩写为 gcd。
在 素数 一节中,我们已经介绍了约数的概念。
一组数的公约数,是指同时是这组数中每一个数的约数的数。而最大公约数,则是指所有公约数里面最大的一个。

欧几里德算法

如果我们已知两个数 \(a\)\(b\) ,如何求出二者的最大公约数呢?
不妨设 \(a > b\)
我们发现如果 \(b\)\(a\) 的约数,那么 \(b\) 就是二者的最大公约数。 下面讨论不能整除的情况,即 \(a = b \times q + r\) ,其中 \(r < b\)
我们通过证明可以得到 \(\gcd(a,b)=\gcd(b,a \bmod b)\) ,过程如下:
\(a=bk+c\) ,显然有 \(c=a \bmod b\) 。设 \(d|a\\\ d|b\)(\(\mid\)为整除符号) ,则 \(c=a-bk\) \(\frac{c}{d}=\frac{a}{d}-\frac{b}{d}k\) 由右边的式子可知 \(\frac{c}{d}\) 为整数,即 \(d|c\) 所以对于 \(a,b\) 的公约数,它也会是 \(a \bmod b\) 的公约数。
反过来也需要证明
\(d|b\ \ \ d|(a \bmod b)\) ,我们还是可以像之前一样得到以下式子 \(\frac{a\bmod b}{d}=\frac{a}{d}-\frac{b}{d}k\) \(\frac{a\bmod b}{d}+\frac{b}{d}k=\frac{a}{d}\) 因为左边式子显然为整数,所以 \(\frac{a}{d}\) 也为整数,即 \(d|a\) ,所以 \(b,a\bmod b\) 的公约数也是 \(a,b\) 的公约数。
既然两式公约数都是相同的,那么最大公约数也会相同。
所以得到式子 \(\gcd(a,b)=\gcd(b,a\bmod b)\)
既然得到了 \(\gcd(a, b) = \gcd(b, r)\) ,这里两个数的大小是不会增大的,那么我们也就得到了关于两个数的最大公约数的一个递归求法。

int gcd(int a, int b) {
  if (b == 0) return a;
  return gcd(b, a % b);
}

非递归写法

public int GCD(int a, int b){
    while(b != 0 ){
        int temp = b;
        b = a%b;
        a = temp;
    }
    return a;
}

递归至 b0 (即上一步的 a%b0 ) 的情况再返回值即可。
上述算法被称作欧几里德算法(Euclidean algorithm)。
如果两个数 \(a\)\(b\) 满足 \(\gcd(a, b) = 1\) ,我们称 \(a\)\(b\) 互质。

欧拉函数

欧拉函数(Euler's totient function),即 \(\varphi(n)\) ,表示的是小于等于 \(n\)\(n\) 互质的数的个数
比如说 \(\varphi(1) = 1\)
当 n 是质数的时候,显然有 \(\varphi(n) = n - 1\)
利用唯一分解定理,我们可以把一个整数唯一地分解为质数幂次的乘积,
\(n = p_1^{k_1}p_2^{k_2} \cdots p_s^{k_s}\) ,其中 \(p_i\) 是质数,那么定义 \(\varphi(n) = n \times \prod_{i = 1}^s{\frac{p_i - 1}{p_i}}\)

欧拉函数的一些神奇性质

  • 欧拉函数是积性函数。
    积性是什么意思呢?如果有 \(\gcd(a, b) = 1\) ,那么 \(\varphi(a \times b) = \varphi(a) \times \varphi(b)\)
    特别地,当 \(n\) 是奇数时 \(\varphi(2n) = \varphi(n)\)

  • \(n = \sum_{d | n}{\varphi(d)}\)
    利用 莫比乌斯反演 相关知识可以得出。
    也可以这样考虑:如果 \(\gcd(k, n) = d\) ,那么 \(\gcd(\frac{k}{d},\frac{n}{d}) = 1\) 。( \(k < n\)
    如果我们设 \(f(x)\) 表示 \(\gcd(k, n) = x\) 的数的个数,那么 \(n = \sum_{i = 1}^n{f(i)}\)
    根据上面的证明,我们发现, \(f(x) = \varphi(\frac{n}{x})\) ,从而 \(n = \sum_{d | n}\varphi(\frac{n}{d})\) 。注意到约数 \(d\)\(\frac{n}{d}\) 具有对称性,所以上式化为 \(n = \sum_{d | n}\varphi(d)\)

  • \(n = p^k\) ,其中 \(p\) 是质数,那么 \(\varphi(n) = p^k - p^{k - 1}\) 。 (根据定义可知)

如何求欧拉函数值
如果只要求一个数的欧拉函数值,那么直接根据定义质因数分解的同时求就好了。

int ol(int x) {
  int res=x;
  for(int i=2;i*i<=x;i++) {
      if(x%i==0) {
         res=res-res/i;
         while(x%i==0)
             x/=i;
      }
  }
  if(x>1) res=res-res/x;
  return res;
}

如果是多个数的欧拉函数值,可以利用后面会提到的线性筛法来求得。 详见:筛法求欧拉函数

欧拉定理
与欧拉函数紧密相关的一个定理就是欧拉定理。其描述如下:
\(\gcd(a, m) = 1\) ,则 \(a^{\varphi(m)} \equiv 1 \pmod{m}\)

扩展欧拉定理(降幂公式)
当然也有扩展欧拉定理

\[a^b\equiv a^{b\bmod\varphi(p)+\varphi(p)} \pmod p, \gcd(a,p) \ne 1,b \ge \varphi(p) \]

证明和习题详见欧拉定理

同余与同余方程

裴蜀定理

裴蜀定理,又称贝祖定理(Bézout's lemma/Bézout's identity);若a,b是整数,且gcd(a,b)=d,那么对于任意的整数x,y,ax+by都一定是d的倍数,特别地,一定存在整数x,y,使ax+by=d成立
证明见裴蜀定理

问题:有两个容量分别为 x升 和 y升 的水壶以及无限多的水。请判断能否通过使用这两个水壶,从而可以得到恰好 z升 的水?

public boolean canMeasureWater(int x, int y, int z) {
    // 两个水壶都倒满也得不到所求
    if(x + y < z) return false;
    if( x == z || y == z || x + y == z ) return true;
    // 裴蜀定理:x,y不全为0时,一定存在整数a,b;使得ax+by % gcd(x,y) = 0;
    return z%GCD(x, y) == 0;
}

public int GCD(int a, int b){
    while(b != 0 ){
        int temp = b;
        b = a%b;
        a = temp;
    }
    return a;
}

快速幂和矩阵快速幂

快速幂,二进制取幂(Binary Exponentiation,也称平方法),是一个在 \(\Theta(\log n)\) 的时间内计算 \(a^n\) 的小技巧,而暴力的计算需要 \(\Theta(n)\) 的时间。而这个技巧也常常用在非计算的场景,因为它可以应用在任何具有结合律的运算中。其中显然的是它可以应用于模意义下取幂、矩阵幂等运算。

求$a^n = a^{c_1} * a^{c_2} ... a*{c_k} \(,其中c1,c2,...,ck是b对应二进制表示中为一的权值;时间复杂度O(\)log_2N$)。
将上述过程说得形式化一些,如果把 \(n\) 写作二进制为 $(n_t n_{t-1} ... n_1 n_0)_2 $ ,那么有:

\[n = n_t2^t + n_{t-1}2^{t-1} + n_{t-2}2^{t-2} + \cdots + n_12^1 + n_02^0 \]

其中 \(n_i\in{0,1}\) 。那么就有

\[\begin{aligned} a^n & = (a^{n_t 2^t + \cdots + n_0 2^0}) = a^{n_0 2^0} \times a^{n_1 2^1}\times \cdots \times a^{n_t2^t} \end{aligned} \]

根据上式我们发现,原问题被我们转化成了形式相同的子问题的乘积,并且我们可以在常数时间内从 \(2^i\) 项推出 \(2^{i+1}\) 项。

int binPow(int a,int b)
{
    int ans = 1,base = a;
    while(b!=0)
    {
        if(b&1)
            ans *= base;
        base *= base;
        b>>=1;
    }
    return ans;

矩阵形式

public static int[][] multiply(int [][]a,int[][]b){
    int[][]arr=new int[a.length][b[0].length];
    for(int i=0;i<a.length;i++){
        for(int j=0;j<b[0].length;j++){
            for(int k=0;k<a[0].length;k++){
                arr[i][j]+=a[i][k]*b[k][j];
            }
        }
    }
    return arr;
}
// 矩阵快速幂a^n
public static int[][] matrixPower(int[][]a,int n){
    int[][] res=new int[a.length][a[0].length];
    // 初始化为单位矩阵I
    for(int i=0;i<res.length;i++){
        for(int j=0;j<res[0].length;j++){
            if(i==j)
                res[i][j]=1;
            else
                res[i][j]=0;
        }

    }
    while(n!=0){
        if((n&1)==1)
            res=multiply(res,a);
        n>>=1;
        a=multiply(a,a);
    }
    return res;
}

模意义下取模

计算 $x^y \mod M \( 注意:根据[费马小定理](https://oi-wiki.org/math/fermat/),如果M是一个质数,我们可以计算\)x^{n \mod (m-1)}$来加速算法过程。

int pow_mod(long x,int y,int M) {
    long res=1;
    while(y>0) {
        if( y % 2 > 0 ){
            res = res * x % M;
        }
        // 快速幂
        x = x * x % M;
        y /= 2;
    }
    return (int)res;
}

数根

在数学中,数根(又称位数根数字根Digital root)是自然数的一种性质,换句话说,每个自然数都有一个数根。
数根是将一正整数的各个位数相加(即横向相加),若加完后的值大于10的话,则继续将各位数进行横向相加直到其值小于十为止,或是,将一数字重复做数字和,直到其值小于十为止,则所得的值为该数的数根。例如54817的数根为7,因为5+4+8+1+7=25,25大于10则再加一次,2+5=7,7小于十,则7为54817的数根。

性质

  • 1.两数之和的数字根等于这两个数的数字根的和 $ dr_b(a_1+a_2) = dr_b(dr_b(a_1) + dr_b(a_2)) $
  • 2.两数之差的数字根同余这两个数的数字根的差模b-1 $ dr_b(a_1-a_2) \equiv (dr_b(a_1) - dr_b(a_2)) \mod b-1 $
  • 3.两数之积的数字根等于这两个数的数字根的积 $ dr_b(a_1a_2) = dr_b(dr_b(a_1)*dr_b(a_2)) $

计算公式

见附录

用途

数根可以计算模运算同余,对于非常大的数字的情况下可以节省很多时间。
数字根可作为一种检验计算正确性的方法。例如,两数字的和的数根等于两数字分别的数根的和。
另外,数根也可以用来判断数字的整除性,如果数根能被3或9整除,则原来的数也能被3或9整除。

其他数学问题

约瑟夫环

定义:N个人围成一圈,第一个人从0开始报数,报M-1的将被杀掉,下一个人接着从0开始报。如此反复,最后剩下一个,求最后的胜利者。
解法:
初始条件\(f(1,M) = 0\)
递推公式:$f(N,M) = (f(N-1,M) + M) \% N \( 推导: 根据规则,当有人出列之后,下一个位置的人又从0开始报数,则以上列表可调整为以下形式(即以M位置开始,N–1之后再接上0、1、2……,形成环状): M M+1 M+2 … N-2 N-1 0 1 … M-3 M-2 按上面排列的顺序从0开始重新编号,可得到下面的对应关系: M M+1 M+2 … N-2 N-1 0 1 … M-3 M-2 0 1 2 … N-(M+2) N-(M+1) N-M N-(M-1) … N-3 N-2 这里,**假设上一行的数为x,下一行的数为y,则对应关系**为:\)y = (x - M + N) \% N \(,反过来有\) x = (y + M) \% N $

问题中的规模最小时是什么情况?就是只有1个人时(N=1),报数到(M–1)的人出列,这时最后出列的是谁?当然只有编号为0这个人。因此,可设有以下函数:
F(1) = 0
那么,当N=2,报数到(M–1)的人出列,最后出列的人是谁?应该是只有一个人报数时得到的最后出列的序号加上M,因为报到M-1的人已出列,只有2个人,则另一个出列的就是最后出列者,利用公式$ x = (y + M) \% N $,可表示为以下形式:
F(2) = [F(1) + M] % N

斐波那契数列(黄金分割数列)

斐波那契数列(意大利语:Successione di Fibonacci),又译为菲波拿契数列、菲波那西数列、斐氏数列、黄金分割数列。
在数学上,斐波那契数列是以递归的方法来定义:
F(0)=0
F(1)=1
F(n0=F(n-1)+F(n-2)(n≧2)

兔子对的数量就是斐波那契数列。
可通过编程观察斐波那契数列。分为两类问题,一种已知数列中的某一项,求序数。第二种是已知序数,求该项的值
可通过递归递推的算法解决此两个问题。 事实上当n相当巨大的时候,O(n)的递推/递归非常慢……这时候要用到矩阵快速幂这一技巧,可以使递归加速到O(logn)。

开普勒发现数列前、后两项之比1/2 ,2/3 , 3/5 ,5/8 ,8/13 ,13/21 ,21/34 ,...... ,也组成了一个数列,会趋近黄金分割
$ \frac{f_{n+1}}{f_n} \approx \frac{1}{2}(1+\sqrt{5}) = \varphi \approx 1.618 \( 通项公式为\)F_n = \frac{1}{\sqrt{5}} [(\frac{1+\sqrt{5}}{2})^n - (\frac{1-\sqrt{5}}{2})^n] = \frac{\varphi ^n}{\sqrt{5}} - \frac{(1-\varphi)^n}{\sqrt{5}}$

解析解

解析解即公式解。我们有斐波那契数列的通项公式(Binet's Formula):

\[F_n = \frac{\left(\frac{1 + \sqrt{5}}{2}\right)^n - \left(\frac{1 - \sqrt{5}}{2}\right)^n}{\sqrt{5}} \]

这个公式可以很容易地用归纳法证明,当然也可以通过生成函数的概念推导,或者解一个方程得到。
当然你可能发现,这个公式分子的第二项总是小于 \(1\) ,并且它以指数级的速度减小。因此我们可以把这个公式写成

\[F_n = \left[\frac{\left(\frac{1 + \sqrt{5}}{2}\right)^n}{\sqrt{5}}\right] \]

这里的中括号表示取离它最近的整数。
这两个公式在计算的时侯要求极高的精确度,因此在实践中很少用到。

矩阵形式

斐波那契数列的递推可以用矩阵乘法的形式表达:

\[\begin{bmatrix}F_{n-1} & F_{n} \cr\end{bmatrix} = \begin{bmatrix}F_{n-2} & F_{n-1} \cr\end{bmatrix} \cdot \begin{bmatrix}0 & 1 \cr 1 & 1 \cr\end{bmatrix} \]

\(P = \begin{bmatrix}0 & 1 \cr 1 & 1 \cr\end{bmatrix}\) ,我们得到

\[\begin{bmatrix}F_n & F_{n+1} \cr\end{bmatrix} = \begin{bmatrix}F_0 & F_1 \cr\end{bmatrix} \cdot P^n \]

于是我们可以用矩阵乘法在 \(\Theta(\log n)\) 的时间内计算斐波那契数列。

和为S的连续正数序列

public ArrayList<ArrayList<Integer> > FindContinuousSequence(int sum) {
    // 时间复杂度O(n)
    
    //存放结果
    ArrayList<ArrayList<Integer> > result = new ArrayList<>();
    //两个起点,相当于动态窗口的两边,根据其窗口内的值的和来确定窗口的位置和大小
    int plow = 1,phigh = 2;
    while(phigh > plow){
        //由于是连续的,差为1的一个序列,那么求和公式是(a0+an)*n/2
        int cur = (phigh + plow) * (phigh - plow + 1) / 2;
        //相等,那么就将窗口范围的所有数添加进结果集
        if(cur == sum){
            ArrayList<Integer> list = new ArrayList<>();
            for(int i=plow;i<=phigh;i++){
                list.add(i);
            }
            result.add(list);
            plow++;
            //如果当前窗口内的值之和小于sum,那么右边窗口右移一下
        }else if(cur < sum){
            phigh++;
        }else{
            //如果当前窗口内的值之和大于sum,那么左边窗口右移一下
            plow++;
        }
    }
    return result;
}

求众数

// Boyer-Moore 投票算法
// 假设数组是非空的,并且给定的数组总是存在众数。
public int majorityElement(int[] nums) {
    if(nums.length == 1){
        return nums[0];
    }
    // 用来保存数组中遍历到的某个数字
    int major = 0;
	// 如果数字与之前 candidate 保存的数字相同,则 count 加 1
	// 如果数字与之前 candidate 保存的数字不同,则 count 减 1
    int count = 0;
    for(int i:nums){
        if(0 == count){
            major = i;
            count += 1;
        }else if(i == major){
            count ++;
        }else{
            count --;
        }
    }
    return major;
}

杨辉三角形

        1
       1 1
      1 2 1
     1 3 3 1
    1 4 6 4 1

性质

  • 帕斯卡三角以正整数构成,数字左右对称,每行由1开始逐渐变大,然后变小,回到1。
  • 帕斯卡三角每一行的平方和在杨辉三角出现奇数次。
  • 帕斯卡三角第2的幂行所有数都是奇数。
  • 帕斯卡三角每一行的和是2的幂。
  • 第n行的数字个数为n个。
  • 第n行的第k个数字为组合数\(C_{n-1}^{k-1}\)
  • 第n行数字和为\(2^{n-1}\)
  • 除每行最左侧与最右侧的数字以外,每个数字等于它的左上方与右上方两个数字之和(也就是说,第n行第k个数字等于第n-1行的第k-1个数字与第k个数字的和)。这是因为有组合恒等式:\(C_{n+1}^{i+1}=C_{n}^{i}+C_{n}^{i+1}\)。可用此性质写出整个杨辉三角形。

输出杨辉三角的第 k 行
杨辉三角n行m列通项公式为:\(C_{n-1}^{m-1} = \frac{(n-1)!}{(m-1)!(n-m)!}\)
组合数又有如下规律:$C_{n}^{m+1} = C_{n}^{m} * \frac{n-m}{m+1} $

// 杨辉三角n行m列通项公式为:$C_{n-1}^{m-1} = \frac{(n-1)!}{(m-1)!(n-m)!}$
// 组合数又有如下规律:$C_{n-1}^{m-1} = C_{n-1}^{m-2} * \frac{n-m}{m-1} $
public List<Integer> getRow(int rowIndex) {
    // note(a-lan-ruo-2): C(4,1)=C(4,0)*4/1,C(4,2)=C(4,1)*3/2,
    //C(4,3)=C(4,2)*2/3,C(4,4)=C(4,3)*1/4:
    //找到规律
    List<Integer> res = new ArrayList<Integer>(rowIndex+1);
    long nk = 1;
    for(int i = 0; i <= rowIndex; i++){
        res.add((int)nk);
        // C_{rowIndex}^{i}到C_{rowIndex}^{i+1}的递推公式
        nk = nk * (rowIndex - i) / (i + 1);
    }
    return res;
}

丑数

定义1:把只包含质因子2、3和5的数称作丑数(Ugly Number),习惯上我们把1当做是第一个丑数。
计算:求按从小到大的顺序的第N个丑数。
动态规划的记忆存储化思想

	  public int nthUglyNumber(int n) {
        // 0-6的丑数分别为0-6
        if(n < 7) return n;
        // 因为每次只选择一个最小的数,所以该用数组和for循环(最大循环次数就是n)
        int[] uglyArray = new int[n+1];
        //i2,i3,i5分别为三个队列的指针,newNum为从队列头选出来的最小数
        int i2=0,i3=0,i5=0,currMin=1;
        uglyArray[0]=1;
        for(int i=1;i<n;i++)
        {
            int m2=uglyArray[i2]*2;
            int m3=uglyArray[i3]*3;
            int m5=uglyArray[i5]*5;
            // 选出三个队列头最小的数
            currMin=Math.min(m2,Math.min(m3,m5));
            // 有序加入list中
            uglyArray[i]=currMin;
            // 这三个if有可能进入一个或者多个
            if(currMin==m2)i2++;
            if(currMin==m3)i3++;
            if(currMin==m5)i5++;
        }
        return uglyArray[n-1];
    }

定义2:超级丑数是指其所有质因数都是长度为 k 的质数列表 primes 中的正整数。
计算:查找第 n 个超级丑数。
动态规划的记忆存储化思想

public int nthSuperUglyNumberI(int n, int[] primes) {
    // 时间复杂度O(NM),m是给定质数列表长度,n是参数第那个丑数
    int[] ugly = new int[n];
    // note(核心数据结构) 下标对应primes每个质数,值对应超级丑数数组的指针
    int[] idx = new int[primes.length];

    ugly[0] = 1;
    for (int i = 1; i < n; i++) {
        // 第一个循环用来计算第i个超级丑数
        ugly[i] = Integer.MAX_VALUE;
        for (int j = 0; j < primes.length; j++){
            ugly[i] = Math.min(ugly[i], primes[j] * ugly[idx[j]]);
        }
        // 第二个循环用来设置
        for (int j = 0; j < primes.length; j++) {
            while (primes[j] * ugly[idx[j]] <= ugly[i]){
                idx[j]+=1;
            }
        }
    }
    return ugly[n - 1];
}

Nim游戏(了解即可)

两人轮流捡石子,每次最多可捡1到k个,谁最后拿不到石子为输;Nim游戏是博弈论中最经典的模型之一,它又有着十分简单的规则和无比优美的结论 Nim游戏是组合游戏(Combinatorial Games)的一种,准确来说,属于公平组合游戏(以下简称ICG);ICG 是指在游戏中,两个玩家所能进行的移动是完全相同的。

Nim游戏的定义

Nim游戏是经典的公平组合游戏(ICG),对于ICG游戏我们有如下定义:

  • 两名选手
  • 两名选手轮流行动,每一次行动可以在有限合法操作集合中选择一个
  • 游戏的任何一种可能的局面(position),合法操作集合只取决于这个局面本身,不取决于轮到哪名选手操作、以前的任何操作、骰子的点数或者其它因素;局面的改变称为“移动”(move)
  • 如果轮到某名选手移动,且这个局面的合法的移动集合为空(也就是说此时无法进行移动),则这名选手负

对于第三条,我们有更进一步的定义Position,我们将Position分为两类:

  • P-position:在当前的局面下,先手必败
  • N-position:在当前的局面下,先手必胜

它们有如下性质:

附录

posted @ 2019-11-08 09:31  混沌战神阿瑞斯  阅读(477)  评论(0编辑  收藏