数学编程问题


总结一些刷题过程中遇到的数学知识与数学运算,以及数学推导方面的经典题目。

内存基础

先列出在32位编译环境下的内存字节大小:
1字节:bool、 char (字符型,表示的字符与ASCII码表一一对应)
2字节:short (短整型,不管是不是signed或unsigned)
4字节:int、long、float、指针类型 (不管是不是signed或unsigned)
8字节:double、long long、long double
如果等号是 ‘=’ 这种字符常量的话,占1字节,如果是 “=” 这种字符串常量占2字节。

64位操作系统数据类型字节数不同点:
无论指针的类型是什么,32位平台永远是4,64位平台永远是8。
64位系统的 long 类型字节占用为8

struct变量的总长度等于所有成员长度之和。union变量所占用的内存长度等于最长的成员的内存长度,union的一个用法就是可以用来测试CPU是大端模式还是小端模式。
注意:struct 和 union 的{}后面要加上分号 “;”
struct的所有成员都存在;但在任何同一时刻, union中只存放了一个被选中的成员。
struct的不同成员赋值是互不影响的;union的不同成员赋值, 将会对其它成员重写, 原来成员的值就不存在了。

举例:

struct X1
{
	char a;
	int b;
	short c;
};

sizeof(X1)的大小是12,得出的步骤:

union test
{
     char mark;
     long num;
     float score;
};
union test a;

sizeof(a)的值为4。因union中的所有成员起始地址都是一样的,这些数据共享同一段内存,以达到节省空间的目的,所以&a.mark、&a.num和&a.score的值都是一样的。

数量积(点乘)

数量积又称为标积、内积、点积、点乘,结果为一个标量。

二维向量点积公式: a ⃗ ( x 1 , y 1 ) \vec{a} (x_{1},y_{1}) a (x1y1), b ⃗ ( x 2 , y 2 ) \vec{b}(x_{2},y_{2}) b (x2y2),则 a ⃗ ⋅ b ⃗ = ( x 1 y 1 - x 2 y 2 ) \vec{a}\cdot\vec{b}=(x_{1}y_{1}-x_{2}y_{2}) a b (x1y1x2y2)

几何意义:
∣ a ⃗ ⋅ b ⃗ ∣ = ∣ a ⃗ ∣ ∣ b ⃗ ∣ cos ⁡ θ |\vec{a}\cdot\vec{b} |=|\vec{a}| |\vec{b}|\cos\theta a b =a ∣∣b cosθ θ \theta θ a ⃗ \vec{a} a b ⃗ \vec{b} b (共起点)之间的夹角)

用处:向量的点乘可以用来计算两个向量之间的夹角,进一步判断这两个向量是否正交(垂直)等方向关系。同时,还可以用来计算一个向量在另一个向量方向上的投影长度。

向量积(叉乘)

向量积,数学中又称外积、叉积,物理中称矢积、叉乘,是一种在向量空间中向量的二元运算。与点积不同,它的运算结果是一个向量而不是一个标量。并且两个向量的叉积与这两个向量和垂直,方向满足右手定则(四指指向 v ⃗ \vec{v} v 方向,转向 u ⃗ \vec{u} u 方向,则大拇指方向即为叉乘 v ⃗ × u ⃗ \vec{v}×\vec{u} v ×u 的方向,叉乘的方向垂直于 v ⃗ \vec{v} v u ⃗ \vec{u} u 所决定的平面)。

二维向量叉乘公式: a ⃗ ( x 1 , y 1 ) \vec{a} (x_{1},y_{1}) a (x1y1), b ⃗ ( x 2 , y 2 ) \vec{b}(x_{2},y_{2}) b (x2y2),则 a ⃗ × b ⃗ = ( x 1 y 2 - x 2 y 1 ) \vec{a}×\vec{b}=(x_{1}y_{2}-x_{2}y_{1}) a ×b (x1y2x2y1)

叉乘的几何意义:
大小表示两个向量围成的平行四边形的面积,正负表示两个向量的相对位置。
用处:求三角形/平行四边形面积或者判断两点的相对位置,以及判断三点共线。

1)叉乘结果正负的几何意义:
叉乘结果的正负其实就是 c ⃗ \vec{c} c 轴的符号,由叉乘 a ⃗ × b ⃗ = − b ⃗ × a ⃗ \vec{a}×\vec{b}=-\vec{b}×\vec{a} a ×b b ×a 的性质,可以根据叉乘的正负值,来判断 a ⃗ \vec{a} a b ⃗ \vec{b} b 的相对位置(旋转方向)(也是三个点的相对位置),即 b ⃗ \vec{b} b 是出于 a ⃗ \vec{a} a 的顺时针还是逆时针。

p 0 p_0 p0为参考点,
如果 a ⃗ × b ⃗ > 0 \vec{a}×\vec{b} > 0 a ×b >0,乘积multi大于0,则 p 2 p_2 p2 p 1 p_1 p1的逆时针方向;
如果 a ⃗ × b ⃗ < 0 \vec{a}×\vec{b} < 0 a ×b <0,multi小于0,则 p 2 p_2 p2 p 1 p_1 p1的顺时针方向,
a ⃗ × b ⃗ = 0 \vec{a}×\vec{b} = 0 a ×b =0 ,multi等于0,则 p 0 p_0 p0 p 1 p_1 p1 p 2 p_2 p2三点共线, a ⃗ \vec{a} a b ⃗ \vec{b} b 平行。
(两个非零向量 a ⃗ \vec{a} a b ⃗ \vec{b} b 平行,当且仅当 a ⃗ × b ⃗ = 0 \vec{a}×\vec{b} = 0 a ×b =0)
(可以判断三点是否共线:由三点构成的两个向量的叉乘结果不为零时,表示三点不共线,可以构成一个三角形)

