【1】326. 3 的幂
日期:2.06
2.类型:数学
3.方法一:试除法(一次题解)
循环条件:n && n % 3 == 0
n 不为 0(0 不是 3 的幂,且除以 3 无意义)
当前数能被 3 整除
循环体:n /= 3,去掉一个因子 3
循环结束后,检查 n == 1。如果成立,则原数是 3 的幂;否则不是。
关键代码:
cpp
while(n&&n%3==0){
n/=3;
}
return n==1;
4.方法二:判断是否为最大 3 的幂的约数(一次题解)
在题目给定的 32 位有符号整数的范围内,最大的 3 的幂为 319=1162261467。只需要判断 n 是否是 319 的约数即可。
关键代码:
cpp
return n > 0 && 1162261467 % n == 0;
【2】343. 整数拆分
日期:2.07
2.类型:数学,动态规划
3.方法一:动态规划(官方题解)
定义 dp[i] 表示将正整数 i 拆分成至少两个正整数之和所能得到的最大乘积。注意,i 从 2 开始,因为 1 无法拆分(至少两个数)。
对于每个 i,尝试将其拆分成两个部分:j 和 i-j,其中 j 的取值范围是 1 到 i-1。对于每一对 (j, i-j),有两种可能性:
不再继续拆分 i-j,此时乘积为 j * (i - j);
继续拆分 i-j,即利用之前计算好的 dp[i-j],此时乘积为 j * dp[i - j]。
取这两种情况的最大值,作为当前拆分方式下的候选值。遍历所有 j,更新当前 i 的最大值,即 dp[i] = max( max(j*(i-j), j*dp[i-j]) ) 对所有 j 取最大。最终 dp[n] 即为所求。、
关键代码:
cpp
vector<int> dp(n+1);
for(int i=2;i<=n;i++){
int curMax=0;
for(int j=1;j<i;j++) {
// 比较不拆分 i-j 和拆分 i-j 两种情况
curMax=max(curMax, max(j*(i-j),j*dp[i-j]));
}
dp[i] = curMax;
}
日期:2.08
2.类型:数学,组合数学
3.方法一:排列组合(半解)
该问题可以用组合数学直接求解,不需要遍历所有数字。分别统计所有位数小于等于 n 且各位数字互不相同的数的个数,然后累加。
一位数(0~9):共 10 个(包括 0)。
两位数:首位不能是 0(否则就是一位数),所以首位有 9 种选择(1~9),次位可以是 0~9 中除去首位已选的数字,有 9 种,共 9 × 9 = 81 个。
三位数:首位同样有 9 种,次位有 9 种,第三位有 8 种,共 9 × 9 × 8 = 648 个。
依此类推,对于 k 位数(2 ≤ k ≤ n),个数为:
9 × 9 × 8 × ... × (9 - k + 2),即 9 × P(9, k-1)(排列数)。
最终答案就是一位数个数加上所有 k 位数(k=2..n)的个数之和。
关键代码:
cpp
if(n==0){
return 1;
}
if(n==1){
return 10;
}
int ans=10;
int cur=9; // cur 用于逐步计算 k 位数的个数,初始为 9 表示首位可选数
for(int i=0;i<n-1;++i){
cur*=(9-i); // 第 i+2 位数的个数:乘上当前位可选的数字个数(9,8,7,...)
ans+=cur;
}
return ans;
【4】342. 4的幂
日期:2.09
2.类型:数学,位运算
3.方法一:二进制表示中 1 的位置(官方题解)
4 的幂首先一定是 2 的幂(因为 4^k = 2^{2k}),其次它的二进制表示中唯一的 1 必须出现在偶数位(从第 0 位开始计数)。例如:
1 的二进制 0001(第 0 位是 1,偶数位)
4 的二进制 0100(第 2 位是 1,偶数位)
16 的二进制 10000(第 4 位是 1,偶数位)
而其他 2 的幂(如 2、8、32...)的 1 在奇数位,不满足条件。
关键代码:
cpp
return n > 0 && (n & (n - 1)) == 0 && (n & 0xaaaaaaaa) == 0;
日期:2.10
2.类型:数学,二分查找
3.方法一:暴力(一次题解)
如果 num 为完全平方数,那么一定存在正整数 x 满足 x×x=num。于是从 1 开始,从小到大遍历所有正整数,寻找是否存在满足 x×x=num 的正整数 x。在遍历中,如果出现正整数 x 使 x×x>num,那么更大的正整数也不可能满足 x×x=num,不需要继续遍历了。
关键代码:
cpp
long x=1,square=1;
while(square<=num){
if(square==num){
return true;
}
++x;
square=x*x;
}
return false;
4.方法二:二分查找(一次题解)
考虑使用二分查找来优化方法二中的搜索过程。因为 num 是正整数,所以若正整数 x 满足 x×x=num,则 x 一定满足 1≤x≤num。于是将 1 和 num 作为二分查找搜索区间的初始边界。因为在移动左侧边界 left 和右侧边界 right 时,新的搜索区间都不会包含被检查的下标 mid,所以搜索区间的边界始终是没有检查过的。因此,当left=right 时,仍需要检查 mid=(left+right)/2。
关键代码:
cpp
int left=0,right=num;
while(left<=right){
int mid=(right-left)/2+left;
long square=(long) mid*mid;
if(square<num){
left=mid+1;
}else if (square>num){
right=mid-1;
}else{
return true;
}
}
【6】371. 两整数之和
日期:2.11
2.类型:数学,位运算
3.方法一:位运算(官方题解)
a & b ------ 找出需要进位的位
按位与运算:对于每一位,只有 a 和 b 同时为 1 时,该位才会产生进位。
(a & b) << 1 ------ 进位左移一位
进位需要加到更高一位上,所以左移一位。
注意:这里强制转换为 unsigned int 再左移,是为了避免有符号整数左移时的未定义行为(当符号位被左移时可能产生负数,但进位应当是无符号处理)。
a = a ^ b ------ 无进位加法
异或运算:对于每一位,a 和 b 不同则为 1,相同则为 0。这恰好模拟了二进制加法不考虑进位的情况(0+0=0,0+1=1,1+0=1,1+1=0,但要产生进位)。
循环处理进位
将无进位和 a 与进位 carry 再次相加,直到进位为 0。
第一次循环后,a 变为无进位和,b 变为进位值。如果进位不为 0,说明还需要加一次,继续循环。最终当进位为 0 时,a 就是真正的和。
关键代码:
cpp
while(b!=0){
unsigned int carry=(unsigned int)(a & b)<<1;
a=a^b;
b=carry; // 将进位赋给 b,下一轮继续加
}
return a; // 最终无进位,a 即为结果
日期:2.12
2.类型:数学,动态规划
3.方法一:动态规划(半解)
如果区间只有一个数 i,那么不用猜,代价为 0(f[i][i] = 0)。
对于区间 [i, j](i < j),可以先猜一个数 k(i ≤ k ≤ j),然后根据反馈:
如果猜对了(概率情况,但我们要考虑最坏情况),实际上考虑的是最坏情况,所以需要取两个分支中较大的代价。
如果猜的数字 k 比目标大,则下一步在 [i, k-1] 中猜,代价为 f[i][k-1];
如果猜的数字 k 比目标小,则下一步在 [k+1, j] 中猜,代价为 f[k+1][j]。
本次猜错要支付金额 k,所以总代价为 k + max(f[i][k-1], f[k+1][j])。
可以在所有可能的 k 中,选择使得这个最坏代价最小的那个,即:
f[i][j] = min_{k ∈ [i,j]} { k + max(f[i][k-1], f[k+1][j]) }
关键代码:
cpp
for(int i=n-1;i>=1;i--){
for(int j=i+1;j<=n;j++){
f[i][j]=j+f[i][j-1];
for(int k=i;k<j;k++){
f[i][j]=min(f[i][j], k+max(f[i][k-1],f[k+1][j]));
}
}
}
【8】390. 消除游戏
日期:2.13
2.类型:数学
3.方法一:等差数列模拟(半解)
每轮删除后,剩余数字个数减半(cnt >>= 1),间隔加倍(step <<= 1)。关键在于更新新的第一个数字 a1:
从左到右删除(k % 2 == 0):第一个数字会被删除,新的第一个变为原来的第二个,即 a1 += step。
从右到左删除(k % 2 == 1):第一个数字是否被删除取决于当前个数 cnt 的奇偶性:
若 cnt 为奇数,则第一个会被删除,新第一个为 a1 + step;
若 cnt 为偶数,则第一个保留,a1 不变。
关键代码:
cpp
while(cnt>1){
if(k%2==0)a1+=step; // 正向
else a1=(cnt%2==0)?a1:a1+step; // 反向
k++;
cnt>>=1; // 个数减半
step<<=1; // 步长加倍
}
【9】396. 旋转函数
日期:2.14
2.类型:数学,迭代
3.方法一:迭代(半解)
记数组 nums 的元素之和为 numSum。根据公式,可以得到:
F(0)=0×nums[0]+1×nums[1]+...+(n−1)×nums[n−1]
F(1)=1×nums[0]+2×nums[1]+...+0×nums[n−1]=F(0)+numSum−n×nums[n−1]
更一般地,当 1≤k<n 时,F(k)=F(k−1)+numSum−n×nums[n−k]。可以不停迭代计算出不同的 F(k),并求出最大值。
关键代码:
cpp
int f=0,n=nums.size();
int numSum=accumulate(nums.begin(),nums.end(),0);
for(int i=0;i<n;i++){
f+=i*nums[i];
}
int res=f;
// 从最后一个元素开始,依次计算 F(1) 到 F(n-1)
for(int i=n-1;i>0;i--){
f+=numSum-n*nums[i];
res=max(res, f);
}
return res;