| |
|
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
| -> 数据结构与算法 -> 算法提升:并查集的十个经典题目 -> 正文阅读 |
|
|
[数据结构与算法]算法提升:并查集的十个经典题目 |
|
目录 最长连续序列题目 给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。 请你设计并实现时间复杂度为?O(n) 的算法解决此问题。 示例 1: 输入:nums = [100,4,200,1,3,2] 输出:4 解释:最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4。 示例 2: 输入:nums = [0,3,7,2,5,8,4,6,0,1] 输出:9 提示: 0 <= nums.length <= 105 -109 <= nums[i] <= 109 解析 并查集绝大多数的题目属于只要你知道有并查集这个东西就非常easy ,这个题之所以放在第一个就是想说并查集的强大。
代码 #include <iostream>
#include <unordered_map>
#include <vector>
using namespace std;
class UnionFind {
public:
unordered_map<int,int> ancestor_; // 用来存储祖先的
unordered_map<int, size_t> size_; // 用来存储每一个集合的大小的
public:
//并查集初始化
UnionFind(const vector<int>& vec) {
for(auto c:vec) {
ancestor_.insert({c,c});
size_.insert({c,1});
}
}
// 查找元素属于哪一个集合
int Find(int c) {
//这里假设传入的字符C都是集合中已经存在的值
int ancestor = -1;
int findc = c;
while(ancestor!= findc) {
ancestor = findc;
findc = ancestor_[ancestor];
}
//这里可以做一个优化,把这个树化扁平,以减少未来的查找时间复杂度
ancestor_[c] = ancestor;
return ancestor;
}
void Union(int u1, int u2) {
// 找到他们的祖先
int anc1 = Find(u1);
int anc2 = Find(u2);
// 如果祖先相同就不做任何事情
if (anc1 == anc2) return ;
// 判断谁的size 大 && 若u1 size == u2 size,将u2挂在u1上
int maxu = size_[anc1] >= size_[anc2] ? anc1:anc2;
int minu = maxu == anc1? anc2:anc1;
ancestor_[minu] = maxu;
}
};
class solution {
public:
int function(vector<int> vec) {
int max = 0;
int maxkey =0;
UnionFind u = UnionFind(vec);
for(auto [key,value]: u.ancestor_) {
if (u.ancestor_.find(key+1)!=u.ancestor_.end()) {
u.Union(key,key+1);
}
}
for(auto [key,value]:u.size_) {
if(max < value) {
max = value;
maxkey = key;
}
}
return max;
}
};
被围绕的区域题目 给你一个 m x n 的矩阵 board ,由若干字符 'X' 和 'O' ,找到所有被 'X' 围绕的区域,并将这些区域里所有的 'O' 用 'X' 填充。 示例 1:
示例 2:
提示:
解析 题意:
分析:
实现:
大流程:
static int index(int i, int j, int w){
//x、y 表示二维坐标值,w 表示矩阵宽度。
return i * w + j;
}
代码 class Solution {
class UnionFind{
private:
std::vector<int> parent_;
std::vector<int> size_;
std::vector<int> help_;
int cnt_;
public:
UnionFind(int N){
cnt_ = N;
parent_.resize(N);
size_.resize(N);
help_.resize(N);
for (int i = 0; i < N; ++i) {
parent_[i] = i;
size_[i] = 1;
}
}
void merge(int i, int j){
int ri = findRoot(i);
int rj = findRoot(j);
if(ri != rj){
if(size_[ri] >= size_[rj]){
parent_[rj] = ri;
size_[ri] += size_[rj];
}else{
parent_[ri] = rj;
size_[rj] += size_[ri];
}
--cnt_;
}
}
bool isConnected(int i, int j){
return findRoot(i) == findRoot(j);
}
private:
int findRoot(int i){
int hi = 0;
while (parent_[i] != i){
help_[hi++] = i;
i = parent_[i];
}
for (hi--; hi >= 0; --hi) {
parent_[help_[hi]] = i;
}
return i;
}
};
private:
static int index(int i, int j, int w){
//x、y 表示二维坐标值,w 表示矩阵宽度。
return i * w + j;
}
public:
void solve(vector<vector<char>>& board) {
if(board.empty()){
return;
}
int m = board.size(); // 矩阵行数
int n = board[0].size(); // 矩阵列数(宽度),即第一行元素数
UnionFind unionFind(m * n + 1); // 初始化并查集,大小为矩阵大小+1, 给 dummy 留?个额外位置
int dummy = m * n; // dummy索引
// 与边角上的`O`相连的 O 与 dummy 连通
std::vector<std::vector<int>> dirs {{1, 0},{-1, 0},{0, 1}, {0, -1}};
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (board[i][j] == 'O') {
// 边界节点是O
if (i == 0 || j == 0 || i == m - 1 || j == n - 1) {
unionFind.merge(index(i, j, n), dummy);
} else {
// 非边界节点是O,那么看它上下左右是否有O
for (auto dir : dirs) {
int x = i + dir[0];
int y = j + dir[1];
if (board[x][y] == 'O') { //如果是,那么将它们打通
unionFind.merge(index(i, j, n), index(x, y, n));
}
}
}
}
}
}
// 所有不和 dummy 连通的 O, 都要被替换
for (int i = 1; i < m - 1; i++){
for (int j = 1; j < n - 1; j++){
if (board[i][j] == 'O'){
if (!unionFind.isConnected(index(i, j, n), dummy)){
board[i][j] = 'X';
}
}
}
}
}
};
岛屿数量题目 给你一个由?'1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。 岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。 此外,你可以假设该网格的四条边均被水包围。 示例 1: 输入:grid = [ ["1","1","1","1","0"], ["1","1","0","1","0"], ["1","1","0","0","0"], ["0","0","0","0","0"] ] 输出:1 示例 2: 输入:grid = [ ["1","1","0","0","0"], ["1","1","0","0","0"], ["0","0","1","0","0"], ["0","0","0","1","1"] ] 输出:3 提示:
解析
代码 class Solution {
class UnionFind{
private:
std::vector<int> parent_;
std::vector<int> size_;
std::vector<int> help_;
int cnt_{};
int wid_;
int index(int i, int j) const{
return i * wid_ + j; //x、y 表示二维坐标值,w 表示矩阵宽度。
}
int findRoot(int i){
int hi = 0;
while (parent_[i] != i){
help_[hi++] = i;
i = parent_[i];
}
for (hi--; hi >= 0; --hi) {
parent_[help_[hi]] = i;
}
return i;
}
public:
explicit UnionFind(vector<vector<char>>& board){
int m = board.size(); // 矩阵行数
int n = board[0].size(); // 矩阵列数(宽度),即第一行元素数
parent_.resize(m * n);
size_.resize(m * n);
help_.resize(m * n);
wid_ = n;
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
if(board[i][j] == '1'){
int idx = index(i, j);
parent_[idx] = idx;
size_[idx] = 1;
++cnt_;
}
}
}
}
void merge(int i1, int j1, int i2, int j2){
int i = index(i1, j1);
int j = index(i2, j2);
int ri = findRoot(i);
int rj = findRoot(j);
if(ri != rj){
if(size_[ri] >= size_[rj]){
parent_[rj] = ri;
size_[ri] += size_[rj];
}else{
parent_[ri] = rj;
size_[rj] += size_[ri];
}
--cnt_;
}
}
int count() const {
return cnt_;
}
bool isConnected(int i, int j){
return findRoot(i) == findRoot(j);
}
};
public:
int numIslands(vector<vector<char>>& board) {
if(board.empty()){
return 0;
}
int m = board.size(); // 矩阵行数
int n = board[0].size(); // 矩阵列数(宽度),即第一行元素数
UnionFind unionFind(board);
// 第一列(除了(0, 0)外)
for (int i = 1; i < m; ++i) {
if(board[i - 1][0] == '1' && board[i][0] == '1'){
unionFind.merge(i - 1, 0, i, 0);
}
}
// 第一行(除了(0, 0)外)
for (int j = 1; j < n; ++j) {
if(board[0][j - 1] == '1' && board[0][j] == '1'){
unionFind.merge(0, j - 1, 0, j);
}
}
for (int i = 1; i < m; ++i) {
for (int j = 1; j < n; ++j) {
if(board[i][j] == '1' ){ // 只看左&上
if(board[i - 1][j] == '1'){
unionFind.merge(i, j, i - 1, j);
}
if(board[i][j - 1] == '1'){
unionFind.merge(i, j, i, j - 1);
}
}
}
}
return unionFind.count();
}
};
岛屿的最大面积题目 给你一个大小为 m x n 的二进制矩阵 grid 。 岛屿?是由一些相邻的?1?(代表土地) 构成的组合,这里的「相邻」要求两个 1 必须在 水平或者竖直的四个方向上 相邻。你可以假设?grid 的四个边缘都被 0(代表水)包围着。 岛屿的面积是岛上值为 1 的单元格的数目。 计算并返回 grid 中最大的岛屿面积。如果没有岛屿,则返回面积为 0 。 示例 1:
输入:grid = [[0,0,1,0,0,0,0,1,0,0,0,0,0],[0,0,0,0,0,0,0,1,1,1,0,0,0],[0,1,1,0,1,0,0,0,0,0,0,0,0],[0,1,0,0,1,1,0,0,1,0,1,0,0],[0,1,0,0,1,1,0,0,1,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,1,0,0],[0,0,0,0,0,0,0,1,1,1,0,0,0],[0,0,0,0,0,0,0,1,1,0,0,0,0]] 输出:6 解释:答案不应该是 11 ,因为岛屿只能包含水平或垂直这四个方向上的 1 。 示例 2: 输入:grid = [[0,0,0,0,0,0,0,0]] 输出:0 提示: m == grid.length n == grid[i].length 1 <= m, n <= 50 grid[i][j] 为 0 或 1 解析 岛屿问题升级版本,一个非常典型的并查集题目,用并查集把相邻点一合并,然后提取最大size 的集合,输出size。 当然也可以使用图的遍历算法做。 代码 class Solution {
private:
vector<vector<pair<int,int>>>ancestor_;
vector<vector<int>> size_;
public:
bool isZeroAll (vector<vector<int>>& grid) {
for(auto line :grid) {
for(int i : line) {
if (i!=0) return false;
}
}
return true;
}
int maxAreaOfIsland(vector<vector<int>>& grid) {
if(isZeroAll(grid)) return 0;
init(grid);
for(int i = 0 ; i<grid.size();i++) {
for(int j= 0;j<grid[0].size();j++) {
if(grid[i][j] == 1 ) {
if(i+1 < grid.size() && grid[i+1][j] == 1) merge({i,j},{i+1,j});
if(j+1 < grid[0].size() && grid[i][j+1] == 1) merge({i,j},{i,j+1});
}
}
}
int max =0;
for(auto line : size_) {
for(int size : line) {
max = max>=size? max:size;
}
}
return max;
}
void init(vector<vector<int>>& grid) {
ancestor_.resize(grid.size());
size_.resize(grid.size());
for(int i = 0; i < grid.size();i++ ) {
ancestor_[i].resize(grid[0].size());
size_[i].resize(grid[0].size());
for(int j = 0;j<grid[0].size();j++) {
ancestor_[i][j] = {i,j};
size_[i][j] = 1;
}
}
}
pair<int,int> findAncestor(pair<int,int> point) {
pair<int,int> p = point;
while(1) {
pair<int,int> anc = ancestor_[p.first][p.second];
if(anc.first == p.first && anc.second == p.second) {
return p;
}
p = anc;
}
}
void merge(pair<int,int> p1, pair<int,int> p2) {
pair<int,int> a1 = findAncestor(p1);
pair<int,int> a2 = findAncestor(p2);
if(a1 == a2) return;
pair<int,int> amax = size_[a1.first][a1.second] > size_[a2.first][a2.second]?ancestor_[a1.first][a1.second] :ancestor_[a2.first][a2.second] ;
pair<int,int> amin = amax == ancestor_[a1.first][a1.second]?ancestor_[a2.first][a2.second]:ancestor_[a1.first][a1.second];
size_[amax.first][amax.second] += size_[amin.first][amin.second];
// size_[amin.first][amin.second] = 0;
ancestor_[amin.first][amin.second] = {amax.first,amax.second};
}
};
朋友圈问题题目 班上有 N 名学生。其中有些人是朋友,有些则不是。他们的友谊具有是传递性。如果已知 A 是 B 的朋友,B 是 C 的朋友,那么我们可以认为 A 也是 C 的朋友。所谓的朋友圈,是指所有朋友的集合。 给定一个 N * N 的矩阵 M,表示班级中学生之间的朋友关系。如果M[i][j] = 1,表示已知第 i 个和 j 个学生互为朋友关系,否则为不知道。你必须输出所有学生中的已知的朋友圈总数。 示例 1:
[[1,1,0], [1,1,0], [0,0,1]]
第2个学生自己在一个朋友圈。所以返回 2 。 示例 2:
[[1,1,0], [1,1,1], [0,1,1]]
提示:
解析 题意:给定一个二维数组,它一定是一个正方形
分析:
流程:
代码 class Solution {
class UnionFind{
private:
std::vector<int> parent_; // parent[i] = k : i的父亲是k
std::vector<int> size_; // size[i] = k : 如果i是代表节点,size[i]才有意义( i所在的集合大小是多少),否则无意义
std::vector<int> help_; // 辅助结构
int cnt_; //一共有多少个集合
public:
explicit UnionFind(int n){
cnt_ = n;
parent_.resize(n);
size_.resize(n);
help_.resize(n);
for (int i = 0; i < n; ++i) {
parent_[i] = i;
size_[i] = 1;
}
}
// 返回i的代表节点
// 这个过程要做路径压缩
int findRoot(int i){
int hi = 0;
while (i != parent_[i]){
help_[hi++] = parent_[i];
i = parent_[i];
}
for (hi--; hi >= 0; --hi) {
parent_[help_[hi]] = i;
}
return i;
}
void merge(int i, int j){
int f1 = findRoot(i);
int f2 = findRoot(j);
if(f1 != f2){
if(size_[f1] >= size_[f2]){
parent_[f2] = f1;
size_[f1] = size_[f1] + size_[f2];
}else{
parent_[f1] = f2;
size_[f2] = size_[f2] + size_[f1];
}
--cnt_;
}
}
int counts() const{
return cnt_;
}
};
public:
int findCircleNum(vector<vector<int>>& isConnected) {
int N = isConnected.size();
UnionFind unionFind(N); //先初始化集合: {0} {1} {2} {N-1}
// 遍历这个二维数组的右上角
for (int i = 0; i < N; ++i) {
for (int j = i + 1; j < N; ++j) {
if(isConnected[i][j] == 1){ // i和j相互认识就合并
unionFind.merge(i, j);
}
}
}
// 有多少个集合就表示有多少个朋友圈
return unionFind.counts();
}
};
除法求值(hard)题目 给你一个变量对数组 equations 和一个实数值数组 values 作为已知条件,其中 equations[i] = [Ai, Bi] 和 values[i] 共同表示等式 Ai / Bi = values[i] 。每个 Ai 或 Bi 是一个表示单个变量的字符串。 另有一些以数组 queries 表示的问题,其中 queries[j] = [Cj, Dj] 表示第 j 个问题,请你根据已知条件找出 Cj / Dj = ? 的结果作为答案。 返回 所有问题的答案 。如果存在某个无法确定的答案,则用 -1.0 替代这个答案。如果问题中出现了给定的已知条件中没有出现的字符串,也需要用 -1.0 替代这个答案。 注意:输入总是有效的。你可以假设除法运算中不会出现除数为 0 的情况,且不存在任何矛盾的结果。 示例 1: 输入:equations = [["a","b"],["b","c"]], values = [2.0,3.0], queries = [["a","c"],["b","a"],["a","e"],["a","a"],["x","x"]] 输出:[6.00000,0.50000,-1.00000,1.00000,-1.00000] 解释: 条件:a / b = 2.0, b / c = 3.0 问题:a / c = ?, b / a = ?, a / e = ?, a / a = ?, x / x = ? 结果:[6.0, 0.5, -1.0, 1.0, -1.0 ] 示例 2: 输入:equations = [["a","b"],["b","c"],["bc","cd"]], values = [1.5,2.5,5.0], queries = [["a","c"],["c","b"],["bc","cd"],["cd","bc"]] 输出:[3.75000,0.40000,5.00000,0.20000] 示例 3: 输入:equations = [["a","b"]], values = [0.5], queries = [["a","b"],["b","a"],["a","c"],["x","y"]] 输出:[0.50000,2.00000,-1.00000,-1.00000] 提示: 1 <= equations.length <= 20 equations[i].length == 2 1 <= Ai.length, Bi.length <= 5 values.length == equations.length 0.0 < values[i] <= 20.0 1 <= queries.length <= 20 queries[i].length == 2 1 <= Cj.length, Dj.length <= 5 Ai, Bi, Cj, Dj 由小写英文字母与数字组成 解析 本题用图来实现算mid级别的难度,但是如果用并查集就是hard级别。 这个题目看到了二维数组:equations、queries,一个代表给出的条件,一个代表要求的内容,大概的思路就是从equations 中的除法关系推出queries的关系,这里有一个很重要的信息,就是“除法”,加减乘除都是属于四则运算,只要是单个符号出现,或者加减一起出现或者乘除一起出现都是满足结合率和分配律的,换句话说就是具备传染性。 二维数组,元素之间的关系具备传染性,且可以得到两个元素之间的关系(不是权),典型的图论解决的范畴,所以在这个题目中,可以直接使用图的搜索算法做:
PS: 1. 可以在构建图的时候搞一个hash,在queries 的时候先看数在不在。
解决之后开始想优化方法: 求最短路径:从有向图找路径这个题目来说,首先想到的就是上面做的BFS、DFS这样的搜索算法,然后稍微高级一点就是找找最短路径,这里是不带权的最短路径,所以一些经典的算法基本上都可以使用,比如prim、狄杰斯特拉、弗洛伊德之类的; 但是不管再怎么求最短路径其实都是有时间复杂度都是比较高的,所以可以考虑用空间去换时间,这里可以使用到带权的并查集。 带权并查集的使用思路很简单: 比如 x/y = A,x的节点作为头和y的节点合并 权值设置为A; 之后 z/y = B,那么z节点、y节点分别去现有集合find,发现y在之前的集合中已经存在,然后这一对变量就直接加入,并换算得到x/z = A/B,然后x的祖先就是x,y、z 的祖先均为x。
如果 之后来的是m/n = C,m与n的祖先都是自己,与之前的集合没有公共祖先,就自己形成一个集合 n的祖先为m; 然后来了 m/x = D, m的祖先是自己,是一个集合,x的祖先也是自己,也是一个集合,就将两个集合合并; 最后讲形成多个集合,每个集合都有一个公共祖先。 queries 中取出数后,就可以直接看两个数是不是同一个祖先,如果是的话,就可以直接计算拿出结果,如果不是一个公共祖先就返回 -1. 这样的话查找的时间复杂度会非常低,变成常数时间O(1) 代码 class Solution {
public:
int findf(vector<int>& f, vector<double>& w, int x) {
if (f[x] != x) {
int father = findf(f, w, f[x]);
w[x] = w[x] * w[f[x]];
f[x] = father;
}
return f[x];
}
void merge(vector<int>& f, vector<double>& w, int x, int y, double val) {
int fx = findf(f, w, x);
int fy = findf(f, w, y);
f[fx] = fy;
w[fx] = val * w[y] / w[x];
}
vector<double> calcEquation(vector<vector<string>>& equations, vector<double>& values, vector<vector<string>>& queries) {
int nvars = 0;
unordered_map<string, int> variables;
int n = equations.size();
for (int i = 0; i < n; i++) {
if (variables.find(equations[i][0]) == variables.end()) {
variables[equations[i][0]] = nvars++;
}
if (variables.find(equations[i][1]) == variables.end()) {
variables[equations[i][1]] = nvars++;
}
}
vector<int> f(nvars);
vector<double> w(nvars, 1.0);
for (int i = 0; i < nvars; i++) {
f[i] = i;
}
for (int i = 0; i < n; i++) {
int va = variables[equations[i][0]], vb = variables[equations[i][1]];
merge(f, w, va, vb, values[i]);
}
vector<double> ret;
for (const auto& q: queries) {
double result = -1.0;
if (variables.find(q[0]) != variables.end() && variables.find(q[1]) != variables.end()) {
int ia = variables[q[0]], ib = variables[q[1]];
int fa = findf(f, w, ia), fb = findf(f, w, ib);
if (fa == fb) {
result = w[ia] / w[ib];
}
}
ret.push_back(result);
}
return ret;
}
};
情侣牵手(hard)题目 n 对情侣坐在连续排列的 2n?个座位上,想要牵到对方的手。 人和座位由一个整数数组 row 表示,其中 row[i] 是坐在第 i 个座位上的人的 ID。情侣们按顺序编号,第一对是?(0, 1),第二对是?(2, 3),以此类推,最后一对是?(2n-2, 2n-1)。 返回 最少交换座位的次数,以便每对情侣可以并肩坐在一起。 每次交换可选择任意两人,让他们站起来交换座位。 示例 1: 输入: row = [0,2,1,3] 输出: 1 解释: 只需要交换row[1]和row[2]的位置即可。 示例 2: 输入: row = [3,2,0,1] 输出: 0 解释: 无需交换座位,所有的情侣都已经可以手牵手了。 提示: 2n == row.length 2 <= n <= 30 n?是偶数 0 <= row[i] < 2n row?中所有元素均无重复 解析 这个题目作为hard 有点名不副实,只需要搞明白一个点就很清晰了。 那就是 (2i,2i+1)构成一对情侣,所以可以给情侣对编号,比如N这个value就属于N/2 这个情侣对 步骤:(假设就是数组下标% 2 等于0的为女朋友、为1的是男朋友)
代码 class Solution {
private:
vector<int>ancestor_;
vector<int>size_;
public:
int minSwapsCouples(vector<int>& row) {
init(row);
for(int i =0;i<row.size();i+=2) {
merge(row[i]/2,row[i+1]/2);
}
int count = 0;
for(int s :size_) {
count+=(s-1);
}
return count;
}
void init(vector<int>& row) {
ancestor_.resize(row.size()/2);
size_.resize(row.size()/2);
for(int i =0;i<row.size()/2;i++) {
ancestor_[i] = i;
size_[i] =1;
}
}
int findAncestor(int point) {
int p = point;
while(1) {
int anc = ancestor_[p];
if(anc == p) {
return p;
}
p = anc;
}
}
void merge(int p1, int p2) {
int a1 = findAncestor(p1);
int a2 = findAncestor(p2);
if(a1 ==a2) return;
int amax = size_[a1] > size_[a2]?ancestor_[a1] :ancestor_[a2] ;
int amin = amax == ancestor_[a1]?ancestor_[a2]:ancestor_[a1];
size_[amax] += size_[amin];
size_[amin] = 1;
ancestor_[amin] =amax;
}
};
打砖块(hard)题目 有一个 m x n 的二元网格?grid?,其中 1 表示砖块,0 表示空白。砖块 稳定(不会掉落)的前提是:
给你一个数组 hits ,这是需要依次消除砖块的位置。每当消除?hits[i] = (rowi, coli) 位置上的砖块时,对应位置的砖块(若存在)会消失,然后其他的砖块可能因为这一消除操作而 掉落 。一旦砖块掉落,它会 立即 从网格?grid?中消失(即,它不会落在其他稳定的砖块上)。 返回一个数组 result ,其中 result[i] 表示第 i 次消除操作对应掉落的砖块数目。 注意,消除可能指向是没有砖块的空白位置,如果发生这种情况,则没有砖块掉落。 示例 1:
[[1,0,0,0], [1,1,1,0]] 消除 (1,0) 处加粗的砖块,得到网格: [[1,0,0,0] [0,1,1,0]] 两个加粗的砖不再稳定,因为它们不再与顶部相连,也不再与另一个稳定的砖相邻,因此它们将掉落。得到网格: [[1,0,0,0], [0,0,0,0]] 因此,结果为 [2] 。 示例 2:
[[1,0,0,0], [1,1,0,0]] 消除 (1,1) 处加粗的砖块,得到网格: [[1,0,0,0], [1,0,0,0]] 剩下的砖都很稳定,所以不会掉落。网格保持不变: [[1,0,0,0], [1,0,0,0]] 接下来消除 (1,0) 处加粗的砖块,得到网格: [[1,0,0,0], [0,0,0,0]] 剩下的砖块仍然是稳定的,所以不会有砖块掉落。 因此,结果为 [0,0] 。 解析 分析: (1)什么样子的【砖块】不会掉落?
(2)每一枪(hits[i])会带来什么后果?
(3)打碎和掉落分别是什么意思?
(4)如何判断砖块是否会掉落呢?
(5)上面的思路有什么问题?
(6)怎么办?
思路:这道题我们可以用接在天花板上的砖的数量变化为收集答案 看个例子: (1)先预处理grid
(2)把剩下的砖块建立并查集
(3)逆序遍历hits,补上砖块,计算出会增多少个砖块粘到房顶
(3)很明显,hit的位置导致砖头掉落要分情况讨论
代码 class Solution {
class UnionFind {
int N;
int M_;
int cellingAll_; // 有多少块砖,连到了天花板上
vector<vector<int>> grid_; // 原始矩阵,因为炮弹的影响,1 -> 2
std::vector<bool> cellingSet_;// cellingSet_[i] = true; i 是头节点,所在的集合是天花板集合
std::vector<int> parent_;
std::vector<int> size_;
std::vector<int> help_;
void initSpace(vector<vector<int>> &matrix) {
grid_ = matrix;
N = grid_.size(), M_ = grid_[0].size();
int all = N * M_;
cellingAll_ = 0;
cellingSet_.resize(all);
parent_.resize(all);
size_.resize(all);
help_.resize(all);
for (int row = 0; row < N; ++row) {
for (int col = 0; col < M_; ++col) {
if (grid_[row][col] == 1) {
int index = row * M_ + col;
parent_[index] = index;
size_[index] = 1;
if (row == 0) {
cellingSet_[index] = true;
cellingAll_++;
}
}
}
}
}
void initConnect(){
for (int row = 0; row < N; ++row) {
for (int col = 0; col < M_; ++col) {
merge(row, col, row - 1, col);
merge(row, col, row + 1, col);
merge(row, col, row, col - 1);
merge(row, col, row, col + 1);
}
}
}
bool valid(int row, int col) {
return row >= 0 && row < N && col >= 0 && col < M_ && grid_[row][col] == 1;
}
void merge(int r1, int c1, int r2, int c2) {
if(valid(r1, c1) && valid(r2, c2)){
int father1 = find(r1, c1);
int father2 = find(r2, c2);
if(father1 != father2){
int size1 = size_[father1];
int size2 = size_[father2];
bool status1 = cellingSet_[father1];
bool status2 = cellingSet_[father2];
if (size1 <= size2) {
parent_[father1] = father2;
size_[father2] = size1 + size2;
if (status1 ^ status2) {
cellingSet_[father2] = true;
cellingAll_ += status1 ? size2 : size1;
}
} else {
parent_[father2] = father1;
size_[father1] = size1 + size2;
if (status1 ^ status2) {
cellingSet_[father1] = true;
cellingAll_ += status1 ? size2 : size1;
}
}
}
}
}
int find(int row, int col){
int stackSize = 0;
int idx = row * M_ + col;
while (idx != parent_[idx]){
help_[stackSize++] = idx;
idx = parent_[idx];
}
while (stackSize != 0){
parent_[help_[--stackSize]] = idx;
}
return idx;
}
public:
UnionFind(vector<vector<int>> &matrix) {
initSpace(matrix);
initConnect();
}
int cellingNum() {
return cellingAll_;
}
int finger(int row, int col){
// 填上grid
grid_[row][col] = 1;
// 对当前行对应的那个集合操作
int cur = row * M_ + col; // 这个位置对应的集合
if (row == 0) { // 如果当前是天花板上的砖块
cellingSet_[cur] = true;
cellingAll_++;
}
parent_[cur] = cur;
size_[cur] = 1;
int pre = cellingAll_;
merge(row, col, row - 1, col);
merge(row, col, row + 1, col);
merge(row, col, row, col - 1);
merge(row, col, row, col + 1);
int now = cellingAll_;
if (row == 0) {
return now - pre; //pre之前已经加过1了
} else {
return now == pre ? 0 : now - pre - 1;
}
}
};
public:
vector<int> hitBricks(vector<vector<int>>& grid, vector<vector<int>>& hits) {
// 预处理,正序敲掉砖块
for (int i = 0; i < hits.size(); ++i) {
if (grid[hits[i][0]][hits[i][1]] == 1) {
grid[hits[i][0]][hits[i][1]] = 2;
}
}
// 逆序补回砖块
UnionFind unionFind (grid);
std::vector<int> ans(hits.size());
for (int i = hits.size() - 1; i >= 0; --i) {
if (grid[hits[i][0]][hits[i][1]] == 2) { // 只有当前位置有砖块时才去合并
ans[i] = unionFind.finger(hits[i][0], hits[i][1]);
}
}
return ans;
}
};
最大人工岛(hard)题目 给你一个大小为 n x n 二进制矩阵 grid 。最多 只能将一格?0 变成?1 。 返回执行此操作后,grid 中最大的岛屿面积是多少? 岛屿 由一组上、下、左、右四个方向相连的?1 形成。 示例 1: 输入: grid = [[1, 0], [0, 1]] 输出: 3 解释: 将一格0变成1,最终连通两个小岛得到面积为 3 的岛屿。 示例 2: 输入: grid = [[1, 1], [1, 0]] 输出: 4 解释: 将一格0变成1,岛屿的面积扩大为 4。 示例 3: 输入: grid = [[1, 1], [1, 1]] 输出: 4 解释: 没有0可以让我们变成1,面积依然为 4。 提示: n == grid.length n == grid[i].length 1 <= n <= 500 grid[i][j] 为 0 或 1 解析 首先看到二维数组就可以直接想到图算法,图算法中最常见的算法就是BFS、DFS,本题也确实可以使用图的遍历算法做: 使用遍历图中为0的节点,假设反转为1然后利用BFS做面积计算,直到找完全部的值,然后求最大值作为面积。 然后就是优化,每一个节点都去做图的遍历找面积,有很多浪费的步骤:
上述的重点就是第一个,重复遍历岛的面积,所以需要一个数据结构把之前的岛的面积存下来,那么就可以顺理成章的使用并查集做这个工作了; 步骤:
代码 class Solution {
public:
const static inline vector<int> dirs = {-1, 0, 1, 0, -1};
int largestIsland(vector<vector<int>>& grid) {
int n = grid.size();
vector<int> p(n * n);
vector<int> size(n * n, 1);
iota(p.begin(), p.end(), 0);
function<int(int)> find;
find = [&](int x) {
if (p[x] != x) {
p[x] = find(p[x]);
}
return p[x];
};
int ans = 1;
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
if (grid[i][j]) {
for (int k = 0; k < 4; ++k) {
int x = i + dirs[k], y = j + dirs[k + 1];
if (x >= 0 && x < n && y >= 0 && y < n && grid[x][y]) {
int pa = find(x * n + y), pb = find(i * n + j);
if (pa == pb) continue;
p[pa] = pb;
size[pb] += size[pa];
ans = max(ans,size[pb]);
}
}
}
}
}
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
if (!grid[i][j]) {
int t = 1;
unordered_set<int> vis;
for (int k = 0; k < 4; ++k) {
int x = i + dirs[k], y = j + dirs[k + 1];
if (x >= 0 && x < n && y >= 0 && y < n && grid[x][y]) {
int root = find(x * n + y);
if (!vis.count(root)) {
vis.insert(root);
t += size[root];
}
}
}
ans = max(ans, t);
}
}
}
return ans;
}
};
相似字符串组(hard)题目 如果交换字符串?X 中的两个不同位置的字母,使得它和字符串?Y 相等,那么称 X 和 Y 两个字符串相似。如果这两个字符串本身是相等的,那它们也是相似的。 例如,"tars" 和 "rats" 是相似的 (交换 0 与 2 的位置);?"rats" 和 "arts" 也是相似的,但是 "star" 不与 "tars","rats",或 "arts" 相似。 总之,它们通过相似性形成了两个关联组:{"tars", "rats", "arts"} 和 {"star"}。注意,"tars" 和 "arts" 是在同一组中,即使它们并不相似。形式上,对每个组而言,要确定一个单词在组中,只需要这个词和该组中至少一个单词相似。 给你一个字符串列表 strs。列表中的每个字符串都是 strs 中其它所有字符串的一个字母异位词。请问 strs 中有多少个相似字符串组? ?示例 1:
示例 2:
提示:
解析 题目大意:给出一个字符串数组,要求找出这个数组中,“不相似”的字符串有多少种? “相似字符串”的定义:
那么,什么叫做“相似字符串组呢”?即相似字符串组中的每个字符串都有另外至少一个字符串和它相似。比如对于 {“tars”, “rats”, “arts”} 这个相似字符串组而言,相似关系是 “tars” <=> “rats” <=> “arts” 。 可以看到“相似字符串组之间的关系有传递性,对于这种群组分类问题,是并查集的经典应用场合:
怎么判断字符串A和字符串B是否相似呢?只要按位置对比字符,若不相等则 diff 自增1,若 diff 大于2了直接返回 false,因为只有 diff 正好等于2或者0的时候才相似。 这个题目作为hard 有点名不副实,理解清楚题意即可 PS: 这个题目有一个点比较关键,就是会存在重复的字符串,如果没有重复的字符串,就可以不用并查集,直接做一个compare函数做相似判断,然后O(N^2),做一次一一对比,求相似对就可以了; 但是因为有相似的数组,所以会出现重复记录的情况,因此需要利用并查集提前知道是否存在已经被记录的相同字符串。 代码 class Solution {
class UnionFind{
private:
std::vector<int> parent_; // parent[i] = k : i的父亲是k
std::vector<int> size_; // size[i] = k : 如果i是代表节点,size[i]才有意义( i所在的集合大小是多少),否则无意义
std::vector<int> help_; // 辅助结构
int cnt_; //一共有多少个集合
int findRoot(int i){
int hi = 0;
while (i != parent_[i]){
help_[hi++] = parent_[i];
i = parent_[i];
}
for (hi--; hi >= 0; --hi) {
parent_[help_[hi]] = i;
}
return i;
}
public:
explicit UnionFind(int n){
cnt_ = n;
parent_.resize(n);
size_.resize(n);
help_.resize(n);
for (int i = 0; i < n; ++i) {
parent_[i] = i;
size_[i] = 1;
}
}
void merge(int i, int j){
int f1 = findRoot(i);
int f2 = findRoot(j);
if(f1 != f2){
if(size_[f1] >= size_[f2]){
parent_[f2] = f1;
size_[f1] = size_[f1] + size_[f2];
}else{
parent_[f1] = f2;
size_[f2] = size_[f2] + size_[f1];
}
--cnt_;
}
}
int counts() const{
return cnt_;
}
bool isConnected(int i, int j){
return findRoot(i) == findRoot(j);
}
};
public:
int numSimilarGroups(vector<string>& strs) {
int N = strs.size();
UnionFind unionFind(N);
for (int i = 0; i < N; ++i) {
for (int j = 0; j < N; ++j) {
// 已经连接了,不用判断相似性
if(unionFind.isConnected(i, j)){
continue;
}
if(isSimilar(strs[i], strs[j])){
unionFind.merge(i, j);
}
}
}
return unionFind.counts();
}
private:
bool isSimilar(const string& A,const string& B){
int diff = 0;
for(int i = 0; i < A.size(); i++){
if(A[i] == B[i]){
continue;
}
diff++;
}
return diff <= 2;
}
};
|
|
|
|
|
| 上一篇文章 下一篇文章 查看所有文章 |
|
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
| 360图书馆 购物 三丰科技 阅读网 日历 万年历 2025年10日历 | -2025/10/29 5:08:07- |
|
| 网站联系: qq:121756557 email:121756557@qq.com IT数码 |