2)叉乘模的几何意义:
∣ c ⃗ ∣ = ∣ a ⃗ × b ⃗ ∣ = ∣ a ⃗ ∣ ∣ b ⃗ ∣ sin ⁡ α |\vec{c}|=|\vec{a}×\vec{b} |=|\vec{a}| |\vec{b}|\sinα c =a ×b =a ∣∣b sinα α α α a ⃗ \vec{a} a b ⃗ \vec{b} b (共起点)之间的夹角)
∣ c ⃗ ∣ = a ⃗ 与 b ⃗ 构成的平行四边形的面积 |\vec{c}| = \vec{a} 与 \vec{b}构成的平行四边形的面积 c =a b 构成的平行四边形的面积 (如下图所示的平行四边形)

与数量积的区别
注:向量积≠向量的积(向量的积一般指点乘)
一定要清晰地区分开向量积(矢积)与数量积(标积)。见下表。

名称标积/内积/数量积/点积/点乘矢积/外积/向量积/叉积/叉乘
运算式(a,b和c表示向量) a ⋅ b = ∣ a ∣ ∣ b ∣ ⋅ cos ⁡ θ a·b=|a||b|·\cosθ ab=a∣∣bcosθ a × b = c a×b=c a×b=c,其中 ∣ c ∣ = ∣ a ∣ ∣ b ∣ ⋅ sin ⁡ θ |c|=|a||b|·\sinθ c=a∣∣bsinθ c c c的方向遵守右手定则
几何意义向量a在向量b方向上的投影与向量b的模的乘积c是垂直a、b所在平面,且以|b|·sinθ为高、|a|为底的平行四边形的面积
运算结果标量矢量

应用1.1037. 有效的回旋镖
给定一个数组 points ,其中 p o i n t s [ i ] = [ x i , y i ] points[i] = [x_i, y_i] points[i]=[xi,yi]表示 X-Y 平面上的一个点,如果这些点构成一个 回旋镖 则返回 true 。
回旋镖 定义为一组三个点,这些点 各不相同 且 不在一条直线上 。

示例 1:
输入:points = [[1,1],[2,3],[3,2]]
输出:true

【解答】计算从 points [ 0 ] \textit{points}[0] points[0]开始,分别指向 points [ 1 ] \textit{points}[1] points[1] points [ 2 ] \textit{points}[2] points[2] 的向量 v ⃗ 1 \vec{v}_1 v 1 v ⃗ 2 \vec{v}_2 v 2 。「三点各不相同且不在一条直线上」等价于「这两个向量的叉乘结果不为零」:

v ⃗ 1 × v ⃗ 2 ≠ 0 ⃗ \vec{v}_1 \times \vec{v}_2 \ne \vec{0} v 1×v 2=0

bool isBoomerang(vector<vector<int>>& points) {
        int a1 = points[0][0] - points[1][0];
        int a2 = points[0][0] - points[2][0];
        int b1 = points[0][1] - points[1][1];
        int b2 = points[0][1] - points[2][1];
        return a1 *b2 != b1 *a2;
    }

应用2. 判断一个点是否在矩形内部
只需要判断该点是否在上下两条边和左右两条边之间。判断一个点是否在两条线段之间夹着就转化成,判断一个点是否在某条线段的一边上,就可以利用叉乘的方向性(边的旋转方向)。

比如判断点E是否在矩形ABCD内,可以分别判断点E是否在边AB与边CD内夹着,若在这两条线段内夹着,则从边AB向边AE方向为顺时针,从边CD向边CE方向也为顺时针,这就说明叉乘 A B ⃗ × A E ⃗ \vec{AB}×\vec{AE} AB ×AE C D ⃗ × C E ⃗ \vec{CD} ×\vec{CE} CD ×CE 的正负号相同,即 ( A B ⃗ × A E ⃗ ) ∗ ( C D ⃗ × C E ⃗ ) ≥ 0 (\vec{AB}×\vec{AE})*(\vec{CD} ×\vec{CE}) \geq 0 (AB ×AE )(CD ×CE )0
点E与另外两条线段BC、DA的相对位置判断也类似, ( B C ⃗ × B E ⃗ ) ∗ ( D A ⃗ × D E ⃗ ) ≥ 0 (\vec{BC}×\vec{BE})*(\vec{DA} ×\vec{DE}) \geq 0 (BC ×BE )(DA ×DE )0
最后只需要判断 ( A B ⃗ × A E ⃗ ) ∗ ( C D ⃗ × C E ⃗ ) ≥ 0 & & ( B C ⃗ × B E ⃗ ) ∗ ( D A ⃗ × D E ⃗ ) ≥ 0 (\vec{AB}×\vec{AE})*(\vec{CD} ×\vec{CE}) \geq 0 \&\& (\vec{BC}×\vec{BE})*(\vec{DA} ×\vec{DE}) \geq 0 (AB ×AE )(CD ×CE )0&&(BC ×BE )(DA ×DE )0
规律:这四条边其实是按照AB、 BC、 CD、 DA的顺时针顺序,每条边的顶点再与E组合

代码如下:

#include <iostream>
#include <algorithm>
using namespace  std;
struct Point
{
    float x;
    float y;
    Point(float x,float y):x(x), y(y){}
};
// 計算 |p1 p2| X |p1 p|
float GetCross(Point& p1, Point& p2,Point& p)
{
    return (p2.x - p1.x) * (p.y - p1.y) -(p.x - p1.x) * (p2.y - p1.y);
}
//判断点否在5X5 以原点为左下角的正方形內
bool IsPointInMatrix(Point& p)
{
    Point p1(0,5);
    Point p2(0,0);
    Point p3(5,0);
    Point p4(5,5);
    return GetCross(p1,p2,p) * GetCross(p3,p4,p) >= 0 && GetCross(p2,p3,p) * GetCross(p4,p1,p) >= 0;
}

