剑指 Offer 刷题笔记 3

第 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 的位置

意义
nums7564
nums 中第 x 大4231
树状数组下标4231

那么其实我的区间查询查询到的是比第 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=x1ln(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 出现的次数

一开始我本来是想先讨论百位数,然后递推到千位数,万位数

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+110k

这个时候还剩下 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 010n1 之中 1 出现的个数

那么可以发现, d p [ n ] = 10 ∗ d p [ n − 1 ] + 1 0 n − 1 dp[n] = 10 * dp[n-1] + 10^{n-1} dp[n]=10dp[n1]+10n1

其中 10 ∗ d p [ n − 1 ] 10 * dp[n-1] 10dp[n1] 是因为在计算 d p [ n ] dp[n] dp[n] 的时候,观察的是 0 ∼ 1 0 n − 1 0\sim10^{n}-1 010n1,其中 0 ∼ 1 0 n − 1 − 1 0 \sim 10^{n-1}-1 010n11 重复循环了 10 次

又因为在观察 0 ∼ 1 0 n − 1 0\sim10^{n}-1 010n1 的过程中,会出现 1 0 n − 1 10^{n-1} 10n1 次最右位为 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]=10dp[n1]+10n1,两边同除 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]=10n1dp[n1]+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=bn1+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]=n10n1

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;
    }
};
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值