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

506 阅读8分钟

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

写在前面

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

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

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

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

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

2021.6.10 每日一题

518. 零钱兑换 II

这道题是比较标准的物品无限的背包问题,解决起来也很快,容量是当前硬币价格总数,价值是能得到当前价格总数的组合数。容量最小就是当前这种面值硬币一枚的价格,最大就是 amount。写一下状态方程。

dp[i]=dp[i]+dp[icur_coin]\texttt{dp}[\texttt{i}] = \texttt{dp}[\texttt{i}] + \texttt{dp}[\texttt{i} - \texttt{cur\_coin}]

应该不用再解释了,这个转移方程就是刨除一枚当前选的这种硬币之后的组合数加上不使用当前这种硬币的组合数。

代码如下


class Solution {
public:
    int change(int amount, vector<int>& coins) {
        vector<int> dp(amount + 1);
        dp[0] = 1;
        for (int& cur_coin : coins) {
            for (int i = cur_coin; i <= amount; i++) {
                dp[i] += dp[i - cur_coin];
            }
        }
        return dp[amount];
    }
};

image.png

果然这个月是动态规划月,不是什么前缀和月。。。

2021.6.10 每日一题下面的题

1787. 使所有区间的异或结果为零

这道题要用到异或的性质,同样的东西异或之后结果是 0。可以自己列个真值表试一下。

然后放到这道题里,随便取一个已经调整过的数组,对于从 i 开始的 k 个数异或的结果是 0,对于从 i + 1 开始的 k 个数结果是 0, 那么这两个式子直接异或能得到什么呢?中间重复的元素直接消掉了,就像求等比数列的推导一样,错位抵消了,只剩下头和尾,那么得到了 nums[i]nums[i+k]=0\texttt{nums}[\texttt{i}] \oplus \texttt{nums}[\texttt{i} + \texttt{k}] = 0,或者说 nums[i]=nums[i+k]\texttt{nums}[\texttt{i}] = \texttt{nums}[\texttt{i} + \texttt{k}]

到这里我就不太会了,我感觉后面应该是动态规划,然后用这种关系找状态转移,但是我死活找不出来。后来还是看题解了。原来这种周期性的重复应该是用到哈希表,这么一说有点明白了,哈希表存每个元素出现的次数,和第一次出现的位置以及数组大小有关。然后开始考虑状态转移方程的问题,状态转移时把调整过的数组看作多把错齿的梳子,梳子里的元素下标对 k 同余(也就是说总共就 k 把梳子),梳子的齿周期性出现,好多把梳子把齿按规律插进其他梳子的缝隙里。设当前处理到第 i 把梳子,当前整个数组里每个梳子上取一个齿,异或结果是 mask,然后修改了梳子 i 上所有的齿,设修改后的值是 x,那么修改后异或的结果是 mask XOR x,这样状态转移方程就慢慢写出来了。状态转移方程里的 size(i) 是当前处理第 i 把梳子上齿的个数,count[i][x] 是当前第 i 把梳子上齿的值等于 x 的齿的个数,也就是说梳子 i 上所有的齿的值都变成 x 只需要修改 size[i] - count[i][x] 这么多的齿。

dp[i][mask]=minx{dp[i1][maskx]+size[i]count[i][x]}=size[i]+minx{dp[i1][maskx]count[i][x]}\texttt{dp}[\texttt{i}][\texttt{mask}] = \underset{\texttt{x}}{min} \left \{ \texttt{dp}[\texttt{i} − 1][\texttt{mask} \oplus \texttt{x}] + \texttt{size}[\texttt{i}] − \texttt{count}[\texttt{i}][\texttt{x}] \right \} \\ \quad \quad \quad \quad \quad = \texttt{size}[\texttt{i}] + \underset{\texttt{x}}{min} \left \{ \texttt{dp}[\texttt{i} − 1][\texttt{mask} \oplus \texttt{x}] − \texttt{count}[\texttt{i}][\texttt{x}] \right \}

因为最小值的自变量是 x,所以可以把与 x 无关的 size[i] 拿出来。

后面要优化这个式子,这里的空间复杂度没有看明白。。。我按照这个思路写代码确实超时了。。。

这个题我真的还需要再研究研究,把我难住了。。。


2021/6/11 更新