int main(){
    Point testPoint(0,0);
    cout << "输入点坐标(以空格分隔):" << endl;
    cin >> testPoint.x >> testPoint.y;
    cout << "该点坐标为: "<< testPoint.x << " "<< testPoint.y << endl;
    cout << "该点" << (IsPointInMatrix(testPoint)? "在5X5以原点为左下角的正方形內.": "不在5X5以原点为左下角的正方形內" )<< endl;
    return 0;
}

三角形面积公式

1.已知三角形三边a,b,c,则 海伦公式:

p = a + b + c 2 p=\frac{a+b+c}{2} p=2a+b+c

S = p ( p − a ) ( p − b ) ( p − c ) S=\sqrt{p(p-a)(p-b)(p-c)} S=p(pa)(pb)(pc)

2.已知三角形两边a,b,这两边夹角C,则

S = a × b × s i n C 2 S=\frac{a×b×sinC}{2} S=2a×b×sinC

即两夹边之积乘夹角正弦值的一半。

3.设三角形三边分别为a、b、c,内切圆半径为r

则三角形面积 S = ( a + b + c ) r 2 S=\frac{(a+b+c)r}{2} S=2(a+b+c)r

4.二维向量叉乘公式 a ⃗ ( x 1 , y 1 ) \vec{a} (x_{1},y_{1}) a (x1y1), b ⃗ ( x 2 , y 2 ) \vec{b}(x_{2},y_{2}) b (x2y2),则 a ⃗ × b ⃗ = ( x 1 y 2 - x 2 y 1 ) \vec{a}×\vec{b}=(x_{1}y_{2}-x_{2}y_{1}) a ×b (x1y2x2y1)
叉积的长度 ∣ a ⃗ × b ⃗ ∣ |\vec{a}×\vec{b} | a ×b 可以解释成这两个叉乘向量 a ⃗ \vec{a} a b ⃗ \vec{b} b 共起点时,所构成平行四边形的面积。

设三角形三个顶点的坐标为 ( x 1 , y 1 ) (x_1, y_1) (x1,y1) ( x 2 , y 2 ) (x_2, y_2) (x2,y2) ( x 3 , y 3 ) (x_3, y_3) (x3,y3),则三角形面积 S 可以用行列式的绝对值表示
S = 1 2 ∥ x 1 y 1 1 x 2 y 2 1 x 3 y 3 1 ∥ = 1 2 ∣ x 1 y 2 + x 2 y 3 + x 3 y 1 − x 1 y 3 − x 2 y 1 − x 3 y 2 ∣ S = \frac{1}{2} \left\lVert \begin{matrix} x_1 & y_1 & 1\\ x_2 & y_2 & 1 \\ x_3 & y_3 & 1 \end{matrix} \right\rVert =\frac{1}{2}\lvert x_1 y_2 + x_2y_3 + x_3y_1 - x_1y_3 - x_2y_1 - x_3 y_2 \rvert S=21 x1x2x3y1y2y3111 =21x1y2+x2y3+x3y1x1y3x2y1x3y2
例题:812. 最大三角形面积
给定包含多个点的集合,从其中取三个点组成三角形,返回能组成的最大三角形的面积。

示例:
输入: points = [[0,0],[0,1],[1,0],[0,2],[2,0]]
输出: 2
解释:
这五个点如下图所示。组成的橙色三角形是最大的,面积为2。

【解答】枚举所有的三角形,然后计算三角形的面积并找出最大的三角形面积
【注意点】叉乘可能出现负值,因此需要将结果取绝对值abs

	double countsize(vector<int> &a, vector<int>& b, vector<int>& c){
        double m = a[0] * (b[1] - c[1]) - a[1] * (b[0] - c[0]) + b[0] * c[1] - c[0] * b[1];
        return abs(m) * 0.5;
    }
    double largestTriangleArea(vector<vector<int>>& points) {
        int n = points.size();
        double maxsize = 0.0;
        for(int i = 0; i < n; ++i){
            for(int j = i + 1; j < n; ++j){
                for(int k = j + 1; k < n; ++k){
                        maxsize = max(maxsize, countsize(points[i], points[j], points[k]));
                    }
                }
            }
        return maxsize;
    }

平面几何形状判断

平行四边形判定

  1. 两组对边分别平行的四边形是平行四边形(定义判定法)
  2. 一组对边平行且相等的四边形是平行四边形
  3. 两组对边分别相等的四边形是平行四边形
  4. 两组对角分别相等的四边形是平行四边形
  5. 对角线互相平分的四边形是平行四边形

菱形的判定:

  1. 有一组邻边相等的平行四边形是菱形。(菱形的定义)
  2. 四条边都相等的四边形是菱形。
  3. 对角线互相垂直平分的平行四边形是菱形。
    注意:一组对角线平分一组对角的四边形不是菱形,也可能是筝形(有一条对角线所在直线为对称轴的四边形)

矩形的判定:

  1. 有三个角是直角的四边形是矩形;
  2. 对角线互相平分且相等的四边形是矩形;
  3. 有一个角为直角的平行四边形是矩形;
  4. 对角线相等的平行四边形是矩形。

正方形判定:

  1. 对角线互相垂直平分且相等的四边形是正方形。
  2. 邻边相等且有一个内角是直角的平行四边形是正方形。
  3. 有一组邻边相等的矩形是正方形 。
  4. 有一个内角是直角的菱形是正方形。
  5. 对角线相等的菱形是正方形。
  6. 对角线互相垂直的矩形是正方形。
  7. 有三个内角为直角且有一组邻边相等的四边形是正方形。
    判别正方形的一般顺序:先说明它是平行四边形;再说明它是菱形(或矩形);最后说明它是矩形(或菱形)。

593. 有效的正方形
给定2D空间中四个点的坐标 p1, p2, p3 和 p4,如果这四个点构成一个正方形,则返回 true 。
点的坐标 pi 表示为 [xi, yi] 。输入 不是 按任何顺序给出的。
一个 有效的正方形 有四条等边和四个等角(90度角)。

