0%

数学类问题

矩阵问题
LC73 矩阵置零
LC54 螺旋矩阵
LC59 螺旋矩阵II
LC48 旋转图像
LC74 Search a 2D Matrix
LC240 Search a 2D Matrix II
LC378 Kth Smallest Element in a Sorted Matrix

基本运算
LC371 两整数之和
LC369, 给单链表加一
LC55 加一
LC2 两数相加
LC415 字符串相加
LC445 两数相加II
LC43 字符串相乘
LC29 两数相除
LC50 快速幂
LC96 Sqrt(x)

浮点数比较
LC149 直线上最多的点数

日期间隔

溢出处理:
LC7 整数反转

进制转换:
LC13 罗马数字转整数 [优先匹配2字符]
LC168 Excel表列名称 [没有0,商借1给余数]

常见数列:
LC118 二项式系数
LC119 二项式系数II

随机数:
LC470 用Rand7()实现Rand10()

日期间隔

矩阵

矩阵查找

LC74 Search a 2D Matrix
LC240 Search a 2D Matrix II
二分查找#矩阵查找

LC378 Kth Smallest Element in a Sorted Matrix
顺序统计量#矩阵上的TopK

矩阵置零

LC73 矩阵置零

最开始的想法:遍历矩阵的每一个元素,记录下所有要置零的行号和列号。之后再对这些行和列置零。最差情况为所有行和列都需要置零,则空间复杂度为$O(M + N)$。
改进:是否能把这最多(M + N)整合进原矩阵,又不损失原矩阵的信息呢?
我们的遍历顺序是从小到大的,因此我们只要把(M + N)的信息记录在我们已经遍历过的地方就可以了。最直接的办法就是记录在第一行和第一列。但是第一行加上第一列总共有(M + N - 1)个元素,我们还需要再用额外一个元素,就可以记录(M + N)个信息了。
设矩阵A大小为M x N

  • 遍历第一行和第一列,将第一行是否需要置0记录在A[0,0],将第一列是否需要置零记录在额外的空间上。
  • 按从小到大的顺序遍历第i行第j列,如果A[i,j] = 0,则把A[0, j]和A[i, 0]变为0
  • 检查第(2 … M)行第一个元素,如果是零,将该行置0
  • 检查第一行第(2 … N)个元素,如果是零,将该列置0
    现在我们可以处理第一行和第一列了:
  • 检查A左上角元素,如果是0,将第一行置0
  • 检查额外空间上记录的第一列是否需要置零,如果需要,将第一列置0

关于在C++的vector上使用memset
memset on vector C++, stackoverflow

If your vector contains POD types, it is safe to use memset on it - the storage of a vector is guaranteed to be contiguous.
memset(&vec[0], 0, sizeof(vec[0]) * vec.size());
Edit: Sorry to throw an undefined term at you - POD stands for Plain Old Data, i.e. the types that were available in C and the structures built from them.
Edit again: As pointed out in the comments, even though bool is a simple data type, vector<bool> is an interesting exception and will fail miserably if you try to use memset on it.

矩阵遍历

LC54 螺旋矩阵

C++详细题解, Leetcode题解

  • 设定上下左右边界:
    • up = 0, bot = m - 1, left = 0, right = n - 1
  • 按照右下左上的顺时针顺序遍历。
    • 向右遍历到右边界后,向下移动上边界。
    • 向下遍历到下边界后,向左移动右边界。
    • 向左遍历到左边界后,向上移动下边界。
    • 向上遍历到上边界后,向右移动左边界。
  • 在移动边界时,如果发现上下边界交错 或 左右边界交错 —> 遍历结束,停止遍历。

使用size_t来做逆向遍历时,一定要注意下溢。

常见的size_t下溢处理:

--right < left 改为 right-- < left + 1
for(int i = right; i >= left; --i) 改为 for(int i = right + 1; i-- >= left + 1;)

反向遍历的时候还是尽量用reverse_iterator不要用size_t了。用的话就一定要提高警惕性。


LC59 螺旋矩阵II

同上题,只不过这次是填充。

矩阵转置

LC48 旋转图像

思路1:先转置再逐行reverse。假设$\mathbb{R}^{N\timesN}$方阵,共做swap $\frac{(1 + (N - 1))(N-1)}{2} + N^2 / 2 = N^2 - N/2$次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public:
void rotate(vector<vector<int>>& matrix) {
for(size_t i = 0; i < matrix.size(); ++i)
{
for(size_t j = i + 1; j < matrix.size(); ++j)
{
swap(matrix[i][j], matrix[j][i]);
}
}

for(auto& row : matrix)
{
reverse(row.begin(), row.end());
}
}
};

