leetcode 第 411 场算法比赛

作者: | 更新日期:

最后一题比较有难度,我想了好久。

本文首发于公众号:天空的代码世界,微信号:tiankonguse

零、背景

这次比赛依旧没参加,最后一题我想错方向了,最后看了一眼题解,才知道怎么做

A: 暴力枚举。
B: 简单动态规划。
C: 数位DP。
D: 滑动窗口+二分+前缀和。

排名:无
代码地址: https://github.com/tiankonguse/leetcode-solutions/tree/master/contest/4/411

一、统计满足 K 约束的子字符串数量 I

题意:给一个二进制字符串,问存在多少个子串,0和1的个数都不同时超过 k 个。

思路:数据量只有 50 个,枚举所有子串,统计 01 的个数,判断是否满足要求。

复杂度:O(n^3)

优化:固定起始位置的子串,下个子串可以复用上个子串的统计信息。
复杂度:O(n^2)

二、超级饮料的最大强化能量

题意:有两种有能量值的饮料,每次只能喝一种饮料。
另外,如果从一种饮料切换到另一种饮料,需要等待一个时刻。
问如何选择,最终喝的累计能量值最大。

思路:简单动态规划。

状态定义:dp(i, t)
含义:i 时刻喝 t 饮料时,能得到的最大能力。

状态转移: dp(i, t) = max(dp(i-1,t), dp(i-2, T)) + V[i][t];
含义:要么上个时刻喝相同的饮料,要么上个时刻等待,上上个时刻喝不同的饮料。

复杂度:O(n)

三、找出最大的 N 位 K 回文数

题意:求构造一个最大的长度为 N 的数字,该数字可以整除 K。

思路:由于 K 只有 9个数字,很容易想到去找规律。

例如几个数字很容易找到规律:

1: 9.......9
2: 89.....98
3: 9.......9
4: 889...988
5: 59.....95
6: 无规律
7: 无规律
8: 8889.9888
9: 9999.9999

6 再深入分析,发现其实还是有规律的,分为奇偶性。
偶数长度为 89.989.98
奇数长度为 89.9779.98

而对于 7,也是有规律的,不过需要区分的情况更多了。

另外,如果 N 较小时,上面的规律也不满足,需要特殊处理。
所以走枚举这条路风险很大,很容易遗漏某种情况。

其实,这道题是经典的数位 DP 题型。

状态定义:dp(i, m)
含义:前 i-1 个位置填充数据之后,前 i-1 个位置 与 后 i-1 个位置取模 k 后剩余为 m 时, 下个位置最大的存在答案的值。

状态转移方程如下:

dp(i, m) = -1 默认不存在答案
dp(i, m) = max(v if dp(i+1, M)>=0);

接下来看几个如何得到 m。

最开始,没有前缀和后缀,所以 m 等于 0。
假设第 i 位填充 v,取模满足分配率,可以推导出

r = i
l = n - 1 - i
M = (v * 10^r + v * 10^l + m) % k

枚举 v 的时候,从大到小枚举即可。
复杂度:O(n * k^2)

四、统计满足 K 约束的子字符串数量 II

题意:给一个二进制字符串,有多个询问,问一个区间内存在多少个子串,0和1的个数都不同时超过 k 个。

思路:6种算法。

算法1:枚举询问,枚举子串,统计01个数,判断答案。
复杂度:O(n^4)

算法2: 算法1基础上,相同前缀的子串,复用统计信息。
复杂度:O(n^3)

算法3: 反向思维。
题意要求两个都不超过 K,等价与所有子串 减去 两个都超过 k 的子串。
对于起始位置固定的所有前缀字符串,一旦找到第一个都超过 k 的子串,之后的子串也都满足要求。

预先统计所有前缀 [0,i] 的 0 与 1 个数 count[0/1][i]

假设询问的是 [l,r], 枚举每个位置 i 为起始位置,其中第一个满足要求的 0与1 的个数预期为 count[0/1][i-1] + k + 1
这个可以通过二分查找 count[0/1] 找到,不妨设为 c, 则答案为 max(r-c, 0)
复杂度:O(n^2 log(n))

算法4:反向思维 + 索引优化。
反向思维算法中,通过二分查找count[0/1]指定值的下标。
值与下标的关系可以预处理储存在数组中,这样就可以O(1)找到下标了。
复杂度:O(n^2)

一种错误的前缀和优化算法,导致走进死胡同。

反向思维基本逻辑是遍历每个询问区间,枚举区间内每个位置,一把计算出这个位置为起始位置的子字符串的答案。
枚举每个位置是 O(n) 的复杂度,必然会超时。

很容易想到如果有某个算法可以求出前缀区间 f(p) 的答案,则可以通过两个前缀求差得到区间的答案 f(r)-f(l-1)

但是又一想,上面的前缀减法不对。
还存在部分子串穿过 l 左右边界的,这部分子串不在 f(l-1) 里面。

所以还需要定义一个状态:f1(p) 含义:p 左边至少有一个字符的所有满足要求的答案。

但是再一想,新的状态又多计算了。
例如对于子串 [l-1, r+1] 满足 f1(l) 的定义,但是不应该计算在内。

所以还需要减去所有覆盖 [l-1,r+1]的子串。

这样推导下去,发现没有尽头了。
就这样我想了一天,没想出解法。

算法5:坐标系法

第二天看了题解,发现应该分析O(n^2)算法的特征,找出规律,然后再降低复杂度。

反向思维基本逻辑是遍历每个询问区间,枚举区间内每个位置,一把计算出这个位置为起始位置的子字符串的答案。
如果我们预处理出计算整个字符串每个位置为起始位置的答案,则可以画出一条曲线。

假设要求区间 [2,6]的答案,可以发现,[2,4]为起始位置的所有答案子串都不超过6,所以都是答案。
而对于[5,6]为起始位置的答案大于6,需要进行截断。

在图上展示出来,就是求坐标系中单杠阴影部分的面积。
而单杠阴影的面积等于整个阴影部分的面积 减去 下方双杠梯形的面积。

整个阴影部分的面积分为两部分。
其中,[2,4]的答案可以通过前缀和求差得到。
[5,6] 的答案,则是长方形的面积,直接可以计算得到。

现在的问题是,如何找到这个边界,即左半部使用前缀和计算,右半部使用长方形计算。
可以发现,曲线是单点递增的,所以可以二分查找。

就这样,就可以做出这道题了。 复杂度: O(n log(n))

算法6: 索引优化

坐标系法通过求面积,计算出了答案。
其中找分界线时,使用了二分查找。
其实这个二分查找也可以通过预先建立反查索引,通过可以做到O(n)找到目标。

复杂度:O(n)

五、最后

这次比赛最后一题最终有 6 个算法,复杂度从 O(n^4) 一步步优化到 O(n)

而且,在去掉 O(log(n)) 的优化中,还发现如果二分查找,都可以通过建立索引的方法,来做到O(1)查找。

《完》

-EOF-

本文公众号:天空的代码世界
个人微信号:tiankonguse
公众号ID:tiankonguse-code

本文首发于公众号:天空的代码世界,微信号:tiankonguse
如果你想留言,可以在微信里面关注公众号进行留言。

关注公众号,接收最新消息

tiankonguse +
穿越