示例 1:
输入: p1 = [0,0], p2 = [1,1], p3 = [1,0], p4 = [0,1]
输出: True

【常用思路】
判别正方形的一般顺序为先说明它是平行四边形;再说明它是菱形(或矩形);最后说明它是矩形(或菱形)。那么我们可以从枚举四边形的两条斜边入手来进行判断:

如果两条斜边的中点相同:则说明以该两条斜边组成的四边形为「平行四边形」。
在满足「条件一」的基础上,如果两条斜边的长度相同:则说明以该两条斜边组成的四边形为「矩形」。
在满足「条件二」的基础上,如果两条斜边的相互垂直:则说明以该两条斜边组成的四边形为「正方形」。
【简单思路】
求出6条边,四条边长和两条对角线。
四条边长都相等的只有正方形和菱形,对角线又相等的只有正方形
所以排序判断边长以及对角线是否相等即可

	int len(vector<int>& a, vector<int>& b){
        return pow(a[0] - b[0], 2) + pow(a[1] - b[1], 2);
    }
    //对角线相等就是矩形了,然后两条邻边相等就是正方形
    bool validSquare(vector<int>& p1, vector<int>& p2, vector<int>& p3, vector<int>& p4) {
        if(p1 == p2) return false; //所有点都是(0, 0)点的情况
        vector<int> length;
        length.emplace_back(len(p1, p2));
        length.emplace_back(len(p1, p3));
        length.emplace_back(len(p1, p4));
        length.emplace_back(len(p2, p3));
        length.emplace_back(len(p2, p4));
        length.emplace_back(len(p3, p4));
       sort(length.begin(), length.end());
       return length[0] == length[3] && length[4] == length[5];
    }

字典序问题

什么是字典序?
简而言之,就是根据数字的前缀进行排序,

比如 10 < 9,因为 10 的前缀是 1,比 9 小。

再比如 112 < 12,因为 112 的前缀 11 小于 12。

这样排序下来,会跟平常的升序排序会有非常大的不同。比如说一个数乘 10,或者加 1,反而后者会更大。

可以用字典树来帮助理解:

每一个节点都拥有 10 个孩子节点,因为作为一个前缀 ,它后面可以接 0~9 这十个数字。而且可以发现,整个字典序排列就是对十叉树进行先序遍历。1, 10, 100, 101, … 11, 110 …
结论:前序遍历字典树即可得到字典序从小到大的数字序列

例题:[字节跳动最常考题之一]
440. 字典序的第K小数字

给定整数 n 和 k,返回 [1, n] 中字典序第 k 小的数字。

示例 1:
输入: n = 13, k = 2
输出: 10
解释: 字典序的排列是 [1, 10, 11, 12, 13, 2, 3, 4, 5, 6, 7, 8, 9],所以第二小的数字是 10。

示例 2:
输入: n = 1, k = 1
输出: 1

【分析】
难点:确定指定前缀 cur 下所有子节点数,包括 cur 节点本身
思路:分层用每层的 min(n, last) - first + 1,分别求出每一层的节点总数,然后将总数累加,就可以得到该子树的所有节点总数。

//难点:计算以 cur 开头的子树结点个数,包括 cur 本身
//first 指向第 i 层的最左侧的孩子节点, last 指向第 i 层的最右侧的孩子节点,
//由于所有的节点都需要满足小于等于 n,所以第 i 层的最右侧节点应该为 min(n, last),
//不断迭代直到 first > n 则终止向下搜索。

	//注意1:用long类型,否则超出int范围了
    int getnum(long cur, long n) {
        long first = cur;
        long last = cur;
        int num = 0;
        while(first <= n){
            //一层层地叠加,每一层最大不超过n(因为一共n个数)
            //注意2:min函数中,两个参数性质需要相同
            num += min(last, n) - first + 1; 
            first *= 10;
            last = last * 10 + 9;
        }
        return num;
    }
    int findKthNumber(int n, int k) {
        int cur = 1;
        //注意3:需要k==1时输出当前的元素cur
        while(k>1) {
        	//得到以 cur 为顶点的字典子树结点总数
            int num = getnum(cur, n);
            //注意4:不可以 >= ,若相等,K直接减为 0 了 
            if(k > num) {
                k -= num;
                //转移到右兄弟子树上
                //如当前节点为 1 时,转移到以 2为头结点的字典树上,
                //当前节点为 10 时,转移到以 11、12...为头结点的右兄弟子树上
                ++cur;
            }
            else{
                cur *= 10;  //转移到左孩子,比如以10为头结点的字典树上
                --k;       //因为走过了一个头结点,所以-1
            }
        }
        return cur;
    }

连续整数求和

829. 连续整数求和
给定一个正整数 n,返回 连续正整数满足所有数字之和为 n 的组数 。

示例 1:
输入: n = 5
输出: 2
解释: 5 = 2 + 3,共有两组连续整数([5],[2,3])求和后为 5。

示例 2:
输入: n = 9
输出: 3
解释: 9 = 4 + 5 = 2 + 3 + 4

示例 3:
输入: n = 15
输出: 4
解释: 15 = 8 + 7 = 4 + 5 + 6 = 1 + 2 + 3 + 4 + 5

【分析】
【第一种推导式复杂分析】
如果正整数 n 可以表示成 k 个连续正整数之和,则由于 k 个连续正整数之和的最小值是(k+1)k/2
因此有 n >= (k+1)k/2 k个连续正整数之和
枚举每个符合 k(k+1) ≤ 2n 的正整数 k,判断正整数 n 是否可以表示成 k 个连续正整数之和。
假设这 k 个连续正整数中的最小正整数是 x,最大正整数是 y,则有y=x+k−1,根据等差数列求和公式有 n = k(x+y) / 2= k(2x+k−1)/2,x= n/k − (k−1)/2 ,根据 k(k+1)≤2n 可知 x > 0。分别考虑 k 是奇数和偶数的情况,看能否推出x是整数。