思路二:
每次在四元组(a,b,c,d),其中a为左上角,设其为(i,j)内顺时针转动:

  • i每增加1,矩阵都缩小1
  • j每增加1,所有的点都顺时针移动1。a向右,b向下,c向左,d向上
  • a在左上角(i, j)
  • b一开始在右上角(0, n - 1),向下动。一定位于矩阵的最右边那一行(0 + n - 1 - i),而且j增加1就往下移动一格,所以猜测是(j, n - 1 - i)
  • c一开始在右下角(n - 1, n - 1),向左动。一定位于矩阵的最后一行,j增加1就向左一行,所以(n - 1 - i, n - 1 - j)
  • d一开始在左下角(n - 1, 0),向上动。一定位于矩阵最左边那一列,j增加1就向上一行(n - 1 - j, i);
  • i 在 0 到 (n + 1)/2 变化。j 在 0 到 n/2 变化。

基本运算

位运算实现加法

LC371 两整数之和

题目的思路就是运用了【XOR是不进位加法】 和 【位与之后左移一位是进位】:

  • 先对两个数做XOR —> sum
  • 再对两个数做位与后左移一位 —> carry。
  • 用前两步的结果(sum 和 carry)再重复前面两步,直到第二步不再产生进位。

来自剑指Offer 56,以17和5为例子
十进制(XOR替换为不进位加法,位与左移替换为进位):

1
2
3
4
| SUM | CARRY |
| ----| ----- |
| 12 | 10 |
| 20 | 0 |

二进制:(5: 00101, 17: 10001)

1
2
3
4
| SUM  | CARRY |
| ---- | ----- |
| 10100| 00010 |
| 10110| 00000 |

注意Leetcode里面不允许负数左移,所以需要cast一下


大数加法

大数加法的思路:

  • 从两个数的最低位开始
  • 设一个变量为carry = 0
  • 当第一个数没遍历完 或 第二个数没遍历完 或 carry != 0
    • 设一个临时变量tmp = carry
    • 如果第一个数没遍历完tmp +=第一个数这一位,第一个数前进一位
    • 如果第二个数没遍历完tmp +=第二个数这一位,第二个数前进一位
    • 相加结果为tmp % 10,推入一个数组中
    • 进位结果为tmp / 10
  • 把之前储存结果的数组逆序即可

LC369, 给单链表加一


LC55 加一

在数组上实现加一。初始化carry = 1,反向遍历数组进行大数加法。如果数组遍历完成后依然有carry == 1,则说明需要在数组头部再插入一个1


LC2 两数相加

这个题已经帮你翻转过来了,非常轻松


LC445 两数相加II

这个题是单链表,并且数字不是逆序放置,也不让修改链表,将链表逆序。

思路:
无论如何都得从小往大加,只能通过两个栈来帮助逆序了。有些解法通过递归来逆序,然而我觉得还不如显式用栈来减少函数调用开销。毫无意义的一个题。


LC415 字符串相加


LC67 二进制求和

大数加法的二进制版本

乘法

LC43 字符串相乘

思路一:普通竖式乘法。

  • 用一个数字从后往前的每一位,乘上另一个数字从后往前的每一位。每次前进一位不要忘了补0。之后再用大数加法将结果加起来。速度非常的慢。

思路二:改进的竖式乘法
Easiest JAVA Solution with Graph Explanation

  • M位的数和N位的数相乘,结果最多M+N位,比如(123(3位) x 999(3位) < 123 x 1000 = 123000(6位))
  • 从左往右数,第一个数的第i位 和 第二个数的第j位 相乘,结果最多也只能是两位数。并且应该被放置在i + j和i + j + 1位上。
  • 我们仍然从右往左算,但要注意进位。在从右往左的过程中,只有i + j + 1和原来的地方有重叠。

注意:

  • 在这题当中第一次用到逆序的size_t做index,不可以像下面这么写
    1
    2
    3
    4
    5
    // this is wrong!
    for(size_t i = container.size() - 1; i >= 0; --i)
    {
    ...
    }

因为当 i == 0时,--i还会再发动一次,这就导致size_t类型直接下溢。

  • 正确的写法是
1
2
3
4
for(size_t i = container.size(); i--;)
{
...
}

直接把decreasing放在条件处,并使用后置形式。这样在第一次进入循环时,i == container.size() - 1,并且最后一次时也不会下溢。

