文章目录
605. 种花问题 (贪心)
虽然是贪心,但是这题目处理贪心的思路是我没有想到的。这个题目首先是找到了一个规律,对于没有种花的一段区域,左端点是 l l l, 右端点是 r r r。可以得到在这个区域内部可以种植的是 ( l − r ) / 2 (l-r)/2 (l−r)/2。这个对于奇数和偶数都是适用的。
因此我们需要维护一下之前种花的位置。也就是pre。但是我们需要特殊情况,也就是开头和结尾情况。一般情况下,两个空挡是无法种植的。但是如果是开头和结尾的位置的是可以种在边界上的。
因此我们把初始前一个有花的位置设置为-2,最后有花的位置是m+1。
class Solution {
public boolean canPlaceFlowers(int[] flowerbed, int n) {
int count = 0;
int m = flowerbed.length;
int prev = -2;
for (int i = 0; i < m; i++) {
if (flowerbed[i] == 1) {
count += (i - prev - 2) / 2;
prev = i;
}
}
count += (m - prev - 1) / 2;
return count >= n;
}
}
239. 滑动窗口最大值 模板题 单调栈
单调栈问题的母题,单调栈内存放的是索引,排序的依据是数组的大小。
- 操作顺序:四步!!!!
- 维护单调性,弹栈
- 入栈
- 维护区间要求,弹出超出区间的
- 取得最大值。
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
Deque<Integer> queue = new LinkedList<>();
int n = nums.length;
int[] ans = new int[n-k+1];
for(int i = 0;i<n ;i++){
while(!queue.isEmpty() && nums[i]>=nums[queue.peekLast()]){
queue.pollLast();
}
queue.offerLast(i);
if(i>=k-1){
while(i - queue.peekFirst()>=k){
queue.pollFirst();
}
ans[i-k+1] = nums[queue.peekFirst()];
}
}
return ans;
}
}
86. 分隔链表
难倒是不难,但是这个链表问题属实不太熟练。
class Solution {
public ListNode partition(ListNode head, int x) {
ListNode dummysmall = new ListNode();
ListNode pre = dummysmall;
ListNode dummylarge = new ListNode();
ListNode prel = dummylarge;
while(head!=null){
if(head.val<x){
dummysmall.next = head;
dummysmall = dummysmall.next;
}else{
dummylarge.next = head;
dummylarge = dummylarge.next;
}
head = head.next;
}
dummylarge.next = null; // 结束的标志!!!!!!这一步一定不能省略。
dummysmall.next = prel.next;
return pre.next;
}
}
399. 除法求值 (模板题 并查集)
能够想到使用并查集的方法其实并不难。但是这里需要维护并查集的比例关系这个还是比较复杂。当我们进行并查集的父子连接的时候,因为被合并节点的父节点改变了,因此两个父节点直接也存在了比例关系。因此这个被合并节点的其他子节点也需要修改自己与新的根节点的比值关系。
核心两步:
- 维护根节点的比例关系。(在合并的时候)
- 修改根节点上全部子节点的数值。(在维护路径压缩的时候)
class Solution {
public double[] calcEquation(List<List<String>> equations, double[] values, List<List<String>> queries) {
int n = queries.size();
double[] ans = new double[n];
int m = equations.size();
HashMap<String, Integer> map = new HashMap<>();
int index = 0;
UF uf = new UF(100);
for(int i = 0;i<m;i++){
List<String> l= equations.get(i);
String num1 = l.get(0);
String num2 = l.get(1);
int n1;
int n2;
if(map.containsKey(num1)){
n1 = map.get(num1);
}else{
n1 = index;
map.put(num1, n1);
index++;
}
if(map.containsKey(num2)){
n2 = map.get(num2);
}else{
n2 = index;
map.put(num2, n2);
index++;
}
if(!uf.isUnion(n1,n2)){
uf.union(n1,n2,values[i]);
}
}
for(int i = 0;i<n;i++){
List<String> l= queries.get(i);
String num1 = l.get(0);
String num2 = l.get(1);
int n1;
int n2;
if(map.containsKey(num1)){
n1 = map.get(num1);
}else{
n1 = index;
index++;
}
if(map.containsKey(num2)){
n2 = map.get(num2);
}else{
n2 = index;
index++;
}
if(uf.isUnion(n1,n2)){
ans[i] = uf.getCount(n1)/uf.getCount(n2);
}else{
ans[i] = -1.0;
}
}
return ans;
}
class UF{
int[] size;
int[] fa;
double[] count;
private UF(int N){
size = new int[N];
fa = new int[N];
count = new double[N];
for(int i = 0;i<N;i++) {
size[i] = 1;
fa[i] = i;
count[i] = 1.0;
}
}
private int findfa(int i){
if(i == fa[i]) return i;
int ifa = findfa(fa[i]); // 新的根节点,同时已经维护好了父亲这一支的权值关系。
count[i] *= count[fa[i]];// 这一步和上一步的顺序是不可调换的。因为前一行维护好了count[fa[i]]。
fa[i] = ifa;
return fa[i];
}
public boolean isUnion(int i , int j){
return findfa(i) == findfa(j);
}
private void union(int i, int j, double v){
int ifa = findfa(i);
int jfa = findfa(j);
if (jfa == ifa) return;
// 不进行压缩了,所有的ifa都合并到jfa上。
fa[ifa] = jfa;
//count[ifa] = count[j]*v/count[i]; // 这个其实等价,因为count[jfa]一定是等于1
count[ifa] = count[j]*count[jfa]*v/count[i];// vali = count[i]*count[ifa] valj = count[j]*count[jfa]; vali = valj*v
}
public double getCount(int i){
return count[i];
}
}
}
1202. 交换字符串中的元素 M (并查集,连通元素有序性)
并查集的简单升级,除了利用并查集判断连通性,还额外维护了连通块元素的顺序。
为了加快速度,并不是在每次合并的时候维护顺序,而是在完成了全部的连通性以后。借助字典,key是根,value是一个堆。实现维护连通元素的顺序。
这个借助字典的方法很常用。
省略了UF
class Solution {
String s;
public String smallestStringWithSwaps(String s, List<List<Integer>> pairs) {
this.s = s;
int n = s.length();
UF uf = new UF(n);
int m = pairs.size();
for(int i = 0;i<m;i++){
uf.union(pairs.get(i).get(0),pairs.get(i).get(1));
}
HashMap<Integer, PriorityQueue<Integer>> map = new HashMap<>();
for(int i = 0;i<n;i++){
int cur = uf.findfa(i);
if(!map.containsKey(cur)){
map.put(cur, new PriorityQueue<>((a,b)->s.charAt(a)-s.charAt(b)));
}
map.get(cur).offer(i);
}
StringBuffer sb = new StringBuffer();
for(int i = 0;i<n;i++){
sb.append(s.charAt(map.get(uf.findfa(i)).poll()));
}
return sb.toString();
}
}
1203. 项目管理 H(多层次拓扑排序)
比较复杂的一道题目。需要用到两次不同级别的拓扑排序。
class Solution {
/**
* groupGraph 组间的拓扑图,组之间的依赖
* itemGraph 组内项目之间的拓扑图,组内项目之间的依赖
* groupItem 每个小组负责的项目
* groupTopSort 组间的拓扑排序
* @param n 项目数量
* @param m 组的数量
* @param group the group
* @param beforeItems the before items
* @return the int [ ]
*
* 首先我们可以按照已经完成分配的任务进行分组,没有进行分配的任务是单独的一系列。我们要先建立这些小组之间的依赖性。也就是首先完成
* 在小组之间的拓扑排序,这里需要维护小组之间的入度。这需要我们建立从项目映射到小组的标记,也就是group。
*
* 在完成小组之间的拓扑排序之后,我们继续建立小组内部的拓扑排序。这里我们需要从小组映射到任务,利用任务之间的依赖也就是彼此的入度,完成拓扑排序。
*/
public int[] sortItems(int n, int m, int[] group, List<List<Integer>> beforeItems) {
// 组间的依赖关系。
List<List<Integer>> groupGraph = new ArrayList<>();
// 组内的依赖关系。
List<List<Integer>> itemGraph = new ArrayList<>();
for(int i = 0;i<m+n;i++){
groupGraph.add(new ArrayList<Integer>());
}
for(int i = 0; i<n;i++){
itemGraph.add(new ArrayList<Integer>());
}
int[] groupDegree = new int[m+n+1];
int[] itemDegree = new int[n+1];
List<Integer> id = new ArrayList<>();
for(int i = 0;i<n+m;i++){
id.add(i);
}
// 每个小组的项目。
List<List<Integer>> graphItem = new ArrayList<>();
for(int i = 0;i<n+m;i++){
graphItem.add(new ArrayList<Integer>());
}
// 没有小组的任务,都是单独编号给一个小组
int leftItem = m;
for (int i = 0; i<n;i++){
if (group[i] == -1) {
group[i] = leftItem;
leftItem++;
}
graphItem.get(group[i]).add(i);
}
for(int i = 0 ;i<n;i++){
int curGroupId = group[i];
for(int before:beforeItems.get(i)){ // before 想执行当前任务需要的先前任务
int needGroupId = group[before];
// 如果是同一个小组,维护组内的任务拓扑
if(curGroupId == needGroupId){
itemGraph.get(before).add(i); // 钥匙交给before
itemDegree[i]++; // 加上一把锁
}else{
// 不是一个小组,维护组间的拓扑
groupGraph.get(needGroupId).add(curGroupId);
groupDegree[curGroupId]++;
}
}
}
// 首先进行组间的拓扑排序。
List<Integer> groupTop = graphTopSort(groupDegree, groupGraph, id);
if(groupTop.size() == 0)return new int[0];
int[] ans = new int[n];
int index = -1;
// 依据组间拓扑的排序,维护组内的任务排序。
for(int curGroup:groupTop){
if(graphItem.get(curGroup).size() == 0)continue;
List<Integer> itemTop = graphTopSort(itemDegree, itemGraph, graphItem.get(curGroup));
if (itemTop.size() == 0)return new int[0];
for(int i:itemTop){
ans[++index] = i;
}
}
return ans;
}
======================!!!!!!!!!!!!!拓扑排序的板子!!!!!!!!!!!!!!!=========================
/**
* @param degree 节点的入度表
* @param graph 节点的邻接表, 依赖关系
* @param id 全体节点表
* @return List<Integer> 拓扑排序的结果
*
*/
public List<Integer> graphTopSort(int[] degree, List<List<Integer>> graph, List<Integer> id){
Queue<Integer> queue = new LinkedList<>();
List<Integer> res = new ArrayList<>();
for(int i:id){
if(degree[i] == 0){
queue.offer(i);
res.add(i);
}
}
while(!queue.isEmpty()){
int cur = queue.poll();
for(int next:graph.get(cur)){
degree[next]--;
if(degree[next] == 0){
queue.offer(next);
res.add(next);
}
}
}
return res.size() == id.size() ? res : new ArrayList<Integer>();
}
}
803. 打砖块 H (反向并查集)
引用官方解答的解析。当时在第一次做这个题目的时候,可以想到类似并查集,但是总觉着那里很别扭。因为并查集起到了合并的作用,但是这里是分解连通量。
逆向的思路关键在于,拆分开连通量。并且倒序连接起来全部的量,等价于每次连接多少砖头被连接起来。
class Solution {
// 逆向思维,反向利用并查集。
// 并查集的特点是讲两个连通量合并为成为一个连通量。而我们的问题是讲一个连通量拆分为两个连通量。
public int[] hitBricks(int[][] grid, int[][] hits) {
int n = grid.length;
int m = grid[0].length;
int[][] copy = new int[n][m];
int[][] dis = {{0, 1}, {1, 0}, {-1, 0}, {0, -1}};
// 第 1 步:把 grid 中的砖头全部击碎,通常算法问题不能修改输入数据,这一步非必需,可以认为是一种答题规范
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
copy[i][j] = grid[i][j];
}
}
for (int[] h:hits){
copy[h[0]][h[1]] = 0;
}
int size = n*m;
// size表示屋顶 !!!!! 很经典的思路,利用一个特殊的量表示某种安全岛。
UnionFind uf = new UnionFind(size+1);
// 横坐标是0的砖全部与屋顶连接
for(int i = 0;i<m;i++){
if(copy[0][i] == 1){
uf.union(size, i);
}
}
// 连接剩余的连通量 单侧合并,保证不重不漏 只看左侧和上侧
for(int i = 1; i < n; i++){
for (int j = 0; j < m; j++) {
if(copy[i][j] == 1){
if(copy[i-1][j] == 1){
uf.union((i-1)*m+j, i*m+j);
}
if(j>0 && copy[i][j-1] == 1){
uf.union(i*m+j, i*m+j-1);
}
}
}
}
int k = hits.length;
int[] ans = new int[k];
// 需要按照逆序添加hits !!!! 这一步是关键,反应了我们的算法其实是一个逆向的并查集思路
for (int i = k-1; i >= 0; i--) {
int res = uf.getSize(size);
int x = hits[i][0];
int y = hits[i][1];
if(grid[x][y] == 0)continue;
// 如果是0,需要与屋顶连接
if(x == 0){
uf.union(y, size);
}
for(int[] d:dis){
int nx = x+d[0];
int ny = y+d[1];
if(nx>=0 && nx<n && ny>=0 && ny<m && copy[nx][ny] == 1){
uf.union(nx*m+ny, x*m+y);
}
}
int cur = uf.getSize(size); // 添加砖头之后,连接屋顶的数量的
ans[i] = Math.max(0, cur-res-1);
// 最后记着补上
copy[x][y] = 1;
}
return ans;
}
947. 移除最多的同行或同列石头 H (二维并查集合并)
对于二维的并查集的合并问题,两个思路:一个是合并点,一个是合并边。
合并点的思路是,维护了两个字典,一个是横轴,一个是纵轴。分别记录了首次出现横纵坐标时候的索引。以后再出现一样的索引时候就可以直接合并。这里有一个APIrow.computeIfAbsent(stones[i][0], val->tem);
当字典不存在时候,令val为tem,否则返回值。
合并边的思路复杂度会稍微低。我们把二维左边变成一位的形式,也就是展开。x->x,y->y+10000。这样的话,每次把点的横纵坐标都进行合并。
但是无论怎么样,我们最后都需要统计连通块的数量。这个方法就是计算根节点的数量。这个是无法简化的。
class Solution {
public int removeStones(int[][] stones) {
int n = stones.length;
/*
UF uf = new UF(n);
// 按照点连接,维护两个哈希表,分别是首次出现横纵坐标时候的索引。
// 并查集进行合并的时候,会两次检查,首先看横坐标上能不能合并,然后看纵坐标能不能合并。
HashMap<Integer, Integer> row = new HashMap<>();
HashMap<Integer, Integer> col = new HashMap<>();
for(int i = 0;i<n;i++){
int tem = i;
row.computeIfAbsent(stones[i][0], key->tem);
col.computeIfAbsent(stones[i][1], key->tem);
uf.union(i,row.get(stones[i][0])); // 如果横坐标相等,可以合并
uf.union(i,col.get(stones[i][1])); // 如果纵坐标相等,可以合并
}
*/
// 按照边进行合并
UF uf = new UF(20000);
for(int i = 0;i<n;i++){
uf.union(stones[i][0], stones[i][1]+10000);
}
HashSet<Integer> set = new HashSet<>();
for (int[] s:stones){
set.add(uf.findfa(s[0]));
}
return n-set.size();
}
}
其他题目主要是思路
189. 旋转数组
主要就是三步旋转解决。首先反转整个数组,然后反转0,k-1的数组。最后反转k到末尾的数组。具体的反转通过头尾双指针交换。