1)当 k 是奇数时,k−1 是偶数,因此 n 可以被 k 整除。并且当 n 可以被 k 整除时,可得x是正整数,因此 n 可以表示成 k 个连续正整数之和。
【结论】当 k 是奇数时,「正整数 n 可以表示成 k 个连续正整数之和」等价于「正整数 n 可以被 k 整除」。

2)当 k 是偶数时,2x+k−1 是奇数。,n 不可以被 k 整除,又由于 2x+k−1= 2n/k 是整数,因此 2n 可以被 k 整除,且2n/k是奇数。
当 n 不可以被 k 整除且 2n 可以被 k 整除时,可得 x 是整数,因此 n 可以表示成 k 个连续正整数之和。
【结论】当 k 是偶数时,「正整数 n 可以表示成 k 个连续正整数之和」等价于「正整数 n 不可以被 k 整除且正整数 2n 可以被 k 整除」。

根据上述分析,可以得到判断正整数 n 是否可以表示成 k 个连续正整数之和的方法:
枚举每个符合 k(k+1) ≤ 2n 的正整数 k,
如果 k 是奇数,则当 n 可以被 k 整除时,正整数 n 可以表示成 k 个连续正整数之和;
如果 k 是偶数,则当 n 不可以被 k 整除且 2n 可以被 k 整除时,正整数 n 可以表示成 k 个连续正整数之和。

【另一种简单思维】
遍历每个符合 k(k+1) ≤ 2n 的正整数 k,
其实就是n的奇因子的个数; 利用等差数列求和公式,
如果拆成奇数项,则 n = 中间项×项数(此时项数为奇数)—> n 可以被 k 整除
如果拆成偶数项,则 n = 中间两项和×项数的一半(此时中间两项和为奇数,因为这两项为连续数)—>n 可以被 k/2 整除,且 n 不可以被 k 整除

	int consecutiveNumbersSum(int n) {
        int ans = 0;
        int bound = 2 * n;
        for(int k = 1; k * (k+1) <= bound; ++k){
            if(isKcorrect(k, n)){
                ++ans;
            }
        }
        return ans;
    }
    bool isKcorrect(int k, int n){
        if(k % 2 == 1){
            return n % k == 0;
        }else{
            return n % k != 0 && 2 * n % k == 0;
        }
    }

15. 三数之和
给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组。
注意:答案中不可以包含重复的三元组。

示例 1:
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]

【分析】
难点在去重,第一个、第二个与第三个数都要与之前选取的数不一样

	vector<vector<int>> threeSum(vector<int>& nums) {
        vector<vector<int>> ans;
        int n = nums.size();
        sort(nums.begin(), nums.end());
        for(int i = 0; i < n; ++i){
            // 第一个数大于 0,后面都是递增正数,不可能相加为零了
            if(nums[i] > 0) return ans;
            // 去重:如果此数已经选取过,跳过
            while(i > 0 && i < n && nums[i] == nums[i-1]){
                ++i;
            }
            int l = i + 1;
            int r = n - 1;
            while(l < r){
                while(l < r && nums[l] + nums[r] > -nums[i]){
                    --r;
                }
                while(l < r && nums[l] + nums[r] < -nums[i]){
                    ++l;
                }
                // 找到一个和为零的三元组,添加到结果中,左右指针内缩,继续寻找
                if(l < r && nums[l] + nums[r] == -nums[i]){
                    ans.push_back({nums[i], nums[l], nums[r]});
                    ++l; 
                    --r;
                    // 去重:第二个数和第三个数也不重复选取
                    while(l < r && nums[l] == nums[l-1]) ++l;
                    while(l < r && nums[r] == nums[r+1]) --r;
                }
            }
        }
        return ans;
    }

整数反转

给你一个 32 位的有符号整数 x ,返回将 x 中的数字部分反转后的结果。
如果反转后整数超过 32 位的有符号整数的范围 [ − 2 31 , 2 31 − 1 ] [−2^{31}, 2^{31} − 1] [231,2311] ,就返回 0。
假设环境不允许存储 64 位整数(有符号或无符号)。

示例 1:
输入:x = 123
输出:321

示例 2:
输入:x = -123
输出:-321

示例 3:
输入:x = 120
输出:21

示例 4:
输入:x = 0
输出:0

【注意点】
int占4字节,32位,边界为INT_MIN 和 INT_MAX,这俩数都是32位,因此需要用INT_MIN/10来防止越32位界。

	int reverse(int x) {
        int ans = 0;
        while(x){
            //运算过程中的数字必须在 32 位有符号整数的范围内,INT_MIN可能越界
            //若x不为0,则之后ans还需要×10,因此在此之前就用INT_MIN/10来判断是否越界
            if(ans < INT_MIN / 10 || ans > INT_MAX / 10)
                return 0;
            int res = x % 10;
            ans = ans * 10 + res;
            x /= 10;
        }
        //不需要用flag来标识数字是否为负数,也不用额外×-1,这样反而出错
        //当x为负数时,res也为负数,每次累加和乘10之后也是负数,因此不必考虑正负号
        //if(flag) ans *= -1; 
        return ans;
    }

461. 汉明距离
两个整数之间的 汉明距离 指的是这两个数字对应二进制位不同的位置的数目。
给你两个整数 x 和 y,计算并返回它们之间的汉明距离。
【分析】
计算 x 和 y 之间的汉明距离,可以先计算 x ⊕ y x \oplus y xy,然后统计结果中等于 1 的位数。
现在,原始问题转换为位计数问题。位计数有多种思路。

方法一:C++内置位计数功能
利用 __builtin_popcount(x) 函数统计二进制下“1”的个数。

