🌟欢迎来到 我的博客 —— 探索技术的无限可能!
8.使数组连续的最少操作数
题目链接:2009. 使数组连续的最少操作数
给你一个整数数组 nums 。每一次操作中,你可以将 nums 中 任意 一个元素替换成 任意 整数。
如果 nums 满足以下条件,那么它是 连续的 :
- nums 中所有元素都是 互不相同 的。
- nums 中 最大 元素与 最小 元素的差等于 nums.length - 1 。
比方说,nums = [4, 2, 5, 3] 是 连续的 ,但是 nums = [1, 2, 3, 5, 6] 不是连续的 。
请你返回使 nums 连续 的 最少 操作次数。
示例 1:
输入:nums = [4,2,5,3]
输出:0
解释:nums 已经是连续的了。
示例 2:
输入:nums = [1,2,3,5,6]
输出:1
解释:一个可能的解是将最后一个元素变为 4 。
结果数组为 [1,2,3,5,4] ,是连续数组。
示例 3:
输入:nums = [1,10,100,1000]
输出:3
解释:一个可能的解是:
- 将第二个元素变为 2 。
- 将第三个元素变为 3 。
- 将第四个元素变为 4 。
结果数组为 [1,2,3,4] ,是连续数组。
提示:
1 <= nums.length <= 105
1 <= nums[i] <= 109
题解:
方法:排序 滑动窗口
正难则反,考虑最多保留多少个元素不变。
设 x 是修改后的连续数字的最大值,则修改后的连续数字的范围为闭区间 [x−n+1,x],其中 nnn 是 nums 的长度。在修改前,对于已经在 [x−n+1,x] 中的数,我们无需修改。那么,x 取多少,可以让无需修改的数最多呢?
由于元素的位置不影响答案,且要求所有元素互不相同,我们可以将 nums从小到大排序,并去掉重复元素。设 a 为 nums 排序去重后的数组。把 a[i] 画在一条数轴上,本题相当于有一个长度为 n 的滑动窗口,我们需要计算窗口内最多可以包含多少个数轴上的点。
定理:只需要枚举 a[i] 作为窗口的右端点。
证明:在窗口从左向右滑动的过程中,如果窗口右端点处没有点,那么继续滑动,在滑到下一个点之前,窗口内包含的点的个数是不会增多的。
为了算出窗口内有多少个点,我们需要知道窗口包含的最左边的点在哪,设这个点的位置是 a[left],则它必须大于等于窗口的左边界,即
a[left]≥a[i]−n+1
此时窗口内有 i−left+1个点,取其最大值,就得到了最多保留不变的元素个数。最后用 n 减去保留不变的元素个数,就得到了答案。
class Solution {
public int minOperations(int[] nums) {
Arrays.sort(nums); // 对数组进行排序
int n = nums.length;
int m = 1; // 记录去重后数组的长度,初始值为 1
// 原地去重,将不重复的数字前移,保留第一个出现的相同数字
for (int i = 1; i < n; i++) {
if (nums[i] != nums[i - 1]) {
nums[m++] = nums[i];
}
}
int ans = 0; // 初始化结果值为 0
int left = 0; // 滑动窗口的左边界
for (int i = 0; i < m; i++) {
while (nums[left] < nums[i] - n + 1) { // 确保窗口内的元素数量不小于 n
left++; // 左边界向右移动,直到窗口内的元素数量满足要求
}
ans = Math.max(ans, i - left + 1); // 更新结果值,取当前窗口大小的最大值
}
return n - ans; // 返回需要操作的最小次数
}
}
9.正整数和负整数的最大计数
题目链接:2529. 正整数和负整数的最大计数
给你一个按 非递减顺序 排列的数组 nums ,返回正整数数目和负整数数目中的最大值。
- 换句话讲,如果 nums 中正整数的数目是 pos ,而负整数的数目是 neg ,返回 pos 和 neg二者中的最大值。
注意:0 既不是正整数也不是负整数。
示例 1:
输入:nums = [-2,-1,-1,1,2,3]
输出:3
解释:共有 3 个正整数和 3 个负整数。计数得到的最大值是 3 。
示例 2:
输入:nums = [-3,-2,-1,0,0,1,2]
输出:3
解释:共有 2 个正整数和 3 个负整数。计数得到的最大值是 3 。
示例 3:
输入:nums = [5,20,66,1314]
输出:4
解释:共有 4 个正整数和 0 个负整数。计数得到的最大值是 4 。
提示:
1 <= nums.length <= 2000
-2000 <= nums[i] <= 2000
nums 按 非递减顺序 排列。
进阶:你可以设计并实现时间复杂度为 O(log(n)) 的算法解决此问题吗?
题解:
方法:遍历
遍历数组,统计负数数目 neg 和正数数目 pos。
最后返回 max(neg,pos)。
public class Solution {
public int maximumCount(int[] nums) {
int neg = 0; // 记录负数的个数
int pos = 0; // 记录正数的个数
for (int x : nums) { // 遍历数组中的每个元素
if (x < 0) {
neg++; // 若元素小于 0,则负数个数加 1
} else if (x > 0) {
pos++; // 若元素大于 0,则正数个数加 1
}
}
return Math.max(neg, pos); // 返回负数和正数中较大的个数
}
}
方法:二分查找
public class Solution {
public int maximumCount(int[] nums) {
int neg = lowerBound(nums, 0); // 获取第一个大于等于0的数的位置(相当于负数的个数)
// 第一个 > 0 的位置,等价于第一个 >= 1 的位置
int pos = nums.length - lowerBound(nums, 1); // 获取第一个大于等于1的数的位置(相当于正数的个数)
return Math.max(neg, pos); // 返回负数和正数中较大的个数
}
// 返回 nums 中第一个 >= target 的数的下标
// 如果不存在这样的数,返回 nums.length
// 详见 https://www.bilibili.com/video/BV1AP41137w7/
private int lowerBound(int[] nums, int target) {
// 二分范围:开区间 (left, right)
int left = -1;
int right = nums.length;
// 开区间不为空
while (left + 1 < right) {
// 循环不变量:
// nums[left] < target
// nums[right] >= target
int mid = left + (right - left) / 2;
if (nums[mid] >= target) {
// 二分范围缩小至 (left, mid)
right = mid;
} else {
// 二分范围缩小至 (mid, right)
left = mid;
}
}
// 此时 left 等于 right - 1
// 因为 nums[right - 1] < target 且 nums[right] >= target,所以答案是 right
return right;
}
}
10.修改后的最大二进制字符串
题目链接:1702. 修改后的最大二进制字符串
给你一个二进制字符串 binary ,它仅有 0 或者 1 组成。你可以使用下面的操作任意次对它进行修改:
-
操作 1 :如果二进制串包含子字符串 “00” ,你可以用 “10” 将其替换。
- 比方说, “00010” -> “10010”
-
操作 2 :如果二进制串包含子字符串 “10” ,你可以用 “01” 将其替换。
- 比方说, “00010” -> “00001”
请你返回执行上述操作任意次以后能得到的 最大二进制字符串 。如果二进制字符串 x 对应的十进制数字大于二进制字符串 y 对应的十进制数字,那么我们称二进制字符串 x 大于二进制字符串 y 。
示例 1:
输入:binary = “000110”
输出:“111011”
解释:一个可行的转换为:
“000110” -> “000101”
“000101” -> “100101”
“100101” -> “110101”
“110101” -> “110011”
“110011” -> “111011”
示例 2:
输入:binary = “01”
输出:“01”
解释:“01” 没办法进行任何转换。
提示:
1 <= binary.length <= 105
binary 仅包含 ‘0’ 和 ‘1’ 。
题解:
方法:贪心
class Solution {
public String maximumBinaryString(String binary) {
int i = binary.indexOf('0'); // 找到第一个 '0' 的位置
if (i < 0) { // 如果没有 '0',则直接返回原字符串
return binary;
}
char[] s = binary.toCharArray(); // 将字符串转换为字符数组
int cnt1 = 0; // 统计从第一个 '0' 开始到末尾的 '1' 的个数
for (i++; i < s.length; i++) {
cnt1 += s[i] - '0'; // 统计 [i, n-1] 中 '1' 的个数
}
// 构造最大二进制字符串
return "1".repeat(s.length - 1 - cnt1) + '0' + "1".repeat(cnt1);
}
}
11.互质树
题目链接:1766. 互质树
给你一个 n 个节点的树(也就是一个无环连通无向图),节点编号从 0 到 n - 1 ,且恰好有 n - 1 条边,每个节点有一个值。树的 根节点 为 0 号点。
给你一个整数数组 nums 和一个二维数组 edges 来表示这棵树。nums[i] 表示第 i 个点的值,edges[j] = [uj, vj] 表示节点 uj 和节点 vj 在树中有一条边。
当 gcd(x, y) == 1 ,我们称两个数 x 和 y 是 互质的 ,其中 gcd(x, y) 是 x 和 y 的 最大公约数 。
从节点 i 到 根 最短路径上的点都是节点 i 的祖先节点。一个节点 不是 它自己的祖先节点。
请你返回一个大小为 n 的数组 ans ,其中 ans[i]是离节点 i 最近的祖先节点且满足 nums[i] 和 nums[ans[i]] 是 互质的 ,如果不存在这样的祖先节点,ans[i] 为 -1 。
示例 1:
输入:nums = [2,3,3,2], edges = [[0,1],[1,2],[1,3]]
输出:[-1,0,0,1]
解释:上图中,每个节点的值在括号中表示。
- 节点 0 没有互质祖先。
- 节点 1 只有一个祖先节点 0 。它们的值是互质的(gcd(2,3) == 1)。
- 节点 2 有两个祖先节点,分别是节点 1 和节点 0 。节点 1 的值与它的值不是互质的(gcd(3,3) == 3)但节点 0 的值是互质的(gcd(2,3) == 1),所以节点 0 是最近的符合要求的祖先节点。
- 节点 3 有两个祖先节点,分别是节点 1 和节点 0 。它与节点 1 互质(gcd(3,2) == 1),所以节点 1 是离它最近的符合要求的祖先节点。
示例 2:
输入:nums = [5,6,10,2,3,6,15], edges =
[[0,1],[0,2],[1,3],[1,4],[2,5],[2,6]]输出:[-1,0,-1,0,0,0,-1]
提示:
nums.length == n
1 <= nums[i] <= 50
1 <= n <= 105
edges.length == n - 1
edges[j].length == 2
0 <= uj, vj < n
uj != vj
题解:
方法:深度优先搜索 数学 回溯 数论
DFS 中记录节点值的深度和编号,回溯写法
class Solution {
private static final int MX = 51;
private static final int[][] coprime = new int[MX][MX];
static {
// 预处理
// coprime[i] 保存 [1, MX) 中与 i 互质的所有元素
for (int i = 1; i < MX; i++) {
int k = 0;
for (int j = 1; j < MX; j++) {
if (gcd(i, j) == 1) {
coprime[i][k++] = j;
}
}
}
}
public int[] getCoprimes(int[] nums, int[][] edges) {
int n = nums.length;
List<Integer>[] g = new ArrayList[n];
Arrays.setAll(g, i -> new ArrayList<>());
for (int[] e : edges) {
int x = e[0];
int y = e[1];
g[x].add(y);
g[y].add(x);
}
int[] ans = new int[n];
Arrays.fill(ans, -1);
int[] valDepth = new int[MX];
int[] valNodeId = new int[MX];
dfs(0, -1, 1, g, nums, ans, valDepth, valNodeId);
return ans;
}
private void dfs(int x, int fa, int depth, List<Integer>[] g, int[] nums, int[] ans, int[] valDepth, int[] valNodeId) {
// x 的节点值
int val = nums[x];
// 计算与 val 互质的祖先节点值中,节点深度最大的节点编号
int maxDepth = 0;
for (int j : coprime[val]) {
if (j == 0) {
break;
}
if (valDepth[j] > maxDepth) {
maxDepth = valDepth[j];
ans[x] = valNodeId[j];
}
}
// tmpDepth 和 tmpNodeId 用于恢复现场
int tmpDepth = valDepth[val];
int tmpNodeId = valNodeId[val];
// 保存 val 对应的节点深度和节点编号
valDepth[val] = depth;
valNodeId[val] = x;
// 向下递归
for (int y : g[x]) {
if (y != fa) {
dfs(y, x, depth + 1, g, nums, ans, valDepth, valNodeId);
}
}
// 恢复现场
valDepth[val] = tmpDepth;
valNodeId[val] = tmpNodeId;
}
private static int gcd(int a, int b) {
return b == 0 ? a : gcd(b, a % b);
}
}
12.找到冠军 I
题目链接:2923. 找到冠军 I
一场比赛中共有 n 支队伍,按从 0 到 n - 1 编号。
给你一个下标从 0 开始、大小为 n * n 的二维布尔矩阵 grid 。对于满足 0 <= i, j <= n - 1 且 i != j 的所有 i, j :如果 grid[i][j] == 1,那么 i 队比 j 队 强 ;否则,j 队比 i 队 强 。
在这场比赛中,如果不存在某支强于 a 队的队伍,则认为 a 队将会是 冠军 。
返回这场比赛中将会成为冠军的队伍。
示例 1:
输入:grid = [[0,1],[0,0]]
输出:0
解释:比赛中有两支队伍。
grid[0][1] == 1 表示 0 队比 1 队强。所以 0 队是冠军。
示例 2:
输入:grid = [[0,0,1],[1,0,1],[0,0,0]]
输出:1
解释:比赛中有三支队伍。
grid[1][0] == 1 表示 1 队比 0 队强。
grid[1][2] == 1 表示 1 队比 2 队强。
所以 1 队是冠军。
提示:
n == grid.length
n == grid[i].length
2 <= n <= 100
grid[i][j] 的值为 0 或 1
对于所有 i, grid[i][i] 等于 0.
对于满足 i != j 的所有 i, j ,grid[i][j] != grid[j][i] 均成立
生成的输入满足:如果 a 队比 b 队强,b 队比 c 队强,那么 a 队比 c 队强
题解:
方法:****
-
如果 grid[i][j]=1,说明 i 队比 j 队强。
-
如果 grid[i][j]=0,说明 i 队比 j 队弱。
-
没有平手。
所以 a 队要是冠军,a 队就要比其它 n−1 个队都要强。
如果 grid[i] 有 n−1 个 1,即元素和为 n−1,说明 i 队比其它 n−1 个队都要强,i 队是冠军。
也可以判断,对于这一行的所有不等于 i 的 j,都有 grid[i][j]=1。这样可以在遇到0 的时候,提前退出循环。
class Solution {
public int findChampion(int[][] grid) {
next:
for (int i = 0; ; i++) {
for (int j = 0; j < grid.length; j++) {
if (j != i && grid[i][j] == 0) {
continue next;
}
}
return i;
}
}
}
如果第 j 列的元素值都是 0,说明没有队伍可以击败 j 队,j 队是冠军。
class Solution {
public int findChampion(int[][] grid) {
next:
for (int j = 0; ; j++) {
for (int[] row : grid) {
if (row[j] != 0) {
continue next;
}
}
return j;
}
}
}
假设冠军是 ans=0,我们从 i=1 开始遍历,寻找可以击败 ans 的队伍,也就是 grid[i][ans]=1。
如果没有出现 grid[i][ans]=1,那么答案就是 ans,否则冠军可能是 i,更新 ans=i。然后从 i+1 继续向后遍历,因为 [1,i−1] 中没有比 0 强的队,更别说比 i 强了。重复上述过程,最后返回 ans。
class Solution {
public int findChampion(int[][] grid) {
int ans = 0;
for (int i = 1; i < grid.length; i++) {
if (grid[i][ans] == 1) {
ans = i;
}
}
return ans;
}
}
13.找到冠军 II
题目链接:2924. 找到冠军 II
一场比赛中共有 n 支队伍,按从 0 到 n - 1 编号。每支队伍也是 有向无环图(DAG) 上的一个节点。
给你一个整数 n 和一个下标从 0 开始、长度为 m 的二维整数数组 edges 表示这个有向无环图,其中 edges[i] = [ui, vi] 表示图中存在一条从 ui 队到 vi 队的有向边。
从 a 队到 b 队的有向边意味着 a 队比 b 队 强 ,也就是 b 队比 a 队 弱 。
在这场比赛中,如果不存在某支强于 a 队的队伍,则认为 a 队将会是 冠军 。
如果这场比赛存在 唯一 一个冠军,则返回将会成为冠军的队伍。否则,返回 -1 。
注意
环 是形如 a1, a2, …, an, an+1 的一个序列,且满足:节点 a1 与节点 an+1 是同一个节点;节点 a1, a2, …, an 互不相同;对于范围 [1, n] 中的每个 i ,均存在一条从节点 ai 到节点 ai+1 的有向边。
有向无环图 是不存在任何环的有向图。
示例 1:
输入:n = 3, edges = [[0,1],[1,2]]
输出:0
解释:1 队比 0 队弱。2 队比 1 队弱。所以冠军是 0 队。
示例 2:
输入:n = 4, edges = [[0,2],[1,3],[1,2]]
输出:-1
解释:2 队比 0 队和 1 队弱。3 队比 1 队弱。但是 1 队和 0 队之间不存在强弱对比。所以答案是 -1 。
提示:
1 <= n <= 100
m == edges.length
0 <= m <= n * (n - 1) / 2
edges[i].length == 2
0 <= edge[i][j] <= n - 1
edges[i][0] != edges[i][1]
生成的输入满足:如果 a 队比 b 队强,就不存在 b 队比 a 队强
生成的输入满足:如果 a 队比 b 队强,b 队比 c 队强,那么 a 队比 c 队强
题解:
class Solution {
public int findChampion(int n, int[][] edges) {
boolean[] isWeak = new boolean[n];
for (int[] e : edges) {
isWeak[e[1]] = true; // 不是冠军
}
int ans = -1;
for (int i = 0; i < n; i++) {
if (isWeak[i]) {
continue;
}
if (ans != -1) {
return -1; // 冠军只能有一个
}
ans = i;
}
return ans;
}
}
14.设计哈希集合
题目链接:705. 设计哈希集合
不使用任何内建的哈希表库设计一个哈希集合(HashSet)。
实现 MyHashSet 类:
void add(key) 向哈希集合中插入值 key 。
bool contains(key) 返回哈希集合中是否存在这个值 key 。
void remove(key) 将给定值 key 从哈希集合中删除。如果哈希集合中没有这个值,什么也不做。
示例:
输入:
[“MyHashSet”, “add”, “add”, “contains”, “contains”, “add”, “contains”,
“remove”, “contains”][[], [1], [2], [1], [3], [2], [2], [2], [2]]
输出:
[null, null, null, true, false, null, true, null, false]
解释:
MyHashSet myHashSet = new MyHashSet();
myHashSet.add(1); // set = [1]
myHashSet.add(2); // set = [1, 2]
myHashSet.contains(1); // 返回 True
myHashSet.contains(3); // 返回 False ,(未找到)
myHashSet.add(2); // set = [1, 2]
myHashSet.contains(2); // 返回 True
myHashSet.remove(2); // set = [1]
myHashSet.contains(2); // 返回 False ,(已移除)
提示:
0 <= key <= 106
最多调用 104 次 add、remove 和 contains
题解:
方法:数组 哈希表
class MyHashSet {
private boolean[] data = new boolean[1000001];
public MyHashSet() {
}
public void add(int key) {
data[key] = true;
}
public void remove(int key) {
data[key] = false;
}
public boolean contains(int key) {
return data[key];
}
}
/**
* Your MyHashSet object will be instantiated and called as such:
* MyHashSet obj = new MyHashSet();
* obj.add(key);
* obj.remove(key);
* boolean param_3 = obj.contains(key);
*/