leetcode 周赛 485 - 最小字典序

作者: | 更新日期:

带条件的单调栈

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

零、背景

这次比赛最后一题有点难,要求字符串的最小字典序,需要使用带条件的单调栈。

A 题:统计(题意简单,略)
B 题:线段树 / 二分单调栈 / 动态规划等多种解法
C 题:map 统计
D 题:贪心算法

最终排名:148名
代码仓库:详见 https://github.com/tiankonguse/leetcode-solutions

一、元音辅音得分

题意:给定一个字符串,问元音个数除以辅音个数的值。

思路:统计元音和辅音的个数,然后计算它们的比值即可。

二、预算下的最大总容量

题意:每个物品有一个成本和价值,给定一个最大预算 C,问在不超过预算且最多选择两个物品的情况下的最大价值。

思路:有多种解法。

解法一:线段树

由于最多可以选择两个物品,可以先枚举一个物品,然后计算另一个满足条件的物品的最大价值。
假设枚举的物品成本为 c0,价值为 v0,则需要找到成本不超过 C-c0 的最大价值。
即在线段树中求区间 [1, C-c0-1] 的最大值。

为了避免选择两个相同的物品,可以在枚举物品后再将其加入线段树,这样可以保证选择的是两个不同的物品。

解法二:二分单调栈

先把所有物品按成本排序,这样可以得到一个有序的成本序列。

另外,显然如果一个物品成本很小且价值很大,则成本较大但价值较小的物品就不需要考虑。
所以可以维护一个递增的单调栈来筛选物品。

与线段树一样,为了避免重复,只需要在已经枚举的成本里选择第二个物品的最大值。
由于枚举的第一个物品的成本是升序的,则另一个物品的最大成本是降序的,即成本大于最大成本 C-c0-1 的物品都不需要考虑。

所以,我们需要在单调栈中找到成本不大于 C-c0-1 的最大值,可以用二分查找实现。

解法三:动态规划

状态定义:preMax[i] 表示成本不大于 i 的最大价值。

同样是先枚举一个物品,然后直接在 preMax 中查找另一个物品的最大值。

如何更新 preMax 呢?
显然,假如一个物品成本是 c0,价值是 v0,则状态转移方程为:

preMax[c0] = max(preMax[c0], max(preMax[c0-1], v0));

而由于成本不是连续的,所以我们需要维护一个游标,用于填补空缺的成本。

int nowMaxCost = 0;
while (nowMaxCost < cost) {
  preMax[nowMaxCost + 1] = preMax[nowMaxCost];
  nowMaxCost++;
}

不考虑排序的复杂度:O(n + V)(其中 n 为物品数量,V 为最大预算)
考虑排序的复杂度:O(n log n)

三、设计拍卖系统

题意:每个人可以对某个物品进行多次报价,也可以取消报价,问在某个时刻某个物品报价最高的用户是谁。
如果有多个用户报价最高,返回用户编号最大的一个。

思路:正反查 map 索引

维护一个物品到用户到价格的索引,再维护一个物品到价格到用户集合的索引。

// item -> cost -> userSet
unordered_map<int, map<int, set<int>>> itemMaxCost;
// item -> user -> cost
unordered_map<int, unordered_map<int, int>> itemUserCost;

删除时,先查询价格,然后删除两个索引,同时将空的索引也清除。

void removeBid(int userId, int itemId) {  //
  auto& maxCost = itemMaxCost[itemId];
  auto& userCost = itemUserCost[itemId];
  if (userCost.count(userId) == 0) {
    return;
  }
  int cost = userCost[userId];
  maxCost[cost].erase(userId);
  if (maxCost[cost].empty()) {
    maxCost.erase(cost);
  }
  userCost.erase(userId);
}

更新时,先删除原有报价再插入新报价即可。
查询时,利用索引的有序性,直接返回最大值即可。

int getHighestBidder(int itemId) {
  if (itemMaxCost[itemId].empty()) {
    return -1;
  }
  return *itemMaxCost[itemId].rbegin()->second.rbegin();
}

四、删除重复字符后的字典序最小字符串

题意:给定一个字符串,对于重复字符,可以执行任意多次删除操作,问删除后字典序最小的字符串。

思路:贪心

由于要求字典序最小,所以只需要关注相邻两个字符的关系。

假设前缀 s[0...i-1] 已经得到最优答案,保存在 ans 中。

对于 s[i] 字符,如果小于 ans.back(),则删除 ans.back(),然后插入 s[i] 可以得到更小的字典序。

但有一种特殊情况:如果 ans.back() 是该字符最后一次出现,则无法删除。
所以需要统计每个字符出现的次数,如果只剩下一次,则不能删除,否则可以删除。

另外这个过程是递归的,所以需要不断判断,直到条件不再满足为止。

string ans;
for (auto c : s) {
  while (ans.size() > 0 && ans.back() > c && counts[ans.back() - 'a'] > 1) {
    counts[ans.back() - 'a']--;
    ans.pop_back();
  }
  ans.push_back(c);
}

最终计算出答案后,发现后缀可能有重复字符,这时只需要保留一个即可。

五、最后

这次比赛最后一题一开始方向想错了,最后 5 分钟才想到只需要对比相邻字符,但时间不够,比赛后 3 分钟才通过,有点遗憾。

《完》

-EOF-

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

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

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

tiankonguse +
穿越