int hammingDistance(int x, int y) {
        return __builtin_popcount(x ^ y);
    }

方法二:移位实现位计数

	//a ^ a = 0
    int hammingDistance(int x, int y) {
        int z = x ^ y, ans = 0;
        while(z){
            ans += z & 1;
            z >>= 1;
        }
        return ans;
    }

方法三:Brian Kernighan 算法
在方法二中,对于 s = ( 10001100 ) 2 s=(10001100)_2 s=(10001100)2 的情况,需要循环右移 8 8 8 次才能得到答案。而实际上如果我可以跳过两个 1 1 1 之间的 0 0 0,直接对 1 1 1 进行计数,那么就只需要循环 3 3 3 次即可。

可以使用 Brian Kernighan \text{Brian Kernighan} Brian Kernighan 算法进行优化,具体地,该算法可以被描述为这样一个结论:记 f ( x ) = x   &   ( x − 1 ) f(x)=x~\&~(x-1) f(x)=x & (x1), 表示 x x x x − 1 x-1 x1 进行与运算所得的结果,那么 f ( x ) f(x) f(x) 恰为 x x x 删去其二进制表示中最右侧的 1 1 1 的结果

基于该算法,当我们计算出 s = x ⊕ y s = x \oplus y s=xy,只需要不断让 s = f ( s ) s = f(s) s=f(s),直到 s = 0 s=0 s=0 即可。这样每循环一次, s s s 都会删去其二进制表示中最右侧的 1 1 1,最终循环的次数即为 s s s 的二进制表示中 1 1 1 的数量。

	int hammingDistance(int x, int y) {
        int z = x ^ y, ans = 0;
        while(z){
            ++ans;
            z &= z - 1;
        }
        return ans;
    }

计数质数

质数的定义:在大于 1 的自然数中,除了 1 和它本身以外不再有其他因数的自然数。
统计 [ 2 , n ] [2,n] [2,n] 中质数的数量是一类很常见的题目。
经典例题:204. 计数质数

方法一:枚举(试除法)
很直观的思路是我们枚举每个数判断其是不是质数。
对于每个数 x x x,要判断 x x x 是否为质数,我们可以从小到大枚举 [ 2 , x ] [2,x] [2,x] 中的每个数 y y y,判断 y y y 是否为 x x x 的因数。但这样判断一个数是否为质数的时间复杂度最差情况下会到 O ( n ) O(n) O(n),无法通过所有测试数据。

考虑到如果 y y y x x x 的因数,那么 x y \frac{x}{y} yx 也必然是 x x x 的因数,因此我们只要校验 y y y 或者 x y \frac{x}{y} yx 即可。而如果我们每次选择校验两者中的较小数,则不难发现较小数一定落在 [ 2 , x ] [2,\sqrt{x}] [2,x ] 的区间中,因此我们只需要枚举 [ 2 , x ] [2,\sqrt{x}] [2,x ] 中的所有数,看这些数是否是 x x x 的因数即可,这样单次检查的时间复杂度从 O ( n ) O(n) O(n) 降低至了 O ( n ) O(\sqrt{n}) O(n )

	bool isprime(int x){
        for(int i = 2; i * i <= x; ++i){
            if(x % i == 0){
                return false;
            }
        }
        return true;
    }
    int countPrimes(int n) {
        int ans = 0;
        for(int i = 2; i < n; ++i){
            ans += isprime(i);
        }
        return ans;
}

时间复杂度: O ( n n ) O(n\sqrt{n}) O(nn )。单个数检查的时间复杂度为 O ( n ) O(\sqrt{n}) O(n ),一共要检查 O ( n ) O(n) O(n) 个数,因此总时间复杂度为 O ( n n ) O(n\sqrt{n}) O(nn )

方法二:埃氏筛

枚举没有考虑到数与数的关联性,因此难以再继续优化时间复杂度。接下来我们介绍一个常见的算法,该算法由希腊数学家厄拉多塞( E r a t o s t h e n e s E r a t o s t h e n e s \rm EratosthenesEratosthenes EratosthenesEratosthenes)提出,称为厄拉多塞筛法,简称埃氏筛。

我们考虑这样一个事实:如果 x x x 是质数,那么大于 x x x x x x 的倍数 2 x , 3 x , … 2x,3x,\ldots 2x,3x, 一定不是质数,因此我们可以从这里入手。

我们设 isPrime [ i ] \textit{isPrime}[i] isPrime[i] 表示数 i i i 是不是质数,如果是质数则为 1,否则为 0。从小到大遍历每个数,如果这个数为质数,则将其所有的倍数都标记为合数(除了该质数本身),即 0,这样在运行结束的时候我们即能知道质数的个数。

这种方法的正确性是比较显然的:这种方法显然不会将质数标记成合数;另一方面,当从小到大遍历到数 x x x 时,倘若它是合数,则它一定是某个小于 x x x 的质数 y y y 的整数倍,故根据此方法的步骤,我们在遍历到 y y y 时,就一定会在此时将 x x x 标记为 isPrime [ x ] = 0 \textit{isPrime}[x]=0 isPrime[x]=0。因此,这种方法也不会将合数标记为质数。

当然这里还可以继续优化,对于一个质数 x x x,如果按上文说的我们从 2 x 2x 2x 开始标记其实是冗余的,应该直接从 x ⋅ x x\cdot x xx 开始标记,因为 2 x , 3 x , … 2x,3x,\ldots 2x,3x, 这些数一定在 x x x 之前就被其他数的倍数标记过了,例如 2 2 2 的所有倍数, 3 3 3 的所有倍数等。

