目录
第 29 天 动态规划(困难)
剑指 Offer 49. 丑数
一开始我想找规律
于是我觉得应该是有一个 旧的丑数 * 质因子 = 新的丑数
的规律
于是我就想试试直接这么生成可不可以
看了一下题解,这样是可以的,用哈希来去重
但是我有个问题是,像这里,从 1 到 2 3 5 的话,那么 4 不就被跳过了吗?那么比如我要去的是第 4 个元素的话,那么加入了 2 3 5 之后,如果现在要取,就会取到 5
那么他是这样的,就是为了保证没有缺掉元素,它一定要考虑到当前数组中的元素数量要大于要取的 n
就比如我这里,数组中的元素刚好为 n 时他会少一些元素,而之所以会少,是因为没有考虑到这 n 个元素中所有元素所产生的新的丑数,也就是说只考虑到了前面的一些丑数,序号为 n-2 n-1 n 的丑数可能产生的新丑数没有考虑
所以要考虑完这 n 个旧丑数,那么也就是有 n 次产生新丑数的过程,那么也就是会产生 3n 个
然后再通过最小堆,输出 n 次就得到了第 n 个数
按需前进的多指针
之后看到动规的话,很强
他主要是把这个乘质因子加入数组的过程再次化简了
因为我们一般加入新元素都是单指针,指针指到谁,谁就直接乘三个质因子放进去
如果这样单指针,就会有之前的,每一次新生成的丑数的大小顺序之间可能不一样的问题
时间消耗主要在最小堆对这个大小顺序的排列上
但是很显然,我明明是从小的丑数生成大的丑数,我明明是已经有了一个大小关系的,只是我不能一下子找到
所以为了解决这个大小顺序的问题,他就拆成三个指针,然后三个指针分别判断 *2 *3 *5
,谁生成的新的丑数最小,谁就前进
如果有多个指针生成的新的丑数一样大,那就是发生了重复,那么就都前进
就是这种按需前进的策略,就能保证顺序一定是对的
class Solution {
public:
int nthUglyNumber(int n) {
vector<int> dp(n, 1);
int p2 = 0, p3 = 0, p5 = 0;
int num1 = 0, num2 = 0, num3 = 0, min_tmp = 0;
for (int i = 0; i < n-1; ++i) {
num1 = dp[p2] * 2;
num2 = dp[p3] * 3;
num3 = dp[p5] * 5;
min_tmp = min(min(num1, num2), num3);
dp[i + 1] = min_tmp;
if (min_tmp == num1) ++p2;
if (min_tmp == num2) ++p3;
if (min_tmp == num3) ++p5;
}
return dp[n-1];
}
};
剑指 Offer 60. n个骰子的点数
点数之和的范围为 [n, 6n]
那么可以设 dp[n][5n+1]
那么 dp[i][j] 表示投掷 i+1 次,和为 j+n 的概率?
这样想似乎不对
应该是点数之和的范围随投掷次数的改变而改变
假设所有情况都对齐到左边的话
那么 dp[0] 表示 1 到 6
和为 1 是 dp[0][0] 2 是 dp[0][1] 3 是 dp[0][2] 4 是 dp[0][3] …
dp[1] 表示 2 到 11
和为 2 是 dp[1][0] 3 是 dp[1][1] …
那么如果我希望状态转移方程就是 dp[i][j] = dp[i-1][j] + 1
那就需要在投掷次数较小的时候,把它的和的下标与最终的和的每一个区间的下限的下标对齐?
好像也不对
实际上只需要这样算就好了
于是我写成:
class Solution {
public:
vector<double> dicesProbability(int n) {
vector<vector<int>> dp(n, vector<int>(5 * n + 1, 0));
// 第一次的投掷次数全为 1
for (int j = 0; j < 6; ++j) {
dp[0][j] = 1;
}
//
for (int i = 1; i < n; ++i) {
// 上一次是第 i 次投掷
// 上一次的投掷点数之和的范围是 [i, 6i]
// 上一次的投掷点数之和的种类数为 5i+1
// 也就是上一行有 5i+1 个情况
for (int j = 0; j < 5 * i + 1; ++j) {
// 每一个情况要加 6 次到下一行
for (int k = 1; k <= 6; ++k) {
dp[i][j + k - 1] += (dp[i - 1][j] + k);
}
}
}
vector<int> count = dp[n - 1];
sort(count.begin(), count.end());
vector<double> ans(5 * n + 1, 0);
int sum_count = 0;
for (int i = 0; i < 5 * n + 1; ++i) {
sum_count += count[i];
}
for (int i = 0; i < 5 * n + 1; ++i) {
ans[i] = (double)count[i] / (double)sum_count;
}
return ans;
}
};
但是之后我才发现我写错了
dp 是表示投掷到的次数
于是每一次加的时候应该加 1
class Solution {
public:
vector<double> dicesProbability(int n) {
vector<vector<int>> dp(n, vector<int>(5 * n + 1, 0));
// 第一次的投掷次数全为 1
for (int j = 0; j < 6; ++j) {
dp[0][j] = 1;
}
//
for (int i = 1; i < n; ++i) {
// 上一次是第 i 次投掷
// 上一次的投掷点数之和的范围是 [i, 6i]
// 上一次的投掷点数之和的种类数为 5i+1
// 也就是上一行有 5i+1 个情况
for (int j = 0; j < 5 * i + 1; ++j) {
// 每一个情况要加 6 次到下一行
for (int k = 1; k <= 6; ++k) {
dp[i][j + k - 1] += (dp[i - 1][j] + 1);
}
}
}
vector<double> ans(5 * n + 1, 0);
int sum_count = 0;
for (int j = 0; j < 5 * n + 1; ++j) {
sum_count += dp[n - 1][j];
}
for (int j = 0; j < 5 * n + 1; ++j) {
ans[j] = (double)dp[n - 1][j] / (double)sum_count;
}
return ans;
}
};
但是这里到了 n = 3 还是错了
最后才发现这个是,其实不用每次加一个 1,直接继承上一次的数就行了
因为他其实表示的是投掷次数嘛,所以说本次的投掷次数应该是上一次的所有的相关的投掷次数之和
class Solution {
public:
vector<double> dicesProbability(int n) {
vector<vector<int>> dp(n, vector<int>(5 * n + 1, 0));
// 第一次的投掷次数全为 1
for (int j = 0; j < 6; ++j) {
dp[0][j] = 1;
}
for (int i = 1; i < n; ++i) {
// 上一次是第 i 次投掷
// 上一次的投掷点数之和的范围是 [i, 6i]
// 上一次的投掷点数之和的种类数为 5i+1
// 也就是上一行有 5i+1 个情况
for (int j = 0; j < 5 * i + 1; ++j) {
// 每一个情况要加 6 次到下一行
for (int k = 1; k <= 6; ++k) {
dp[i][j + k - 1] += dp[i - 1][j];
}
}
}
vector<double> ans(5 * n + 1, 0);
int sum_count = 0;
for (int j = 0; j < 5 * n + 1; ++j) {
sum_count += dp[n - 1][j];
}
for (int j = 0; j < 5 * n + 1; ++j) {
ans[j] = (double)dp[n - 1][j] / (double)sum_count;
}
return ans;
}
};
第 30 天 分治算法(困难)
剑指 Offer 17. 打印从1到最大的n位数
这个就太简单了
class Solution {
public:
vector<int> printNumbers(int n) {
int count = 1;
for(int i = 1; i <= n; ++i){
count *= 10;
}
count--;
vector<int> ans;
for(int i = 1; i <= count; ++i){
ans.push_back(i);
}
return ans;
}
};
剑指 Offer 51. 数组中的逆序对
https://leetcode.cn/problems/shu-zu-zhong-de-ni-xu-dui-lcof/solution/shu-zu-zhong-de-ni-xu-dui-by-leetcode-solution/
在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。
示例 1:
输入: [7,5,6,4]
输出: 5
限制:
0 <= 数组长度 <= 50000
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/shu-zu-zhong-de-ni-xu-dui-lcof
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
暴力解,o(n^2) 超时了
class Solution {
public:
int reversePairs(vector<int>& nums) {
int count = 0;
for(int i = 0; i < nums.size(); ++i){
for(int j = i+1; j < nums.size(); ++j){
if(nums[i] > nums[j]){
count++;
}
}
}
return count;
}
};
然后我再考虑,如果先求出顺序的有多少个,然后
7 6 5 4
7 5 6 4
这里可以看出,全是逆序的话,逆序数就是 n*(n-1)/2
那么有 x 个顺序的话,逆序数就是 n*(n-1)/2-x
但是我也不知道怎么不用 o(n^2) 求顺序数……
然后我看了题解说是用归并排序
于是我一开始写成这样
class Solution {
public:
int reversePairs(vector<int>& nums) {
return mergeSort(nums, 0, nums.size() - 1);
}
int mergeSort(vector<int>& nums, int left, int right) {
if (left >= right) return 0;
int middle = left + (right - left) / 2;
int inv_count = mergeSort(nums, left, middle) + mergeSort(nums, middle + 1, right);
int i = left, j = middle + 1;
// 如果左区间指针指向的数小于右区间指针指向的数
// 那么左区间指针++
// 左区间指针移动时,可以考虑到右区间指针在哪里
// 右区间指针之前的元素,小于当前左区间指针指向的元素,也就是逆序,例如
// 左区间 8 12 16 22 100
// 右区间 9 26 55 64 91
// 左区间指针指向 12,右区间指针指向 26 时,12 < 26,因此左区间指针++,这时 12 大于右区间指针左边的 9
// 那么逆序数就 + 1
// 这里要考虑两个数相等时会怎么行动,例如
// 左区间 8 12 12
// 右区间 9 12 12
// 左区间指针指向第一个 12,右区间指针指向第一个 12 时
// 如果时右区间指针不断++,那么左区间指针就始终不会移动
// 又因为只有左区间指针移动时才计算逆序数的增加
// 所以这时就会缺少一些对 9 的逆序数
// 因此,遇到相等的数,应该是左区间指针++
while (i <= middle && j <= right) {
if (nums[i] <= nums[j]) {
// 这里的 j-middle-1 就是右区间指针左边的元素的个数
inv_count += j - middle - 1;
++i;
}
else {
++j;
}
}
// 有一个问题是,如果右区间指针超出了范围该怎么办,例如
// 左区间 8 9 10
// 右区间 1 2 3
// 那么右区间指针会一路自增到 3,然后退出上面的 while。第二个例子:
// 左区间 8 12 16 22 100
// 右区间 9 26 55 64 91
// 最终左区间指针会在 100 这里,此时右区间指针还是指向 91,然后自增,退出上面的 while
// 可以看到,如果左区间最后一个元素大于右区间所有元素,就会让右区间指针先退出
// 如果右指针先退出
if (j == right + 1) {
// 一直移动左指针
while (i <= middle) {
inv_count += j - middle - 1;
++i;
}
}
// 如果左指针先退出,例如:
// 左区间 1 2 3
// 右区间 4 5 6
// 那么对于 inv_count 没有影响,就不用管了
return inv_count;
}
};
但是会解答错误
我看到错误的例子是 7 5 6 4
然后在判断:
左区间:7 5
右区间:6 4
的时候会有错误
那么这个错误就很明显是,左右两个区间没有顺序的话,就没有了原来的性质
具体来说的话,左区间如果没有顺序,就不能保证左区间当前指向的元素及其之后的元素,都会大于右区间当前指向的元素之前的元素
例如这个情况下右区间指针指到 4 之后再自增一步,退出 while
那么这个时候应该是左区间指针再自增,每自增一步执行一次 inv_count += j - middle - 1;
这里认为左区间剩下的元素都会大于右区间指针左边的 j - middle - 1 个元素
但是在这里可以看到,由于左区间是无序的,所以并没有这个性质
同理,右区间没有顺序的话,也会失去右区间指针左侧的数都比当前指针指向的数小的性质
因此最终写为:
class Solution {
public:
int reversePairs(vector<int>& nums) {
return mergeSort(nums, 0, nums.size() - 1);
}
int mergeSort(vector<int>& nums, int left, int right) {
if (left >= right) return 0;
int middle = left + (right - left) / 2;
int inv_count = mergeSort(nums, left, middle) + mergeSort(nums, middle + 1, right);
int i = left, j = middle + 1;
vector<int> tmp(right - left + 1, 0);
int tmp_idx = 0;
while (i <= middle && j <= right) {
if (nums[i] <= nums[j]) {
// 这里的 j-middle-1 就是右区间指针左边的元素的个数
inv_count += j - middle - 1;
// 排序相关
tmp[tmp_idx++] = nums[i++];
}
else {
// 排序相关
tmp[tmp_idx++] = nums[j++];
}
}
// 如果右指针先退出
if (j == right + 1) {
// 一直移动左指针
while (i <= middle) {
inv_count += j - middle - 1;
// 排序相关
tmp[tmp_idx++] = nums[i++];
}
}
// 如果左指针先退出
if (i == middle + 1) {
// 一直移动右指针
while (j <= right) {
// 那么对于 inv_count 没有影响
// 排序相关
tmp[tmp_idx++] = nums[j++];
}
}
// 排序
for (int i = left; i <= right; ++i) {
nums[i] = tmp[i - left];
}
return inv_count;
}
};
执行用时:396 ms, 在所有 C++ 提交中击败了14.98% 的用户
内存消耗:106 MB, 在所有 C++ 提交中击败了23.42% 的用户
官方题解是一次性申请了一个辅助数组,而不用在递归函数里面申请,很强
class Solution {
public:
int reversePairs(vector<int>& nums) {
vector<int> tmp(nums.size(), 0);
return mergeSort(nums, tmp, 0, nums.size() - 1);
}
int mergeSort(vector<int>& nums, vector<int>& tmp, int left, int right) {
if (left >= right) return 0;
int middle = left + (right - left) / 2;
int inv_count = mergeSort(nums, tmp, left, middle) + mergeSort(nums, tmp, middle + 1, right);
int i = left, j = middle + 1;
int tmp_idx = left;
while (i <= middle && j <= right) {
if (nums[i] <= nums[j]) {
// 这里的 j-middle-1 就是右区间指针左边的元素的个数
inv_count += j - middle - 1;
// 排序相关
tmp[tmp_idx++] = nums[i++];
}
else {
// 排序相关
tmp[tmp_idx++] = nums[j++];
}
}
// 如果右指针先退出
if (j == right + 1) {
// 一直移动左指针
while (i <= middle) {
inv_count += j - middle - 1;
// 排序相关
tmp[tmp_idx++] = nums[i++];
}
}
// 如果左指针先退出
if (i == middle + 1) {
// 一直移动右指针
while (j <= right) {
// 那么对于 inv_count 没有影响
// 排序相关
tmp[tmp_idx++] = nums[j++];
}
}
// 排序
for (int i = left; i <= right; ++i) {
nums[i] = tmp[i];
}
return inv_count;
}
};
执行用时:180 ms, 在所有 C++ 提交中击败了41.71% 的用户
内存消耗:43.3 MB, 在所有 C++ 提交中击败了67.95% 的用户
看了一下另外一个算法
其中有一个 x&(-x)
当 x 为奇数时:结果为1
当 x 为0时 :结果为0
当 x 为偶数时:得到的是能整除这个偶数的最大的二次幂
我自己尝试了一下,确实……就很神奇
然后他就写成这样的树状数组
class BIT {
private:
vector<int> tree;
int n;
public:
BIT(int _n) : n(_n), tree(_n + 1) {}
static int lowbit(int x) {
return x & (-x);
}
// 下标为 1~x 区间求和
int query(int x) {
int ret = 0;
while (x) {
ret += tree[x]; // 从右往左累加求和
x -= lowbit(x); // 左边一个节点的下标
}
return ret;
}
// 单点更新
void update(int x) {
while (x <= n) {
++tree[x]; // 具体的更新式,根据不同需求可以更改式子
x += lowbit(x); // 右边一个节点的下标
}
}
};
class Solution {
public:
int reversePairs(vector<int>& nums) {
int n = nums.size();
vector<int> tmp = nums;
// 离散化
sort(tmp.begin(), tmp.end());
for (int& num : nums) {
num = lower_bound(tmp.begin(), tmp.end(), num) - tmp.begin() + 1;
}
// 树状数组统计逆序对
BIT bit(n);
int ans = 0;
for (int i = n - 1; i >= 0; --i) {
ans += bit.query(nums[i] - 1);
bit.update(nums[i]);
}
return ans;
}
};
他这里前面把 tmp
写为 nums
的排序,那么 lower_bound(tmp.begin(), tmp.end(), num)
其实就是返回的 num
在原来的 nums
中按从小到大排是第几个数
比如 vector<int> nums({ 7,5,6,4 });
的话,那么 7
就是第 4,5
就是第 2
又因为 lower_bound
返回的是
在 [first,last) 标记的有序序列中可以插入 value,而不会破坏容器顺序的第一个位置,而这个位置标记了一个不小于 value 的值
所以第 4 大的数会返回 begin() + 3
所以之后是 lower_bound(tmp.begin(), tmp.end(), num) - tmp.begin() + 1
才能得到 4
然后要从后往前遍历,这里是因为,树状数组的区间查询的功能就是查询 1~x 之间的和,那么我在离散化我的 nums
的时候,是相当于第 x 大的数放在了树状数组的下标为 x 的位置
意义 | 数 | |||
---|---|---|---|---|
nums | 7 | 5 | 6 | 4 |
nums 中第 x 大 | 4 | 2 | 3 | 1 |
树状数组下标 | 4 | 2 | 3 | 1 |
那么其实我的区间查询查询到的是比第 x 大更小的数的个数之和
而顺序的意义是,出现在序列中某元素 A 之后的元素 B,B < A
所以如果遍历更新之后的,或者说离散化之后的,意义为第 nums[i]
大的这个 nums
数组,就会得到一个顺序数
反之就会得到一个逆序数
一般的树状数组的模板
int lowbit(int i)
{
return i & -i;//或者是return i-(i&(i-1));表示求数组下标二进制的非0最低位所表示的值
}
void update(int i,int val)//单点更新
{
while(i<=n){
C[i]+=val;
i+=lowbit(i);//由叶子节点向上更新树状数组C,从左往右更新
}
}
int sum(int i)//求区间[1,i]内所有元素的和
{
int ret=0;
while(i>0){
ret+=C[i];//从右往左累加求和
i-=lowbit(i);
}
return ret;
}
第 31 天 数学(困难)
剑指 Offer 14- II. 剪绳子 II
一开始看得是别人的找规律,就是除了 2 和 3,割出来的绳子最大长度是 3 最小长度是 2
因此只需要每次把剩下的长度为 2 的绳子 + 1 变成长度 3,如果全是长度 3,那么就将这个长度 3 变成两个长度 2 的绳子
数学的话就是,根据算术平均值 >= 几何平均值的不等式,等号成立的条件是 n 个数相等
所以假设将绳子切成 n 个等长的数是乘积最大的
那么这个时候设每一段的长度为 x,那么乘积 p r o d = x n / x = ( x 1 / x ) n prod = x^{n/x} = (x^{1/x})^n prod=xn/x=(x1/x)n
设 y = x 1 / x y = x^{1/x} y=x1/x,求它的最大值
变换 x 1 / x = e ln ( x 1 / x ) = e ln ( x ) / x x^{1/x} = e^{\ln(x^{1/x})} = e^{\ln(x)/x} x1/x=eln(x1/x)=eln(x)/x
设 z = ln ( x ) / x z = \ln(x)/x z=ln(x)/x 求它的最大值
z ′ = 1 − ln ( x ) x z' = \dfrac{1-\ln(x)}{x} z′=x1−ln(x)
极值点在 lnx = 1 也就是 x = e,那么取整数的话,x 在 2 或 3 取最大值
class Solution {
public:
int cuttingRope(int n) {
if(n == 2) return 1;
if(n == 3) return 2;
vector<int> ropes(2, 2);
n = n - 4;
int idx = 0;
while(n > 0){
if(ropes[idx] == 2){
++ropes[idx];
if(idx < ropes.size() - 1) ++idx;
}
else if(ropes[idx] == 3){
ropes[idx] = 2;
ropes.push_back(2);
}
--n;
}
int prod = 1;
for (int i = 0; i < ropes.size(); ++i) {
prod = (prod * ropes[i]) % (int)(1e9 + 7);
}
return prod;
}
};
然后就有一个大数求余问题
大数求余问题
因为这里的 prod * ropes[i]
可能就已经超出了 int32 的 2^31-1 = 21 4748 3647
21 亿,可能在几亿的时候乘一个 3 也可能大于 21 亿,那么 int32 就表示不了了
signed integer overflow: 865810542 * 3 cannot be represented in type 'int' (solution.cpp)
于是我的解决方法就是,在被乘的数很大的时候,把乘法改成加法
class Solution {
public:
int cuttingRope(int n) {
if(n == 2) return 1;
if(n == 3) return 2;
vector<int> ropes(2, 2);
n = n - 4;
int idx = 0;
while(n > 0){
if(ropes[idx] == 2){
++ropes[idx];
if(idx < ropes.size() - 1) ++idx;
}
else if(ropes[idx] == 3){
ropes[idx] = 2;
ropes.push_back(2);
}
--n;
}
int prod = 1;
for (int i = 0; i < ropes.size(); ++i) {
if(prod > INT_MAX/3){
int tmp = prod;
int count = ropes[i] - 1;
while(count > 0){
prod = (prod + tmp) % (int)(1e9 + 7);
--count;
}
}
else{
prod = (prod * ropes[i]) % (int)(1e9 + 7);
}
}
return prod;
}
};
快速幂
这里其实是已经知道了有很多个长度为 3 的绳子,并且只有 1 个或者 2 个长度为 2 的绳子
因此其实可以统计长度为 3 的绳子有多少个
之前的题也用过快速幂
class Solution {
public:
int cuttingRope(int n) {
if (n == 2) return 1;
if (n == 3) return 2;
vector<int> ropes(2, 2);
n = n - 4;
int idx = 0;
while (n > 0) {
if (ropes[idx] == 2) {
++ropes[idx];
if (idx < ropes.size() - 1) ++idx;
}
else if (ropes[idx] == 3) {
ropes[idx] = 2;
ropes.push_back(2);
}
--n;
}
int pow_three = ropes.size();
for (int i = ropes.size() - 1; i >= 0; --i) {
if (ropes[i] == 3) {
break;
}
--pow_three;
}
int prod = 1;
if(pow_three > 0) prod = fast_power(3, pow_three);
for (int i = ropes.size() - pow_three; i > 0; --i) {
prod = (prod * 2) % (int)(1e9 + 7);
}
return prod;
}
int fast_power(int a, int power) {
int res = 1;
while (power > 1)
{
// 如果为奇数
if ((power & 1) == 1) {
res = (res * a) % (int)(1e9 + 7);
a = (a * a) % (int)(1e9 + 7);
power = power / 2;
}
else {
a = (a * a) % (int)(1e9 + 7);
power = power / 2;
}
}
return (a * res) % (int)(1e9 + 7);
}
};
但是这里的问题仍然是,在算底数 * 底数时仍然可能溢出
所以还是要把计算过程的变量改为 long
class Solution {
public:
int cuttingRope(int n) {
if (n == 2) return 1;
if (n == 3) return 2;
vector<int> ropes(2, 2);
n = n - 4;
int idx = 0;
while (n > 0) {
if (ropes[idx] == 2) {
++ropes[idx];
if (idx < ropes.size() - 1) ++idx;
}
else if (ropes[idx] == 3) {
ropes[idx] = 2;
ropes.push_back(2);
}
--n;
}
int pow_three = ropes.size();
for (int i = ropes.size() - 1; i >= 0; --i) {
if (ropes[i] == 3) {
break;
}
--pow_three;
}
int prod = 1;
if(pow_three > 0) prod = fast_power(3, pow_three);
for (int i = ropes.size() - pow_three; i > 0; --i) {
prod = (prod * 2) % (int)(1e9 + 7);
}
return prod;
}
int fast_power(long a, int power) {
long res = 1;
while (power > 1)
{
// 如果为奇数
if ((power & 1) == 1) {
res = (res * a) % (int)(1e9 + 7);
a = (a * a) % (int)(1e9 + 7);
power = power / 2;
}
else {
a = (a * a) % (int)(1e9 + 7);
power = power / 2;
}
}
return (a * res) % (int)(1e9 + 7);
}
};
剑指 Offer 43. 1~n 整数中 1 出现的次数
一开始我本来是想先讨论百位数,然后递推到千位数,万位数
![](https://i-blog.csdnimg.cn/blog_migrate/d17f1fb98254c726186ea6b4bcb3bbdf.png)
class Solution {
public:
int countDigitOne(int n) {
}
int range_one_to_hundred(int num){
int count = 0;
if(num > 0) ++count;
else return count;
if(num > 9) ++count;
else return count;
if(num > 10) count += 2;
else return count;
if(num < 20){
count += num - 11;
}
else return count;
// (21-11)/10 = 1
// (30-11)/10 = 1
// (31-11)/10 = 2
// (99-11)/10 = 8
count += (num-11)/10;
}
};
但是之后我觉得这样,外推的规律很乱
重复出现的规律
每一个数位上都有自己的规律,因此枚举每一个数位
然后先对任意一个数位找规律,例如对于 1234567
对于百位,他这里有一个思路就是,大于百位的,其实是对百位以后的数位做一个循环
这个循环的思路就是,一般我都不会这么想
我对于数位的直观感受就是“叠加”而不是“循环”,比如看到 200 我会想到这是 100 + 100,但是我不会想到这代表了最后两位从 0 到 99 循环了两遍
所以我之前就没有这种感觉
那么其实现在对于百位及其后面的位,就是循环了大于百位的数的次数
对于 1234567 来说就是 000 ~ 999 循环了 1234 遍
这里的计算就是 n / 1 0 k + 1 n/10^{k+1} n/10k+1 k = 3 也就是百位
枚举每个数位就只考虑这个数位相关的 1
原题解说的是,000 ~ 999 这一个循环中,会出现一百次 1
那么 00 ~ 99 的循环应该是有出现 10 个 1 才对,但是我一看 1 … 10 11 12 … 19 … 21 … 31 … 41 … 91 … 99 这里出现了 1 + 1 + 2 + 8 + 8 = 20 个 1
实际上他这里是只考虑当前数位上的 1,其他数位上的 1 不管
因为是枚举每个数位嘛,必须要互相独立的
所以 000 ~ 999 中 1xx 出现了 100 次,也就是 100 ~ 199
所以在之前的循环的 1234 遍中要出现的 1 的次数就是 1234 * 100
也就是 n / 1 0 k + 1 ∗ 1 0 k n/10^{k+1} * 10^k n/10k+1∗10k
这个时候还剩下 567,也就是还要知道 000~567 中,百分位上 1 的次数出现了多少
显然,如果这个数小于 100,那么一次也不出现,如果这个数在 100 ~ 199 之间,那么出现 1 的次数就是 num - 100 + 1,如果大于 199,那么就是出现了 100 次
然后就是类推到 k = 0,1,2,…
数位 dp
1.dp 的规律
现在是找
0~0
0~9
0~99
0~999
之中 1 出现的个数的规律
dp[0] = 0~0 之中 1 出现的个数 0
dp[1] = 0~9 之中 1 出现的个数 1
dp[2] = 0~99 之中 1 出现的个数 20
dp[3] = 0~999 之中 1 出现的个数 300
…
dp[n] = 0 ∼ 1 0 n − 1 0\sim10^{n}-1 0∼10n−1 之中 1 出现的个数
那么可以发现, d p [ n ] = 10 ∗ d p [ n − 1 ] + 1 0 n − 1 dp[n] = 10 * dp[n-1] + 10^{n-1} dp[n]=10∗dp[n−1]+10n−1
其中 10 ∗ d p [ n − 1 ] 10 * dp[n-1] 10∗dp[n−1] 是因为在计算 d p [ n ] dp[n] dp[n] 的时候,观察的是 0 ∼ 1 0 n − 1 0\sim10^{n}-1 0∼10n−1,其中 0 ∼ 1 0 n − 1 − 1 0 \sim 10^{n-1}-1 0∼10n−1−1 重复循环了 10 次
又因为在观察 0 ∼ 1 0 n − 1 0\sim10^{n}-1 0∼10n−1 的过程中,会出现 1 0 n − 1 10^{n-1} 10n−1 次最右位为 1 的情况
例如研究 0~99,会出现 10 次第 2 位为 1 的情况,也就是 10 11 12 … 19
因此得出的这个递推公式
可以直接用这个递推公式得到各个元素的值
也可以算一算通项公式
查了一下高中是怎么解这个递归方程的
对于 d p [ n ] = 10 ∗ d p [ n − 1 ] + 1 0 n − 1 dp[n] = 10 * dp[n-1] + 10^{n-1} dp[n]=10∗dp[n−1]+10n−1,两边同除 1 0 n 10^{n} 10n
d p [ n ] 1 0 n = d p [ n − 1 ] 1 0 n − 1 + 1 10 \dfrac{dp[n]}{10^n} = \dfrac{dp[n-1]}{10^{n-1}} + \dfrac{1}{10} 10ndp[n]=10n−1dp[n−1]+101
令 b n = d p [ n ] 1 0 n b_n = \dfrac{dp[n]}{10^n} bn=10ndp[n],得 b n = b n − 1 + 1 b_n = b_{n-1} + 1 bn=bn−1+1,其中 b 0 = 0 b_0 = 0 b0=0
因此 b n = n 10 b_n = \dfrac{n}{10} bn=10n
因此 d p [ n ] 1 0 n = n 10 \dfrac{dp[n]}{10^n} = \dfrac{n}{10} 10ndp[n]=10n => d p [ n ] = n ∗ 1 0 n − 1 dp[n] = n*10^{n-1} dp[n]=n∗10n−1
2.拆分数字的规律
举个例子,以 234 为例,并继续使用规律 1 的 dp 数组。
dp[0]=0,dp[1]=1,dp[2]=20 …
234 包含的 1 的个数可来自于:
[0,99]、[100,199]、[200,234] 这三个区间的 1。
在 [0,99]、[100,199] 这两个间的 1 的个数之和为 (234/100)dp[2]+pow(10,2)=220+100=140 (1)
注:pow(10,2) 表示 100-199 之间百位贡献的 1
在 [200,234] 区间的 1 的个数之和其实等于 [0,34] 区间的 1 的个数之和。
因此该部分继续划分。
[0,34] 的 1 可来自于
[0,9]、[10,19]、[20,29]、[30,34] 之间的 1。
[0,9]、[10,19]、[20,29] 区间的 1 的个数为 (34/10)dp[1]+pow(10,1)=31+10=13 (2)
在 [30,34] 区间的 1 的个数有等于 [0,4] 区间 1 的个数,为 (4/1)*dp[0]+pow(10,0)=1 (3)
最后,上面的 (1)+(2)+(3)=154。
作者:fenjue
链接:https://leetcode.cn/problems/1nzheng-shu-zhong-1chu-xian-de-ci-shu-lcof/solution/by-fenjue-nice/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
因此得到答案:
class Solution {
public:
int countDigitOne(int n) {
int dig_count = 0;
int tmp = n;
while (tmp != 0) {
++dig_count;
tmp = tmp / 10;
}
int* dp = new int[dig_count];
// 单独给 dp[0] 赋值,因为 pow(10, -1) != 0
dp[0] = 0;
for (int i = 1; i < dig_count; ++i) {
dp[i] = i * (int)pow(10, i-1);
}
// 考虑 1 2345 6789
// 拆分为,第一部分:0~9999 9999 出现 1 的次数为 dp[8]
// 第二部分:1 0000 0000~1 2345 6789 其中有 2345 6789 + 1 次首位为 1
// 考虑 2345 6789
// 拆分为,第一部分 0~999 9999 1000 0000~1999 9999 出现 1 的次数为 2*dp[7]
// 其中 1000 0000~1999 9999 首位出现了 1000 0000 次 1
// 第二部分 2000 0000~2345 6789 其中没有首位为 1 的时候
// 考虑 345 6789
tmp = n;
int dig_val = 0;
int ans = 0;
// 输入 123 有 3 位数
// 一开始应该除以 100 = 10^2
dig_count = dig_count - 1;
while (tmp != 0) {
dig_val = tmp / (int)pow(10, dig_count);
tmp = tmp % (int)pow(10, dig_count);
ans += dig_val * dp[dig_count];
if (dig_val == 1) ans += (tmp + 1);
else if (dig_val > 1) ans += (int)pow(10, dig_count);
--dig_count;
}
return ans;
}
};
剑指 Offer 44. 数字序列中某一位的数字
这个找规律还挺快的
class Solution {
public:
int findNthDigit(int n) {
if(n == 0) return 0;
// 1~9 9 个 1 位数
// 10~99 90 个 2 位数
// 100~999 900 个 3 位数
// 1000~9999 9000 个 4 位数
int dig_count = 1; // 这个区间的数字的位数
int left = 1; // 区间左端在整个序列中的序号
// 边界情况:n = 10,那么 n 应该 >= 1+9 才能进入 left = 10
while(n >= left + 9 * pow(10, dig_count-1) * dig_count){
left += 9 * pow(10, dig_count-1) * dig_count;
++dig_count;
}
n -= left; // 得到 n 指向的数字在区间中的整个序列中的序号
int idx = n/dig_count; // n 指向的数字在区间中的,以每一个数字为整体的序号,区间左端序号为 0
int res = dig_count-n%dig_count; // 数字的倒数第几位
int num = pow(10, dig_count-1) + idx; // n 指向的数字
int ans = 0;
while(res > 0){
ans = num%10;
num = num/10;
--res;
}
return ans;
}
};