周赛题练习-mid模块
Score1600:1864. 构成交替字符串需要的最小交换次数
题目要求:给定0
和1
组成的字符串,求0
和1
交换的最小次数组成010..
或101..
解题思路
- 由于最终结果不是
010..
就是101..
,因此对字符串的字符进行遍历,比较0
开头和1
开头最终字符串中的0和1差异数 - 对结果进行条件判定:
- 如果两种的结果都无需交换,直接返回0
- 如果两种都不等,即没法交换产生
01
相邻字符串,返回-1 - 两种都能交换,返回最小;否则返回能交换的一种
class Solution {
public int minSwaps(String s) {
int len = s.length();
int dif00 = 0, dif01 = 0;
int dif10 = 0, dif11 = 0;
for (int i = 0; i < len; i++) {
if (i % 2 == 0) {
if (s.charAt(i) != '0') {
dif00++;
} else {
dif10++;
}
} else {
if (s.charAt(i) != '1') {
dif01++;
} else {
dif11++;
}
}
}
if (dif00 == 0 && dif01 == 0 || dif10 == 0 && dif11 == 0) {
return 0;
}
if (dif00 != dif01 && dif10 != dif11) {
return -1;
}
if (dif00 == dif01 && dif10 == dif11) {
return Math.min(dif00, dif10);
} else if (dif00 == dif01) {
return dif00;
} else {
return dif10;
}
}
}
Score1601: 926. 将字符串翻转到单调递增
题目要求:给定0
和1
组成的二进制字符串,其中可以进行0
->1
或者1
->0
翻转,求该字符串翻转成为单调递增的最小翻转次数,例如00110
最少翻转1次得到单调递增的字符串00111
解题思路
反面教材解法(典型的面向用例编程,只能通过 72 / 93 个用例,10011111110010111011
过不了)
- 看起来是个大贪心题,先算个前缀和,然后根据特定的规则来算反转次数
- 从尾部遍历前缀和数组
- 规则一:当尾部开始相等时,代表是尾部的递增序列,再继续不等时就是不满足递增序列了,开始统计不满足的1个数
- 规则二:当相邻相等时,即没有严格单调递增,补偿一下,相当于统计
0
->1
,我写的比较变扭了 - 最后将规则一 (不满足的1个数置0) 和规则二 (补1满足单调)进行取最小
- 复盘一下为啥无法AC:对于用例
10011111110010111011
,本方法输出6,预期是5,也就是说预期的方法是将第一个1
->0
,剩下的0
->1
,做题大忌是面向用例找规则,务必提前抽象设计出合适的方法
class Solution {
public int minFlipsMonoIncr(String s) {
int n = s.length();
int[] preSum = new int[n + 1];
preSum[0] = 0;qian
for (int i = 1; i <= n; i++) {
char c = s.charAt(i - 1);
preSum[i] = preSum[i - 1] + c - '0';
}
int disCnt = 0; // 最短不递增
int sameCnt = 0; // 增量补偿
int lastNum = preSum[n];
boolean flag = false;
for (int i = n - 1; i >= 0; i--) {
if (preSum[i] != lastNum) {
if (flag) {
disCnt++;
}
} else {
if (lastNum != 0) {
sameCnt++;
}
if (!flag) {
flag = true;
}
}
lastNum = preSum[i];
}
return Math.min(sameCnt, disCnt);
}
}
解法一:前缀和
- 题目的要求是将字符串翻转成
0011
or1111
or0000
的形式,由此可见在前缀和中定位最佳的索引,使得翻转次数最少 - 索引从 0-n 遍历
- 将左边
1
->0
,即preSum[i]
- 将右边
0
->1
,即 n - i - (preSum[n] - preSum[i]),表示索引右边1的目标总数 - 索引右边1的当前总数 - 最后取 Math.min(res, l + r)
- 将左边
class Solution {
public int minFlipsMonoIncr(String s) {
int n = s.length();
int[] preSum = new int[n + 1];
char[] chars = s.toCharArray();
for (int i = 1; i <= n; i++) {
char c = chars[i - 1];
preSum[i] = preSum[i - 1] + c - '0';
}
int res = n;
for (int i = 0; i <= n; i++) {
int l = preSum[i]; // 左边将 1 转为 0
int r = (n - i) - (preSum[n] - preSum[i]); // 右边将 0 转为 1
res = Math.min(res, l + r);
}
return res;
}
}
解法二:转化为LIS问题,贪心解+二分
数组总长度 - 数组中最长的非严格递增子序列的长度 即为答案
class Solution {
public int minFlipsMonoIncr(String s) {
int[] nums = new int[s.length()];
for (int i = 0; i < s.length(); i++) {
nums[i] = s.charAt(i) - '0';
}
return s.length() - lengthOfLIS(nums);
}
public int lengthOfLIS(int[] nums) {
int n = nums.length;
if (n <= 1) {
return n;
}
int[] g = new int[n];
g[0] = nums[0];
int size = 1;
for (int i = 1; i < n; i++) {
if (nums[i] >= g[size - 1]) {
g[size++] = nums[i];
} else {
int l = 0, r = size - 1;
while (l <= r) {
int mid = l + (r - l) / 2;
if (g[mid] > nums[i]) {
r = mid - 1;
} else { // 搜索右边界(小于等于nums[i]的值的索引)
l = mid + 1;
}
}
g[r+1] = nums[i]; // 索引后一个位置替换,例如对于0011,插入变为0011`1`
}
}
return size;
}
}
Score1602:统计圆内格点数目
题目要求:统计坐标点中至少在一个圆上的点数
解题思路
-
前置:满足一个点(x,y)是否在圆(xi,yi,r)内:(x-xi)2 + (y-yi)2 <= r2
-
思路一:遍历每个圆的方形点,满足在圆上就加入set,最后输出size,效率过低,不推荐
-
思路二:由于数据规模为:1<=xi,yi<=100; 1<=ri<=min(xi, yi), 可以推出所有圆在[0,0] 到[200,200]的区域,因此可以遍历所有圆,确定目标区域能够容纳所有圆:[minX~maxX, minY~maxY]
-
思路三:对思路二中的圆按半径大小倒序,能更早遇到包含当前枚举点的圆
//思路1
class Solution {
public int countLatticePoints(int[][] circles) {
int maxY = -1, minY = 200, maxX = -1, minX = 200;
Set<List<Integer>> set = new HashSet<>();
for (int[] circle : circles) {
int xi = circle[0], yi = circle[1], ri = circle[2];
for (int x = xi - ri; x <= xi + ri; x++) {
for (int y = yi - ri; y <= yi + ri; y++) {
if ((x - xi)* (x - xi) + (y - yi)* (y - yi) <= ri * ri) {
set.add(Arrays.asList(x, y));
}
}
}
}
return set.size();
}
}
// 思路2
class Solution {
public int countLatticePoints(int[][] circles) {
int maxY = -1, minY = 200, maxX = -1, minX = 200;
for (int[] circle : circles) {
int xi = circle[0], yi = circle[1], ri = circle[2];
maxY = Math.max(maxY, yi + ri);
maxX = Math.max(maxX, xi + ri);
minY = Math.min(minY, yi - ri);
minX = Math.min(minX, xi - ri);
}
int res = 0;
for (int x = minX; x <= maxX; x++) {
for (int y = minY; y <= maxY; y++) {
for (int[] circle : circles) {
int xi = circle[0], yi = circle[1], ri = circle[2];
if ((x - xi)*(x - xi) + (y - yi)*(y - yi) <= ri * ri) {
res++;
break;
}
}
}
}
return res;
}
}
// 思路3:对思路2优化
class Solution {
public int countLatticePoints(int[][] circles) {
int maxY = -1, minY = 200, maxX = -1, minX = 200;
for (int[] circle : circles) {
int xi = circle[0], yi = circle[1], ri = circle[2];
maxY = Math.max(maxY, yi + ri);
maxX = Math.max(maxX, xi + ri);
minY = Math.min(minY, yi - ri);
minX = Math.min(minX, xi - ri);
}
int res = 0;
Arrays.sort(circles, (o1, o2) -> { // 根据半径大小倒序
return o2[2] - o1[2];
});
for (int x = minX; x <= maxX; x++) {
for (int y = minY; y <= maxY; y++) {
for (int[] circle : circles) {
int xi = circle[0], yi = circle[1], ri = circle[2];
if ((x - xi)*(x - xi) + (y - yi)*(y - yi) <= ri * ri) {
res++;
break;
}
}
}
}
return res;
}
}
Score1604:2316. 统计无向图中无法互相到达点对数
题目要求:在一张无向图中,求无法互相到达的不同点对数目,[1,0]和[0,1]重复了只算1个点对。由于一个连通集的点都能互相到达,不同连通集的点都无法互相到达,因此该题是要我们求连通集,再进行计算无法互相到达的点对数目。
解题思路
- 使用DFS遍历全图,用vis[i]标记是否遍历过,依次来遍历每一个连通集,计数连通集数量,返回数量
- 计算无法互相到达的方法
- 用变量
curCnt
表示为当前累计的连通集的点数量和,cnt
表示该次DFS计算出新连通集的点数量,相乘累加到结果res
,更新curCnt
。即新连通集点数*
老的总连通集点数,依次迭代 - 还可以这样做,依次累加 (n - cnt)*cnt,最后/2。这种计算方法可以理解为:当前连通集点数和剩余的连通集点数都是不互相到达的,因此直接相乘,最后去重/2
- 用变量
class Solution {
public long countPairs(int n, int[][] edges) {
List<Integer>[] list = new ArrayList[n];
Arrays.setAll(list, e -> new ArrayList<>());
boolean[] vis = new boolean[n];
for (int[] edge : edges) {
int x = edge[0];
int y = edge[1];
list[x].add(y);
list[y].add(x);
}
long res = 0;
long curCnt = 0;
for (int i = 0; i < n; i++) {
long cnt = countPairs(i, list, vis);
res = res + curCnt * cnt; // 也可以 res += (n - cnt) * cnt,结果返回 res / 2
curCnt += cnt;
}
return res;
}
private long countPairs(int i, List<Integer>[] list, boolean[] vis) {
if (vis[i]) {
return 0;
}
vis[i] = true;
long count = 1;
for (int j : list[i]) {
count += countPairs(j, list, vis);
}
return count;
}
}
Score1604:809. 情感丰富的文字
题目要求:给定字符串S
, 一组单词,单词可以将一些字母扩张达到连续3或者3个以上,使得与S
相同,则为可扩张。
解题思路
字符串、列表、数组、整数比较对象的出场形式:
<比较模板>
while(A 没完 && B 没完)
A 的当前字符
B 的当前字符
A 的当前字符长度
B 的当前字符长度
判读符合比较条件吗
判断 A B 都走完了吗
<加法模板>
while ( A 没完 || B 没完))
A 的当前位
B 的当前位
和 = A 的当前位 + B 的当前位 + 进位carry
当前位 = 和 % 10;
进位 = 和 / 10;
判断还有进位吗
class Solution {
public int expressiveWords(String s, String[] words) {
if (s == null || s.length() == 0 || words == null || words.length == 0) {
return 0;
}
int res = 0;
for (String word : words) {
if (isStrechy(s, word)) {
res++;
}
}
return res;
}
private boolean isStrechy(String s, String t) {
int n = s.length(), m = t.length();
if (n < m) {
return false;
}
int i = 0, j = 0;
while (i < n && j < m) {
char c1 = s.charAt(i);
char c2 = t.charAt(j);
int cnt1 = 0;
while (i < n && c1 == s.charAt(i)) {
i++;
cnt1++;
}
int cnt2 = 0;
while (j < m && c2 == t.charAt(j)) {
j++;
cnt2++;
}
// 情况一:c1和c2不同;情况二:c1的长度 < c2的长度;情况三:c1,c2长度不同,c1长度为1或2
if (c1 != c2 || cnt1 < cnt2 || cnt1 <= 2 && cnt1 != cnt2) {
return false;
}
}
return i == n && j == m;
}
}
Score1604:2424. 最长上传前缀
题目要求:区间为[1,n]的视频服务器,每次可以上传一个video到服务器中,要求返回最长上传前缀的长度l,如果l不为,就是说[1, l]之间都上传了video。注意,1 <= n <= 10 5, 上传和查看最长上传前缀次数不超过 2 * 105。
解题思路
- 根据数据范围,每次直接遍历查看最长上传前缀会超时,因此考虑维护一个最长上传前缀的单指针
- 预先分配服务器数组,每次上传视频后,再上一次的最长上传前缀开始遍历
class LUPrefix {
private int n;
private int[] videoes;
private int maxPrex;
public LUPrefix(int n) {
this.n = n;
videoes = new int[n];
maxPrex = 0;
}
public void upload(int video) {
videoes[video - 1] = 1;
for (int i = maxPrex; i < n; i++) {
if (videoes[i] == 1) {
maxPrex = Math.max(maxPrex, i + 1);
} else {
break;
}
}
}
public int longest() {
return maxPrex;
}
}
Score1606:1604. 警告一小时内使用相同员工卡大于等于三次的人
题目要求:给定keyname[]
和keyTime[]
,表示打卡员工的名字和时间,时间格式为24小时制,"HH:MM" ,求在1小时内打卡次数大于等于三次的员工,按字典升序返回结果
解题思路
- 由于[keyName[i], keyTime[i]]唯一,即一个员工的打卡时间是不同的,因此适合对每个员工的打卡时间进行记录,使用Map<String, List
>结构映射每个员工和对应的打卡时间 - "HH:MM"时间格式转为分钟,便于计算,对员工的打卡时间进行升序,三个相邻元素的最大最小值之差超过60即为收到警告员工
class Solution {
public List<String> alertNames(String[] keyName, String[] keyTime) {
Map<String, List<Integer>> timeMap = new HashMap<>();
for (int i = 0; i < keyName.length; i++) {
List<Integer> timeList = timeMap.getOrDefault(keyName[i], new ArrayList<>());
String[] timeStr = keyTime[i].split(":");
timeList.add(Integer.valueOf(timeStr[0]) * 60 + Integer.valueOf(timeStr[1]));
timeMap.put(keyName[i], timeList);
}
List<String> res = new ArrayList<>();
for (Map.Entry<String, List<Integer>> entry : timeMap.entrySet()) {
Collections.sort(entry.getValue());
for (int i = 2; i < entry.getValue().size(); i++) {
if (entry.getValue().get(i) - entry.getValue().get(i - 2) <= 60) {
res.add(entry.getKey());
break;
}
}
}
Collections.sort(res);
return res;
}
}
Score1606:1300. 转变数组后最接近目标值的数组和
题目要求:给定整数数组arr
和一个目标值target
,返回value
,使得数组中所有大于value
的值变为value
后,数组总和最接近target
解题思路
- 若数组
arr
的每个值都>=target/arr.len
,则可直接取得目标值,考虑到非整除的情况,因此根据绝对值误差最小来取target/arr.len
:target/arr.len + 1
- 如果不满足
1
,表示数组arr
有一些值小于target/arr.len
,无法变为该值,因此返回值务必比target/arr.len
大一些。由于返回值具有单调性,此时考虑二分法进行搜索:
- 左边界:
target/arr.len
, 右边界:Math.max(arr)
- 对数组改造后的总和
sum
和target
比较- 如果大于
target
,r = mid - 1 - 如果小于
target
,l = mid + 1 - 如果等于
target
,这里有点意思,可以使用左边界或者右边界- 使用右边界时,即寻找满足小于等于
target
的index,此时mid=target时,left=mid+1,最后返回right。由于此时的right的数组总和小于等于target
,所以答案为right : right + 1 - 使用左边界时,即寻找满足大于等于
target
的index,此时mid=target时,right=mid-1,最后返回left。同理,答案为left-1 : left
- 使用右边界时,即寻找满足小于等于
- 如果大于
class Solution {
public int findBestValue(int[] arr, int target) {
int len = arr.length;
int curVal = target / len;
boolean flag = true;
int maxNum = Integer.MIN_VALUE;
for (int num : arr) {
maxNum = Math.max(maxNum, num);
if (num < curVal) {
flag = false;
}
}
if (flag) {
return Math.abs(target - curVal * len) <= Math.abs(target - (curVal + 1) * len)
? curVal : curVal + 1;
}
int l = curVal, r = maxNum;
while (l <= r) {
int mid = l + (r - l) / 2;
if (getSum(arr, mid) > target) { // 使用左边界 >=
r = mid - 1;
} else {
l = mid + 1;
}
}
return Math.abs(target - getSum(arr, r)) <= Math.abs(target - getSum(arr, r + 1))
? r : r + 1; // 使用左边界 Math.abs(target - getSum(arr, l)) < Math.abs(target - getSum(arr, l - 1)) ? l : l - 1;
}
private int getSum(int[] arr, int bestVal) {
int sum = 0;
for (int num : arr) {
sum += num < bestVal ? num : bestVal;
}
return sum;
}
}
Score1607:1123. 最深叶节点的最近公共祖先
题目要求:返回二叉树最深叶子节点的最近公共祖先
解析思路
使用常规的前序遍历,从根节点开始,分别求左右子树的高度left、right
- left = right,两边子树最大深度相同,返回本节点
- left < right,说明最深节点在右子树,返回右子树的递归结果
- left > right,说明最深节点在左子树,返回左子树的递归结果
class Solution {
public TreeNode lcaDeepestLeaves(TreeNode root) {
int left = getDepth(root.left);
int right = getDepth(root.right);
if (left == right) {
return root;
} else if (left < right) {
return lcaDeepestLeaves(root.right);
} else{
return lcaDeepestLeaves(root.left);
}
}
public int getDepth(TreeNode node) {
if (node == null) {
return 0;
}
return Math.max(getDepth(node.left), getDepth(node.right)) + 1;
}
}
Score1678:2317. 操作后的最大异或和
题目要求:选择 任意 非负整数 x
和一个下标 i
,更新 nums[i]
为 nums[i] AND (nums[i] XOR x)
解题思路
nums[i]
逐位异或任意非负整数,相当于把nums[i]
修改为任意非负整数nums[i]
逐位与任意非负整数,nums[i]
上的位只能1->0
,无法0->1
。nums
中所有元素 最大 逐位异或和,如果nums
的某个比特位有奇数个1
,那么这个比特位异或和的结果就是1
。由2可知,只要保证该比特位有1个1
即可- 因此通过逐位或运算求出
nums
在哪些比特位有1
能够的带最大 逐位异或和
class Solution {
public int maximumXOR(int[] nums) {
int res = 0;
for (int num : nums) {
res = res | num;
}
return res;
}
}
本文来自博客园,作者:LogBiao,转载请注明原文链接:https://www.cnblogs.com/logbiao/p/16464337.html