有些题要让它在脑子里发酵一段时间,再拿出来做可能就好一点。我这里先不怎么分析复杂度了,反正分析复杂度也只是为了提醒自己优化的方向。我已经指导自己超时了,这里确实要优化一下。

题目里给出的数组里每个元素的值都有一个上限,就是 10 位二进制数,这一点很重要,因为我们想尽量减少元素修改次数,所以尽量找到原本就有的数字,修改成一样的,这样才是减少修改次数的正道。如果不限制元素修改次数,我全部修改成 0 岂不是简单粗暴又好理解?所以,x 实际上也有一个上限,和数组里每个元素的值的上限是一样的,都是 10 位二进制数。这样,我们就能估算一下最坏情况(阴间测试用例)里的时间复杂度了。最坏情况下每一把梳子的齿都要改,然后齿的取值是10位二进制数,还要把所有可能的 x 都试一遍,这样就是 O(220k)O(2^{20} \texttt{k}) 这样的复杂度。。。这就是超时的原因。

这个优化的思路很巧妙,反应过来以后发现自己是能想到的。因为是求最小值,原本的集合里面已经有最小值了,我加一些比最小值大的东西也不会影响我找最小值。把原来最小值这里的表达式破开来看。就算测试用例再阴间,也不会把全部的 10 位二进制数都遍历完,也就是说,在 10 位二进制数里还有一些数不在数组里,在试 x 的取值时有可能试到一个梳子齿上原本没出现过的数,这个时候 count[i][x] = 0,然后这种时候最小修改次数就是梳子的长度了,也就是 size[i]。这种情况加到找最小值的集合里,肯定是不会影响找到原本的最小值的,(其实也没有引入新元素,只是把找 count[i][x] 的工作量省掉了),这里降低了时间复杂度。

破开来看的第二部分是 x 还在梳子齿上出现的数里面取一个,这样就和原本的状态转移一样,不会给找最小值集合里面引入什么新东西,这里应该降低不了复杂度了,我也不知道 x 在之前的梳子齿上出现了多少次。

这样,就又可以把状态转移方程改一改了,关键思路就是把最小值集合里面的数分段计算。

dp[i][mask]=size[i]+minx{dp[i1][maskx],dp[i1][maskx]count[i][x]}\texttt{dp}[\texttt{i}][\texttt{mask}] = \texttt{size}[\texttt{i}] + \underset{\texttt{x}}{min} \left \{ \texttt{dp}[\texttt{i} − 1][\texttt{mask} \oplus \texttt{x}], \texttt{dp}[\texttt{i} − 1][\texttt{mask} \oplus \texttt{x}] − \texttt{count}[\texttt{i}][\texttt{x}] \right \}

现在这个状态转移方程就有内味儿了,前面是“看到了但不取,就是玩儿”,后面是“看到了取一下,咱看后续”。

这里可以再简单看一下最坏情况,“就是玩儿”这里的复杂度是 O(210)O(2^{10}),“咱看后续”这里(在哈希表里找)的复杂度是 O(size[i])O(\texttt{size}[\texttt{i}]),也就是说一个梳子上状态转移方程的复杂度是 O(210+size[i])O(2^{10} + \texttt{size}[\texttt{i}]),那么一共 k 把梳子,就能得到总的复杂度是 O(210k+n)O(2^{10}\texttt{k} + n),最后这个 n 是整个数组的长度。这样,最后 dp[k-1][0] 就是需要的结果(梳子编号从下标 0 开始,在最后的状态里异或的结果就是 0)。

要省空间复杂度,状态转移方程里就要少一个变量,就是当前正在看的梳子下标。然后用两个大小都为 O(210)O(2^{10}) 的一维数组代替二维数组。

初始条件是 dp[-1][0] = 0,其他的 dp[-1][] = infinity 都是没意义的。

接下来的代码,去看官方题解吧,我原来的代码东改西改也和那个差不多了,然后改完各种不太规范的写法,注释一加,和官方题解的代码就非常非常像了。底下放官方题解通过的截图,这道题的通过率真这么高吗?还是都和这张截图里一样

image.png

真不容易。

小结

物品无限的背包问题,状态转移方程把求最值分开算降低复杂度

参考链接

第二道题的题解,我看这个还行,我是没想明白。

leetcode-cn.com/problems/ma…