2021.01.25-由斜杠划分区域959
题目描述:
在由 1 x 1 方格组成的 N x N 网格 grid 中,每个 1 x 1 方块由 /、\ 或空格构成。这些字符会将方块划分为一些共边的区域。返回区域的数目。
题目解析:
连通性问题可使用BFS DFS 并查集,因为此题需要的是数目,并非连通的路径,所以推荐使用并查集
难点在于将1 x 1的方格根据grid分割为nn份的小方格,一个小方格由四个三角形构成,求区域即求4 * n *n个区域中连通的个数,一个三角形为一个节点,可分别从[向左,向上]、[向左,向下]、[向右,向上]和[向右、向下]进行分析,选择[向右,向下]遍历所有的节点,判断连通并计算连通图的个数
时间复杂度:O(N^2 log N) 双循环遍历+find父节点,空间复杂度O(N^2)
public class RegionsBySlashes {
public int regionsBySlashes(String[] grid) {
int length = grid.length;
// 定义小三角形的个数
int size = 4 * length * length;
// 构造并查集
UnionFind unionFind = new UnionFind(size);
// i是行j是列,向右扩展j,向下扩展i
for (int i = 0; i < length; i++) {
// 二维转一维
char[] row = grid[i].toCharArray();
for (int j = 0; j < length; j++) {
// 一个单元格的编号
int index = 4 * (i * length + j);
char c = row[j];
// 单元格内合并
if (c == '/') {
// 合并0 3;1 2
unionFind.union(index, index + 3);
unionFind.union(index + 1, index + 2);
} else if (c == '\\') {
// 合并0 1;2 3
unionFind.union(index, index + 1);
unionFind.union(index + 2, index + 3);
} else {
// 空格,合并0 1 2 3
unionFind.union(index, index + 1);
unionFind.union(index + 1, index + 2);
unionFind.union(index + 2, index + 3);
}
// 单元格间合并
// 向右合并,当前单元格的1和右边单元格的3
if (j + 1 < length) {
unionFind.union(index + 1, 4 * (i * length + j + 1) + 3);
}
// 向下合并,当前单元格的2和下边单元格的0
if (i + 1 < length) {
unionFind.union(index + 2, 4 * ((i + 1) * length + j));
}
}
}
return unionFind.count;
}
public class UnionFind {
// 记录每个节点的父节点
int[] parent;
// 记录独立节点的个数
int count;
/**
* 初始化定义并查集的节点,其中每个节点的父节点初始值为下标值,即节点本身
* @param n
*/
public UnionFind(int n) {
parent = new int[n];
for (int i = 0; i < n; i++) {
parent[i] = i;
}
// 初始定义所有的节点都是独立的
count = n;
}
/**
* 查找x的根节点;如果节点本身为根节点,直接返回x即可
* @param x
* @return
*/
private int findRoot(int x) {
int xRoot = x;
while (xRoot != parent[xRoot]) {
xRoot = parent[xRoot];
}
return xRoot;
}
/**
* 合并两个节点,先找到x和y的根节点,如果相同则说明本身就是一个集合中的,不做任何处理;
* 否则说明是独立的两个集合,做并集后count--
* @param x
* @param y
*/
private void union(int x, int y) {
int xRoot = findRoot(x);
int yRoot = findRoot(y);
// 发现不连通的节点,现在需要构造,则独立节点数--
if (xRoot != yRoot) {
parent[xRoot] = yRoot;
count--;
}
}
}
public static void main(String[] args) {
RegionsBySlashes regionsBySlashes = new RegionsBySlashes();
String[] grid = {" /","/ "};
System.out.println(regionsBySlashes.regionsBySlashes(grid));
}
}
2021.01.26-等价多米诺骨牌对的数量1128
题目描述:
给你一个由一些多米诺骨牌组成的列表 dominoes。
如果其中某一张多米诺骨牌可以通过旋转 0 度或 180 度得到另一张多米诺骨牌,我们就认为这两张牌是等价的。
形式上,dominoes[i] = [a, b] 和 dominoes[j] = [c, d] 等价的前提是 a=c 且 b=d,或是 a=d 且 b=c。
在 0 <= i < j < dominoes.length 的前提下,找出满足 dominoes[i] 和 dominoes[j] 等价的骨牌对 (i, j) 的数量。
题目解析:根据题目描述,是一个n*2的二维数组,现在需要以行遍历数组,判断是否有元素相同的行存在(元素相同指的是对于[a,b],要么有[a,b],要么有[b,a]
这里需要注意,对于一个行,和它等价的行之间也是相互等价的,例如a与b等价,a与c等价,那么b与c也等价,所以计算出a的等价数目,总等价数+=a的等价数目n(n+1)/2
一开始想的是用集合,因为没注意到只有两列,想着使用数组转集合,以及集合间reverse的方法。后续发现如果有多列的话,转集合的Lists接口不支持(需要导入外来包),需要手动遍历一行将元素加入集合中,操作麻烦;另外没有集合判等的方法,只有一个a.containsAll(b)方法,检查a是否是b的父集合
public class NumEquivDominoPairs {
/**
* 将二维数组转为一维的List集合,遍历一维数组,进行集合间的比较,
* 判断集合是否相同/与其reverse结果是否相同,如果相同count++。遍历过的集合visited=true
* 并且对于每一个的count,最后等价数量为count(count-1)/2,因为a与b等价,a与c等价,那么b和c也等价
*
* 不要忘记只有两列dominoes[i] = [a,b];
* @param dominoes
* @return
*/
public int numEquivDominoPairs(int[][] dominoes) {
// 根据题设条件二维数组长度最小为1,所以不判断dominoes.length是否为0
if (dominoes == null || dominoes[0].length == 0) {
return 0;
}
int length = dominoes.length;
// 将二维数组转为一个一维集合数组
List<Integer>[] rowList = new List[dominoes.length];
// 初始化集合
for (int i = 0; i < length; i++) {
rowList[i] = new ArrayList<>();
}
for (int i = 0; i < length; i++) {
rowList[i].add(dominoes[i][0]);
rowList[i].add(dominoes[i][1]);
}
boolean[] visited = new boolean[length];
for (int i = 0; i < length; i++) {
visited[i] = false;
}
int result = 0;
for (int i = 0; i < length; i++) {
// 对于一个比较目标的等价个数
int count = 0;
if (!visited[i]) {
visited[i] = true;
for (int j = i + 1; j < length; j++) {
// containsAll方法判断两个集合是否元素相同(顺序无所谓)a.containsAll(b)判断a中是否包含b的全部元素(a为b的父集合)
if (!visited[j] && rowList[i].containsAll(rowList[j]) && rowList[j].containsAll(rowList[i])){
count++;
visited[j] = true;
}
}
count = count * (count + 1) / 2;
}
result += count;
}
return result;
}
public static void main(String[] args) {
NumEquivDominoPairs numEquivDominoPairs = new NumEquivDominoPairs();
int[][] dominoes = {{1,2},{2,1},{1,1},{1,2},{2,2}};
System.out.println(numEquivDominoPairs.numEquivDominoPairs(dominoes));
}
}
还有一个方法,利用HashSet元素唯一的性质,将每一行元素加入到hashSet中,set类型是int[],当往集合中添加元素的时候会判断是否相等,如果相等则遇到等价行(需要重写set的hash和equals方法,因为反转也是相等的),则count++,否则直接加入。因为每一行可能对应多个对等行,所以使用Map集合类型,key为set存储一行数据,value为count存储对等个数,初始值为1
时间复杂度:O(N)O(N),其中 NN 是输入数组的长度;空间复杂度:O(A)O(A),这里 AA 是哈希表中键的总数
/**
* 使用HashMap
* @param dominoes
* @return
*/
public int numEquivDominoPairs(int[][] dominoes) {
// 创建HashMap
Map<Pair, Integer> dominoMap = new HashMap<>();
// 遍历二维数组,将元素加入Map集合,二维转成一维遍历,使用foreach更为合适(不用确定每个行的具体位置)
for (int[] domino : dominoes) {
// 定义Pair
Pair pair = new Pair(domino[0], domino[1]);
// 将pair加入Map集合中
// Map集合的getOrDefault(key,defaultValue)方法,如果key存在则返回get(key),否则返回defaultValue
// 如果key存在返回value+1并覆盖到pair的value中;如果不存在加入集合,value=1
// set集合相同元素不能插入,add返回false;Map集合相同key可以插入,覆盖掉原有key-value
dominoMap.put(pair,dominoMap.getOrDefault(pair,0) + 1);
}
// 定义结果对等数量
int result = 0;
// 遍历Map集合的value值,叠加计算总的对等数量
for (int count : dominoMap.values()) {
result += count * (count - 1) / 2;
}
return result;
}
/**
* 定义hashSet集合
*/
public class Pair {
// 第一个元素
int one;
// 第二个元素
int two;
public Pair(int one, int two) {
this.one = one;
this.two = two;
}
// 判断是否相等,是先求hash然后求是否equals
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Pair pair = (Pair) o;
// 重新写相等的条件
return (one == pair.one &&
two == pair.two) || (one == pair.two && two == pair.one);
}
@Override
public int hashCode() {
// 重新构造hash方法(两位数,让相同的数映射到同一位置)
return one > two ? one * 10 + two : two * 10 + one;
}
}
参考:https://leetcode-cn.com/problems/number-of-equivalent-domino-pairs/solution/deng-jie-duo-mi-nuo-gu-pai-dui-de-shu-li-08z8/
第三种方法,因为是两位数,例如12和21,可以同一转为12,然后以行遍历。计算num=12,并且将其放置到数组count[num]中,这样每遇到一个相同的num都会count++,每次result+=count(1个对等,result=0+1;2个对象,result=0+1+2;三个对等,result=0+1+2+3.对于下一个行对等,又从0开始叠加到result)
时间复杂度:O(N);空间复杂度O(A^2) A为二位数的最大值
/**
* 因为数据元素的范围为0-9,所以二位数最大值为99,给一个100长度的一维数组(00-99),存储对等个数
* 将一行数据转为有序的二位数,遍历计算num,并存储数组中,num相同即下标对应数组值++
* @param dominoes
* @return
*/
public int numEquivDominoPairs(int[][] dominoes) {
// 定义二位数数组
int[] nums = new int[100];
// 定义结果对等值
int result = 0;
// 遍历二维数组
for (int[] domino : dominoes) {
// 统一定义为小值在十位,大值在个位
// if (domino[0] > domino[1]) {
// int temp = domino[0];
// domino[0] = domino[1];
// domino[1] = temp;
// }
// int num = domino[0] * 10 + domino[1];
// 优化为三元表达式
int num = domino[0] > domino[1] ? domino[1] * 10 + domino[0] : domino[0] * 10 + domino[1];
// 起始值为0;如果之前已经有一个num(num对应值为1),那么这次遇到相同的即发现一个对等
result += nums[num];
// 在原有基础上++,记录每一个num的对等值个数
nums[num]++;
}
return result;
}
2021.01.27-保证图可完全遍历1579
题目描述:
题目解析:一看到图想到的就是并查集 BFS DFS,这道题和其他图不一样的地方在于边有三种类型,那么在遍历节点的时候就需要根据边的类型来进行区分,比如说对于(1,1,2)说明1和2间只能Alice遍历,对于(2,1,2)说明1和2之间只能Bob遍历,对于(3,1,2)说明1和2之间Alice和Bob都可遍历
判断是否图可完全遍历即从图中任意一点出发都可以到图中的其他节点的条件是,所有的节点都在一个集合(有一个根节点)/DFS可连通所有的节点
题的难点在于如何判断出图中对于Alice和Bob都多余的连线并计数
有一个想法是,现判断Alice是否可连通,并且筛选出未遍历的边成为一个集合;让Bob在不用这个集合的基础上判断是否可连通,如果不可放边,剩几条就是可以删几条,集合最后为0则可以删的边数为0;如果删完依旧不是连通图,结果为-1
总结:
1,需要一个判断是否为连通图的方法,对于Alice不是直接返回-1
2,需要一个记录边是否被使用过的集合
3,需要遍历未被使用过的集合,放边判断Bob是否为连通图
https://leetcode-cn.com/problems/remove-max-number-of-edges-to-keep-graph-fully-traversable/solution/bao-zheng-tu-ke-wan-quan-bian-li-by-leet-mtrw/
没有考虑到的:
对于Alice边/Bob边而言,公共边是首要需要留下的,所以遍历节点现加上公共边
如果两个节点使用一个集合,那么添加这一条公共边无意义;
如果两个节点不在同一个集合,那么就一定添加这条公共边,合并两个集合
使用并查集(有查询根节点+合并集合的操作)
时间复杂度:O(m⋅α(n)),m是数组edges的长度,α是阿克曼函数的反函数;
空间复杂度:O(n),n为并查集所需要的空间
public class MaxNumEdgesToRemove {
/**
* 使用并查集判断构成Alice和Bob连通最多可删除的边数
* @param n
* @param edges
* @return
*/
public int maxNumEdgesToRemove(int n, int[][] edges) {
// 构造并查集
UnionFind ufa = new UnionFind(n);
UnionFind ufb = new UnionFind(n);
// 记录可删除的边数
int result = 0;
// 节点编号改为从0开始(第一个节点从0开始,方便处理parent)
for (int[] edge : edges) {
--edge[1];
--edge[2];
}
// 公共边处理
for (int[] edge : edges) {
if (edge[0] == 3) {
// 本身就在同一集合中
if (!ufa.union(edge[1], edge[2])) {
result++;
} else {
// 使用公共边连接两个节点,即此时对于Alice和Bob而言,两个节点都可以连通
ufb.union(edge[1], edge[2]);
}
}
}
// 处理独占边
for (int[] edge : edges) {
// Alice独占
if (edge[0] == 1) {
// 本身连通,无需加边
if (!ufa.union(edge[1], edge[2])) {
result++;
}
} else if (edge[0] == 2) {
// Bob独占边
if (!ufb.union(edge[1], edge[2])) {
result++;
}
}
}
// 判断a和b的集合数,如果最后不等于1的话说明其中有不连通的
if (ufa.count != 1 || ufb.count != 1) {
return -1;
}
return result;
}
// 定义并查集
class UnionFind {
// 根节点
int[] parent;
// 树的高度==集合的大小(集合越大,树越高)
int[] rank;
// 节点个数
int n;
// 此时独立的数目
int count;
public UnionFind(int n) {
this.n = n;
// 初始独立数目为n,即有n个小集合
this.count = n;
this.parent = new int[n];
this.rank = new int[n];
for (int i = 0; i < n; i++) {
parent[i] = i;
}
// 使用Arrays类的fill静态方法,适用于对数组中初始化同样的值
Arrays.fill(rank, 1);
}
/**
* 发现根节点
* @param x
* @return
*/
private int find(int x) {
int xRoot = x;
while (xRoot != parent[xRoot]) {
xRoot = parent[xRoot];
}
return xRoot;
}
/**
* 合并集合
* @param x
* @param y
*/
private boolean union(int x, int y) {
int xRoot = find(x);
int yRoot = find(y);
// 本身就在一个集合中
if (xRoot == yRoot) {
return false;
}
// else if (rank[xRoot] > rank[yRoot]) {
// parent[yRoot] = xRoot;
// rank[xRoot] += rank[yRoot];
// } else {
// parent[xRoot] = yRoot;
// rank[yRoot] += rank[xRoot];
// }
// 优化集合合并的操作,小集合并入到大集合中
if (rank[xRoot] > rank[yRoot]) {
// 交换一下
int temp = xRoot;
xRoot = yRoot;
yRoot = temp;
}
parent[xRoot] = yRoot;
rank[yRoot] += rank[xRoot];
// 合并之后,集合数目-1
count--;
return true;
}
}
public static void main(String[] args) {
MaxNumEdgesToRemove maxNumEdgesToRemove = new MaxNumEdgesToRemove();
int[][] edges = {{3,1,2},{3,2,3},{1,1,3},{1,2,4},{1,1,2},{2,3,4}};
int n = 4;
System.out.println(maxNumEdgesToRemove.maxNumEdgesToRemove(n, edges));
}
}
2021.01.28-寻找数组的中心索引724
题目描述:
中心索引的定义:数组中心索引的左侧所有元素相加的和等于右侧所有元素相加的和
给一个整数数组nums,返回其中心索引。如果不存在,返回-1;如果有多个,返回最靠近左边的一个即索引最小的一个
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fXeHzPaU-1611807565821)(C:\Users\cici\Desktop\刷题\724.1)]
题目解析:题目理解很简单,最简单的方法就是遍历数组,然后分别左右加和,直到左右相等将索引输出即可。
麻烦的地方在于数组的长度范围为[0,10000],然后元素取值范围为[-1000,1000],求和一个个的加和性能太差
有一个想法,从中间开始遍历,然后根据两边值的大小比较,选择性移动下标,往大的一边移动
但是遇到的情况就是临界条件太多,人力暴力枚举每次都有想不到的,实在是可恨可气!!!
扪心自问:简单问题复杂化就是宁的强项???
public class PivotIndex {
/**
* 从数组中间开始遍历,判断两边加和情况,然后往大的一边移动
* @param nums
* @return
*/
public int pivotIndex(int[] nums) {
// 获取数组长度
int length = nums.length;
// 根据题中给出的条件,数组长度最小为0.为0的时候不必判断,直接返回-1即可
if (length == 0) {
return -1;
}
// 获取中心索引初始值
int index = length / 2;
// 计算左右两侧的加和值,因为会多次计算,所以整一个private方法
// 这里想到一个知识点,既然反射可以访问到private方法,为啥还要用。
// 原因是private是为了规范编写,规范方法间的访问
int left = getAddResult(0, index, nums);
int right = getAddResult(index + 1, length, nums);
// 防止出现左右移动的现象,即左移一位左<右,右移一位左>右,定义一个visited数组,记录index的遍历,访问过的不再访问
boolean[] visited = new boolean[length];
// 初始化为false
Arrays.fill(visited, false);
int result = length;
while (!visited[index] && index >= 0 && index < length - 1) {
visited[index] = true;
// 标记上一次是向哪边移动
int flag = 0;
// index右移
// 使用abs其实是不恰当的,因为如果一正一负,无法适用
if (Math.abs(left) < Math.abs(right)) {
left += nums[index];
right -= nums[index+1];
index++;
flag = 1;
} else if (Math.abs(left) > Math.abs(right)) {
left -= nums[index-1];
right += nums[index];
index--;
} else if (left == right){
// 防止是绝对值相等
result = Math.min(result, index);
if (flag == 1) {
left += nums[index];
right -= nums[index+1];
index++;
} else if (flag == 0 && index >0) {
left -= nums[index-1];
right += nums[index];
index--;
}
}
}
return result == length ? -1 : result;
}
/**
* 根据start和end下标加和nums数组获取加和值
* @param start
* @param end
* @param nums
* @return
*/
private int getAddResult(int start, int end, int[] nums) {
int result = 0;
for (int i = start; i < end; i++) {
result += nums[i];
}
return result;
}
public static void main(String[] args) {
PivotIndex pivotIndex = new PivotIndex();
// int[] nums = {1,7,3,6,5,6};
// index边界值的考虑
// int[] nums = {1,2,3};
// 复数使用绝对值进行比较
// int[] nums = {-1,-1,-1,-1,-1,0};
// 记录visited
// int[] nums = {-1,-1,-1,-1,-1,-1};
// 找最小的中间索引
int[] nums = {-1,-1,-1,0,1,1};
System.out.println(pivotIndex.pivotIndex(nums));
}
}
实际上本身是找最小的索引,就直接从头开始遍历就可,使用暴力破解法
/**
* 从0开始遍历,使用暴力破解法
* @param nums
* @return
*/
public int pivotIndex(int[] nums) {
int length = nums.length;
// 根据题中给出的条件,数组长度最小为0.为0的时候不必判断,直接返回-1即可
if (length == 0) {
return -1;
}
int left = 0;
int right = getAddResult(1, length, nums);
for (int i = 0; i < length; i++) {
if (left == right) {
return i;
}
left += nums[i];
if (i < length -1) {
right -= nums[i+1];
}
}
return -1;
}
/**
* 根据start和end下标加和nums数组获取加和值
* @param start
* @param end
* @param nums
* @return
*/
private int getAddResult(int start, int end, int[] nums) {
int result = 0;
for (int i = start; i < end; i++) {
result += nums[i];
}
return result;
}
public static void main(String[] args) {
PivotIndex pivotIndex = new PivotIndex();
// int[] nums = {1,7,3,6,5,6};
// index边界值的考虑
// int[] nums = {1,2,3};
// 复数使用绝对值进行比较
// int[] nums = {-1,-1,-1,-1,-1,0};
// 记录visited
int[] nums = {-1,-1,-1,-1,-1,-1};
// 找最小的中间索引
// int[] nums = {-1,-1,-1,0,1,1};
System.out.println(pivotIndex.pivotIndex(nums));
}
}
参考:https://leetcode-cn.com/problems/find-pivot-index/solution/xun-zhao-shu-zu-de-zhong-xin-suo-yin-by-gzjle/
使用数学公式法,l元素之和为total,移动下标,right = total - left - nums[index],如果相等的话有left = total - left - nums[index],即2 * left + nums[index] = total
时间复杂度:O(n);空间复杂度O(1)
/**
* 使用数学公式法,2 * left + nums[index] = total
* @param nums
* @return
*/
public int pivotIndex(int[] nums) {
// 计算total
int total = Arrays.stream(nums).sum();
int left = 0;
for (int i = 0; i < nums.length; i++) {
if (2 * left + nums[i] == total) {
return i;
}
left += nums[i];
}
return -1;
}
使用stream速度变慢
优化一下,不使用stream,而是普通的加和
/**
* 使用数学公式法,2 * left + nums[index] = total
* @param nums
* @return
*/
public int pivotIndex(int[] nums) {
// 计算total
// int total = Arrays.stream(nums).sum();
int total = getAddResult(0, nums.length, nums);
int left = 0;
for (int i = 0; i < nums.length; i++) {
if (2 * left + nums[i] == total) {
return i;
}
left += nums[i];
}
return -1;
}
/**
* 根据start和end下标加和nums数组获取加和值
* @param start
* @param end
* @param nums
* @return
*/
private int getAddResult(int start, int end, int[] nums) {
int result = 0;
for (int i = start; i < end; i++) {
result += nums[i];
}
return result;
}
2021.01.29-最小体力消耗路径1631
题目描述:
题目解析:第一反应最小路径求解。将row*column个节点组成一个图,求从开始节点(0,0)到终止节点(row-1,column-1)的最短距离
其中最短距离:路径中相邻节点的绝对值之差最小
想到了BFS,从(0,0)出发,向右/向下遍历,找到绝对值最小的作为最短路径的第二个节点,然后继续使用递归遍历,直到(row-1,column-1),途中记录绝对值的最小值并输出
难点在于有些路径开始的时候绝对值不是最小的,但是总的绝对值最小,这点如何实现是一个问题:现在想的是第一个节点所有分支都遍历,其余节点取最小的(其实应该是不全面的,因为其他节点也会存在先小后大的情况)
可以考虑构造每一个节点,left right up down指针,next指针指向相邻最小路径;构造好后通过next遍历获取最短距离
md实在解决不出来,看题解也似懂非懂,我是废物!
二分法+BFS:参考https://leetcode-cn.com/problems/path-with-minimum-effort/solution/zui-xiao-ti-li-xiao-hao-lu-jing-by-leetc-3q2j/
public class MinMumEffortPath {
// 定义方向数组
int[][] dirs = {{-1,0},{1,0},{0,-1},{0,1}};
/**
* 使用二分查找法
* @param heights
* @return
*/
public int minimumEffortPath(int[][] heights) {
int rows = heights.length;
int columns = heights[0].length;
int left = 0;
// 元素最大个数为100*100,元素的值最大为1000000,则取0-999999
int right = 999999;
int result = 0;
while (left <= right) {
int mid = (left + right) / 2;
Queue<int[]> queue = new LinkedList<>();
// 将初始节点(0,0)加入队列中,x=0;y=0
queue.offer(new int[]{0,0});
// 定义访问数组,个数为数组中元素的个数
boolean[] visited = new boolean[rows * columns];
visited[0] = true;
while (!queue.isEmpty()) {
// 弹出队列的第一个元素,元素类型为一维数组
int[] cell = queue.poll();
int x = cell[0];
int y = cell[1];
for (int i = 0; i < 4; i++) {
// 向上/下/左/右移动,确定下一个节点
int nx = x + dirs[i][0];
int ny = y + dirs[i][1];
if (nx >=0 && nx < rows && ny >= 0 && ny < columns
&& !visited[nx * columns + ny] && Math.abs(heights[x][y] - heights[nx][ny]) <= mid) {
queue.offer(new int[]{nx, ny});
// 移动后,下一个节点索引值就是x*columns+y
visited[nx * columns + ny] = true;
}
}
}
// 最后一个节点访问到了
if (visited[rows * columns - 1]) {
result = mid;
right = mid - 1;
} else {
// 往右继续遍历
left = mid + 1;
}
}
return result;
}
}
2021.01.30-水位上升的泳池中游泳778
题目描述:
和昨天的题很像,借用官方的思路使用并查集解决问题
public class SwimInWater {
// 定义方向
public static final int[][] dirs = {{0,1},{0,-1},{1,0},{-1,0}};
/**
* 使用并查集解决问题
* @param grid
* @return
*/
public int swimInWater(int[][] grid) {
int length = grid.length;
int n = length * length;
// 数组下标为方格的高度,值为对应的坐标
int[] index = new int[n];
for (int i = 0; i < length; i++) {
for (int j = 0; j < length; j++) {
index[grid[i][j]] = i * length + j;
}
}
UnionFind unionFind = new UnionFind(n);
for (int i = 0; i < n; i++) {
// 第几行
int x = index[i] / length;
// 第几列
int y = index[i] % length;
for (int[] dir : dirs) {
int newX = x + dir[0];
int newY = y + dir[1];
if (newX >= 0 && newX < length && newY >= 0 && newY < length && grid[newX][newY] <= i) {
unionFind.union(index[i], newX * length + newY);
}
if (unionFind.findRoot(0) == unionFind.findRoot(n - 1)) {
return i;
}
}
}
return -1;
}
// 定义并查集
private class UnionFind {
// 父节点数组
private int[] parent;
// 构造并查集
public UnionFind(int n) {
parent = new int[n];
for (int i = 0; i < n; i++) {
parent[i] = i;
}
}
// 查找根节点
public int findRoot(int x) {
while (x != parent[x]) {
x = parent[x];
}
return x;
}
public void union (int x, int y) {
x = findRoot(x);
y = findRoot(y);
if (x == y) {
return;
}
// 合并集合
parent[x] = y;
}
}
}
2021.01.31-相似字符串组839
题目描述:
题目解析:天惹 看到题和1128等价多米诺骨牌对的数量太像了,都是求相似的数量
题的思路很清楚,遍历数组发现于其相似的字符串并加入到集合中,判断到底有几个这样的相似组集合(像不像并查集的味道?只不过合并的条件不再是节点相连,而是两个字符串相似)
难点在于:如何判断两个字符串是相似的。逻辑上讲就是对A字符串交换了其中两个字符的位置变为B字符串,则A与B相似
想到一个方法(萝卜坑的思想)A=“tars”,B=“rats”,对于A来讲假设每个元素都是在各自的坑中,现在遍历B字符串,发现坑与萝卜不匹配的地方;例如第一个下标0坑不是0号元素而是2号元素,去2号坑找发现2号坑的元素是0号坑的元素,交换判断是否与A相同,如果相同则A与B相似;否则直接不相似
注意此处需要留意字符串中一个字符多次存在的现象
时间复杂度:O(n2m2+nlogn) n为字符串数组长度,m为遍历判断字符串是否相似的时间空间复杂度:O(n) 并查集使用
public class NumSimilarGroups {
/**
* 遍历字符串数组,发现相邻元素并加入到集合中,最后取集合个数
* @param strs
* @return
*/
public int numSimilarGroups(String[] strs) {
int length = strs.length;
// 记录元素被访问过
// 定义是否被访问过没有意义,因为union的时候会直接判断是否属于同一集合
// boolean[] visited = new boolean[length];
// 构造并查集
UnionFind unionFind = new UnionFind(length);
// 遍历数组
for (int i = 0; i < length; i++) {
for (int j = i + 1; j < length; j++) {
// 判断是否相似,若相似则加入到同一集合中
if (isSimilar(strs[i], strs[j])) {
unionFind.union(i, j);
}
}
}
return unionFind.count;
}
class UnionFind {
int[] parent;
// 独立节点的个数
int count;
public UnionFind (int n) {
parent = new int[n];
// 初始每个节点都独立
count = n;
for (int i = 0; i < n; i++) {
parent[i] = i;
}
}
public int findRoot (int x) {
while (x != parent[x]) {
x = parent[x];
}
return x;
}
public void union(int x, int y) {
x = findRoot(x);
y = findRoot(y);
// 本身就在一个集合
if (x == y) {
return;
}
parent[x] = y;
count--;
}
}
private boolean isSimilar (String x, String y) {
char[] charX = x.toCharArray();
char[] charY = y.toCharArray();
int length = charX.length;
if (x.equals(y)) {
return true;
}
for (int i = 0; i < length; i++) {
if (charX[i] != charY[i]) {
// 找到charX[i]在Y中的位置并交换元素
for (int j = i + 1; j < length; j++) {
if (charY[j] == charX[i]) {
char temp = charY[i];
charY[i] = charY[j];
charY[j] = temp;
String newY = String.valueOf(charY);
if (x.equals(newY)) {
return true;
}
charY = y.toCharArray();
}
}
return false;
}
}
// 两个字符串相同,返回true
return true;
}
public static void main(String[] args) {
NumSimilarGroups numSimilarGroups = new NumSimilarGroups();
// String[] strs = {"tars","rats","arts","star"};
// String[] strs = {"blw", "bwl", "wlb"};
String[] strs = {"jmijc", "imjjc", "jcijm", "cmijj", "mijjc"};
System.out.println(numSimilarGroups.numSimilarGroups(strs));
}
}
fine.性能极差2333,但好歹自己写出来个难题✌
看了题解,对于相似的判断其实可以这么来…
直接遍历两个字符串,看相同下标对应不相同元素的个数,超过2返回false(等于2的话说明刚好元素交换后字符串相同),等于2或者等于0即字符串相似(我傻了)
/**
* 遍历字符串数组,发现相邻元素并加入到集合中,最后取集合个数
* @param strs
* @return
*/
public int numSimilarGroups(String[] strs) {
int length = strs.length;
// 记录元素被访问过
// 定义是否被访问过没有意义,因为union的时候会直接判断是否属于同一集合
// boolean[] visited = new boolean[length];
// 构造并查集
UnionFind unionFind = new UnionFind(length);
// 遍历数组
for (int i = 0; i < length; i++) {
for (int j = i + 1; j < length; j++) {
// 判断是否相似,若相似则加入到同一集合中
if (isSimilar(strs[i], strs[j])) {
unionFind.union(i, j);
}
}
}
return unionFind.count;
}
class UnionFind {
int[] parent;
// 独立节点的个数
int count;
public UnionFind (int n) {
parent = new int[n];
// 初始每个节点都独立
count = n;
for (int i = 0; i < n; i++) {
parent[i] = i;
}
}
public int findRoot (int x) {
while (x != parent[x]) {
x = parent[x];
}
return x;
}
public void union(int x, int y) {
x = findRoot(x);
y = findRoot(y);
// 本身就在一个集合
if (x == y) {
return;
}
parent[x] = y;
count--;
}
}
private boolean isSimilar (String x, String y) {
char[] charX = x.toCharArray();
char[] charY = y.toCharArray();
int length = charX.length;
int diffCount = 0;
for (int i = 0; i < length; i++) {
if (charX[i] != charY[i]) {
diffCount++;
}
if (diffCount > 2) {
return false;
}
}
// 不存在diffCount==1的情况,因为字符串是由相同个数的字符组成的
// 不存在A="abbc",B="bbbc"
return true;
}
肉眼可见的,速度提升了超级多。做题的时候还是要将能省的步骤给省掉,简单处理
总结
这周LeetCode记录结束,对并查集有了一定的掌握,分析问题也有点起色了,继续加油!
努力就一定会有好结果的💪