【总结】从 2 到 n 遍历每个数,如果这个数为质数,则将其所有的倍数从 x ⋅ x x\cdot x xx 开始都标记为合数,即 0,因为 2 x , 3 x , … 2x,3x,\ldots 2x,3x, 这些数一定在 x x x 之前就被其他数的倍数标记过了,这样在运行结束的时候我们即能知道质数的个数

	int countPrimes(int n) {
        int ans = 0;
        vector<int> prime(n, 1);
        for(int i = 2; i < n; ++i){
            if(prime[i]){
                ++ans;
                //用long long,不然超范围
                if((long long)i * i < n){
                    for(int x = i * i; x < n; x += i){
                        prime[x] = 0;
                    }
                }
            }
        }
        return ans;
    }

时间复杂度: O ( n log ⁡ log ⁡ n ) O(n\log \log n) O(nloglogn)

方法三:线性筛
埃氏筛其实还是存在冗余的标记操作,比如对于 45 这个数,它会同时被 3,5 两个数标记为合数,因此我们优化的目标是让每个合数只被标记一次,这样时间复杂度即能保证为 O ( n ) O(n) O(n),这就是我们接下来要介绍的线性筛。

相较于埃氏筛,我们多维护一个 primes \textit{primes} primes 数组表示当前得到的质数集合。我们从小到大遍历,如果当前的数 x x x 是质数,就将其加入 primes \textit{primes} primes 数组

另一点与埃氏筛不同的是,「标记过程」不再仅当 x x x 为质数时才进行,而是对每个整数 x x x 都进行标记。对于整数 x x x,我们不再标记其所有的倍数 x ⋅ x x\cdot x xx, x ⋅ ( x + 1 ) x\cdot (x+1) x(x+1), … \ldots ,而是只标记质数集合中的数与 x x x 相乘的数,即 x ⋅ primes 0 , x ⋅ primes 1 , … x\cdot\textit{primes}_0,x\cdot\textit{primes}_1,\ldots xprimes0,xprimes1, 且在发现 x   m o d   primes i = 0 x\bmod \textit{primes}_i=0 xmodprimesi=0 的时候结束当前标记

核心点在于:如果 x x x 可以被 primes i \textit{primes}_i primesi 整除,那么对于合数 y = x ⋅ primes i + 1 y=x\cdot \textit{primes}_{i+1} y=xprimesi+1 而言,它一定在后面遍历到 x primes i ⋅ primes i + 1 \frac{x}{\textit{primes}_i}\cdot\textit{primes}_{i+1} primesixprimesi+1 这个数的时候会被标记,其他同理,这保证了每个合数只会被其「最小的质因数」筛去,即每个合数被标记一次。

线性筛还有其他拓展用途,有能力的读者可以搜索关键字「积性函数」继续探究如何利用线性筛来求解积性函数相关的题目。

	int countPrimes(int n) {
        vector<int> prime;
        vector<int> isprime(n, 1);
        for(int i = 2; i < n; ++i){
            if(isprime[i]){
                prime.emplace_back(i);
            }
            for(int j = 0; j < prime.size() && i * prime[j] < n; ++j){
                isprime[i * prime[j]] = 0;
                if(i % prime[j] == 0){
                    break;
                }
            }
        }
        //prime数组还保存了找到的全部质数
        return prime.size();
    }

时间复杂度: O ( n ) O(n) O(n)

题目拓展:
1175. 质数排列
【分析】求符合条件的方案数,使得所有质数都放在质数索引上,所有合数放在合数索引上,质数放置和合数放置是相互独立的,总的方案数即为「所有质数都放在质数索引上的方案数」 × \times ×「所有合数都放在合数索引上的方案数」。求「所有质数都放在质数索引上的方案数」,即求质数个数 numPrimes \textit{numPrimes} numPrimes 的阶乘。「所有合数都放在合数索引上的方案数」同理。求质数个数时,可以使用试除法。

	const int MOD = 1e9 + 7;
    bool isprime(int n){
        for(int i = 2; i * i <= n; ++i){
            if(n % i == 0){
                return false;
            }
        }
        return true;
    }
    int numPrimeArrangements(int n) {
      // int x = sqrt(n);
       int z = 0;
       for(int i = 2; i <= n; ++i){
           z += isprime(i);
       }
       int s = n - z;
       long ans1 = 1, ans2 = 1;
       for(int i = 1; i <= z; ++i){
           ans1 = (ans1 * i) % MOD;
       } 
       for(int i = 1; i <= s; ++i){
           ans2 = (ans2 * i) % MOD;
       }
       return (ans1  * ans2 ) % MOD; 
    }

进制运算与位运算

868. 二进制间距
给定一个正整数 n,找到并返回 n 的二进制表示中两个 相邻 1 之间的 最长距离 。如果不存在两个相邻的 1,返回 0 。
如果只有 0 将两个 1 分隔开(可能不存在 0 ),则认为这两个 1 彼此 相邻 。两个 1 之间的距离是它们的二进制表示中位置的绝对差。例如,“1001” 中的两个 1 的距离为 3 。

示例 1:
输入:n = 22
输出:2
解释:22 的二进制是 “10110” 。
在 22 的二进制表示中,有三个 1,组成两对相邻的 1 。
第一对相邻的 1 中,两个 1 之间的距离为 2 。
第二对相邻的 1 中,两个 1 之间的距离为 1 。
答案取两个距离之中最大的,也就是 2 。

【分析】
使用一个循环从 二进制表示的低位开始进行遍历,并找出所有的 1。我们用一个变量 last 记录上一个找到的 1 的位置。如果当前在第 i 位找到了 1,那么就用 i−last 更新答案,再将last 更新为 i 即可。

在循环的每一步中,我们可以使用位运算 n & 1 获取 n 的最低位,判断其是否为 1。在这之后,我们将 n 右移一位: n = n >> 1,这样在第 i 步时, n & 1 得到的就是初始n 的第 i 个二进制位。

	int binaryGap(int n) {
        int l = -1, ans = 0;
        for(int i = 0; n; ++i){
            if(n & 1){
                if(l != -1){
                    ans = max(ans, i-l);
                }
                l = i;
            }
            n >>= 1;
        }
        return ans;
    }

