下了三题,真实水平,每题都被罚时也是醉了。
13 分,1000+ 名
题目
T1. 替换所有的问号
题目
给你一个仅包含小写英文字母和 '?'
字符的字符串 s
,请你将所有的 '?'
转换为若干小写字母,使最终的字符串不包含任何 连续重复 的字符。
注意:你 不能 修改非 '?'
字符。
题目测试用例保证 除 '?'
字符 之外,不存在连续重复的字符。
在完成所有转换(可能无需转换)后返回最终的字符串。如果有多个解决方案,请返回其中任何一个。可以证明,在给定的约束条件下,答案总是存在的。
示例 1:
输入:s = "?zs" 输出:"azs" 解释:该示例共有 25 种解决方案,从 "azs" 到 "yzs" 都是符合题目要求的。只有 "z" 是无效的修改,因为字符串 "zzs" 中有连续重复的两个 'z' 。
示例 2:
输入:s = "ubv?w" 输出:"ubvaw" 解释:该示例共有 24 种解决方案,只有替换成 "v" 和 "w" 不符合题目要求。因为 "ubvvw" 和 "ubvww" 都包含连续重复的字符。
示例 3:
输入:s = "j?qg??b" 输出:"jaqgacb"
示例 4:
输入:s = "??yw?ipkj?" 输出:"acywaipkja"
提示:
-
1 <= s.length <= 100
-
s
仅包含小写英文字母和'?'
字符
题解思路
- 最多需要三个字母替换:
a
,b
,c
- 参考代码中
get(char)
使用字典序替换处理
- 参考代码中
- 特殊处理第一个、最后一个
- 中间的都需要看左右相邻
参考代码
public String modifyString(String s) {
if (s.equals("?")) return "a";
char[] arr = s.toCharArray();
for (int i = 0; i < arr.length; i++) {
if (arr[i] == '?') {
if (i == 0) arr[i] = get(arr[i + 1]);
else if (i == arr.length - 1) arr[i] = get(arr[i - 1]);
else arr[i] = get(arr[i - 1], arr[i + 1]);
}
}
return new String(arr);
}
private char get(char l, char r) {
if (r == '?') return get(l);
char res = get(r);
while (res == r || res == l)
res = get(res);
return res;
}
private char get(char nb) {
if (nb == '?') return 'a';
if (nb == 'z') return 'a';
return (char) (nb + 1);
}
复杂度分析
- 时间复杂度:
O(n)
- 空间复杂度:
O(1)
T2. 数的平方等于两数乘积的方法数
题目
给你两个整数数组 nums1
和 nums2
,请你返回根据以下规则形成的三元组的数目(类型 1 和类型 2 ):
- 类型 1:三元组
(i, j, k)
,如果nums1[i]2 == nums2[j] * nums2[k]
其中0 <= i < nums1.length
且0 <= j < k < nums2.length
- 类型 2:三元组
(i, j, k)
,如果nums2[i]2 == nums1[j] * nums1[k]
其中0 <= i < nums2.length
且0 <= j < k < nums1.length
示例 1:
输入:nums1 = [7,4], nums2 = [5,2,8,9] 输出:1 解释:类型 1:(1,1,2), nums1[1]^2 = nums2[1] * nums2[2] (4^2 = 2 * 8)
示例 2:
输入:nums1 = [1,1], nums2 = [1,1,1] 输出:9 解释:所有三元组都符合题目要求,因为 1^2 = 1 * 1 类型 1:(0,0,1), (0,0,2), (0,1,2), (1,0,1), (1,0,2), (1,1,2), nums1[i]^2 = nums2[j] * nums2[k] 类型 2:(0,0,1), (1,0,1), (2,0,1), nums2[i]^2 = nums1[j] * nums1[k]
示例 3:
输入:nums1 = [7,7,8,3], nums2 = [1,2,9,7] 输出:2 解释:有两个符合题目要求的三元组 类型 1:(3,0,2), nums1[3]^2 = nums2[0] * nums2[2] 类型 2:(3,0,1), nums2[3]^2 = nums1[0] * nums1[1]
示例 4:
输入:nums1 = [4,7,9,11,23], nums2 = [3,5,1024,12,18] 输出:0 解释:不存在符合题目要求的三元组
提示:
1 <= nums1.length, nums2.length <= 1000
1 <= nums1[i], nums2[i] <= 10^5
题解思路
- 枚举
i
、j
,利用二分查找j + 1
第一个满足的k
- 排序后可用二分快速查找
- 只需求总方案数,故重排序不影响结果
Map
记录排序后的(value, indexList)
,用于快速计算k
之后有几个相同值
参考代码
public int numTriplets(int[] nums1, int[] nums2) {
Arrays.sort(nums1);
Arrays.sort(nums2);
Map<Integer, List<Integer>> map1 = getMap(nums1), map2 = getMap(nums2);
return cnt(nums1, nums2, map1, map2) + cnt(nums2, nums1, map2, map1);
}
private int cnt(int[] nums1, int[] nums2, Map<Integer, List<Integer>> map1, Map<Integer, List<Integer>> map2) {
int cnt = 0;
int last = 0;
// 枚举 i
for (int i = 0; i < nums1.length; i++) {
int v = nums1[i];
if (i > 0 && v == nums1[i - 1]) { // nums1 同值,快速计算
cnt += last;
continue;
}
long v2 = (long) v * (long) v;
int curr = 0; // 当前 i 的结果
// 枚举 j
for (int j = 0; j < nums2.length - 1; j++) {
// 二分查找 k:从 j + 1 开始,第一个满足的
int l = j + 1, r = nums2.length - 1;
while (l < r) {
int mid = l + (r - l) / 2;
if ((long) nums2[mid] * (long) nums2[j] < v2) l = mid + 1;
else r = mid;
}
if ((long) nums2[l] * (long) nums2[j] == v2) {
// 还要看 l 后面有几个与 nums2[l] 相同
List<Integer> list = map2.get(nums2[l]);
curr += list.get(list.size() - 1) - l + 1;
}
}
cnt += curr; // 累计
last = curr; // 记录缓存
}
// System.out.println();
return cnt;
}
// (value, indexList)
private Map<Integer, List<Integer>> getMap(int[] arr) {
Map<Integer, List<Integer>> map = new HashMap<>();
for (int i = 0; i < arr.length; i++) {
if (!map.containsKey(arr[i]))
map.put(arr[i], new ArrayList<>());
map.get(arr[i]).add(i);
}
return map;
}
复杂度分析
- 时间复杂度:
O(mnlog(n) + nmlog(m))
- 空间复杂度:
O(m + n)
T3. 避免重复字母的最小删除成本
题目
给你一个字符串 s
和一个整数数组 cost
,其中 cost[i]
是从 s
中删除字符 i
的代价。
返回使字符串任意相邻两个字母不相同的最小删除成本。
请注意,删除一个字符后,删除其他字符的成本不会改变。
示例 1:
输入:s = "abaac", cost = [1,2,3,4,5] 输出:3 解释:删除字母 "a" 的成本为 3,然后得到 "abac"(字符串中相邻两个字母不相同)。
示例 2:
输入:s = "abc", cost = [1,2,3] 输出:0 解释:无需删除任何字母,因为字符串中不存在相邻两个字母相同的情况。
示例 3:
输入:s = "aabaa", cost = [1,2,3,4,1] 输出:2 解释:删除第一个和最后一个字母,得到字符串 ("aba") 。
提示:
s.length == cost.length
1 <= s.length, cost.length <= 10^5
1 <= cost[i] <= 10^4
s
中只含有小写英文字母
题解思路
- 两两比较,删成本大的
last
删后上一个字母,删谁都一样lastIndex
删后上一个字母的位置,删谁有不同
参考代码
public int minCost(String s, int[] cost) {
char[] arr = s.toCharArray();
int n = arr.length;
if (n == 1) return 0;
int res = 0;
char last = '0';
int lastIndex = -1;
for (int i = 0; i < n; i++) {
if (arr[i] == last) {
if (cost[i] < cost[lastIndex]) {
// 删 arr[i]
res += cost[i];
// lastIndex = lastIndex 无变化
} else {
// 删 arr[lastIndex]
res += cost[lastIndex];
lastIndex = i; // 变化
}
} else lastIndex = i; // 更新
last = arr[i]; // 总是更新
}
return res;
}
复杂度分析
- 时间复杂度:
O(n)
- 空间复杂度:
O(1)
T4. 保证图可完全遍历
题目
Alice 和 Bob 共有一个无向图,其中包含 n 个节点和 3 种类型的边:
- 类型 1:只能由 Alice 遍历。
- 类型 2:只能由 Bob 遍历。
- 类型 3:Alice 和 Bob 都可以遍历。
给你一个数组 edges
,其中 edges[i] = [typei, ui, vi]
表示节点 ui
和 vi
之间存在类型为 typei
的双向边。请你在保证图仍能够被 Alice和 Bob 完全遍历的前提下,找出可以删除的最大边数。如果从任何节点开始,Alice 和 Bob 都可以到达所有其他节点,则认为图是可以完全遍历的。
返回可以删除的最大边数,如果 Alice 和 Bob 无法完全遍历图,则返回 -1 。
示例 1:
输入:n = 4, edges = [[3,1,2],[3,2,3],[1,1,3],[1,2,4],[1,1,2],[2,3,4]] 输出:2 解释:如果删除 [1,1,2] 和 [1,1,3] 这两条边,Alice 和 Bob 仍然可以完全遍历这个图。再删除任何其他的边都无法保证图可以完全遍历。所以可以删除的最大边数是 2 。
示例 2:
输入:n = 4, edges = [[3,1,2],[3,2,3],[1,1,4],[2,1,4]] 输出:0 解释:注意,删除任何一条边都会使 Alice 和 Bob 无法完全遍历这个图。
示例 3:
输入:n = 4, edges = [[3,2,3],[1,1,2],[2,3,4]] 输出:-1 解释:在当前图中,Alice 无法从其他节点到达节点 4 。类似地,Bob 也不能达到节点 1 。因此,图无法完全遍历。
提示:
1 <= n <= 10^5
1 <= edges.length <= min(10^5, 3 * n * (n-1) / 2)
edges[i].length == 3
1 <= edges[i][0] <= 3
1 <= edges[i][1] < edges[i][2] <= n
- 所有元组
(typei, ui, vi)
互不相同
题解思路
- 可用「逐渐添加边」的思路做
- 遍历边,若两点已能连通,则无需此边
- 并查集:判断两点是否已经连通
- 图:利用入度快速判断是否可全通
- 贪心:优先使用共享路径
参考代码
class UnionFind {
private int count;
private int[] parent;
public UnionFind(int n) {
count = n;
parent = new int[n + 1];
for (int i = 1; i <= n; i++)
parent[i] = i;
}
// false: 已可通,无需新增连接
public boolean union(int p, int q) {
int rootP = find(p);
int rootQ = find(q);
if (rootP == rootQ) return false;
parent[rootP] = rootQ;
count--;
return true;
}
public int find(int p) {
int root = p;
while (root != parent[root])
root = parent[root];
while (p != parent[p]) {
int x = p;
p = parent[p];
parent[x] = root;
}
return root;
}
}
public int maxNumEdgesToRemove(int n, int[][] edges) {
if (edges.length == 1) return 0;
// 各点的入度情况
int[][] ins = new int[n + 1][3 + 1];
for (int[] e : edges) {
int type = e[0], a = e[1], b = e[2];
ins[a][type]++;
ins[b][type]++;
}
// 检查无法完全遍历
for (int i = 1; i <= n; i++) {
int[] in = ins[i];
if (in[3] == 0 && (in[1] == 0 || in[2] == 0))
return -1;
}
// 可完全遍历
int res = 0;
// 并查集
UnionFind ufa = new UnionFind(n), ufb = new UnionFind(n);
// 贪心:先选共用 e[0] desc
Arrays.sort(edges, (e1, e2) -> e2[0] - e1[0]);
for (int[] e : edges) {
int type = e[0], a = e[1], b = e[2];
if (type == 1) {
if (!ufa.union(a, b)) res++;
} else if (type == 2) {
if (!ufb.union(a, b)) res++;
} else {
boolean ua = ufa.union(a, b), ub = ufb.union(a, b);
if (!ua && !ub) res++; // Alice, Bob 都不再需要这条边
}
}
return res;
}
复杂度分析
- 时间复杂度:
O(nlog(n))
- 空间复杂度:
O(n)
赛后复盘
- T1 比较简单,自己可多补充一些测试用例
- T2 因为数据范围不大,故枚举还是可以通过的。可用
Set
代替List
。罚时在 TLE,还是应该需要多观察、模拟程序的运行,以优化算法 - T3 感觉比 T2 略简单一些,建议改成 4 分题。罚时在
lastIndex
的处理未根据所删内容而不同。 - T4 比赛时没能想到使用并查集,赛后看了零神的代码才有启发,自己写也不会太麻烦,是一道并查集的好题呀!赛中思路一直落在
adj
与ins
的直接复杂判断。以厉害周赛的经验,若是百般思索、代码逐渐混乱都还找不到端倪,那十有八九应该换个思路了
一些操作类的题目可以采用逆向操作的思维来解,添加的用删除、删除的用添加。难得遇到并查集的题,值得回味。
总体还是能反应真实水平——菜就是菜,光罚时就掉了 300+ 名,再接再厉吧!