单周赛 250 题解
题外话:自己在 okr
中立了个 flag
,希望在 7-8
双月逐渐熟练掌握 python, go
,所以在之后的周赛题解中,我会逐渐引入 python, go
的代码,有兴趣的小伙伴可以一起学习
本次周赛着重讲解一下第三题,前面两题一笔带过,最后一题涉及到 \(01\ trie\) 树,后续单独出一篇文章讲解
本次周赛知识点:字符串,数学,上取整,动态规划,高性能,前缀后缀
可以输入的最大单词数
给定一个字符串 \(w\) 表示一串单词,给定一个字符串 \(b\) 表示破坏的字母,对于 \(w\) 中的所有单词,如果包含 \(b\) 中的字母,那么该单词不可用,计算可用的单词数
题解
用一个数组存放 w
中的单词,用桶存放 b
中的字符,循环判断即可
// cpp
class Solution {
public:
int canBeTypedWords(string t, string b) {
vector<string> w;
string temp = "";
int bucket[27] = {0};
for (int i = 0; i < b.size(); ++i) bucket[b[i] - 'a']++;
for (int i = 0; i <= t.size(); ++i) {
if (!(t[i] >= 'a' && t[i] <= 'z')) w.push_back(temp), temp = "";
else temp += t[i];
}
int ans = 0;
for (auto i: w) {
int f = 0;
for (int j = 0; j < i.size(); ++j) if (bucket[i[j] - 'a']) {f = 1; break;}
if (!f) ans++;
}
return ans;
}
};
# py
class Solution:
def canBeTypedWords(self, t: str, b: str) -> int:
st = set(b)
ans = 0
for word in t.split():
flag = 0
for ch in word:
if ch in st:
flag = 1
break
if not flag:
ans += 1
return ans
// go
func canBeTypedWords(t string, b string) (ans int) {
for _, word := range strings.Split(t, " ") {
if !strings.ContainsAny(word, b) {
ans++
}
}
return ans
}
新增的最少台阶数
给一个严格递增的数组 \(r\),表示台阶的高度,给一个正整数 \(dist\) 表示爬一次最高能上的台阶高度,初始时站在高度 \(0\) 位置,计算最少还需要放置几个台阶,使得可以爬到最后一个台阶
例如 r = [1, 3, 5, 10], dist = 2
,需要放置 2
个台阶,高度分别为 7, 9
(方案不唯一)
例如 r = [3, 6, 8, 10], dist = 3
,不需要放置台阶
例如 r = [3, 4, 6, 7], dist = 2
,需要放置 1
个台阶,高度为 1
题解
对于任意两个台阶,高度分别为 \(i,\ j\),设高度差为 \(h\),则可以放 \(\lceil\frac{h}{dist}\rceil - 1\) 个台阶
因此我们计算出差分数组 \(d\),依次做判断即可
在实现 \(\lceil\frac{a}{b}\rceil - 1\) 时,使用 (a - 1) / b
即可
// cpp
class Solution {
public:
int addRungs(vector<int>& r, int dist) {
int n = r.size();
vector<int> d(n);
d[0] = r[0];
for (int i = 1; i < n; ++i) {
d[i] = r[i] - r[i - 1];
}
int ans = 0;
for (int i = 0; i < n; ++i) {
if (d[i] % dist == 0) ans += d[i] / dist - 1;
else ans += d[i] / dist;
}
return ans;
}
};
# py
class Solution:
def addRungs(self, r: list[int], dist: int) -> int:
pre = ans = 0
for i in r:
h = i - pre
ans += (h - 1) // dist
pre = i
return ans
// go
func addRungs(r []int, dist int) (ans int) {
pre := 0
for _, h := range r {
d := h - pre
ans += (d - 1) / dist
pre = h
}
return ans
}
扣分后的最大得分
给定一个 \(m\times n\) 的矩阵 \(p\),每一个格子有一个权值 \(p[i][j]\)
现在从第一行到最后一行,每一行选一个格子,收益为权值和
但是,对于第 \(i\) 行和第 \(i - 1\) 行,如果分别选了 \(j,\ k\) 列,那么收益减去 \(\mid j - k\mid\)
数据保证
\(1\leq m,\ n,\ m\times n\leq 10^5\)
\(0\leq p[i][j]\leq 10^5\)
题解
考虑二维动态规划,定义 \(dp[i][j]\) 表示走到 \((i,\ j)\) 位置的收益最大值,则状态转移方程为
其中 \(0\leq k < n\)
但是我们需要枚举 \(i,\ j,\ k\),复杂度为 \(O(nmk)\),不能接受,因此考虑优化决策,对于状态转移方程,我们发现实际上要计算的是
对于当前决策,我们只需要分别计算出 \(j\geq k\) 部分的最大值和 \(j < k\) 部分的最大值即可
时间复杂度 \(O(nm \log n)\) 算法
我们用数组 \(h_1,\ h_2\) 分别维护两个值,具体来说
然后用两棵红黑树维护二元组 \((j,\ h_1[j]),\ (j,\ h_2[j])\),这样便可以在 \(O(\log n)\) 时间计算出两段决策的最大值,在 c++
中,我选用 set
来实现,需要重载运算符
随着决策 \(j\) 指针右移,第一棵红黑树中要删去节点 \((j,\ h_1[j])\),第二颗红黑树中要插入节点 \((j,\ h_2[j])\),时间复杂度均为 \(O(\log n)\)
因此总的时间复杂度为 \(O(nm \log n)\)
// cpp
typedef long long LL;
struct node{
int idx;
LL val;
node() {}
node(int _idx, LL _val):
idx(_idx), val(_val) {}
};
bool operator< (const node &x, const node &y) {
if (x.val == y.val) return x.idx < y.idx;
return x.val > y.val;
}
class Solution {
public:
long long maxPoints(vector<vector<int>>& a) {
int m = a.size();
int n = a[0].size();
vector<vector<LL>> dp(m, vector<LL>(n));
for (int j = 0; j < n; ++j) {
dp[0][j] = a[0][j];
}
for (int i = 1; i < m; ++i) {
vector<LL> h1(n), h2(n);
set<node> st1, st2;
for (int j = 0; j < n; ++j) {
h1[j] = dp[i - 1][j] - j;
h2[j] = dp[i - 1][j] + j;
st1.insert(node(j, h1[j]));
}
for (int j = 0; j < n; ++j) {
if (st1.size()) {
auto x = st1.begin();
dp[i][j] = max(dp[i][j], x->val + a[i][j] + j);
}
if (st2.size()) {
auto y = st2.begin();
dp[i][j] = max(dp[i][j], y->val + a[i][j] - j);
}
st1.erase(node(j, h1[j]));
st2.insert(node(j, h2[j]));
/*
for (int k = 0; k < n; ++k) {
dp[i][j] = max(dp[i][j], dp[i - 1][k] + a[i][j] - abs(j - k));
}
*/
}
}
LL ans = 0;
for (int j = 0; j < n; ++j) ans = max(ans, dp[m - 1][j]);
return ans;
}
};
时间复杂度 \(O(nm)\) 算法
用 \(pre[j]\) 维护 \(\max\left\{dp[i - 1][k] + k\mid 0\leq k\leq j\right\}\)
用 \(suf[j]\) 维护 \(\max\left\{dp[i - 1][k] - k\mid j\leq k\leq n - 1\right\}\)
则状态转移方程更新为
同时使用滚动数组把 \(dp\) 数组的第一维状态优化掉
总的时间复杂度为 \(O(nm)\),空间复杂度为 \(O(n)\)
注意此题爆 int
,不开 long long
见祖宗
// cpp
typedef long long LL;
class Solution {
public:
long long maxPoints(vector<vector<int>>& a) {
int m = a.size(), n = a[0].size();
vector<LL> dp(n);
vector<LL> pre(n), suf(n);
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
if (!i) dp[j] = a[i][j];
else {
LL x = max(pre[j] - j, suf[j] + j) + a[i][j];
dp[j] = max(dp[j], x);
}
}
pre[0] = dp[0] + 0;
suf[n - 1] = dp[n - 1] - (n - 1);
for (int j = 1; j < n; ++j)
pre[j] = max(pre[j - 1], dp[j] + j);
for (int j = n - 2; j >= 0; --j)
suf[j] = max(suf[j + 1], dp[j] - j);
}
LL ans = *max_element(dp.begin(), dp.end());
return ans;
}
};
使用前后缀代替红黑树,代码更好写了,运行效率也更快