矩阵坐标变换问题

48. 旋转图像

给定一个 n × n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。
你必须在 原地 旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。

示例 1:

输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[[7,4,1],[8,5,2],[9,6,3]]

【分析】用翻转代替旋转
先将矩阵通过水平轴翻转,再根据主对角线翻转,就可以得到答案
对于水平轴翻转而言,我们只需要枚举矩阵上半部分的元素,和下半部分的元素进行交换,即:
m a t r i x [ r o w ] [ c o l ] → 水平轴翻转​ m a t r i x [ n − r o w − 1 ] [ c o l ] matrix[row][col] \xrightarrow{水平轴翻转​}matrix[n−row−1][col] matrix[row][col]水平轴翻转 matrix[nrow1][col]

对于主对角线翻转而言,我们只需要枚举对角线左侧的元素,和右侧的元素进行交换,即
m a t r i x [ r o w ] [ c o l ] → 主对角线翻转 m a t r i x [ c o l ] [ r o w ] matrix[row][col] \xrightarrow{主对角线翻转} matrix[col][row] matrix[row][col]主对角线翻转 matrix[col][row]
将它们联立即可得到:
matrix [ row ] [ col ] ⇒ 水平轴翻转 matrix [ n − row − 1 ] [ col ] ⇒ 主对角线翻转 matrix [ col ] [ n − row − 1 ] \begin{aligned} \textit{matrix}[\textit{row}][\textit{col}] & \xRightarrow[]{水平轴翻转} \textit{matrix}[n - \textit{row} - 1][\textit{col}] \\ &\xRightarrow[]{主对角线翻转} \textit{matrix}[\textit{col}][n - \textit{row} - 1] \end{aligned} matrix[row][col]水平轴翻转 matrix[nrow1][col]主对角线翻转 matrix[col][nrow1]

	void rotate(vector<vector<int>>& matrix) {       
        int n = matrix.size();
        //整体坐标变换关系 [i][j] -> [j][n-1-i]
        //先上下翻转 [i][j] -> [n-1-i][j]
        for(int i = 0; i < n / 2; ++i){
            for(int j = 0; j < n; ++j){
                swap(matrix[i][j], matrix[n-1-i][j]);
            }
        }
        //再对角线翻转   [n-1-i][j] -> [j][n-1-i]
        for(int i = 0; i < n; ++i){
            for(int j = 0; j < i; ++j){
                swap(matrix[i][j], matrix[j][i]);
            }
        }   
    }

498. 对角线遍历
给你一个大小为 m x n 的矩阵 mat ,请以对角线遍历的顺序,用一个数组返回这个矩阵中的所有元素。

示例 1:

输入:mat = [[1,2,3],[4,5,6],[7,8,9]]
输出:[1,2,4,7,5,3,6,8,9]

【分析】直接模拟,进行坐标变换
矩阵按照对角线进行遍历。设矩阵的行数为 m m m,, 矩阵的列数为 n n n , 仔细观察对角线遍历的规律可以得到如下信息:

一共有 m + n − 1 m + n - 1 m+n1 条对角线,相邻的对角线的遍历方向不同,当前遍历方向为从左下到右上,则紧挨着的下一条对角线遍历方向为从右上到左下;
设对角线 l l l 从上到下的编号为 l ∈ [ 0 , m + n − 2 ] l \in [0, m + n - 2] l[0,m+n2]

①当 l l l 为偶数时,则第 l l l 条对角线的走向是从下往上遍历,每次行索引减 1,列索引加 1,直到矩阵的边缘为止:
l < m l < m l<m 时,则此时对角线遍历的起点位置为 ( l , 0 ) (l,0) (l,0)
l ≥ m l \ge m lm 时,则此时对角线遍历的起点位置为 ( m − 1 , l − m + 1 ) (m - 1, l - m + 1) (m1,lm+1)

②当 l l l 为奇数时,则第 l l l 条对角线的走向是从上往下遍历,每次行索引加 1,列索引减 1,直到矩阵的边缘为止:
l < n l < n l<n 时,则此时对角线遍历的起点位置为 ( 0 , l ) (0, l) (0,l)
l ≥ n l \ge n ln 时,则此时对角线遍历的起点位置为 ( l − n + 1 , n − 1 ) (l - n + 1, n - 1) (ln+1,n1)
根据以上观察得出的结论,模拟遍历所有的对角线即可。

	vector<int> findDiagonalOrder(vector<vector<int>>& mat) {
        vector<int> ans;
        int m = mat.size(), n = mat[0].size();
        int lmax = m + n - 1;
        for(int l = 0; l < lmax; ++l){
            if(l % 2 == 0){
                    int x = l < m ? l : m - 1;
                    int y = l < m ? 0 : l - m + 1;
                    while(x >= 0 && y < n){
                    	ans.emplace_back(mat[x][y]);
                    	--x; ++y;
                    }
                }else{
                        int x = l < n ? 0 : l - n + 1;
                        int y = l < n ? l : n - 1;
                        while(x < m && y >= 0){
                        	ans.emplace_back(mat[x][y]);
                        	++x; --y;
                    }
                }                
            }
        return ans;
    }

补充拓展:
按照对角线 l l l 遍历,对角线以及横纵坐标都是从1开始时的坐标变换关系:

f o r for for l = 2 l=2 l=2 t o to to n n n d o do do /* 计算第 l l l 对角线 */
f o r for for i = 1 i=1 i=1 t o to to n − l + 1 n-l+1 nl+1 d o do do //横坐标
  j = i + l − 1 ; \ j=i+l-1;  j=i+l1; //对应的纵坐标

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

何处微尘

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

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

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

打赏作者

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

抵扣说明:

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

余额充值