Google Kickstart 2021 Round A 的题解,附上我的一些思考过程,希望对你有所帮助。
比 A 轮还是难一点,只拿了 37 分,思维和代码熟练度都欠缺,再接再厉。
- Score: 37
- Rank: 1946
A. Increasing Substring
算法 —— 线性扫描
-
从前到后扫描整个字符串,
dp[i]
表示以i
为结尾的最长单调子串的长度 -
转移方程为:
dp[i] = dp[i]+1, s[i] > s[i-1]
dp[i] = 1, s[i] <= s[i-1]
-
由于状态计算只需要使用上一个下标
dp[i-1]
的值,所以将数组可以优化成一个变量,滚动更新。
时间复杂度
- 扫描一遍字符串:
O(n)
C++ 代码
|
B. Longest Progression
比较复杂的一个分类讨论,当时没有梳理清楚逻辑关系,直接在 2020 RoundE Problem A 的基础上写的暴力(暴力枚举差分改变的地方,然后运行原来的朴素算法),O(N^2)
的所以会在 Test 2 上 TLE。
算法 —— 差分+贪心
- 计算最长等差数列和 2020 RoundE Problem A 一样,是首先计算差分数组,然后统计差分数组里面最长的具有相同数的子串的长度
len
,答案就是len+1
- 例如: 原数组
A=[1,3,4,7,9,11]
,差分数组D=[2,1,3,2,2]
(其中D[i] = A[i+1]-A[i], i = 1,...n-1
),最长相同数子串[2,2]
,长度为 2,对应最长等差数列为[7,9,11]
,长度为 3
- 例如: 原数组
- 现在可以在原来的基础上修改一个数,修改一个数等价于修改两个差分值
- 不妨设你将
A[i]
增加x
,那么修改之后D[i-1]
会增加x
,D[i]
会减少x
,两者的总和保持不变。这个修改对最终的差分数组的影响需要分情况讨论 - 可以先将差分数组分成若干段,每一段都是一个包含全部一样数
d
的子串,不妨设起始下标为D[i]
,长度为k
- 我们一定是修改下个子串的第一个差分值让其等于当前子串的差分值,这样能使答案优于不改变任何数的时候的答案
- 改变了子串的第一个差分值
D[i+k]
之后,会对紧接着的差分值D[i+k+1]
产生影响,需要分情况考虑 - 例子 1 (
D[i+k] + D[i+k+1] != 2d
):D=[1,1],[3],[0,0]
,考虑第 1 个子串,修改第 2 个子串的第一个数之后就是:D=[1,1],[1],[2,0]
,最终的len=3
- 例子 2 (
D[i+k] + D[i+k+1] == 2d, D[i+k+2] != d
):D=[1,1],[2],[0,0]
,考虑第 1 个子串,修改第 2 个子串的第一个数之后就是:D=[1,1],[1],[1,0]
,最终的len=4
- 例子 3 (
D[i+k] + D[i+k+1] == 2d, D[i+k+2] == d
):D=[2],[1],[3],[2,2]
,考虑第 1 个子串,修改第 2 个子串的第一个数之后就是:D=[2],[2],[2],[2,2]
,最终的len=5
- 不妨设你将
时间复杂度
- 预处理差分数组、按照相等值分段:
O(N)
- 依次考虑每块,并且对于每一块,考虑上面 3 种情况:
O(N)
C++ 代码
- 实际实现时,没有必要存储每个子串的长度
k
,可以通过upper_bound
实现,用set
只会增加一个log
的时间复杂度 - 对于例子
A=[1,0,1,1], D=[-1,1,0]
,最优解是修改第 1 和第 2 个差分值,为了避免特殊判断,将 D 反转重新算一遍取最大即可
|
C. Consecutive Primes
比赛的时候想到了先用线性筛计算 [1, sqrt(Z)]
范围内的素数,然后二分小因子的算法,总时间复杂度为 O(sqrt{Z} + log cnt)
,其中 cnt
表示[1, sqrt(Z)]
范围内的素数总个数。显然无法过 Test 3。主要瓶颈在线性筛算法上。
注:根据 lucifer1004 的解释,用线性筛也是可以过的,但是要将线性筛外提,所有的测试用例只运行一次线性筛。不过这是因为 KickStart 评测机的时间限制比较宽裕,我们赛后还是要追求最好的解法。
算法 —— 素数判断
- 官方给的解析反倒更加简单,不需要用素数筛,只需要用素数判定算法即可,比第 2 题更加好实现,关键在于利用了条件「两个素数因子是相邻的」
- 假设最终的答案是
N
(N <= Z
),那么最终两个相邻素数因子一定是 3 种情况:sqrt(Z)
左侧最大、sqrt(Z)
右侧最小,这两个素数之积如果小于Z
,那么一定是答案- 否则,
sqrt(Z)
左侧最近的两个素数,这两个素数之积一定小于Z
- 根据素数的数学性质,可以知道
Z=1e18, sqrt(Z)=1e9
时,临近的质数之差的绝对值不会超过 282,直接暴力的枚举即可 - 这样看来,第三题比第二题是线上还简单很多,主要是有需要一个思维上的转变
时间复杂度
- 枚举三个数:
O(282 * 3)
- 判断是否是质数:
fourth_root(z)
(z
的四次方根)
- 判断是否是质数:
- 总时间复杂度:
O(282 * 3 * fourth_root(z))
C++ 代码
|
D. Truck Delivery
暴力解法
暴力解法只能过 Test 1
算法 —— DFS + 暴力
- DFS 一遍,计算每个点的到 1 的简单路径,记录
pre
数组,这里使用常用的遍历无向树的套路:dfs 参数x
表示正在遍历的节点,father
记录从哪个方向来的,下一个节点不是father
的时候往下遍历 - 初始化答案
ans=0
,对于每一个查询,枚举路径上所有的点,依次比较每条边l
值和当前查询的w
,看是否要更新答案ans = gcd(ans, a)
。
时间复杂度
- DFS:
O(N)
- 遍历所有询问:
O(Q)
- 每个询问需要访问路径上所有的边,最坏情况下最长边为
N-1
:O(N)
- 最多每条边计算一次 gcd
- 计算 N 个数的 gcd 的总时间复杂度为
O(N + log(max{A}))
,注意不是O(N * log(max{A}))
,证明先略去
- 每个询问需要访问路径上所有的边,最坏情况下最长边为
- 总时间复杂度:
O(Q(N + log(max{A})))
C++ 代码
|
优化解法
算法 —— DFS + 线段树
- 基于离线查询进行优化,按照特定顺序填充答案,降低时间复杂度
- 在 DFS 的过程中维护一个表示当前路径的线段树
- 线段树的 key 为每条边的 load
l
- 这里有一个很重要的点,是说,所有的
l
是不同的(启发我们题目的数据范围一定要要仔细看),这就避免出现一个 key 有多个 value 的情况发生
- 这里有一个很重要的点,是说,所有的
- 线段树的 value 为每条边的 amount
a
,初始值为 0 表示不存在- 删除的时候也是直接将 value 置为 0
- 线段树的 merge 方式为
gcd()
- 线段树的 key 为每条边的 load
- 在 DFS 搜索到
x
的时候,如果查询中存在当前节点x
,且负重为w
,只需要在线段数中查询所有<=w
区间范围内的l
的所有a
值的gcd()
时间复杂度
- DFS:
O(N + Q)
- 同时维护线段树:
- 维护的时间复杂度为
log(max{L})
- 最多只需要计算
log(max{L})
个数的 gcd:O(log(max{L}) + log(max{A}))
- 维护的时间复杂度为
- 同时维护线段树:
- 总时间复杂度:
O[(N + Q)(log(max{L}) + log(max{A}))]
C++ 代码
|
总结
- 思路比较乱的时候,先不要实现,不然徒劳无功。真不行了,可以先实现朴素算法,再做改进。
- 没有明确思路的时候,先把所有的题目看完,可能出现后面的题目更简单的情况。(这次都没有留下时间仔细想最后一题)
- 数据范围还是需要仔细看。
开源代码
代码已经放在 GitHub: https://github.com/upupming/algorithm/tree/master/kick-start/2021/RoundB ,大家多多 clone,也可以随手 star 一下~