文章目录
- 周赛345
- [2682. 找出转圈游戏输家](https://leetcode.cn/problems/find-the-losers-of-the-circular-game/)
- 模拟
- [2683. 相邻值的按位异或](https://leetcode.cn/problems/neighboring-bitwise-xor/)
- 方法一:分类讨论(反向思考)
- 方法二:找规律、推公式
- [2684. 矩阵中移动的最大次数](https://leetcode.cn/problems/maximum-number-of-moves-in-a-grid/)(求最大路径长度)
- 方法一:DFS(递归==> 记忆化搜索)
- 方法二:记忆化转DP
- 方法三:BFS
- 🎉相似题目:网格图 DP
- [2685. 统计完全连通分量的数量](https://leetcode.cn/problems/count-the-number-of-complete-components/)
- 方法一:DFS
- 方法二:并查集
周赛345
2682. 找出转圈游戏输家
难度简单0收藏分享切换为英文接收动态反馈
n
个朋友在玩游戏。这些朋友坐成一个圈,按 顺时针方向 从 1
到 n
编号。从第 i
个朋友的位置开始顺时针移动 1
步会到达第 (i + 1)
个朋友的位置(1 <= i < n
),而从第 n
个朋友的位置开始顺时针移动 1
步会回到第 1
个朋友的位置。
游戏规则如下:
第 1
个朋友接球。
- 接着,第
1
个朋友将球传给距离他顺时针方向k
步的朋友。 - 然后,接球的朋友应该把球传给距离他顺时针方向
2 * k
步的朋友。 - 接着,接球的朋友应该把球传给距离他顺时针方向
3 * k
步的朋友,以此类推。
换句话说,在第 i
轮中持有球的那位朋友需要将球传递给距离他顺时针方向 i * k
步的朋友。
当某个朋友第 2 次接到球时,游戏结束。
在整场游戏中没有接到过球的朋友是 输家 。
给你参与游戏的朋友数量 n
和一个整数 k
,请按升序排列返回包含所有输家编号的数组 answer
作为答案。
示例 1:
输入:n = 5, k = 2
输出:[4,5]
解释:以下为游戏进行情况:
1)第 1 个朋友接球,第 1 个朋友将球传给距离他顺时针方向 2 步的玩家 —— 第 3 个朋友。
2)第 3 个朋友将球传给距离他顺时针方向 4 步的玩家 —— 第 2 个朋友。
3)第 2 个朋友将球传给距离他顺时针方向 6 步的玩家 —— 第 3 个朋友。
4)第 3 个朋友接到两次球,游戏结束。
示例 2:
输入:n = 4, k = 4
输出:[2,3,4]
解释:以下为游戏进行情况:
1)第 1 个朋友接球,第 1 个朋友将球传给距离他顺时针方向 4 步的玩家 —— 第 1 个朋友。
2)第 1 个朋友接到两次球,游戏结束。
提示:
1 <= k <= n <= 50
模拟
class Solution {
public int[] circularGameLosers(int n, int k) {
int[] vis = new int[n];
vis[0] = 1;
int cur = 0, t = 1;
while(true){
cur = (cur + t * k) % n;
if(vis[cur] == 1) break;
vis[cur] = 1;
t += 1;
}
List<Integer> res = new ArrayList<>();
for(int i = 0; i < n; i++){
if(vis[i] == 0)
res.add(i+1);
}
return res.stream().mapToInt(Integer::intValue).toArray();
}
}
时间复杂度:O(n)
2683. 相邻值的按位异或
难度中等0
下标从 0 开始、长度为 n
的数组 derived
是由同样长度为 n
的原始 二进制数组 original
通过计算相邻值的 **按位异或(⊕)**派生而来。
特别地,对于范围 [0, n - 1]
内的每个下标 i
:
- 如果
i = n - 1
,那么derived[i] = original[i] ⊕ original[0]
- 否则
derived[i] = original[i] ⊕ original[i + 1]
给你一个数组 derived
,请判断是否存在一个能够派生得到 derived
的 有效原始二进制数组 original
。
如果存在满足要求的原始二进制数组,返回 true ;否则,返回 false 。
- 二进制数组是仅由 0 和 1 组成的数组。
示例 1:
输入:derived = [1,1,0]
输出:true
解释:能够派生得到 [1,1,0] 的有效原始二进制数组是 [0,1,0] :
derived[0] = original[0] ⊕ original[1] = 0 ⊕ 1 = 1
derived[1] = original[1] ⊕ original[2] = 1 ⊕ 0 = 1
derived[2] = original[2] ⊕ original[0] = 0 ⊕ 0 = 0
示例 2:
输入:derived = [1,1]
输出:true
解释:能够派生得到 [1,1] 的有效原始二进制数组是 [0,1] :
derived[0] = original[0] ⊕ original[1] = 1
derived[1] = original[1] ⊕ original[0] = 1
示例 3:
输入:derived = [1,0]
输出:false
解释:不存在能够派生得到 [1,0] 的有效原始二进制数组。
提示:
n == derived.length
1 <= n <= 105
derived
中的值不是 0 就是 1 。
方法一:分类讨论(反向思考)
思路启发:2437. 有效时间的数目,由于二进制数组是仅由 0 和 1 组成的数组。那么原数组只能有两种情况,a[0] = 0
和 a[0] = 1
,分别构造这两原数组
class Solution {
public boolean doesValidArrayExist(int[] derived) {
// 两种情况,a[0] = 0 , a[0] = 1 分别尝试能否构成derived数组
int n = derived.length;
int[] ori = new int[n];
// 先构造a[0] = 0的情况,根据derived[i]取值判断a[i+1]的取值情况,最后根据a[n-1]和a[0]的取值是否合法判断是否为答案
for(int i = 0; i < n; i++){
if(i != n-1){
if(derived[i] == 1){
ori[i+1] = 1 - ori[i];
}else{
ori[i+1] = ori[i];
}
}else{
if((derived[i] == 1 && ori[i] != ori[0]) ||
(derived[i] == 0 && ori[i] == ori[0]))
return true;
}
}
ori = new int[n];
ori[0] = 1;
for(int i = 0; i < n; i++){
if(i != n-1){
if(derived[i] == 1){
ori[i+1] = 1 - ori[i];
}else{
ori[i+1] = ori[i];
}
}else{
if((derived[i] == 1 && ori[i] != ori[0]) ||
(derived[i] == 0 && ori[i] == ori[0]))
return true;
}
}
return false;
}
}
方法二:找规律、推公式
class Solution {
/**
找规律:推公式
由 a ^ a = 0
a ^ b = c ,两边同时异或 a 得到 b = c ^ a
公式一:derived[i] = original[i] ⊕ original[i + 1]
a[0]
a[1] = b[0] ^ a[0]
a[2] = b[1] ^ a[1] = b[1] ^ b[0] ^ a[0]
a[3] = b[2] ^ b[1] ^ b[0] ^ a[0]
公式二:如果 i = n - 1 ,那么 derived[i] = original[i] ⊕ original[0]
a[3] ^ a[0] = b[3]
两个公式合起来(左边异或左边,右边异或右边)
a[0] = b[2] ^ b[1] ^ b[0] ^ a[0] ^ b[3]
==> 0 = b[3] ^ b[2] ^ b[1] ^ b[0]
判断所有数异或和是否为0
*/
public boolean doesValidArrayExist(int[] derived) {
int i = 0;
for(int d : derived) i ^= d;
return i == 0;
}
}
时间复杂度:O(n)
2684. 矩阵中移动的最大次数(求最大路径长度)
难度中等0收藏分享切换为英文接收动态反馈
给你一个下标从 0 开始、大小为 m x n
的矩阵 grid
,矩阵由若干 正 整数组成。
你可以从矩阵第一列中的 任一 单元格出发,按以下方式遍历 grid
:
- 从单元格
(row, col)
可以移动到(row - 1, col + 1)
、(row, col + 1)
和(row + 1, col + 1)
三个单元格中任一满足值 严格 大于当前单元格的单元格。
返回你在矩阵中能够 移动 的 最大 次数。
示例 1:
输入:grid = [[2,4,3,5],[5,4,9,3],[3,4,2,11],[10,9,13,15]]
输出:3
解释:可以从单元格 (0, 0) 开始并且按下面的路径移动:
- (0, 0) -> (0, 1).
- (0, 1) -> (1, 2).
- (1, 2) -> (2, 3).
可以证明这是能够移动的最大次数。
示例 2:
输入:grid = [[3,2,4],[2,1,9],[1,1,7]]
输出:0
解释:从第一列的任一单元格开始都无法移动。
提示:
m == grid.length
n == grid[i].length
2 <= m, n <= 1000
4 <= m * n <= 105
1 <= grid[i][j] <= 106
方法一:DFS(递归==> 记忆化搜索)
class Solution {
int[][] dirts = {{-1, 1}, {0, 1}, {1, 1}};
int[][] grid;
int m, n;
int[][] cache;
public int maxMoves(int[][] grid) {
// dfs求最大路径长
int ans = 0;
m = grid.length; n = grid[0].length;
cache = new int[m][n];
for(int i = 0; i < m; i++)
Arrays.fill(cache[i], -1);
this.grid = grid;
for(int i = 0; i < m; i++){
ans = Math.max(ans, dfs(i, 0));
}
return ans;
}
public int dfs(int i, int j){
int ans = 0;
if(cache[i][j] >= 0) return cache[i][j];
for(int[] d : dirts){
int x = i + d[0], y = j + d[1];
if(x < 0 || x >= m || y < 0 || y >= n)
continue;
if(grid[x][y] > grid[i][j])
ans = Math.max(ans, dfs(x, y) + 1);
}
return cache[i][j] = ans;
}
}
方法二:记忆化转DP
class Solution {
public int maxMoves(int[][] grid) {
int m = grid.length, n = grid[0].length;
// 定义f[i][j]表示从f[i][j]开始按照要求移动能走的最大次数
int[][] f = new int[m][n];
// 思考清楚递推顺序
// f[i][j] 需要 f[i-1][j+1] f[i][j+1] f[i+1][j+1],因此要先枚举j再枚举i
for(int j = n-2; j >= 0; j--){
for(int i = 0; i < m; i++){
for (int k = Math.max(i - 1, 0); k < Math.min(i + 2, m); k++)
if (grid[k][j + 1] > grid[i][j])
f[i][j] = Math.max(f[i][j], f[k][j + 1] + 1);
}
}
int ans = 0;
for(int i = 0; i < m; i++)
ans = Math.max(ans, f[i][0]);
return ans;
}
}
/**
class Solution:
def maxMoves(self, grid: List[List[int]]) -> int:
m, n = len(grid), len(grid[0])
@cache
def dfs(i: int, j: int) -> int:
if j == n - 1:
return 0
res = 0
for k in i - 1, i, i + 1:
if 0 <= k < m and grid[k][j + 1] > grid[i][j]:
res = max(res, dfs(k, j + 1) + 1)
return res
return max(dfs(i, 0) for i in range(m))
*/
方法三:BFS
class Solution:
def maxMoves(self, grid: List[List[int]]) -> int:
m, n = len(grid), len(grid[0])
q = range(m) # 记录行号
print(q) # range(0, 4)
vis = [-1] * m
# 从左往右遍历,每一轮向右搜索一列
# 只要能搜索到第 k 列,那么答案至少为k,当队列长度=0时,表示走不到第 k 列了,答案为k-1
for j in range(n-1):
tmp = q
q = []
for i in tmp:
for k in i-1, i, i+1:
if 0 <= k < m and vis[k] != j and grid[k][j + 1] > grid[i][j]:
vis[k] = j # 时间戳标记,避免重复创建 vis 数组
q.append(k)
if len(q) == 0:
return j
return n-1
🎉相似题目:网格图 DP
https://leetcode.cn/problems/maximum-number-of-moves-in-a-grid/submissions/
- 62. 不同路径
- 63. 不同路径 II
- 64. 最小路径和
- 120. 三角形最小路径和
- 931. 下降路径最小和
- 2435. 矩阵中和能被 K 整除的路径
2685. 统计完全连通分量的数量
难度中等2
给你一个整数 n
。现有一个包含 n
个顶点的 无向 图,顶点按从 0
到 n - 1
编号。给你一个二维整数数组 edges
其中 edges[i] = [ai, bi]
表示顶点 ai
和 bi
之间存在一条 无向 边。
返回图中 完全连通分量 的数量。
如果在子图中任意两个顶点之间都存在路径,并且子图中没有任何一个顶点与子图外部的顶点共享边,则称其为 连通分量 。
如果连通分量中每对节点之间都存在一条边,则称其为 完全连通分量 。
示例 1:
输入:n = 6, edges = [[0,1],[0,2],[1,2],[3,4]]
输出:3
解释:如上图所示,可以看到此图所有分量都是完全连通分量。
示例 2:
输入:n = 6, edges = [[0,1],[0,2],[1,2],[3,4],[3,5]]
输出:1
解释:包含节点 0、1 和 2 的分量是完全连通分量,因为每对节点之间都存在一条边。
包含节点 3 、4 和 5 的分量不是完全连通分量,因为节点 4 和 5 之间不存在边。
因此,在图中完全连接分量的数量是 1 。
提示:
1 <= n <= 50
0 <= edges.length <= n * (n - 1) / 2
edges[i].length == 2
0 <= ai, bi <= n - 1
ai != bi
- 不存在重复的边
方法一:DFS
class Solution {
List<Integer>[] g;
boolean[] vis, tmp;
public int countCompleteComponents(int n, int[][] edges) {
g = new ArrayList[n];
Arrays.setAll(g, e -> new ArrayList<>());
for(int[] e : edges){
g[e[0]].add(e[1]);
g[e[1]].add(e[0]);
}
int ans = 0;
vis = new boolean[n];
tmp = new boolean[n];
// 遍历每一个连通分量
for(int i = 0; i < n; i++){
if(vis[i]) continue;
vis[i] = true;
// 统计每一个联通分量 点的个数 和 边的个数,边的个数应该为节点数 的 n(n-1) / 2
// 统计连通分类个数
tmp[i] = true;
int num = dfsnum(i);
int edge = dfs(i) / 2;
if(edge == num * (num-1) / 2)
ans += 1;
}
return ans;
}
// dfs求连通块节点个数
public int dfsnum(int x){
int res = 1;
for(int y : g[x]){
if(!tmp[y]){
tmp[y] = true;
res += dfsnum(y);
}
}
return res;
}
// dfs求连通块边的个数(总个数,最后还要 / 2)
public int dfs(int x){
int res = g[x].size();
for(int y : g[x]){
if(!vis[y]){
vis[y] = true;
res += dfs(y);
}
}
return res;
}
}
简洁写法,在一个dfs中求边和求点
class Solution {
List<Integer>[] g;
boolean[] vis;
int v, e;
public int countCompleteComponents(int n, int[][] edges) {
g = new ArrayList[n];
Arrays.setAll(g, e -> new ArrayList<>());
for(int[] e : edges){
g[e[0]].add(e[1]);
g[e[1]].add(e[0]);
}
int ans = 0;
vis = new boolean[n];
for(int i = 0; i < n; i++){
if(!vis[i]){
v = 0;
e = 0;
dfs(i);
if(e == v * (v-1))
ans += 1;
}
}
return ans;
}
public void dfs(int x){
vis[x] = true;
v++;
e += g[x].size();
for(int y : g[x]){
if(!vis[y])
dfs(y);
}
}
}
方法二:并查集
题解:https://leetcode.cn/u/coco-e1/
并查集维护顶点数和边数
class Solution {
int[] parent, cnt, size;
public int countCompleteComponents(int n, int[][] edges) {
parent = new int[n];
cnt = new int[n]; // 边个数
size = new int[n]; // 节点个数
Arrays.fill(size, 1);
for (int i = 0; i < n; i++) parent[i] = i;
for (int[] e : edges) union(e[0], e[1]);
boolean[] vis = new boolean[n];
int ans = 0;
for (int i = 0; i < n; i++) {
int px = find(i);
if (!vis[px]) {
vis[px] = true;
int s = size[px];
if (cnt[px] == s * (s - 1)) ans++;
}
}
return ans;
}
int find(int x) {
if (parent[x] == x) return x;
return parent[x] = find(parent[x]);
}
void union(int x, int y) {
int px = find(x), py = find(y);
if (px != py) {
parent[px] = py;
cnt[py] += cnt[px] + 2;
size[py] += size[px];
} else cnt[px] += 2;
}
}