Leetcode 每日一题和每日一题的下一题刷题笔记 18/30

401 阅读4分钟

Leetcode 每日一题和每日一题的下一题刷题笔记 18/30

写在前面

这是我参与更文挑战的第18天,活动详情查看:更文挑战

快要毕业了,才发现自己被面试里的算法题吊起来锤。没办法只能以零基础的身份和同窗们共同加入了力扣刷题大军。我的同学们都非常厉害,他们平时只是谦虚,口头上说着自己不会,而我是真的不会。。。乘掘金鼓励新人每天写博客,我也凑个热闹,记录一下每天刷的前两道题,这两道题我精做。我打算每天刷五道题,其他的题目嘛,也只能强行背套路了,就不发在博客里了。

本人真的只是一个菜鸡,解题思路什么的就不要从我这里参考了,编码习惯也需要改进,各位如果想找刷题高手请教问题我觉得去找 宫水三叶的刷题日记 这位大佬比较好。我在把题目做出来之前尽量不去看题解,以免和大佬的内容撞车。

另外我也希望有得闲的大佬提供一些更高明的解题思路给我,欢迎讨论哈!

好了废话不多说开始第十八天的前两道题吧!

2021.6.18 每日一题

483. 最小好进制

这道题是找等比数列的和,题目又是说的花里胡哨的。等比数列求和是有个公式的,进入社会以后不怎么用这个东西渐渐就忘了(也许买基金炒币的人天天在用,他们应该不会忘)。求和公式的推导就是错位相减。下面我给大家献丑推导一下这个马上要用到的求和公式。

n=Sum=1+k+k2+...+kmk×n=k×Sum=k+k2+k3+...+km+1\begin{aligned} n &= Sum = 1 + k + k ^ 2 + ... + k ^ m \\ k \times n &= k \times Sum = k + k ^ 2 + k ^ 3 + ... + k ^ {m + 1} \end{aligned}

这两个式子上下相减,就得到了求和公式

(k1)×n=(k1)×Sum=1+0+0+...+0+km+1n=Sum=km+11k1,k1(k - 1) \times n = (k - 1) \times Sum = -1 + 0 + 0 + ... + 0 + k ^ {m + 1} \\ n = Sum = \frac{k ^ {m + 1} - 1}{k - 1}, k \ne 1

这个公式适合公比不是 1 的所有情况(公比是 1,有几个元素和就是几,公比是 0,一直是 0)

现在回到这道面试题里,这个公式里面有原来的数 n 和 它的“好进制” k,那么对这个求和公式变形之后放缩,就能得到新的关系

km+1=1+n×kn<k×nm<logknk ^ {m + 1} = 1 + n \times k - n < k \times n \\ m < log_k n

题目里有 n 的范围,这样就能大概估计到 m 的范围,

3n1018,k>=2,m<log21018=18log210603 \le n \le 10^{18}, k >= 2, \\ m < log_2 10^{18} = 18 log_2 10 \approx 60

这是一个重要的条件,先记一下。

这之后还是需要放缩,这次是直接不变形对求和公式放缩,求和公式肯定有一个下界,只要这个进制 k 是大于等于 2 的,等比数列是单调增的。所以前 m 项求和肯定是大于最后一项的。另外我们还要找一个上界。上界也好确定,等比数列的每一项系数都是 1,然后每一项的形式和二项式定理里面的每一项的形式是一样的,这次用二项式定理来放缩,整个求和公式肯定小于进制 k 和 1 组成的二项式的幂。

n=1+k+k2+...+km<(m0)+(m1)×k+(m2)×k2+...+(mm)×km=(k+1)m,n<(k+1)mn = 1 + k + k ^ 2 + ... + k ^ m < \binom{m}{0} + \binom{m}{1} \times k + \binom{m}{2} \times k ^ 2 + ... + \binom{m}{m} \times k ^ m = {(k + 1)} ^ m, \\ n < {(k + 1)} ^ m

如果已经工作的老哥们已经看不懂我在说什么了,请看下面这个杨辉三角形,希望这个能唤醒你们上学时一些记忆。