不用乘除取余做除法

LC29 Divide Two Integers

因为不允许使用int64_t,导致边界条件判断非常复杂。再加上LeetCode编译器不允许负数位移,可以说是LeetCode恶心之最。TODO

求整数的平方根

LC96 Sqrt(x)

二分查找#数学上的二分查找##求整数的平方根

快速幂

LC50 快速幂

将幂指数n分解为二进制数,之后进行运算。
举例:
$11 = 1011_{2}$
$x^11 = x^{1 \times 2^0 + 1\times 2^1 + 0 \times 2^2 + 1 \times 2^3} = x^0 \times x^1 \times x^3$

思路:
将幂指数不断右移,查询最后一位,如果是1,就乘上对应的项即可。

注意:

  • 需要处理负数。
  • 神经病一样的测试用例:指数为INT_MIN

浮点数比较

如何正确比较浮点数

What is the most effective way for float and double comparison? : stackoverflow

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
bool approximatelyEqual(float a, float b, float epsilon)
{
return fabs(a - b) <= ( (fabs(a) < fabs(b) ? fabs(b) : fabs(a)) * epsilon);
}

bool essentiallyEqual(float a, float b, float epsilon)
{
return fabs(a - b) <= ( (fabs(a) > fabs(b) ? fabs(b) : fabs(a)) * epsilon);
}

bool definitelyGreaterThan(float a, float b, float epsilon)
{
return (a - b) > ( (fabs(a) < fabs(b) ? fabs(b) : fabs(a)) * epsilon);
}

bool definitelyLessThan(float a, float b, float epsilon)
{
return (b - a) > ( (fabs(a) < fabs(b) ? fabs(b) : fabs(a)) * epsilon);
}

直线上最多的点数

LC149 直线上最多的点数
方法一:
解决浮点数比较的最好办法,就是尽量不要去比较。
思路:对于每一个点$p_i$,遍历所有其他的点$p_j$,统计斜率相等的个数的最大值。遍历所有的点,计算所有最大值的最大值即为所求。
注意:

  • 斜率用浮点数表示会出现精度问题。可以采用分数表示:
    • A = x2 - x2, B = y2 - y1
    • 如果A = 0,统一令B = INT_MAX
    • 如果B = 0, 统一令A = INT_MAX
    • 如果 A != 0 && B != 0
      • 计算A和B的最大公约数,可以用<numeric>库中的std::gcd(C++17)
      • A和B都除最大公约数约分。
      • 注意A和B的符号,如果A和B都为负 或 B负A正,翻转A和B的符号。
  • 测试用例中有重复的点。我们需要记录重复的点的个数,最后加到$p_i$的斜率相等个数最大值上。

Leetcode上的double精度不够,必须得用long double才能通过一些测试。

溢出处理

整数反转

LC7 整数反转

思路:把一个数mod 10取个位,再除10去掉个位。把结果乘10,再把个位加到结果上。
注意:res = res * 10 + pop这一步会溢出。分四种情况:

  • 如果res > INT_MAX / 10必然溢出。
  • 如果res == INT_MAX / 10pop > 7溢出。
  • 如果res < INT_MIN / 10必然溢出。
  • 如果res == INT_MIN / 10pop < -8溢出。

进制转换

十进制转换为N进制

除N取余,逆序排列。


LC168 Excel表列名称

'A' - 'Z'表示1 ~ 26的数字。但是没有0。如果取余出现了0,就要从商中借1给余数,让余数变为26。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public:
string convertToTitle(int n) {
auto ans = string{};
while(n)
{
auto remainder = n % 26;
n = n / 26;

if(remainder == 0)
{
--n;
remainder = 26;
}

ans += static_cast<char>(remainder - 1) + 'A';
}
reverse(ans.begin(), ans.end());
return ans;
}
};

罗马数字

LC13 罗马数字转整数

罗马数字转换就是从左到右累加的过程。

  • 每次优先读取两个字符类的数字:IV = 4, IX = 9, XL = 40, XC = 90, CD = 400, CM = 900
  • 如果无法匹配两个字符,就尝试匹配一个字符:I = 1, V = 5, X = 10, L = 50, C = 100, D = 500, M = 1000
    将所有匹配制作成hashmap即可。

常见数列

二项式系数

二项式系数是$(1 + x)^n$展开后,$x$的系数。其第$n$行第$i$项为组合数$C_n^i$的值:
$(1 + x)^0 = 1$
$(1 + x)^1 = x + 1$
$(1 + x)^2 = x^2 + 2x + 1$
$(1 + x)^3 = x^3 + 3x^2 + 3x + 1$
$\cdots$

