题目
LeetCode - 1935
键盘出现了一些故障,有些字母键无法正常工作。而键盘上所有其他键都能够正常工作。
给你一个由若干单词组成的字符串 text ,单词间由单个空格组成(不含前导和尾随空格);另有一个字符串 brokenLetters ,由所有已损坏的不同字母键组成,返回你可以使用此键盘完全输入的 text 中单词的数目。
示例:
输入:text = "leet code", brokenLetters = "lt" 输出:1 解释:无法输入 "leet" ,因为字母键 'l' 和 't' 已损坏。
题解
签到题,简单模拟即可(虽然是签到题,但我还是花了20分钟😓)。
对于每个单词,需要判断这个单词中是否包含破损的字母,最简单的想法就是:对于所有破损的字母,插入到一个哈希表中,然后对每个单词,遍历单词的全部字母,依次判断每个字母是否在哈希表中出现过即可。由于一共只有26个小写字母,我的做法是直接开一个大小为26的布尔数组,来表示某个字母是否破损。
class Solution {
public int canBeTypedWords(String text, String brokenLetters) {
int ctn = 0;
boolean[] st = new boolean[26];
for (int i = 0; i < brokenLetters.length(); i++) {
int index = brokenLetters.charAt(i) - 'a';
st[index] = true;
}
boolean flag = false; // 某个单词是否存在破损字母
for (int i = 0; i < text.length(); i++) {
// 当前单词已破损, 并且没有达到下一个单词的分隔符, 直接跳过
if (flag && text.charAt(i) != ' ') continue;
else if (text.charAt(i) == ' ') {
// 达到单词分隔符, 判断前一个单词是否破损
if (!flag) ctn++;
flag = false; // 重置flag
continue;
}
int index = text.charAt(i) - 'a';
if (st[index]) flag = true;
}
// 最后一个单词末尾没有空格符, 需要单独判断一下最后的flag
if (!flag) ctn++;
return ctn;
}
}
LeetCode - 1936
给你一个 严格递增 的整数数组 rungs ,用于表示梯子上每一台阶的 高度 。当前你正站在高度为 0 的地板上,并打算爬到最后一个台阶。
另给你一个整数 dist 。每次移动中,你可以到达下一个距离你当前位置(地板或台阶)不超过 dist 高度的台阶。当然,你也可以在任何正 整数 高度处插入尚不存在的新台阶。
返回爬到最后一阶时必须添加到梯子上的 最少 台阶数。
示例:
输入:rungs = [1,3,5,10], dist = 2 输出:2 解释: 现在无法到达最后一阶。 在高度为 7 和 8 的位置增设新的台阶,以爬上梯子。 梯子在高度为 [1,3,5,7,8,10] 的位置上有台阶。
题解
签到题,简单模拟即可。
遍历 数组,当遍历到第 个位置时,若 ,则无需加梯子;若 时,需要添加梯子,需要添加的最少梯子数是
解释:当在两个位置之间添加 个梯子时,最多能够支持连接 的距离,比如想从高度 走到 ,而 ,则只需要在高度 的位置添加一个梯子,就可以达到目的。当添加 个梯子时,最多能支持连接 的距离,比如从高度 走到 ,而 ,则只需要在高度 , 的位置添加 个梯子即可;所以添加 个梯子,能连接 的距离。用 表示需要连接的距离,则最少需要 个梯子。
class Solution {
public int addRungs(int[] rungs, int dist) {
int ctn = 0;
for (int i = 0; i < rungs.length; i++) {
int pre = i == 0 ? 0 : rungs[i - 1]; // 注意是从高度0开始爬梯子
int gap = rungs[i] - pre;
if (gap <= dist) continue; // 无需加梯子
else {
int q = gap / dist;
int r = gap % dist;
if (r > 0) q++; // 向上取整
ctn += q - 1;
}
}
return ctn;
}
}
LeetCode - 1937
给你一个 m x n 的整数矩阵 points (下标从 0 开始)。一开始你的得分为 0 ,你想最大化从矩阵中得到的分数。
你的得分方式为:每一行 中选取一个格子,选中坐标为 (r, c) 的格子会给你的总得分 增加 points[r][c]。
然而,相邻行之间被选中的格子如果隔得太远,你会失去一些得分。对于相邻行 r 和 r + 1 (其中 0 <= r < m - 1),选中坐标为 (r, c1) 和 (r + 1, c2) 的格子,你的总得分 减少 abs(c1 - c2) 。(abs表示取绝对值)
请你返回你能得到的 最大 得分。
示例:
输入:points = [[1,2,3],[1,5,1],[3,1,1] 输出:9 解释: 蓝色格子是最优方案选中的格子,坐标分别为 (0, 2),(1, 1) 和 (2, 0) 你的总得分增加 3 + 5 + 3 = 11 。 但是你的总得分需要扣除 abs(2 - 1) + abs(1 - 0) = 2 你的最终得分为 11 - 2 = 9
题解
经过思考发现,只有相邻两行会导致扣分。我们不妨设定,下面的一行会受上面一行的影响。
举个例子,当在第 行,选择了第 列时。那么前 行一定有一个最大得分,不妨将其设为
比如上面的示例,当处理第 行的所有列时,容易得到 (第一行比较特殊)
当处理第二行时,对于第一列,只需要遍历前一行的所有列,则可得出,第二行选择第一列时,前两行的最大可能得分,容易得到,前一行无论选择 ,,, 的计算结果都一样是 ,对于第二行第二列同理,可以得到 (第一行选择 或者 ),第三列
同理,当处理第 行时,只需要关注第 行的每一列的最大可能得分即可(无需再关注第 行)
则 ,,
我们用 来表示第 行选择第 列时,前 行的最大可能得分。
随后在计算第 行的各个列的最大可能得分时,只需要关注第 行各个列的最大得分即可,因为扣分只是发生在相邻两行,而与更前面的行所选择的列无关。
所以,对于一行中的每一列,只需要维护,选择这一列时,可能的最大得分即可。
如何求出某一行的某一列的最大可能得分呢?只需要枚举上一行所有列的最大得分,依次计算,取最大值即可。
容易得到状态转移方程: ,其中 就是题目给出的整数矩阵points[i][j],
这样的话,对于整个 的矩阵,共有 个状态,而求解每个状态 对应的 ,需要遍历前一行的所有列。即,求解每个 需要的时间复杂度是 ,则总的时间复杂度就是 ,这样会超时。我们需要想办法进行优化。
当 时,;当 时, 。
则上面的状态转移方程可以转化为
所以,求解 , 只需要求解 当 时, 的最大值;当 时, 的最大值,两者再选最大即可。
即对于 的左边的列(),求解 ,对于 右边的列(),求解
则只需要遍历两次 行的所有列,即可求出第 行的所有列的 ,时间复杂度就优化成了
最终的答案就是最后一行的所有列的得分中的最大者,即 ,,注意下标从 开始,这与前面的讲解有点出入。
由于每次只需要关注第 行和 第 行,所以我们可以用 个一维数组进行操作即可。
先上一个比赛过程中我的写法(未优化的)
class Solution {
public long maxPoints(int[][] points) {
int m = points.length, n = points[0].length;
int[] temp = new int[n];
int[] res = new int[n];
int max = 0;
for (int i = 0; i < m; i++) {
// 处理每一行
for (int j = 0; j < n; j++) {
// 处理每一列
if (i == 0) {
temp[j] = points[i][j];
res[j] = points[i][j];
} else {
// 取最大
int t = 0;
for (int k = 0; k < n; k++) {
t = Math.max(t, points[i][j] + temp[k] - Math.abs(k - j));
}
res[j] = t;
}
max = Math.max(res[j], max);
}
System.arraycopy(res, 0, temp, 0, n);
}
return max;
}
}
好家伙!差2个用例(哭
当时弄死就是想不到可以如何优化,唉,还得多练
下面上一个优化后的写法
class Solution {
/**
* f(i, j) = p[i][j] + max { f(i - 1, k) - abs(j - k) }
* ==> 化简
* f(i, j) = p[i][j] + max { f(i - 1, k) - (j - k) } , k <= j
* f(i, j) = p[i][j] + max { f(i - 1, k) - (k - j) } , k > j
* ==> 化简, 提出 j
* f(i, j) = p[i][j] - j + max { f(i - 1, k) + k } , k <= j
* f(i, j) = p[i][j] + j + max { f(i - 1, k) - k } , k > j
*
* 对于当前行的 k 属于 0~n-1 的每个列, 都可以计算出 f(i - 1, k) + k 和 f(i - 1, k) - k, 只需要遍历两次即可, 时间复杂度 O(2n)
* */
public long maxPoints(int[][] points) {
int m = points.length, n = points[0].length;
long[] res = new long[n]; // 用一维数组来存储 f(i,j)
long[] leftMax = new long[n]; // 某一列左侧的最大的 f(i-1,k) + k
long[] rightMax = new long[n]; // 某一列右侧的最大的 f(i-1,k) - k
long ans = 0;
// 初始化第一行
for (int i = 0; i < n; i++) res[i] = points[0][i];
// 从第二行开始
for (int i = 1; i < m; i++) {
// 先处理出上一行的数据, 先找到j右侧的最大值
long max = res[n - 1] - n + 1; // 最后一个k的位置的 f(i-1,k) - k , k = n - 1
for(int j = n - 1; j >= 0; j--) rightMax[j] = max = Math.max(res[j] - j, max);
// 再找到 j 左侧的最大值
max = res[0]; // 第一个k的位置的 f(i-1,k) + k , k = 0
for(int j = 0; j < n; j++) leftMax[j] = max = Math.max(res[j] + j, max);
// 更新该行的数据
for(int j = 0; j < n; j++) res[j] = points[i][j] + Math.max(leftMax[j] - j, rightMax[j] + j);
}
// 处理结束后, 找出最大值
for (int i = 0; i < n; i++) ans = Math.max(res[i], ans);
return ans;
}
}
终于!!搞了2天终于把这道题给搞懂了!(哭
LeetCode - 1938
给你一棵 n个节点的有根树,节点编号从 0 到 n - 1。每个节点的编号表示这个节点的 独一无二的基因值 (也就是说节点 x 的基因值为 x)。两个基因值的 基因差 是两者的 异或和 。给你整数数组 parents ,其中 parents[i] 是节点 i 的父节点。如果节点 x 是树的 根 ,那么 parents[x] == -1 。
给你查询数组 queries ,其中 queries[i] = [nodei, vali] 。对于查询 i ,请你找到 vali 和 pi 的 最大基因差 ,其中 pi 是节点 nodei 到根之间的任意节点(包含 nodei 和根节点)。更正式的,你想要最大化 vali XOR pi 。
请你返回数组 ans ,其中 ans[i] 是第 i 个查询的答案。
题解
先上一个比赛过程中我的写法(暴力,没有优化,报TLE了)
class Solution {
public int[] maxGeneticDifference(int[] parents, int[][] queries) {
int[] ans = new int[queries.length];
for (int i = 0; i < queries.length; i++) {
int node = queries[i][0], val = queries[i][1];
int t = 0;
while(node != -1) {
t = Math.max(node ^ val, t);
node = parents[node];
}
ans[i] = t;
}
return ans;
}
}
可以将所有查询先存起来(离线),然后再用DFS+Trie树(字典树)
具体思路后续补充 TODO
看完题解后,自己动手写了一遍(注意开数组时,在方法内部开,不要用最大的长度开,那样会超时,坑死我了!)
class Solution {
// 最大值 2 * 10^5 , 即最大 2^18, 只需要18位即可
// 用数组模拟来做吧
// 节点的个数为 10^5
final int HIGH_BIT = 17; // 只需要18位, 最高位需要左移 17
int idx; // 节点编号, trie 树
int[][] trie; // trie 树
int[] ctn; // 节点计数, 主要方便从 trie 树中删除某个元素
int[] h, e, ne;
int idx2;
int treeRoot;
// DFS + Trie
// TLE
public int[] maxGeneticDifference(int[] parents, int[][] queries) {
// 动态开数组
// 如果开全局数组, 则需要把长度开到最大, 那样全部用例加起来的执行时间会超时
trie = new int[parents.length * 18][2];
ctn = new int[parents.length * 18];
// 先将全部的查询存起来, 采用 LinkedHashMap, 以保证插入顺序不变
Map<Integer, List<Integer>> queriesMap = new HashMap<>();
List<Pair> queriesList = new ArrayList<>(queries.length);
for (int i = 0; i < queries.length; i++) {
int node = queries[i][0];
int val = queries[i][1];
queriesList.add(new Pair(node, val));
if (queriesMap.containsKey(node)) queriesMap.get(node).add(val);
else queriesMap.put(node, new ArrayList<>(Arrays.asList(val)));
}
h = new int[parents.length];
e = new int[parents.length];
ne = new int[parents.length];
Arrays.fill(h, -1);
Map<Pair, Integer> resMap = new HashMap<>(queries.length);
// 建树
buildTree(parents);
// 深搜
dfs(treeRoot, queriesMap, resMap);
int[] res = new int[queries.length];
for (int i = 0; i < queries.length; i++) {
Pair query = queriesList.get(i);
res[i] = resMap.get(query);
}
return res;
}
public void dfs(int x, Map<Integer, List<Integer>> queriesMap, Map<Pair, Integer> resMap) {
add(x);
if (queriesMap.containsKey(x)) {
List<Integer> list = queriesMap.get(x);
for (int i : list) {
int res = query(i);
resMap.put(new Pair(x, i), res);
}
}
for (int i = h[x]; i != -1 ; i = ne[i]) {
int j = e[i];
dfs(j, queriesMap, resMap);
}
remove(x);
}
public void buildTree(int[] parents) {
for (int i = 0; i < parents.length; i++) {
if (parents[i] == -1) treeRoot = i;
else {
e[idx2] = i;
ne[idx2] = h[parents[i]];
h[parents[i]] = idx2++;
}
}
}
// 添加一个数到 Trie 树
public void add(int x) {
int p = 0; // root
for (int i = HIGH_BIT; i >= 0 ; i--) {
int u = (x >> i) & 1; // 获取当前位
if (trie[p][u] == 0) trie[p][u] = ++idx; // 不存在这个节点, 直接新增一个
ctn[trie[p][u]]++; // 节点计数
p = trie[p][u];
}
}
// 从 Trie 树中移除一个数
public void remove(int x) {
int p = 0;
for (int i = HIGH_BIT; i >= 0 ; i--) {
int u = (x >> i) & 1;
ctn[trie[p][u]]--; // 直接计数减1即可
p = trie[p][u];
}
}
// 从 Trie 数中找到一个和 x 异或结果最大的数, 并输出结果
public int query(int x) {
int p = 0, ans = 0;
for (int i = HIGH_BIT; i >= 0 ; i--) {
int u = (x >> i) & 1;
u = 1 - u;
ans = ans << 1; // 先乘2
if (trie[p][u] > 0 && ctn[trie[p][u]] > 0) {
// 这个节点真实存在, 则走过去
ans++;
p = trie[p][u];
} else {
p = trie[p][1 - u]; // 否则, 走到另一边
}
}
return ans;
}
}
class Pair {
int node;
int val;
public Pair(int node, int val) {
this.node = node;
this.val = val;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Pair pair = (Pair) o;
return node == pair.node &&
val == pair.val;
}
@Override
public int hashCode() {
return Objects.hash(node, val);
}
}
上周比赛时,4道题只做出了前2道。第三道和第四道都只写出了暴力解法,不知道如何下手优化。
好在,经过两天的看题解和撸代码。终于!把这周周赛消化完了!给自己鼓个爪,啪啪啪!
下周再接再厉!
下周见!
(完)