leetcode 第 242 场算法比赛
作者:
| 更新日期:被第二题浮点数坑了。
本文首发于公众号:天空的代码世界,微信号:tiankonguse
零、背景
这次比赛最难的是第二题吧,被浮点数坑了。
第二题做了一个小时,还好其他题都是很快过的,最终所有题都在比赛结束前过了。
一、哪种连续子字符串更长
题意:给一个 01
字符串,问最长的连续 1 和最长的连续 0 谁更长。
思路:基础的动态规划题。
分别求最长的连续 1 和最长的连续 0 ,比较即可。
class Solution {
int Get(const string& s, char c) {
int ans = 0;
int num = 0;
for (auto v : s) {
if (v == c) {
num++;
ans = max(ans, num);
} else {
num = 0;
}
}
return ans;
}
public:
bool checkZeroOnes(const string& s) { //
return Get(s, '1') > Get(s, '0');
}
};
二、准时到达的列车最小时速
题意:给 n 趟整点出发的客车的路程距离,以及最大时间限制。
问能否可以选择一个速度,使得在限制时间内完成路程。
如果可以,输出满足要求的最小速度。
思路:
首先原题的题意就很难理解,我读了好久,甚至怀疑翻译问题去读了英文的题目。
结果发现题目没问题,就是这么的。
确定题意后,发现是找一个速度,来判断是否可以完成车程。
由于涉及的浮点数与整除问题,没法直接求出答案,只能二分速度来做这道题。
由于涉及到时间精度问题,我写了两个浮点数判断大小的函数,然后就是直接在浮点数上二分。
十几分钟敲完代码后,样例通过后,提交奖励了一个Wrong Answer
,还给出了具体的样例。
打印下二分过程,发现被eps = 1e-7
卡精度了。
再次阅读题意,发现虽然是浮点数,但是输入的浮点数最多只有两位。
那一种方法显然是浮点数扩大一百倍,除法转乘法,使用整数来比大小。
一顿修改后,发现不行,打印日志发现2.01 * 100
转整数后得到的是200
。
看到这个后,马上意识到是浮点数储存的问题,便修改为 (2.01 + exp) * 100
。
之后便通过这道题。
class Solution {
double CheckSpeed(vector<int>& dist, ll maxSpeed, double hour) {
ll hourEx = (hour + eps) * 100;
ll preTime = 0;
for (int i = 0; i + 1 < dist.size(); i++) {
auto v = dist[i];
if (v % maxSpeed == 0) {
preTime += v / maxSpeed;
} else {
preTime += v / maxSpeed + 1;
}
}
if (preTime * 100 > hourEx) {
return true;
}
ll leftTime = hourEx - preTime * 100;
return dist.back() * 100 > leftTime * maxSpeed;
}
public:
int minSpeedOnTime(vector<int>& dist, double hour) {
if (CheckSpeed(dist, MAX_ANS, hour)) {
return -1;
}
int left = 1, right = MAX_ANS;
while (left < right) {
int mid = (left + right) / 2;
if (CheckSpeed(dist, mid, hour)) { // 速度太小了,时间超过了
left = mid + 1;
} else {
right = mid;
}
}
return right;
}
};
赛后,再次分析数据,速度最大为1e7
, 路程最小为 1,则所有精度最小为 10e-9
。
所以,将精度修改为 10e-9
也能通过这道题。
当时我发现精度问题后,不信任精度了,便转向转整数的方法去了。
如果打印下日志,应该几分钟就可以通过这道题。
speed = 5000000 minTime=2.0200000000
speed = 7500000 minTime=2.0133333333
speed = 8750000 minTime=2.0114285714
speed = 9375000 minTime=2.0106666667
speed = 9687500 minTime=2.0103225806
speed = 9843750 minTime=2.0101587302
speed = 9921875 minTime=2.0100787402
speed = 9960938 minTime=2.0100392152
speed = 9980469 minTime=2.0100195692
speed = 9990235 minTime=2.0100097745
speed = 9995118 minTime=2.0100048844
speed = 9997559 minTime=2.0100024416
speed = 9998780 minTime=2.0100012201
speed = 9999390 minTime=2.0100006100
speed = 9999695 minTime=2.0100003050
speed = 9999848 minTime=2.0100001520
speed = 9999924 minTime=2.0100000760
speed = 9999962 minTime=2.0100000380
speed = 9999981 minTime=2.0100000190
speed = 9999991 minTime=2.0100000090
speed = 9999996 minTime=2.0100000040
speed = 9999998 minTime=2.0100000020
speed = 9999999 minTime=2.0100000010
三、跳跃游戏 VII
题意:给一个 01
字符串,起点是第一个字符,终点是最后一个字符。
给一个跳跃的最小距离和最大距离,每次只能调到值为 0
的位置。
问能否调到终点。
思路:
方法一、暴力枚举
最基本的方法是维护一个可以到达的点集合。
对于每个可以到达的点,枚举下一步可以到达的区间,判断是否有新的点加入这个集合。
如果最终最后一个字符也加进来了,那就有答案。
复杂度:O(n^2)
方法二、双指针
对于每个字符为 0 的位置,只存在是否能到达。
由于所有下标向后跳跃的最小距离和最大距离是相等的。
从左到右扫描过程中,前一个字符扫描过的位置区间就不需要扫描了。
所以维护一个最远的下标,每次从最远的下标开始扫描即可。
复杂度:O(n)
注意事项:最远的下标不一定连续,即某些点永远无法到达。
bool canReach(string s, int minJump, int maxJump) {
queue<int> que;
int n = s.size();
int r = 0;
que.push(0);
while (!que.empty()) {
int l = que.front();
que.pop();
r = max(r, l + minJump); // r 不一定连续,所以需要更新下一个最小的起点
while (r < n && l + minJump <= r && r <= l + maxJump) {
if (s[r] == '0') {
if (r == n - 1) {
return true;
}
que.push(r);
}
r++;
}
}
return false;
}
四、石子游戏 VIII
题意:现在 Alice 和 Bob 在玩石子游戏,Alice 先走。
有一排石子共 n 个,每个石子有一个初始价值。
游戏规则如下:
1、如果只剩下一个石子,则游戏结束。
2、选手可以从左边拿走 x 个石子,且 x 大于 1。
3、选手拿走石子之后,得分是拿走石子的价值之和。
4、选手拿走石子之后,会在最左边加入一个石子,价值也是拿走石子的价值之和。
游戏结束时,两个人都有得分。
得分之差定义为 Alice 的分数 减去 Bob 的分数。
Alice 的目标是使两个人的得分之差最大。
Bob 的目标是使两个人的得分值差最小。
问双方都采用最优策略,最终两人的得分之差。
思路:博弈里最难理解了,需要慢慢分析题意。
分析一:问题的答案对齐
原题:求最终两人的得分之差。
由于 Alice 先走,问题的目标其实就是 Alice 的目标。
分析二:两人的目标对齐
原题:Alice 的目标是使分数之差最大,Bob 的目标是使分数之差最小。
由于两个人的分数是相减的,所以两个人分别当做主角时,他们的目标其实是一样的。
即自己操作时,都是希望自己的得分与下个人的得分的之差更大。
分析三:拿走石子的价值
由于每次拿走一个前缀石子后,又把前缀石子的总价值当做新的石子放在最左边了。
所以,每次操作,得到的价值都是前缀和,定义为 S(x)
。
分析四:状态定义
状态:f(x)
定义:前 x 个石子已经拿走,第 x 个石子的价值是前 x 个石子的价值之和时,当前选手未来可以得到的最大分数之差。
这里的未来是不考虑之前的得分,只计算之后的得分。
因为历史行为是不确定的,但是 x 一旦固定,石子的序列就固定了,未来的最优值也固定了。
分析五、方程
假设当前选手 A 的状态是 f(x)
,决定拿走 [x, k]
的石子。
当前选手 A 的得分是 S(k)
。
下个选手 B 的最优目标是 f(k)
。
则当前选手 A 的分数之差就是 f(x) = S(k) - f(k)
。
证明:
假设下个选手 B f(k)
达到最优目标时,选手 B 的得分是 B(k)
,选手A 的得分是 A(k)
。
则下个选手 B 的最优目标是 f(k) = B(k) - A(k)
。
当前选手 A 操作的得分是 S(k)
,则总得分是 S(k) + A(k)
。
这样,当前选手的分数之差就是 f(x) = S(k) + A(k) - B(k)
。
由于 f(k) = B(k) - A(k)
,所以f(x) = S(k) - f(k)
。
分析六:状态转移方程
状态:f(x)
对于 x, 可以选择的分别是 [x+1, n]
。
选择之后,得分是前缀和,然后减一下个选择的分数差,就是自己当前的分数差。
证明:见分析五。
最优目标则是所有选择里面的最大值。
转移方程:f(x) = max(S(i) - f(i))
方法一:暴力计算
有了状态转移方程,就可以做这道题了。
由于每个状态都要枚举到最后一个位置,复杂度较高。
复杂度:O(n^2)
方法二、利用上个状态的结果
在上次比赛《leetcode 第 241 场算法比赛》的时候,我分享过动态规划的优化方法:
面对动态规划,复杂度太高时,优化思路是对某些项提前做预处理,或者看某个循环的结果是否可以在下次循环时复用。
所以这里我们分析下方程,可以发现这里也可以利用上一个状态的答案。
这样循环就可以转化为一个比大小了。
优化:f(x) = max(S(x+1) - f(x+1), f(x+1))
int stoneGameVIII(vector<int>& stones) {
int n = stones.size();
vector<int> sum(n + 1, 0);
vector<int> ans(n + 1, 0);
for (int i = 1; i <= n; i++) {
sum[i] = sum[i - 1] + stones[i - 1];
}
int maxVal = sum[n]; // 可以全拿了
for (int i = n - 1; i > 0; i--) {
ans[i] = max(sum[i + 1] - ans[i + 1], maxVal);
maxVal = max(maxVal, ans[i]);
}
return ans[1];
}
五、最后
这次比赛被第二题坑了,其他题都算是基础算法知识。
第一题:初级动态规划
第二题:二分查找
第三题:集合+双指针
第四题:博弈+动态规划
当然,第四题博弈题在 leetcode 上算是比较难的题型,因为分析题意,写出状态转移方程后才是基础的动态规划。
而对于博弈题,最难的是理解题意,转换题意,推导公式。
这里我也手把手的分析了题意,最终转化为了动态规划题。
互动:最后一题你做出来了吗?
加油,算法人。
《完》
-EOF-
本文公众号:天空的代码世界
个人微信号:tiankonguse
公众号ID:tiankonguse-code
本文首发于公众号:天空的代码世界,微信号:tiankonguse
如果你想留言,可以在微信里面关注公众号进行留言。