1m=1121m=21331m=314641m=4......(m0)(m1)......(mm1)(mm)m=m\begin{aligned} \begin{array}{} 1 & m = 1 \\ 1 \quad 2 \quad 1 & m = 2 \\ 1 \quad 3 \quad 3 \quad 1 & m = 3 \\ 1 \quad 4 \quad 6 \quad 4 \quad 1 & m = 4 \\ ...... \\ \binom{m}{0} \quad \binom{m}{1} \quad ...... \quad \binom{m}{m - 1} \quad \binom{m}{m} & m = m \end{array} \end{aligned}

我说的那个二项式的幂它的系数只有两边最高次项和最低次项的系数是 1,其他项的系数已经大于 1 了,比等比数列的和要大是必然的,综上,这次的放缩结果把等比数列的和两头都给出了范围。

km<n<(k+1)m,k<nm<k+1,nm=k,nm=k+1k ^ m < n < {(k + 1)} ^ m, \\ k < \sqrt[m]{n} < k + 1, \\ \lfloor \sqrt[m]{n} \rfloor = k, \lceil \sqrt[m]{n} \rceil = k + 1

这样就又出现一组关系,记好,马上就要用。

写代码的时候我们已经知道 n 了,所以只要随便给一个 m,我们就能算出来一个 k,然后验证这个 k 是不是能算出 n

m 等于 1 的时候整个等比数列就两项,这个好进制 k 是保底的同时也是最大的,我们应该从 m 最大的地方 60 开始试起,这样最先遇到的就是最小好进制。

说到这里代码思路就完了,后面就是翻译部分。


class Solution {
public:
    string smallestGoodBase(string n) {
        long nVal = stol(n);
        int mMax = floor(log(nVal) / log(2));
        for (int m = mMax; m > 1; m--) {
            int k = pow(nVal, 1.0 / m);
            long mul = 1, sum = 1;
            for (int i = 0; i < m; i++) {
                mul *= k;
                sum += mul;
            }
            if (sum == nVal) {
                return to_string(k);
            }
        }
        return to_string(nVal - 1);
    }
};

image.png

这道题如果不想这么用公式推来推去,可以直接拿二进制估计一下大概要试多少个 1,在某个好进制下数就是全 1,最大的那个好进制就是 n - 1,拿二分一个一个试下去就行了,大概就是从二进制和最大好进制 n - 1 中间那个进制开始试,全 1 序列长度不变,一直到能试出来一个好进制为止(如果全 1 序列长度这么长一个都试不出来,就少一位继续试)。试出来好进制的过程是二分的过程,假如现在这个中点进制的全 1 序列转成 10 进制要比原来的数大,我就在中点和小点的中点上继续试,如果转 10 进制比原来的数小,我就在中点和大点的中点上继续试,试到两个端点重合还没有合适的进制,那就得让全 1 序列长度减一了。如果全 1 序列长度为 3 的时候还是试不出来,那好进制就是那个保底的最大好进制了。

实际上不管是什么思路,都是把搜索范围一步步缩小,如果放缩的效果更好,搜索范围能变小的更快,为什么不用更好的放缩呢?

2021.6.18 每日一题下面的题

剑指 Offer 47. 礼物的最大价值

这题有路径嘛,不能斜着走,那就是动态规划了。这道题很经典,让代码写起来更方便,就直接在起点前面加一行加一列。每次的格子用它左边和上边得来的价值最大值和本身的和相加来更新,这样最后右下角就能得到最大价值,如果要把路找出来,要用回溯的思路。


class Solution {
public:
    int maxValue(vector<vector<int>>& grid) {
        int n = grid.size(), m = grid[0].size();
        vector<vector<int>> dp(n + 1, vector<int>(m + 1, 0));
        for (int i = 1; i <= n; i++){
            for (int j = 1; j <= m; j++){
                dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]) + grid[i - 1][j - 1];
            }
        }
        return dp[n][m];
    }
};

image.png

这道题这么写还能再省一些空间,省掉一个维度上的空间,反正一定是从前一个上面转移过来的。


class Solution {
public:
    int maxValue(vector<vector<int>>& grid) {
        int n = grid.size(), m = grid[0].size();
        vector<int> dp(m + 1, 0);
        for (int i = 1; i <= n; i++){
            for (int j = 1; j <= m; j++){
                dp[j] = max(dp[j], dp[j - 1]) + grid[i - 1][j - 1];
            }
        }
        return dp[m];
    }
};

image.png

小结

数学或者无脑二分都是为了缩小搜索范围,动态规划在起点上面和左边多开一行一列来简化边界条件处理代码编写。

参考链接