【1】60. 排列序列
日期:1.6
2.类型:数学
3.方法:数学 + 缩小问题规模(官方题解)
不使用回溯生成所有排列,而是通过数学计算直接确定每一位的数字。
关键概念:
阶乘数组 :factorial[i] 存储 i! 的值
数字分组 :对于第i位,以某个数字开头的排列有 (n-i)! 个
确定顺序 :通过 k / (n-i)! 计算当前位应该取剩余数字中的第几个
关键代码:
cpp
for(int i=1;i<=n;++i){
// factorial[n-i]:剩余(n-i)个数字的全排列数量
int order=k/factorial[n-i]+1;
for(int j=1;j<=n;++j){
// 如果数字j可用,order减1
order-=valid[j];
// 当order减到0时,说明找到了第order个可用数字
if(!order){
ans+=(j+'0'); // 将数字转换为字符加入结果
valid[j]=0; // 标记该数字为已使用
break;
}
}
【2】70. 爬楼梯
日期:1.7
2.类型:数学,动态规划
3.方法一:动态规划(半解)
这是一个典型的斐波那契数列问题:
爬到第1阶:1种方法(爬1阶)
爬到第2阶:2种方法(1+1或直接爬2阶)
爬到第i阶:可以从第(i-1)阶爬1阶上来,或从第(i-2)阶爬2阶上来
关键代码:
cpp
int p=0,q=0,r=1; // p: dp[i-2], q: dp[i-1], r: dp[i]
for(int i=1;i<=n;++i){
p=q; // p 更新为上一轮的 q (dp[i-2])
q=r; // q 更新为上一轮的 r (dp[i-1])
r=p+q; // r 更新为 p + q (dp[i] = dp[i-1] + dp[i-2])
}
4.方法二:通项公式(半解)
核心思想:斐波那契数列的通项公式
对于爬楼梯问题:
爬到第0阶:1种方法(不爬)→ 对应 F(1) = 1
爬到第1阶:1种方法 → 对应 F(2) = 1
爬到第2阶:2种方法 → 对应 F(3) = 2
爬到第n阶:对应 F(n+1)
关键代码:
cpp
// 计算√5的值,斐波那契数列通项公式中需要用到
double sqrt5=sqrt(5);
// 使用斐波那契数列的通项公式直接计算第n+1项
// 注意:爬楼梯问题的解是斐波那契数列的第n+1项
double fibn=pow((1+sqrt5)/2,n+1)pow((1-sqrt5)/2,n+1);
// 将结果除以√5并四舍五入,然后转换为整数
return (int)round(fibn/sqrt5);
【3】69. x 的平方根
日期:1.8
2.类型:数学,二分查找
3.方法一:袖珍计算器算法(半解)
核心思想:利用指数和对数的性质
根据数学公式:sqrt(x) = x^(1/2) = e^(0.5 * ln(x))
其中:ln(x) 是自然对数(以e为底),e^(y) 是指数函数
计算步骤:
计算 log(x) - 自然对数
乘以 0.5 - 相当于开平方
计算 exp(...) - 指数函数,得到平方根的近似值
转换为整数并修正误差
关键代码:
cpp
if(x==0){
return 0;
}
// 使用数学公式:sqrt(x) = exp(0.5 * log(x))
int ans=exp(0.5*log(x));
// 使用long long类型避免乘法溢出
return((long long)(ans+1)*(ans+1)<=x?ans+1:ans);
【4】89. 格雷编码
日期:1.9
2.类型:数学,位运算
3.方法一:位运算(官方题解)
核心思想:镜像反射法
格雷码可以通过递归镜像反射的方式生成:
1位格雷码:0, 1
n位格雷码可以通过以下方式从(n-1)位格雷码生成:
保留(n-1)位格雷码序列
将(n-1)位格雷码序列逆序
在每个逆序的格雷码前添加一位1(即在最高位加1)
关键代码:
cpp
ret.reserve(1<<n); // 1<<n = 2^n
// 格雷码序列总是以0开始
ret.push_back(0);
// 从1位格雷码开始,逐步构建到n位格雷码
for(int i=1;i<=n;i++){
// 获取当前已生成的格雷码数量(即2^(i-1))
int m=ret.size();
for(int j=m-1;j>=0;j--){
// (1 << (i-1)) 创建第i位为1的二进制数
// ret[j] | (1 << (i-1)) 将已有格雷码的第i位置为1
ret.push_back(ret[j] | (1 << (i - 1)));
}
}
日期:1.10
2.类型:数学,栈,数组
3.方法一:栈(一次题解)
核心思想:使用栈计算后缀表达式
逆波兰表达式的计算规则:
-
从左到右扫描表达式
-
遇到数字就压入栈中
-
遇到运算符就从栈中弹出两个数字进行计算
-
将计算结果压回栈中
-
重复上述过程,直到表达式结束
-
栈中最后剩下的数字就是结果
关键代码:
cpp
for(int i=0;i<n;i++){
string& token = tokens[i];
// 如果是数字,压入栈中
if(isNumber(token)){
// 将字符串转换为整数并压栈
stk.push(atoi(token.c_str()));
}else{
// 注意:先弹出的是右操作数,后弹出的是左操作数
int num2=stk.top(); // 右操作数
stk.pop();
int num1=stk.top(); // 左操作数
stk.pop();
// 根据运算符进行计算,并将结果压回栈中
switch(token[0]){
case '+':
stk.push(num1+num2);
break;
case '-':
stk.push(num1-num2);
break;
case '*':
stk.push(num1*num2);
break;
case '/':
stk.push(num1/num2);
break;
}
}
}
return stk.top();
}
【6】189. 轮转数组
日期:1.11
2.类型:数学,双指针,数组
3.方法一:使用额外的数组(一次题解)
可以使用额外的数组来将每个元素放至正确的位置。用 n 表示数组的长度,遍历原数组,将原数组下标为 i 的元素放至新数组下标为 (i+k)modn 的位置,最后将新数组拷贝至原数组即可。
关键代码:
cpp
vector<int> newArr(n); // 创建一个新的数组,大小与原数组相同
for(int i=0;i<n;++i){
// 计算元素在新数组中的位置:(i + k) % n
newArr[(i+k)%n]=nums[i];
}
nums.assign(newArr.begin(), newArr.end());
}
【7】204. 计数质数
日期:1.12
2.类型:数学,枚举
3.方法一:枚举(一次题解)
核心思想:逐个判断每个数是否为质数
对于每个数i(2 ≤ i < n),检查它是否为质数
如果是质数,计数器加1
质数判断方法:
要判断一个数x是否为质数,只需检查它是否能被2到√x之间的任何整数整除:
如果x能被其中任何一个数整除,则x不是质数
如果x不能被任何数整除,则x是质数
关键代码:
cpp
bool isPrime(int x){
// 遍历从2到sqrt(x)的所有数
for(int i=2;i*i<=x;++i){
// 如果x能被i整除,则x不是质数
if(x%i==0){
return false;
}
}
【8】223. 矩形面积
日期:1.13
2.类型:数学,几何
3.方法一:计算重叠面积(半解)
每个矩形由两个点表示:
(ax1, ay1):矩形A的左下角坐标
(ax2, ay2):矩形A的右上角坐标
同样,(bx1, by1)和(bx2, by2)表示矩形B
注意:题目保证ax1 < ax2,ay1 < ay2,bx1 < bx2,by1 < by2
关键代码:
cpp
// 计算两个矩形的面积
int area1=(ax2-ax1)*(ay2-ay1);
int area2=(bx2-bx1)*(by2-by1);
// 计算重叠部分的宽度和高度
int overlapWidth=min(ax2, bx2)-max(ax1, bx1);
int overlapHeight=min(ay2, by2)-max(ay1, by1);
// 计算重叠部分的面积(如果没有重叠,则面积为0)
int overlapArea=max(overlapWidth, 0)*max(overlapHeight, 0);
// 总面积 = 两个矩形面积之和 - 重叠部分面积
return area1+area2-overlapArea;
日期:1.14
2.类型:数学,枚举
3.方法一:逐位计算(官方题解)
对于第k位(从0开始计数,个位是第0位):
设当前位数为10^k(代码中的mulk)
假设我们有一个数abcdefg,当前处理百位(k=2,mulk=100)
高位部分:abc(即n / (mulk * 10) = n / 1000)
当前位:d(即(n % 1000) / 100)
低位部分:efg(即n % 100)
当前位为1的三种情况:
情况1:高位小于abc(0到abc-1),高位可以取0到abc-1,共abc种,每种高位,当前位固定为1
低位可以取0到99(共mulk=100种)
贡献:abc * 100
对应代码:(n / (mulk * 10)) * mulk
n / (mulk * 10)就是高位abc
乘以mulk(即100)
情况2:高位等于abc,当前位等于1,此时数字形式为abc1efg,贡献的低位范围:0到efg,共efg+1种
情况3:高位等于abc,当前位大于1
此时当前位为1时,低位可以取0到99(共100种)
通用公式:
当前位为1的个数 =(高位数字) * 当前位数 +min(max(低位数字 + 1, 0), 当前位数) [当当前位数字=1时] 或当前位数 [当当前位数字>1时]
合并为一个公式:
count = (n / (10 * mulk)) * mulk + min(max(n % (10 * mulk) - mulk + 1, 0), mulk)
关键代码:
cpp
// 循环处理每一位(个位、十位、百位...)
for(int k=0;n>=mulk;++k){
// 公式分为两部分:
// 1. (n / (mulk * 10)) * mulk
// 2. min(max(n % (mulk * 10) - mulk + 1, 0LL), mulk)
ans+=(n/(mulk*10))*mulk +
min(max(n % (mulk * 10) - mulk + 1, 0LL), mulk);
mulk*=10;
}
【10】279. 完全平方数
日期:1.15
2.类型:数学,动态规划
3.方法一:动态规划(一次题解)
状态定义:f[i] 表示组成数字 i 所需的最少完全平方数的个数。
状态转移方程:
对于每个 i,我们考虑所有可能的完全平方数 j*j(其中 j*j ≤ i):
f[i] = min(f[i], f[i - j*j] + 1) 对于所有满足 j*j ≤ i 的 j
其中 f[i - j*j] + 1 表示:如果使用 j*j 这个完全平方数,那么剩下的部分 i - j*j 需要 f[i - j*j] 个完全平方数,再加上当前的 j*j 这一个。
关键代码:
cpp
for(int i=1;i<=n;i++){
int minn=INT_MAX;
// 遍历所有小于等于 i 的完全平方数
for(int j=1;j*j<=i;j++){
// 如果使用 j*j 这个完全平方数,那么剩余部分为 i - j*j
minn=min(minn, f[i-j*j]);
}
// 最终 f[i] 等于找到的最小值加 1(加上当前这个完全平方数)
f[i]=minn+1;
}
return f[n];