计算二项式系数时,可以初始化第0行为1。其后每一行的第一个和最后一个元素都为1。除首尾外,第n行的第i个元素为b[n][i] = b[n - 1][i - 1] + b[n - 1][i],只和前一行相关。


LC118 二项式系数

生成二项式系数的前$n$行。


LC119 二项式系数II

生成二项式系数的第$k$行。反向填表即可。

卡塔兰数

LC96 不同的二叉搜索树

每次我们选一个i当作根节点的时候,其实左右子树的数量和左右区间的具体范围无关,只和左右区间的大小有关。因此我们可以总结出下面的式子:
设$G(n)$为以$1 \cdots n$为节点的二叉搜索树个数,那么以第$0< i <= n$个数字为根结点,左右子树能够产生的组合个数是:

$G(n - i) * G(i - 1)$

那么总的个数就是:

$G(n) = \sum_{i = 1}^{n} G(n - i) * G(i - 1)$

定义边界条件

$G(0) = 1$, $G(1) = 1$

不难看出这是一个动态规划问题,为了避免重复计算,我们可以记录已经计算过的结果。或者从底向上填表。这个题因为状态转移方程中涉及之前所有的状态,所以似乎不能进行很好的空间复杂度的优化。

事实上$G(n)$被称为卡塔兰数,递推公式可以写作$Gn = \frac{4n + 2}{n + 2}G{n -1}$,$G_0 = 1$。通项公式为$G_n = \frac{(2n)!}{(n+1)!n!}$。因此,我们可以在线性时间内,常数空间复杂度的情况下算出$G_n$。


随机数

拒绝采样

LC470 用Rand7()实现Rand10()

参考由rand7生成rand10以及随机数生成方法的讨论 - 毕达哥拉斯半圆

思路一:
调用两次Rand7(),将会生成共$7 \times 7 = 49$种有序数对。我们拒绝采样其中9种有序数对,则剩下的有序数对出现的概率都为$\frac{1}{40}$。我们将剩下的有序数对每10个一组依次对应[1,10]的一个值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
+----------------------+                 +---------------+
| | 1| 2| 3| 4| 5| 6| 7| | |1|2|3|4|5|6|7|
+----------------------+ +---------------+
|1| 1| 2| 3| 4| 5| 6| 7| |1|2|3|4|5|6|7|8|
+----------------------+ +---------------+
|2| 8| 9|10|11|12|13|14| |2|9|A|1|2|3|4|5|
+----------------------+ +---------------+
|3|15|16|17|18|19|20|21| mod 10) + 1 |3|6|7|8|9|A|1|2|
+----------------------+ +------------> +---------------+
|4|22|23|24|25|26|27|28| reject >=41 |4|3|4|5|6|7|8|9|
+----------------------+ +---------------+
|5|29|30|31|32|33|34|35| |5|A|1|2|3|4|5|6|
+----------------------+ +---------------+
|6|36|37|38|39|40|41|42| |6|7|8|9|A|1|X|X|
+----------------------+ +---------------+
|7|43|44|45|46|47|48|49| |7|X|X|X|X|X|X|X|
+----------------------+ +---------------+

第一个随机数auto row = rand7()决定在左表中的行数。第二个随机数auto col = rand7()决定在左表中的列数。则表中的数字可以由行列唯一决定auto num = 7 * (row - 1) + col。如果得到的数字num > 40,就拒绝这一次采样。

设拒绝概率为$P_r$,成功产生随机数时,调用rand7()的期望次数为级数$2 \times (1 + P_r + P_r^2 + P_r^3 + \cdots)$的和。因为$P_r < 1$,所以该级数和收敛于$2 \times \frac{1}{1 - P_r}$,本例中为$2.45$。


思路二:
通过拒绝采样,使得auto row = rand7()只能取得[1,2],auto col = rand7()只能取得[1, 5]。

1
2
3
4
5
6
7
+-+--------------+
| | 1| 2| 3| 4| 5|
+----------------+
|1| 1| 2| 3| 4| 5|
+----------------+
|2| 6| 7| 8| 9|10|
+----------------+

只需返回 5 * (row - 1) + col即可。

两次产生随机数的事件是独立的,那么成功地产生随机数的概率为$P = \frac{5}{7} \times \frac{2}{7} = 10/49$。产生失败的概率为$P = 39/49$。则期望的调用次数为$2 \times \frac{1}{1 - \frac{39}{49}} = 9.8$次。