LeetCode周赛第318场记录
感觉这场周赛的质量还是很高的,题目出得非常有水平,而且也学到了不少东西。
这是本次周赛的第一题,要求将数组元素进行处理之后将所有0元素移动到序列的末尾。
对数组元素进行处理的操作非常简单,只需要按照题意模拟即可。但是对于如何将数组中所有0元素全部移动到序列的尾部,并且同时保持非0元素相对位置不变(稳定性),这本身就是另一道LeetCode题了:
这道题虽然也是一道简单题,但是它的思维难度并不低,这道题证明了上述的问题存在时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( 1 ) O(1) O(1)的高效算法。所以若能把移动0的做法迁移到本题中,这道题的代码性能可以达到一个最优,这是很多人的代码没有做到的:
所以这道题的完整代码如下:
class Solution {
public:
vector<int> applyOperations(vector<int>& nums) {
int n = nums.size();
for(int i = 0 ; i < n - 1 ; ++i) // 这个循环就是按照题意修改数组即可。
{
if(nums[i] == nums[i + 1])
{
nums[i] <<= 1;
nums[i + 1] = 0;
}
}
int Left = 0, Right = 0; // 参考LeetCode283-移动零
while(Right < n) // 时间复杂度O(n),空间复杂度O(1),就地算法
{ // 这是将所有0元素移动到数组最后的最优算法
if(nums[Right])
{
swap(nums[Left], nums[Right]);
++Left;
}
++Right;
}
return nums;
}
};
这道题在比赛时我的第一想法是用前缀和,前缀和的时间复杂度也是O(n),但是很难检测到某一个窗口中是否含有重复的数字。
所以这道题的难点不在于求和,而在于如何能快速地检测出当前窗口中是否含有重复数字,这就需要我们使用Hash表去动态地管理当前窗口中各个数字出现的次数。如果当前窗口中框住的数字不符合要求,那么这个窗口内的数值不能用来更新最大值。
完整的代码和注释如下:
class Solution {
public:
long long maximumSubarraySum(vector<int>& nums, int k) {
long long Ans = 0, Tmp = 0;
unordered_map<int, int> HashTable; // 使用Hash表来检测当前窗口中是否有重复数字
int n = nums.size(), i = 0, j = 0;
for(int i = 0 ; i < k ; ++i) // 首先充满长度为k的窗口
{
Tmp += nums[i]; // 更新临时和
++HashTable[nums[i]]; // 记录当前窗口中每个数字出现的次数
}
if(HashTable.size() == k) // 如果窗口大小正好等于Hash表项数,说明无重复数字
Ans = max(Ans, Tmp);
/*从这里开始,窗口开始向前滑动,每一次加入一个新数字,同时排出一个新数字*/
for(int i = k ; i < n ; ++i)
{
Tmp -= nums[i - k]; // 排出末尾数字,更新当前和
--HashTable[nums[i - k]]; // 递减Hash表中的此项计数值
/*如果当前计数值为0,删去此项,防止对后面造成干扰*/
if(HashTable[nums[i - k]] == 0)
HashTable.erase(nums[i - k]);
Tmp += nums[i]; // 加入新数字更新部分和
++HashTable[nums[i]]; // 记录新加入的数字
if(HashTable.size() == k) // 同上,如果当前窗口内无重复数字的判断
Ans = max(Ans, Tmp);
}
return Ans;
}
};
这道题其实大致也是一个模拟问题,但是一个比较关键的问题在于,我们如何高效地有序地维护开头和结尾的candidates个元素,这里完全可以选用堆,也就是C++中的优先队列。接下来只需要随时保证在还有剩余元素(
i
≤
j
i \le j
i≤j)的前提下,保证充满两个队列,依次贪心求解即可:
class Solution {
public:
long long totalCost(vector<int>& costs, int k, int candidates) {
long long Ans = 0;
int n = costs.size();
int i = 0, j = n - 1, Count = 0; // Count记录着当前已经雇佣的员工个数
priority_queue<int, vector<int>, std::greater<int>> Front, Tail;
for( ; i < candidates ; ++i) // 将前candidates个元素插入队列
Front.push(costs[i]);
/*将后candidates个元素插入队列*/
for( ; j > n - 1 - candidates && j >= i ; --j)
Tail.push(costs[j]);
/*注意,i和j始终指向还没有入队的员工,所以i == j时也可以推入*/
/*但是i和j不可以是i>j,这时后面的元素已经都入队了,不需要再加入前面的队列*/
while(Count < k)
{
/*此循环依次取出两个队列中较小的元素,并及时充满刚刚弹出元素的队列,直到我们找出k个员工为止*/
if(!Front.empty() && !Tail.empty())
{
if(Front.top() > Tail.top())
{
Ans += Tail.top();
Tail.pop();
if(j >= i)
Tail.push(costs[j--]);
}
else // <=
{
Ans += Front.top();
Front.pop();
if(i <= j)
Front.push(costs[i++]);
}
}
else if(!Front.empty() && Tail.empty())
{
Ans += Front.top();
Front.pop();
}
else
{
Ans += Tail.top();
Tail.pop();
}
Count++;
}
return Ans;
}
};
这道题其实可以看作背包问题的增强版,理解这道题可以首先不要考虑复杂的空间优化技巧。另外这道题其实还有贪心的策略,也就是说首先可以对机器人坐标和工厂位置进行排序,找每一个机器人的坐标最近的工厂,这个策略在本题中是最优的,具体证明法是交换邻项法。
代码如下:
/*
f[i][j]表示前i个工厂修理前j个机器人需要的最小移动距离
k表示第i个工厂维修的机器人个数,需要遍历此值来获取最优解,k的取值范围是min(factory[i - 1][1], j)
*/
class Solution {
static const int N = 110;
private:
long long f[N][N];
public:
long long minimumTotalDistance(vector<int>& robot, vector<vector<int>>& factory) {
sort(robot.begin(), robot.end());
sort(factory.begin(), factory.end());
memset(f, 0x3f, sizeof f);
int n = robot.size(), m = factory.size();
for (int i = 0; i <= m; i++)
f[i][0] = 0;
for (int i = 1; i <= m; i++)
for (int j = 1; j <= n; j++) {
f[i][j] = f[i - 1][j];// 第i个工厂不修理机器人
long long cost = 0; // 第i个工厂修理k个机器人,前i-1个工厂修理j-k个机器人
// 枚举k找出最小值
for (int k = 1; k <= min(factory[i - 1][1], j); k++) {
cost += abs(factory[i - 1][0] - robot[j - k]); // 滚动记录
f[i][j] = min(f[i][j], f[i - 1][j - k] + cost);
}
}
return f[m][n];
}
};