算法数据结构必备篇章3

47. (必备)一维差分与等差数列差分

package class047;

// 航班预订统计
// 这里有 n 个航班,它们分别从 1 到 n 进行编号。
// 有一份航班预订表 bookings ,
// 表中第 i 条预订记录 bookings[i] = [firsti, lasti, seatsi]
// 意味着在从 firsti 到 lasti 
//(包含 firsti 和 lasti )的 每个航班 上预订了 seatsi 个座位。
// 请你返回一个长度为 n 的数组 answer,里面的元素是每个航班预定的座位总数。
// 测试链接 : https://leetcode.cn/problems/corporate-flight-bookings/
public class Code01_CorporateFlightBookings {

	// bookings
	// [1,5,6]
	// [2,9,3]
	// ...
	public static int[] corpFlightBookings(int[][] bookings, int n) {
		int[] cnt = new int[n + 2];
		// 设置差分数组,每一个操作对应两个设置
		for (int[] book : bookings) {
			cnt[book[0]] += book[2];
			cnt[book[1] + 1] -= book[2];
		}
		// 加工前缀和
		for (int i = 1; i < cnt.length; i++) {
			cnt[i] += cnt[i - 1];
		}
		int[] ans = new int[n];
		for (int i = 0; i < n; i++) {
			ans[i] = cnt[i + 1];
		}
		return ans;
	}

}
package class047;

// 一开始1~n范围上的数字都是0,一共有m个操作,每次操作为(l,r,s,e,d)
// 表示在l~r范围上依次加上首项为s、末项为e、公差为d的数列
// m个操作做完之后,统计1~n范围上所有数字的最大值和异或和
// 测试链接 : https://www.luogu.com.cn/problem/P4231
// 请同学们务必参考如下代码中关于输入、输出的处理
// 这是输入输出处理效率很高的写法
// 提交以下的code,提交时请把类名改成"Main",可以直接通过

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StreamTokenizer;

public class Code02_ArithmeticSequenceDifference {

	public static int MAXN = 10000005;

	public static long[] arr = new long[MAXN];

	public static int n, m;

	public static void main(String[] args) throws IOException {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		StreamTokenizer in = new StreamTokenizer(br);
		PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
		while (in.nextToken() != StreamTokenizer.TT_EOF) {
			n = (int) in.nval;
			in.nextToken();
			m = (int) in.nval;
			for (int i = 0, l, r, s, e; i < m; i++) {
				in.nextToken(); l = (int) in.nval;
				in.nextToken(); r = (int) in.nval;
				in.nextToken(); s = (int) in.nval;
				in.nextToken(); e = (int) in.nval;
				set(l, r, s, e, (e - s) / (r - l));
			}
			build();
			long max = 0, xor = 0;
			for (int i = 1; i <= n; i++) {
				max = Math.max(max, arr[i]);
				xor ^= arr[i];
			}
			out.println(xor + " " + max);
		}
		out.flush();
		out.close();
		br.close();
	}

	public static void set(int l, int r, int s, int e, int d) {
		arr[l] += s;
		arr[l + 1] += d - s;
		arr[r + 1] -= d + e;
		arr[r + 2] += e;
	}

	public static void build() {
		for (int i = 1; i <= n; i++) {
			arr[i] += arr[i - 1];
		}
		for (int i = 1; i <= n; i++) {
			arr[i] += arr[i - 1];
		}
	}

}
package class047;

// 一群人落水后求每个位置的水位高度
// 问题描述比较复杂,见测试链接
// 测试链接 : https://www.luogu.com.cn/problem/P5026
// 请同学们务必参考如下代码中关于输入、输出的处理
// 这是输入输出处理效率很高的写法
// 提交以下的code,提交时请把类名改成"Main",可以直接通过

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StreamTokenizer;

public class Code03_WaterHeight {

	// 题目说了m <= 10^6,代表湖泊宽度
	// 这就是MAXN的含义,湖泊最大宽度的极限
	public static int MAXN = 1000001;

	// 数值保护,因为题目中v最大是10000
	// 所以左侧影响最远的位置到达了x - 3 * v + 1
	// 所以右侧影响最远的位置到达了x + 3 * v - 1
	// x如果是正式的位置(1~m),那么左、右侧可能超过正式位置差不多30000的规模
	// 这就是OFFSET的含义
	public static int OFFSET = 30001;

	// 湖泊宽度是MAXN,是正式位置的范围
	// 左、右侧可能超过正式位置差不多OFFSET的规模
	// 所以准备一个长度为OFFSET + MAXN + OFFSET的数组
	// 这样一来,左侧影响最远的位置...右侧影响最远的位置,
	// 都可以被arr中的下标表示出来,就省去了很多越界讨论
	// 详细解释看set方法的注释
	public static int[] arr = new int[OFFSET + MAXN + OFFSET];

	public static int n, m;

	public static void main(String[] args) throws IOException {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		StreamTokenizer in = new StreamTokenizer(br);
		PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
		while (in.nextToken() != StreamTokenizer.TT_EOF) {
			// n有多少个人落水,每个人落水意味着四个等差数列操作
			n = (int) in.nval;
			in.nextToken();
			// 一共有多少位置,1~m个位置,最终要打印每个位置的水位
			m = (int) in.nval;
			for (int i = 0, v, x; i < n; i++) {
				in.nextToken(); v = (int) in.nval;
				in.nextToken(); x = (int) in.nval;
				// v体积的朋友,在x处落水,修改差分数组
				fall(v, x);
			}
			// 生成最终的水位数组
			build();
			// 开始收集答案
			// 0...OFFSET这些位置是辅助位置,为了防止越界设计的
			// 从OFFSET+1开始往下数m个,才是正式的位置1...m
			// 打印这些位置,就是返回正式位置1...m的水位
			int start = OFFSET + 1;
			out.print(arr[start++]);
			for (int i = 2; i <= m; i++) {
				out.print(" " + arr[start++]);
			}
			out.println();
		}
		out.flush();
		out.close();
		br.close();
	}

	public static void fall(int v, int x) {
		set(x - 3 * v + 1, x - 2 * v, 1, v, 1);
		set(x - 2 * v + 1, x, v - 1, -v, -1);
		set(x + 1, x + 2 * v, -v + 1, v, 1);
		set(x + 2 * v + 1, x + 3 * v - 1, v - 1, 1, -1);
	}

	public static void set(int l, int r, int s, int e, int d) {
		// 为了防止x - 3 * v + 1出现负数下标,进而有很多很烦的边界讨论
		// 所以任何位置,都加上一个较大的数字(OFFSET)
		// 这样一来,所有下标就都在0以上了,省去了大量边界讨论
		// 这就是为什么arr在初始化的时候要准备OFFSET + MAXN + OFFSET这么多的空间
		arr[l + OFFSET] += s;
		arr[l + 1 + OFFSET] += d - s;
		arr[r + 1 + OFFSET] -= d + e;
		arr[r + 2 + OFFSET] += e;
	}

	public static void build() {
		for (int i = 1; i <= m + OFFSET; i++) {
			arr[i] += arr[i - 1];
		}
		for (int i = 1; i <= m + OFFSET; i++) {
			arr[i] += arr[i - 1];
		}
	}

}

48. (必备)二维前缀和, 二维差分, 离散化技巧

package class048;

// 利用二维前缀和信息迅速得到二维区域和
// 测试链接 : https://leetcode.cn/problems/range-sum-query-2d-immutable/
public class Code01_PrefixSumMatrix {

	class NumMatrix {

		public int[][] sum;

		public NumMatrix(int[][] matrix) {
			int n = matrix.length;
			int m = matrix[0].length;
			sum = new int[n + 1][m + 1];
			for (int a = 1, c = 0; c < n; a++, c++) {
				for (int b = 1, d = 0; d < m; b++, d++) {
					sum[a][b] = matrix[c][d];
				}
			}
			for (int i = 1; i <= n; i++) {
				for (int j = 1; j <= m; j++) {
					sum[i][j] += sum[i][j - 1] + sum[i - 1][j] - sum[i - 1][j - 1];
				}
			}
		}

		public int sumRegion(int a, int b, int c, int d) {
			c++;
			d++;
			return sum[c][d] - sum[c][b] - sum[a][d] + sum[a][b];
		}

	}

}
package class048;

// 边框为1的最大正方形
// 给你一个由若干 0 和 1 组成的二维网格 grid
// 请你找出边界全部由 1 组成的最大 正方形 子网格
// 并返回该子网格中的元素数量。如果不存在,则返回 0。
// 测试链接 : https://leetcode.cn/problems/largest-1-bordered-square/
public class Code02_LargestOneBorderedSquare {

	// 打败比例不高,但完全是常数时间的问题
	// 时间复杂度O(n * m * min(n,m)),额外空间复杂度O(1)
	// 复杂度指标上绝对是最优解
	public static int largest1BorderedSquare(int[][] g) {
		int n = g.length;
		int m = g[0].length;
		build(n, m, g);
		if (sum(g, 0, 0, n - 1, m - 1) == 0) {
			return 0;
		}
		// 找到的最大合法正方形的边长
		int ans = 1;
		for (int a = 0; a < n; a++) {
			for (int b = 0; b < m; b++) {
				// (a,b)所有左上角点
				//     (c,d)更大边长的右下角点,k是当前尝试的边长
				for (int c = a + ans, d = b + ans, k = ans + 1; c < n && d < m; c++, d++, k++) {
					if (sum(g, a, b, c, d) - sum(g, a + 1, b + 1, c - 1, d - 1) == (k - 1) << 2) {
						ans = k;
					}
				}
			}
		}
		return ans * ans;
	}

	// g : 原始二维数组
	// 把g变成原始二维数组的前缀和数组sum,复用自己
	// 不能补0行,0列,都是0
	public static void build(int n, int m, int[][] g) {
		for (int i = 0; i < n; i++) {
			for (int j = 0; j < m; j++) {
				g[i][j] += get(g, i, j - 1) + get(g, i - 1, j) - get(g, i - 1, j - 1);
			}
		}
	}

	public static int sum(int[][] g, int a, int b, int c, int d) {
		return a > c ? 0 : (g[c][d] - get(g, c, b - 1) - get(g, a - 1, d) + get(g, a - 1, b - 1));
	}

	public static int get(int[][] g, int i, int j) {
		return (i < 0 || j < 0) ? 0 : g[i][j];
	}

}
package class048;

// 二维差分模版(洛谷)
// 测试链接 : https://www.luogu.com.cn/problem/P3397
// 请同学们务必参考如下代码中关于输入、输出的处理
// 这是输入输出处理效率很高的写法
// 提交以下的code,提交时请把类名改成"Main",可以直接通过

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StreamTokenizer;

public class Code03_DiffMatrixLuogu {

	public static int MAXN = 1002;

	public static int[][] diff = new int[MAXN][MAXN];

	public static int n, q;

	public static void add(int a, int b, int c, int d, int k) {
		diff[a][b] += k;
		diff[c + 1][b] -= k;
		diff[a][d + 1] -= k;
		diff[c + 1][d + 1] += k;
	}

	public static void build() {
		for (int i = 1; i <= n; i++) {
			for (int j = 1; j <= n; j++) {
				diff[i][j] += diff[i - 1][j] + diff[i][j - 1] - diff[i - 1][j - 1];
			}
		}
	}

	public static void clear() {
		for (int i = 1; i <= n + 1; i++) {
			for (int j = 1; j <= n + 1; j++) {
				diff[i][j] = 0;
			}
		}
	}

	public static void main(String[] args) throws IOException {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		StreamTokenizer in = new StreamTokenizer(br);
		PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
		while (in.nextToken() != StreamTokenizer.TT_EOF) {
			n = (int) in.nval;
			in.nextToken();
			q = (int) in.nval;
			for (int i = 1, a, b, c, d; i <= q; i++) {
				in.nextToken();
				a = (int) in.nval;
				in.nextToken();
				b = (int) in.nval;
				in.nextToken();
				c = (int) in.nval;
				in.nextToken();
				d = (int) in.nval;
				add(a, b, c, d, 1);
			}
			build();
			for (int i = 1; i <= n; i++) {
				out.print(diff[i][1]);
				for (int j = 2; j <= n; j++) {
					out.print(" " + diff[i][j]);
				}
				out.println();
			}
			clear();
		}
		out.flush();
		out.close();
		br.close();
	}

}
package class048;

// 二维差分模版(牛客)
// 测试链接 : https://www.nowcoder.com/practice/50e1a93989df42efb0b1dec386fb4ccc
// 请同学们务必参考如下代码中关于输入、输出的处理
// 这是输入输出处理效率很高的写法
// 提交以下的code,提交时请把类名改成"Main",可以直接通过

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StreamTokenizer;

public class Code03_DiffMatrixNowcoder {

	public static int MAXN = 1005;

	public static int MAXM = 1005;

	public static long[][] diff = new long[MAXN][MAXM];

	public static int n, m, q;

	public static void add(int a, int b, int c, int d, int k) {
		diff[a][b] += k;
		diff[c + 1][b] -= k;
		diff[a][d + 1] -= k;
		diff[c + 1][d + 1] += k;
	}

	public static void build() {
		for (int i = 1; i <= n; i++) {
			for (int j = 1; j <= m; j++) {
				diff[i][j] += diff[i - 1][j] + diff[i][j - 1] - diff[i - 1][j - 1];
			}
		}
	}

	public static void clear() {
		for (int i = 1; i <= n + 1; i++) {
			for (int j = 1; j <= m + 1; j++) {
				diff[i][j] = 0;
			}
		}
	}

	public static void main(String[] args) throws IOException {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		StreamTokenizer in = new StreamTokenizer(br);
		PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
		while (in.nextToken() != StreamTokenizer.TT_EOF) {
			n = (int) in.nval;
			in.nextToken();
			m = (int) in.nval;
			in.nextToken();
			q = (int) in.nval;
			for (int i = 1; i <= n; i++) {
				for (int j = 1; j <= m; j++) {
					in.nextToken();
					add(i, j, i, j, (int) in.nval);
				}
			}
			for (int i = 1, a, b, c, d, k; i <= q; i++) {
				in.nextToken();
				a = (int) in.nval;
				in.nextToken();
				b = (int) in.nval;
				in.nextToken();
				c = (int) in.nval;
				in.nextToken();
				d = (int) in.nval;
				in.nextToken();
				k = (int) in.nval;
				add(a, b, c, d, k);
			}
			build();
			for (int i = 1; i <= n; i++) {
				out.print(diff[i][1]);
				for (int j = 2; j <= m; j++) {
					out.print(" " + diff[i][j]);
				}
				out.println();
			}
			clear();
		}
		out.flush();
		out.close();
		br.close();
	}

}
package class048;

// 用邮票贴满网格图
// 给你一个 m * n 的二进制矩阵 grid
// 每个格子要么为 0 (空)要么为 1 (被占据)
// 给你邮票的尺寸为 stampHeight * stampWidth
// 我们想将邮票贴进二进制矩阵中,且满足以下 限制 和 要求 :
// 覆盖所有空格子,不覆盖任何被占据的格子
// 可以放入任意数目的邮票,邮票可以相互有重叠部分
// 邮票不允许旋转,邮票必须完全在矩阵内
// 如果在满足上述要求的前提下,可以放入邮票,请返回 true ,否则返回 false
// 测试链接 : https://leetcode.cn/problems/stamping-the-grid/
public class Code04_StampingTheGrid {

	// 时间复杂度O(n*m),额外空间复杂度O(n*m)
	public static boolean possibleToStamp(int[][] grid, int h, int w) {
		int n = grid.length;
		int m = grid[0].length;
		// sum是前缀和数组
		// 查询原始矩阵中的某个范围的累加和很快速
		int[][] sum = new int[n + 1][m + 1];
		for (int i = 0; i < n; i++) {
			for (int j = 0; j < m; j++) {
				sum[i + 1][j + 1] = grid[i][j];
			}
		}
		build(sum);
		// 差分矩阵
		// 当贴邮票的时候,不再原始矩阵里贴,在差分矩阵里贴
		// 原始矩阵就用来判断能不能贴邮票,不进行修改
		// 每贴一张邮票都在差分矩阵里修改
		int[][] diff = new int[n + 2][m + 2];
		for (int a = 1, c = a + h - 1; c <= n; a++, c++) {
			for (int b = 1, d = b + w - 1; d <= m; b++, d++) {
				// 原始矩阵中 (a,b)左上角点
				// 根据邮票规格,h、w,算出右下角点(c,d)
				// 这个区域彻底都是0,那么: 
				// sumRegion(sum, a, b, c, d) == 0
				// 那么此时这个区域可以贴邮票
				if (sumRegion(sum, a, b, c, d) == 0) {
					add(diff, a, b, c, d);
				}
			}
		}
		build(diff);
		// 检查所有的格子!
		for (int i = 0; i < n; i++) {
			for (int j = 0; j < m; j++) {
				// 原始矩阵里:grid[i][j] == 0,说明是个洞
				// 差分矩阵里:diff[i + 1][j + 1] == 0,说明洞上并没有邮票
				// 此时返回false
				if (grid[i][j] == 0 && diff[i + 1][j + 1] == 0) {
					return false;
				}
			}
		}
		return true;
	}

	public static void build(int[][] m) {
		for (int i = 1; i < m.length; i++) {
			for (int j = 1; j < m[0].length; j++) {
				m[i][j] += m[i - 1][j] + m[i][j - 1] - m[i - 1][j - 1];
			}
		}
	}

	public static int sumRegion(int[][] sum, int a, int b, int c, int d) {
		return sum[c][d] - sum[c][b - 1] - sum[a - 1][d] + sum[a - 1][b - 1];
	}

	public static void add(int[][] diff, int a, int b, int c, int d) {
		diff[a][b] += 1;
		diff[c + 1][d + 1] += 1;
		diff[c + 1][b] -= 1;
		diff[a][d + 1] -= 1;
	}

}
package class048;

import java.util.Arrays;

// 最强祝福力场
// 小扣在探索丛林的过程中,无意间发现了传说中"落寞的黄金之都"
// 而在这片建筑废墟的地带中,小扣使用探测仪监测到了存在某种带有「祝福」效果的力场
// 经过不断的勘测记录,小扣将所有力场的分布都记录了下来
// forceField[i] = [x,y,side] 
// 表示第 i 片力场将覆盖以坐标 (x,y) 为中心,边长为 side 的正方形区域。
// 若任意一点的 力场强度 等于覆盖该点的力场数量
// 请求出在这片地带中 力场强度 最强处的 力场强度
// 注意:力场范围的边缘同样被力场覆盖。
// 测试链接 : https://leetcode.cn/problems/xepqZ5/
public class Code05_StrongestForceField {

	// 时间复杂度O(n^2),额外空间复杂度O(n^2),n是力场的个数
	public static int fieldOfGreatestBlessing(int[][] fields) {
		int n = fields.length;
		// n : 矩形的个数,x 2*n个坐标
		long[] xs = new long[n << 1];
		long[] ys = new long[n << 1];
		for (int i = 0, k = 0, p = 0; i < n; i++) {
			long x = fields[i][0];
			long y = fields[i][1];
			long r = fields[i][2];
			xs[k++] = (x << 1) - r;
			xs[k++] = (x << 1) + r;
			ys[p++] = (y << 1) - r;
			ys[p++] = (y << 1) + r;
		}
		// xs数组中,排序了且相同值只留一份,返回有效长度
		int sizex = sort(xs);
		// ys数组中,排序了且相同值只留一份,返回有效长度
		int sizey = sort(ys);
		// n个力场,sizex : 2 * n, sizey : 2 * n
		int[][] diff = new int[sizex + 2][sizey + 2];
		for (int i = 0, a, b, c, d; i < n; i++) {
			long x = fields[i][0];
			long y = fields[i][1];
			long r = fields[i][2];
			a = rank(xs, (x << 1) - r, sizex);
			b = rank(ys, (y << 1) - r, sizey);
			c = rank(xs, (x << 1) + r, sizex);
			d = rank(ys, (y << 1) + r, sizey);
			add(diff, a, b, c, d);
		}
		int ans = 0;
		// O(n^2)
		for (int i = 1; i < diff.length; i++) {
			for (int j = 1; j < diff[0].length; j++) {
				diff[i][j] += diff[i - 1][j] + diff[i][j - 1] - diff[i - 1][j - 1];
				ans = Math.max(ans, diff[i][j]);
			}
		}
		return ans;
	}

	// [50,70,30,70,30,60] 长度6
	// [30,30,50,60,70,70]
	// [30,50,60,70] 60 -> 3
	//  1  2  3  4
	// 长度4,
 	public static int sort(long[] nums) {
		Arrays.sort(nums);
		int size = 1;
		for (int i = 1; i < nums.length; i++) {
			if (nums[i] != nums[size - 1]) {
				nums[size++] = nums[i];
			}
		}
		return size;
	}

 	// nums 有序数组,有效长度是size,0~size-1范围上无重复值
 	// 已知v一定在nums[0~size-1],返回v所对应的编号
	public static int rank(long[] nums, long v, int size) {
		int l = 0;
		int r = size - 1;
		int m, ans = 0;
		while (l <= r) {
			m = (l + r) / 2;
			if (nums[m] >= v) {
				ans = m;
				r = m - 1;
			} else {
				l = m + 1;
			}
		}
		return ans + 1;
	}

	// 二维差分
	public static void add(int[][] diff, int a, int b, int c, int d) {
		diff[a][b] += 1;
		diff[c + 1][d + 1] += 1;
		diff[c + 1][b] -= 1;
		diff[a][d + 1] -= 1;
	}

}

49. (必备)滑动窗口技巧以及相关题目

package class049;

// 累加和大于等于target的最短子数组长度
// 给定一个含有 n 个正整数的数组和一个正整数 target
// 找到累加和 >= target 的长度最小的子数组并返回其长度
// 如果不存在符合条件的子数组返回0
// 测试链接 : https://leetcode.cn/problems/minimum-size-subarray-sum/
public class Code01_MinimumSizeSubarraySum {

	public static int minSubArrayLen(int target, int[] nums) {
		int ans = Integer.MAX_VALUE;
		for (int l = 0, r = 0, sum = 0; r < nums.length; r++) {
			sum += nums[r];
			while (sum - nums[l] >= target) {
				// sum : nums[l....r]
				// 如果l位置的数从窗口出去,还能继续达标,那就出去
				sum -= nums[l++];
			}
			if (sum >= target) {
				ans = Math.min(ans, r - l + 1);
			}
		}
		return ans == Integer.MAX_VALUE ? 0 : ans;
	}

}
package class049;

import java.util.Arrays;

// 无重复字符的最长子串
// 给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。
// 测试链接 : https://leetcode.cn/problems/longest-substring-without-repeating-characters/
public class Code02_LongestSubstringWithoutRepeatingCharacters {

	public static int lengthOfLongestSubstring(String str) {
		char[] s = str.toCharArray();
		int n = s.length;
		// char -> int -> 0 ~ 255
		// 每一种字符上次出现的位置
		int[] last = new int[256];
		// 所有字符都没有上次出现的位置
		Arrays.fill(last, -1);
		// 不含有重复字符的 最长子串 的长度
		int ans = 0;
		for (int l = 0, r = 0; r < n; r++) {
			l = Math.max(l, last[s[r]] + 1);
			ans = Math.max(ans, r - l + 1);
			// 更新当前字符上一次出现的位置
			last[s[r]] = r;
		}
		return ans;
	}

}
package class049;

// 最小覆盖子串
// 给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串
// 如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 "" 。
// 测试链接 : https://leetcode.cn/problems/minimum-window-substring/
public class Code03_MinimumWindowSubstring {

	public static String minWindow(String str, String tar) {
		if (str.length() < tar.length()) {
			return "";
		}
		char[] s = str.toCharArray();
		char[] t = tar.toCharArray();
		int[] cnts = new int[256];
		for (char cha : t) {
			cnts[cha]--;
		}
		// 最小覆盖子串的长度
		int len = Integer.MAX_VALUE;
		// 从哪个位置开头,发现的这个最小覆盖子串
		int start = 0;
		for (int l = 0, r = 0, debt = t.length; r < s.length; r++) {
			// s[r] 当前字符 -> int
			// cnts[s[r]] : 当前字符欠债情况,负数就是欠债,正数就是多给的
			if (cnts[s[r]]++ < 0) {
				debt--;
			}
			if (debt == 0) {
				// r位置结尾,真的有覆盖子串!
				// 看看这个覆盖子串能不能尽量短
				while (cnts[s[l]] > 0) {
					// l位置的字符能拿回
					cnts[s[l++]]--;
				}
				// 从while里面出来,
				// l....r就是r位置结尾的最小覆盖子串
				if (r - l + 1 < len) {
					len = r - l + 1;
					start = l;
				}
			}
		}
		return len == Integer.MAX_VALUE ? "" : str.substring(start, start + len);
	}

}
package class049;

// 加油站
// 在一条环路上有 n 个加油站,其中第 i 个加油站有汽油 gas[i] 升。
// 你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升
// 你从其中的一个加油站出发,开始时油箱为空。
// 给定两个整数数组 gas 和 cost ,如果你可以按顺序绕环路行驶一周
// 则返回出发时加油站的编号,否则返回 -1
// 如果存在解,则 保证 它是 唯一 的。
// 测试链接 : https://leetcode.cn/problems/gas-station/
public class Code04_GasStation {

	public static int canCompleteCircuit(int[] gas, int[] cost) {
		int n = gas.length;
		// 车辆尝试从0~n-1出发,看能不能走一圈,l
		// r : 窗口即将进来数字的位置
		// len : 窗口大小
		// sum : 窗口累加和
		for (int l = 0, r = 0, len = 0, sum = 0; l < n; l++) {
			while (sum >= 0) {
				// 当前窗口累加和>=0,尝试扩
				if (len == n) {
					return l;
				}
				// r : 窗口即将进来数字的位置
				r = (l + (len++)) % n;
				sum += gas[r] - cost[r];
			}
			// sum < 0,此时l位置无法转一圈
			len--;
			sum -= gas[l] - cost[l];
		}
		return -1;
	}

}
package class049;

// 替换子串得到平衡字符串
// 有一个只含有 'Q', 'W', 'E', 'R' 四种字符,且长度为 n 的字符串。
// 假如在该字符串中,这四个字符都恰好出现 n/4 次,那么它就是一个「平衡字符串」。
// 给你一个这样的字符串 s,请通过「替换一个子串」的方式,使原字符串 s 变成一个「平衡字符串」。
// 你可以用和「待替换子串」长度相同的 任何 其他字符串来完成替换。
// 请返回待替换子串的最小可能长度。
// 如果原字符串自身就是一个平衡字符串,则返回 0。
// 测试链接 : https://leetcode.cn/problems/replace-the-substring-for-balanced-string/
public class Code05_ReplaceTheSubstringForBalancedString {

	// Q W E R
	// 0 1 2 3
	// "W Q Q R R E"
	// 1 0 0 3 3 2
	// cnts[1] = 1;
	// cnts[0] = 2;
	// cnts[2] = 1;
	// cnts[3] = 2;
	public static int balancedString(String str) {
		int n = str.length();
		int[] arr = new int[n];
		int[] cnts = new int[4];
		for (int i = 0; i < n; i++) {
			char c = str.charAt(i);
			arr[i] = c == 'W' ? 1 : (c == 'E' ? 2 : (c == 'R' ? 3 : 0));
			cnts[arr[i]]++;
		}
		// str : 长度是4的整数倍,n
		// 每种字符出现的个数 : n/4
		int require = n / 4;
		// 至少要修改多长的子串,才能做到四种字符一样多
		int ans = n;
		// 自由变化的窗口l....r
		for (int l = 0, r = 0; l < n; l++) {
			// l = 0, r= 0, 窗口0长度
			// l...r-1 : [l,r)
			while (!ok(cnts, r - l, require) && r < n) {
				// cnts : 窗口之外的统计
				cnts[arr[r++]]--;
			}
			// 1) l...r-1 [l,r) ,做到了!
			// 2) r == n,也没做到
			if (ok(cnts, r - l, require)) {
				ans = Math.min(ans, r - l);
			}
			// [l,r),不被cnts统计到的
			//   l+1
			cnts[arr[l]]++;
		}
		return ans;
	}

	// cnts : l...r范围上的字符不算!在自由变化的窗口之外,每一种字符的词频统计
	// len : 自由变化窗口的长度
	// require : 每一种字符都要达到的数量
	// 返回值 : 请问能不能做到
	public static boolean ok(int[] cnts, int len, int require) {
		for (int i = 0; i < 4; i++) {
			// 0 1 2 3
			if (cnts[i] > require) {
				return false;
			}
			// require - cnts[i] : 20 - 16 = 4
			len -= require - cnts[i];
		}
		return len == 0;
	}

}
package class049;

import java.util.Arrays;

// K个不同整数的子数组
// 给定一个正整数数组 nums和一个整数 k,返回 nums 中 「好子数组」 的数目。
// 如果 nums 的某个子数组中不同整数的个数恰好为 k
// 则称 nums 的这个连续、不一定不同的子数组为 「好子数组 」。
// 例如,[1,2,3,1,2] 中有 3 个不同的整数:1,2,以及 3。
// 子数组 是数组的 连续 部分。
// 测试链接 : https://leetcode.cn/problems/subarrays-with-k-different-integers/
public class Code06_SubarraysWithKDifferentIntegers {

	public static int subarraysWithKDistinct(int[] arr, int k) {
		return numsOfMostKinds(arr, k) - numsOfMostKinds(arr, k - 1);
	}

	public static int MAXN = 20001;

	public static int[] cnts = new int[MAXN];

	// arr中有多少子数组,数字种类不超过k
	// arr的长度是n,arr里的数值1~n之间
	public static int numsOfMostKinds(int[] arr, int k) {
		Arrays.fill(cnts, 1, arr.length + 1, 0);
		int ans = 0;
		for (int l = 0, r = 0, collect = 0; r < arr.length; r++) {
			// r(刚进)
			if (++cnts[arr[r]] == 1) {
				collect++;
			}
			// l.....r    要求不超过3种,已经4种,l往右(吐数字)
			while (collect > k) {
				if (--cnts[arr[l++]] == 0) {
					collect--;
				}
			}
			// l.....r不超过了
			// 0...3
			// 0~3
			// 1~3
			// 2~3
			// 3~3
			ans += r - l + 1;
		}
		return ans;
	}

}
package class049;

import java.util.Arrays;

// 至少有K个重复字符的最长子串
// 给你一个字符串 s 和一个整数 k ,请你找出 s 中的最长子串
// 要求该子串中的每一字符出现次数都不少于 k 。返回这一子串的长度
// 如果不存在这样的子字符串,则返回 0。
// 测试链接 : https://leetcode.cn/problems/longest-substring-with-at-least-k-repeating-characters/
public class Code07_LongestSubstringWithAtLeastKRepeating {

	public static int longestSubstring(String str, int k) {
		char[] s = str.toCharArray();
		int n = s.length;
		int[] cnts = new int[256];
		int ans = 0;
		// 每次要求子串必须含有require种字符,每种字符都必须>=k次,这样的最长子串是多长
		for (int require = 1; require <= 26; require++) {
			Arrays.fill(cnts, 0);
			// collect : 窗口中一共收集到的种类数
			// satisfy : 窗口中达标的种类数(次数>=k)
			for (int l = 0, r = 0, collect = 0, satisfy = 0; r < n; r++) {
				cnts[s[r]]++;
				if (cnts[s[r]] == 1) {
					collect++;
				}
				if (cnts[s[r]] == k) {
					satisfy++;
				}
				// l....r 种类超了!
				// l位置的字符,窗口中吐出来!
				while (collect > require) {
					if (cnts[s[l]] == 1) {
						collect--;
					}
					if (cnts[s[l]] == k) {
						satisfy--;
					}
					cnts[s[l++]]--;
				}
				// l.....r : 子串以r位置的字符结尾,且种类数不超的,最大长度!
				if (satisfy == require) {
					ans = Math.max(ans, r - l + 1);
				}
			}
		}
		return ans;
	}

}

50. (必备)双指针技巧及相关题目

package class050;

// 按奇偶排序数组II
// 给定一个非负整数数组 nums。nums 中一半整数是奇数 ,一半整数是偶数
// 对数组进行排序,以便当 nums[i] 为奇数时,i也是奇数
// 当 nums[i] 为偶数时, i 也是 偶数
// 你可以返回 任何满足上述条件的数组作为答案
// 测试链接 : https://leetcode.cn/problems/sort-array-by-parity-ii/
public class Code01_SortArrayByParityII {

	// 时间复杂度O(n),额外空间复杂度O(1)
	public static int[] sortArrayByParityII(int[] nums) {
		int n = nums.length;
		for (int odd = 1, even = 0; odd < n && even < n;) {
			if ((nums[n - 1] & 1) == 1) {
				swap(nums, odd, n - 1);
				odd += 2;
			} else {
				swap(nums, even, n - 1);
				even += 2;
			}
		}
		return nums;
	}

	public static void swap(int[] nums, int i, int j) {
		int tmp = nums[i];
		nums[i] = nums[j];
		nums[j] = tmp;
	}

}
package class050;

// 寻找重复数
// 给定一个包含 n + 1 个整数的数组 nums ,其数字都在 [1, n] 范围内(包括 1 和 n)
// 可知至少存在一个重复的整数。
// 假设 nums 只有 一个重复的整数 ,返回 这个重复的数 。
// 你设计的解决方案必须 不修改 数组 nums 且只用常量级 O(1) 的额外空间。
// 测试链接 : https://leetcode.cn/problems/find-the-duplicate-number/
public class Code02_FindTheDuplicateNumber {

	// 时间复杂度O(n),额外空间复杂度O(1)
	public static int findDuplicate(int[] nums) {
		if (nums == null || nums.length < 2) {
			return -1;
		}
		int slow = nums[0];
		int fast = nums[nums[0]];
		while (slow != fast) {
			slow = nums[slow];
			fast = nums[nums[fast]];
		}
		// 相遇了,快指针回开头
		fast = 0;
		while (slow != fast) {
			fast = nums[fast];
			slow = nums[slow];
		}
		return slow;
	}

}
package class050;

// 接雨水
// 给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水
// 测试链接 : https://leetcode.cn/problems/trapping-rain-water/
public class Code03_TrappingRainWater {

	// 辅助数组的解法(不是最优解)
	// 时间复杂度O(n),额外空间复杂度O(n)
	// 提交时改名为trap
	public static int trap1(int[] nums) {
		int n = nums.length;
		int[] lmax = new int[n];
		int[] rmax = new int[n];
		lmax[0] = nums[0];
		// 0~i范围上的最大值,记录在lmax[i]
		for (int i = 1; i < n; i++) {
			lmax[i] = Math.max(lmax[i - 1], nums[i]);
		}
		rmax[n - 1] = nums[n - 1];
		// i~n-1范围上的最大值,记录在rmax[i]
		for (int i = n - 2; i >= 0; i--) {
			rmax[i] = Math.max(rmax[i + 1], nums[i]);
		}
		int ans = 0;
		//   x              x
		//   0 1 2 3...n-2 n-1
		for (int i = 1; i < n - 1; i++) {
			ans += Math.max(0, Math.min(lmax[i - 1], rmax[i + 1]) - nums[i]);
		}
		return ans;
	}

	// 双指针的解法(最优解)
	// 时间复杂度O(n),额外空间复杂度O(1)
	// 提交时改名为trap
	public static int trap2(int[] nums) {
		int l = 1, r = nums.length - 2, lmax = nums[0], rmax = nums[nums.length - 1];
		int ans = 0;
		while (l <= r) {
			if (lmax <= rmax) {
				ans += Math.max(0, lmax - nums[l]);
				lmax = Math.max(lmax, nums[l++]);
			} else {
				ans += Math.max(0, rmax - nums[r]);
				rmax = Math.max(rmax, nums[r--]);
			}
		}
		return ans;
	}

}
package class050;

import java.util.Arrays;

// 救生艇
// 给定数组 people
// people[i]表示第 i 个人的体重 ,船的数量不限,每艘船可以承载的最大重量为 limit
// 每艘船最多可同时载两人,但条件是这些人的重量之和最多为 limit
// 返回 承载所有人所需的最小船数
// 测试链接 : https://leetcode.cn/problems/boats-to-save-people/
public class Code04_BoatsToSavePeople {

	// 时间复杂度O(n * logn),因为有排序,额外空间复杂度O(1)
	public static int numRescueBoats(int[] people, int limit) {
		Arrays.sort(people);
		int ans = 0;
		int l = 0;
		int r = people.length - 1;
		int sum = 0;
		while (l <= r) {
			sum = l == r ? people[l] : people[l] + people[r];
			if (sum > limit) {
				r--;
			} else {
				l++;
				r--;
			}
			ans++;
		}
		return ans;
	}

}
package class050;

// 盛最多水的容器
// 给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。
// 找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水
// 返回容器可以储存的最大水量
// 说明:你不能倾斜容器
// 测试链接 : https://leetcode.cn/problems/container-with-most-water/
public class Code05_ContainerWithMostWater {

	// 时间复杂度O(n),额外空间复杂度O(1)
	public static int maxArea(int[] height) {
		int ans = 0;
		for (int l = 0, r = height.length - 1; l < r;) {
			ans = Math.max(ans, Math.min(height[l], height[r]) * (r - l));
			if (height[l] <= height[r]) {
				l++;
			} else {
				r--;
			}
		}
		return ans;
	}

}
package class050;

import java.util.Arrays;

// 供暖器
// 冬季已经来临。 你的任务是设计一个有固定加热半径的供暖器向所有房屋供暖。
// 在加热器的加热半径范围内的每个房屋都可以获得供暖。
// 现在,给出位于一条水平线上的房屋 houses 和供暖器 heaters 的位置
// 请你找出并返回可以覆盖所有房屋的最小加热半径。
// 说明:所有供暖器都遵循你的半径标准,加热的半径也一样。
// 测试链接 : https://leetcode.cn/problems/heaters/
public class Code06_Heaters {

	// 时间复杂度O(n * logn),因为有排序,额外空间复杂度O(1)
	public static int findRadius(int[] houses, int[] heaters) {
		Arrays.sort(houses);
		Arrays.sort(heaters);
		int ans = 0;
		for (int i = 0, j = 0; i < houses.length; i++) {
			// i号房屋
			// j号供暖器
			while (!best(houses, heaters, i, j)) {
				j++;
			}
			ans = Math.max(ans, Math.abs(heaters[j] - houses[i]));
		}
		return ans;
	}

	// 这个函数含义:
	// 当前的地点houses[i]由heaters[j]来供暖是最优的吗?
	// 当前的地点houses[i]由heaters[j]来供暖,产生的半径是a
	// 当前的地点houses[i]由heaters[j + 1]来供暖,产生的半径是b
	// 如果a < b, 说明是最优,供暖不应该跳下一个位置
	// 如果a >= b, 说明不是最优,应该跳下一个位置
	public static boolean best(int[] houses, int[] heaters, int i, int j) {
		return j == heaters.length - 1 
				||
			   Math.abs(heaters[j] - houses[i]) < Math.abs(heaters[j + 1] - houses[i]);
	}

}
package class050;

// 缺失的第一个正数
// 给你一个未排序的整数数组 nums ,请你找出其中没有出现的最小的正整数。
// 请你实现时间复杂度为 O(n) 并且只使用常数级别额外空间的解决方案。
// 测试链接 : https://leetcode.cn/problems/first-missing-positive/
public class Code07_FirstMissingPositive {

	// 时间复杂度O(n),额外空间复杂度O(1)
	public static int firstMissingPositive(int[] arr) {
		// l的左边,都是做到i位置上放着i+1的区域
		// 永远盯着l位置的数字看,看能不能扩充(l++)
		int l = 0;
		// [r....]垃圾区
		// 最好的状况下,认为1~r是可以收集全的,每个数字收集1个,不能有垃圾
		// 有垃圾呢?预期就会变差(r--)
		int r = arr.length;
		while (l < r) {
			if (arr[l] == l + 1) {
				l++;
			} else if (arr[l] <= l || arr[l] > r || arr[arr[l] - 1] == arr[l]) {
				swap(arr, l, --r);
			} else {
				swap(arr, l, arr[l] - 1);
			}
		}
		return l + 1;
	}

	public static void swap(int[] arr, int i, int j) {
		int tmp = arr[i];
		arr[i] = arr[j];
		arr[j] = tmp;
	}

}

51. (必备)二分答案法及相关题目

package class051;

// 爱吃香蕉的珂珂
// 珂珂喜欢吃香蕉。这里有 n 堆香蕉,第 i 堆中有 piles[i] 根香蕉
// 警卫已经离开了,将在 h 小时后回来。
// 珂珂可以决定她吃香蕉的速度 k (单位:根/小时)
// 每个小时,她将会选择一堆香蕉,从中吃掉 k 根
// 如果这堆香蕉少于 k 根,她将吃掉这堆的所有香蕉,然后这一小时内不会再吃更多的香蕉
// 珂珂喜欢慢慢吃,但仍然想在警卫回来前吃掉所有的香蕉。
// 返回她可以在 h 小时内吃掉所有香蕉的最小速度 k(k 为整数)
// 测试链接 : https://leetcode.cn/problems/koko-eating-bananas/
public class Code01_KokoEatingBananas {

	// 时间复杂度O(n * log(max)),额外空间复杂度O(1)
	public static int minEatingSpeed(int[] piles, int h) {
		// 最小且达标的速度,范围[l,r]
		int l = 1;
		int r = 0;
		for (int pile : piles) {
			r = Math.max(r, pile);
		}
		// [l,r]不停二分
		int ans = 0;
		int m = 0;
		while (l <= r) {
			// m = (l + r) / 2
			m = l + ((r - l) >> 1);
			if (f(piles, m) <= h) {
				// 达标!
				// 记录答案,去左侧二分
				ans = m;
				// l....m....r
				// l..m-1
				r = m - 1;
			} else {
				// 不达标
				l = m + 1;
			}
		}
		return ans;
	}

	// 香蕉重量都在piles
	// 速度就定成speed
	// 返回吃完所有的香蕉,耗费的小时数量
	public static long f(int[] piles, int speed) {
		long ans = 0;
		for (int pile : piles) {
			// (a/b)结果向上取整,如果a和b都是非负数,可以写成(a+b-1)/b
			// "讲解032-位图"讲了这种写法,不会的同学可以去看看
			// 这里不再赘述
			ans += (pile + speed - 1) / speed;
		}
		return ans;
	}

}
package class051;

// 分割数组的最大值(画匠问题)
// 给定一个非负整数数组 nums 和一个整数 m
// 你需要将这个数组分成 m 个非空的连续子数组。
// 设计一个算法使得这 m 个子数组各自和的最大值最小。
// 测试链接 : https://leetcode.cn/problems/split-array-largest-sum/
public class Code02_SplitArrayLargestSum {

	// 时间复杂度O(n * log(sum)),额外空间复杂度O(1)
	public static int splitArray(int[] nums, int k) {
		long sum = 0;
		for (int num : nums) {
			sum += num;
		}
		long ans = 0;
		// [0,sum]二分
		for (long l = 0, r = sum, m, need; l <= r;) {
			// 中点m
			m = l + ((r - l) >> 1);
			// 必须让数组每一部分的累加和 <= m,请问划分成几个部分才够!
			need = f(nums, m);
			if (need <= k) {
				// 达标
				ans = m;
				r = m - 1;
			} else {
				l = m + 1;
			}
		}
		return (int) ans;
	}

	// 必须让数组arr每一部分的累加和 <= limit,请问划分成几个部分才够!
	// 返回需要的部分数量
	public static int f(int[] arr, long limit) {
		int parts = 1;
		int sum = 0;
		for (int num : arr) {
			if (num > limit) {
				return Integer.MAX_VALUE;
			}
			if (sum + num > limit) {
				parts++;
				sum = num;
			} else {
				sum += num;
			}
		}
		return parts;
	}

}
package class051;

// 机器人跳跃问题
// 机器人正在玩一个古老的基于DOS的游戏
// 游戏中有N+1座建筑,从0到N编号,从左到右排列
// 编号为0的建筑高度为0个单位,编号为i的建筑的高度为H(i)个单位
// 起初机器人在编号为0的建筑处
// 每一步,它跳到下一个(右边)建筑。假设机器人在第k个建筑,且它现在的能量值是E
// 下一步它将跳到第个k+1建筑
// 它将会得到或者失去正比于与H(k+1)与E之差的能量
// 如果 H(k+1) > E 那么机器人就失去H(k+1)-E的能量值,否则它将得到E-H(k+1)的能量值
// 游戏目标是到达第个N建筑,在这个过程中,能量值不能为负数个单位
// 现在的问题是机器人以多少能量值开始游戏,才可以保证成功完成游戏
// 测试链接 : https://www.nowcoder.com/practice/7037a3d57bbd4336856b8e16a9cafd71
// 请同学们务必参考如下代码中关于输入、输出的处理
// 这是输入输出处理效率很高的写法
// 提交以下的code,提交时请把类名改成"Main",可以直接通过

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StreamTokenizer;

public class Code03_RobotPassThroughBuilding {

	public static int MAXN = 100001;

	public static int[] arr = new int[MAXN];

	public static int n;

	public static void main(String[] args) throws IOException {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		StreamTokenizer in = new StreamTokenizer(br);
		PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
		while (in.nextToken() != StreamTokenizer.TT_EOF) {
			n = (int) in.nval;
			int l = 0;
			int r = 0;
			for (int i = 1; i <= n; i++) {
				in.nextToken();
				arr[i] = (int) in.nval;
				r = Math.max(r, arr[i]);
			}
			out.println(compute(l, r, r));
		}
		out.flush();
		out.close();
		br.close();
	}

	// [l,r]通关所需最小能量的范围,不停二分
	// max是所有建筑的最大高度
	// 时间复杂度O(n * log(max)),额外空间复杂度O(1)
	public static int compute(int l, int r, int max) {
		int m, ans = -1;
		while (l <= r) {
			// m中点,此时通关所需规定的初始能量
			m = l + ((r - l) >> 1);
			if (f(m, max)) {
				ans = m;
				r = m - 1;
			} else {
				l = m + 1;
			}
		}
		return ans;
	}

	// 初始能量为energy,max是建筑的最大高度,返回能不能通关
	// 为什么要给定建筑的最大高度?
	public static boolean f(int energy, int max) {
		// 注意!
		// 如果给的能量值很大,那么后续能量增长将非常恐怖
		// 完全有可能超出long的范围
		// 所以要在遍历时,一定要加入energy >= max的判断
		// 一旦能量超过高度最大值,后面肯定通关了,可以提前返回了
		// 这里很阴
		for (int i = 1; i <= n; i++) {
			if (energy <= arr[i]) {
				energy -= arr[i] - energy;
			} else {
				energy += energy - arr[i];
			}
			if (energy >= max) {
				return true;
			}
			if (energy < 0) {
				return false;
			}
		}
		return true;
	}

}
package class051;

import java.util.Arrays;

// 找出第K小的数对距离
// 数对 (a,b) 由整数 a 和 b 组成,其数对距离定义为 a 和 b 的绝对差值。
// 给你一个整数数组 nums 和一个整数 k
// 数对由 nums[i] 和 nums[j] 组成且满足 0 <= i < j < nums.length
// 返回 所有数对距离中 第 k 小的数对距离。
// 测试链接 : https://leetcode.cn/problems/find-k-th-smallest-pair-distance/
public class Code04_FindKthSmallestPairDistance {

	// 时间复杂度O(n * log(n) + log(max-min) * n),额外空间复杂度O(1)
	public static int smallestDistancePair(int[] nums, int k) {
		int n = nums.length;
		Arrays.sort(nums);
		int ans = 0;
		// [0, 最大-最小],不停二分
		for (int l = 0, r = nums[n - 1] - nums[0], m, cnt; l <= r;) {
			// m中点,arr中任意两数的差值 <= m
			m = l + ((r - l) >> 1);
			// 返回数字对的数量
			cnt = f(nums, m);
			if (cnt >= k) {
				ans = m;
				r = m - 1;
			} else {
				l = m + 1;
			}
		}
		return ans;
	}

	// arr中任意两数的差值 <= limit
	// 这样的数字配对,有几对?
	public static int f(int[] arr, int limit) {
		int ans = 0;
		// O(n)
		for (int l = 0, r = 0; l < arr.length; l++) {
			// l......r r+1
			while (r + 1 < arr.length && arr[r + 1] - arr[l] <= limit) {
				r++;
			}
			// arr[l...r]范围上的数差值的绝对值都不超过limit
			// arr[0...3]
			// 0,1
			// 0,2
			// 0,3
			ans += r - l;
		}
		return ans;
	}

}
package class051;

// 同时运行N台电脑的最长时间
// 你有 n 台电脑。给你整数 n 和一个下标从 0 开始的整数数组 batteries
// 其中第 i 个电池可以让一台电脑 运行 batteries[i] 分钟
// 你想使用这些电池让 全部 n 台电脑 同时 运行。
// 一开始,你可以给每台电脑连接 至多一个电池
// 然后在任意整数时刻,你都可以将一台电脑与它的电池断开连接,并连接另一个电池,你可以进行这个操作 任意次
// 新连接的电池可以是一个全新的电池,也可以是别的电脑用过的电池
// 断开连接和连接新的电池不会花费任何时间。
// 注意,你不能给电池充电。
// 请你返回你可以让 n 台电脑同时运行的 最长 分钟数。
// 测试链接 : https://leetcode.cn/problems/maximum-running-time-of-n-computers/
public class Code05_MaximumRunningTimeOfNComputers {

	// 单纯的二分答案法
	// 提交时把函数名改为maxRunTime
	// 时间复杂度O(n * log(sum)),额外空间复杂度O(1)
	public static long maxRunTime1(int num, int[] arr) {
		long sum = 0;
		for (int x : arr) {
			sum += x;
		}
		long ans = 0;
		// [0, sum],不停二分
		for (long l = 0, r = sum, m; l <= r;) {
			// m中点,让num台电脑共同运行m分钟,能不能做到
			m = l + ((r - l) >> 1);
			if (f1(arr, num, m)) {
				ans = m;
				l = m + 1;
			} else {
				r = m - 1;
			}
		}
		return ans;
	}

	// 让num台电脑共同运行time分钟,能不能做到
	public static boolean f1(int[] arr, int num, long time) {
		// 碎片电量总和
		long sum = 0;
		for (int x : arr) {
			if (x > time) {
				num--;
			} else {
				// x <= time,是碎片电池
				sum += x;
			}
			if (sum >= (long) num * time) {
				// 碎片电量 >= 台数 * 要求
				return true;
			}
		}
		return false;
	}

	// 二分答案法 + 增加分析(贪心)
	// 提交时把函数名改为maxRunTime
	// 时间复杂度O(n * log(max)),额外空间复杂度O(1)
	public static long maxRunTime2(int num, int[] arr) {
		int max = 0;
		long sum = 0;
		for (int x : arr) {
			max = Math.max(max, x);
			sum += x;
		}
		// 就是增加了这里的逻辑
		if (sum > (long) max * num) {
			// 所有电池的最大电量是max
			// 如果此时sum > (long) max * num,
			// 说明 : 最终的供电时间一定在 >= max,而如果最终的供电时间 >= max
			// 说明 : 对于最终的答案X来说,所有电池都是课上讲的"碎片拼接"的概念
			// 那么寻找 ? * num <= sum 的情况中,尽量大的 ? 即可
			// 即sum / num
			return sum / num;
		}
		// 最终的供电时间一定在 < max范围上
		// [0, sum]二分范围,可能定的比较粗,虽然不影响,但毕竟是有点慢
		// [0, max]二分范围!更精细的范围,二分次数会变少
		int ans = 0;
		for (int l = 0, r = max, m; l <= r;) {
			m = l + ((r - l) >> 1);
			if (f2(arr, num, m)) {
				ans = m;
				l = m + 1;
			} else {
				r = m - 1;
			}
		}
		return ans;
	}

	public static boolean f2(int[] arr, int num, int time) {
		// 碎片电量总和
		long sum = 0;
		for (int x : arr) {
			if (x > time) {
				num--;
			} else {
				sum += x;
			}
			if (sum >= (long) num * time) {
				return true;
			}
		}
		return false;
	}

}
package class051;

import java.util.PriorityQueue;

// 计算等位时间
// 给定一个数组arr长度为n,表示n个服务员,每服务一个人的时间
// 给定一个正数m,表示有m个人等位,如果你是刚来的人,请问你需要等多久?
// 假设m远远大于n,比如n <= 10^3, m <= 10^9,该怎么做是最优解?
// 谷歌的面试,这个题连考了2个月
// 找不到测试链接,所以用对数器验证
public class Code06_WaitingTime {

	// 堆模拟
	// 验证方法,不是重点
	// 如果m很大,该方法会超时
	// 时间复杂度O(m * log(n)),额外空间复杂度O(n)
	public static int waitingTime1(int[] arr, int m) {
		// 一个一个对象int[]
		// [醒来时间,服务一个客人要多久]
		PriorityQueue<int[]> heap = new PriorityQueue<>((a, b) -> (a[0] - b[0]));
		int n = arr.length;
		for (int i = 0; i < n; i++) {
			heap.add(new int[] { 0, arr[i] });
		}
		for (int i = 0; i < m; i++) {
			int[] cur = heap.poll();
			cur[0] += cur[1];
			heap.add(cur);
		}
		return heap.peek()[0];
	}

	// 二分答案法
	// 最优解
	// 时间复杂度O(n * log(min * w)),额外空间复杂度O(1)
	public static int waitingTime2(int[] arr, int w) {
		int min = Integer.MAX_VALUE;
		for (int x : arr) {
			min = Math.min(min, x);
		}
		int ans = 0;
		for (int l = 0, r = min * w, m; l <= r;) {
			// m中点,表示一定要让服务员工作的时间!
			m = l + ((r - l) >> 1);
			// 能够给几个客人提供服务
			if (f(arr, m) >= w + 1) {
				ans = m;
				r = m - 1;
			} else {
				l = m + 1;
			}
		}
		return ans;
	}

	// 如果每个服务员工作time,可以接待几位客人(结束的、开始的客人都算)
	public static int f(int[] arr, int time) {
		int ans = 0;
		for (int num : arr) {
			ans += (time / num) + 1;
		}
		return ans;
	}

	// 对数器测试
	public static void main(String[] args) {
		System.out.println("测试开始");
		int N = 50;
		int V = 30;
		int M = 3000;
		int testTime = 20000;
		for (int i = 0; i < testTime; i++) {
			int n = (int) (Math.random() * N) + 1;
			int[] arr = randomArray(n, V);
			int m = (int) (Math.random() * M);
			int ans1 = waitingTime1(arr, m);
			int ans2 = waitingTime2(arr, m);
			if (ans1 != ans2) {
				System.out.println("出错了!");
			}
		}
		System.out.println("测试结束");
	}

	// 对数器测试
	public static int[] randomArray(int n, int v) {
		int[] arr = new int[n];
		for (int i = 0; i < n; i++) {
			arr[i] = (int) (Math.random() * v) + 1;
		}
		return arr;
	}

}
package class051;

// 刀砍毒杀怪兽问题
// 怪兽的初始血量是一个整数hp,给出每一回合刀砍和毒杀的数值cuts和poisons
// 第i回合如果用刀砍,怪兽在这回合会直接损失cuts[i]的血,不再有后续效果
// 第i回合如果用毒杀,怪兽在这回合不会损失血量,但是之后每回合都损失poisons[i]的血量
// 并且你选择的所有毒杀效果,在之后的回合都会叠加
// 两个数组cuts、poisons,长度都是n,代表你一共可以进行n回合
// 每一回合你只能选择刀砍或者毒杀中的一个动作
// 如果你在n个回合内没有直接杀死怪兽,意味着你已经无法有新的行动了
// 但是怪兽如果有中毒效果的话,那么怪兽依然会在血量耗尽的那回合死掉
// 返回至少多少回合,怪兽会死掉
// 数据范围 : 
// 1 <= n <= 10^5
// 1 <= hp <= 10^9
// 1 <= cuts[i]、poisons[i] <= 10^9
// 本题来自真实大厂笔试,找不到测试链接,所以用对数器验证
public class Code07_CutOrPoison {

	// 动态规划方法(只是为了验证)
	// 目前没有讲动态规划,所以不需要理解这个函数
	// 这个函数只是为了验证二分答案的方法是否正确的
	// 纯粹为了写对数器验证才设计的方法,血量比较大的时候会超时
	// 这个方法不做要求,此时并不需要理解,可以在学习完动态规划章节之后来看看这个函数
	public static int fast1(int[] cuts, int[] poisons, int hp) {
		int sum = 0;
		for (int num : poisons) {
			sum += num;
		}
		int[][][] dp = new int[cuts.length][hp + 1][sum + 1];
		return f1(cuts, poisons, 0, hp, 0, dp);
	}

	// 不做要求
	public static int f1(int[] cuts, int[] poisons, int i, int r, int p, int[][][] dp) {
		r -= p;
		if (r <= 0) {
			return i + 1;
		}
		if (i == cuts.length) {
			if (p == 0) {
				return Integer.MAX_VALUE;
			} else {
				return cuts.length + 1 + (r + p - 1) / p;
			}
		}
		if (dp[i][r][p] != 0) {
			return dp[i][r][p];
		}
		int p1 = r <= cuts[i] ? (i + 1) : f1(cuts, poisons, i + 1, r - cuts[i], p, dp);
		int p2 = f1(cuts, poisons, i + 1, r, p + poisons[i], dp);
		int ans = Math.min(p1, p2);
		dp[i][r][p] = ans;
		return ans;
	}

	// 二分答案法
	// 最优解
	// 时间复杂度O(n * log(hp)),额外空间复杂度O(1)
	public static int fast2(int[] cuts, int[] poisons, int hp) {
		int ans = Integer.MAX_VALUE;
		for (int l = 1, r = hp + 1, m; l <= r;) {
			// m中点,一定要让怪兽在m回合内死掉,更多回合无意义
			m = l + ((r - l) >> 1);
			if (f(cuts, poisons, hp, m)) {
				ans = m;
				r = m - 1;
			} else {
				l = m + 1;
			}
		}
		return ans;
	}

	// cuts、posions,每一回合刀砍、毒杀的效果
	// hp:怪兽血量
	// limit:回合的限制
	public static boolean f(int[] cuts, int[] posions, long hp, int limit) {
		int n = Math.min(cuts.length, limit);
		for (int i = 0, j = 1; i < n; i++, j++) {
			hp -= Math.max((long) cuts[i], (long) (limit - j) * (long) posions[i]);
			if (hp <= 0) {
				return true;
			}
		}
		return false;
	}

	// 对数器测试
	public static void main(String[] args) {
		// 随机测试的数据量不大
		// 因为数据量大了,fast1方法会超时
		// 所以在数据量不大的情况下,验证fast2方法功能正确即可
		// fast2方法在大数据量的情况下一定也能通过
		// 因为时间复杂度就是最优的
		System.out.println("测试开始");
		int N = 30;
		int V = 20;
		int H = 300;
		int testTimes = 10000;
		for (int i = 0; i < testTimes; i++) {
			int n = (int) (Math.random() * N) + 1;
			int[] cuts = randomArray(n, V);
			int[] posions = randomArray(n, V);
			int hp = (int) (Math.random() * H) + 1;
			int ans1 = fast1(cuts, posions, hp);
			int ans2 = fast2(cuts, posions, hp);
			if (ans1 != ans2) {
				System.out.println("出错了!");
			}
		}
		System.out.println("测试结束");
	}

	// 对数器测试
	public static int[] randomArray(int n, int v) {
		int[] ans = new int[n];
		for (int i = 0; i < n; i++) {
			ans[i] = (int) (Math.random() * v) + 1;
		}
		return ans;
	}

}

52. (必备)单调栈上

package class052;

// 单调栈求每个位置左右两侧,离当前位置最近、且值严格小于的位置
// 给定一个可能含有重复值的数组 arr
// 找到每一个 i 位置左边和右边离 i 位置最近且值比 arr[i] 小的位置
// 返回所有位置相应的信息。
// 输入描述:
// 第一行输入一个数字 n,表示数组 arr 的长度。
// 以下一行输入 n 个数字,表示数组的值
// 输出描述:
// 输出n行,每行两个数字 L 和 R,如果不存在,则值为 -1,下标从 0 开始。
// 测试链接 : https://www.nowcoder.com/practice/2a2c00e7a88a498693568cef63a4b7bb
// 请同学们务必参考如下代码中关于输入、输出的处理
// 这是输入输出处理效率很高的写法
// 提交以下的code,提交时请把类名改成"Main",可以直接通过

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StreamTokenizer;

public class Code01_LeftRightLess {

	public static int MAXN = 1000001;

	public static int[] arr = new int[MAXN];

	public static int[] stack = new int[MAXN];

	public static int[][] ans = new int[MAXN][2];

	public static int n, r;

	public static void main(String[] args) throws IOException {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		StreamTokenizer in = new StreamTokenizer(br);
		PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
		while (in.nextToken() != StreamTokenizer.TT_EOF) {
			n = (int) in.nval;
			for (int i = 0; i < n; i++) {
				in.nextToken();
				arr[i] = (int) in.nval;
			}
			compute();
			for (int i = 0; i < n; i++) {
				out.println(ans[i][0] + " " + ans[i][1]);
			}
		}
		out.flush();
		out.close();
		br.close();
	}

	// arr[0...n-1]
	public static void compute() {
		r = 0;
		int cur;
		// 遍历阶段
		for (int i = 0; i < n; i++) {
			// i -> arr[i]
			while (r > 0 && arr[stack[r - 1]] >= arr[i]) {
				cur = stack[--r];
				// cur当前弹出的位置,左边最近且小
				ans[cur][0] = r > 0 ? stack[r - 1] : -1;
				ans[cur][1] = i;
			}
			stack[r++] = i;
		}
		// 清算阶段
		while (r > 0) {
			cur = stack[--r];
			ans[cur][0] = r > 0 ? stack[r - 1] : -1;
			ans[cur][1] = -1;
		}
		// 修正阶段
		// 左侧的答案不需要修正一定是正确的,只有右侧答案需要修正
		// 从右往左修正,n-1位置的右侧答案一定是-1,不需要修正
		for (int i = n - 2; i >= 0; i--) {
			if (ans[i][1] != -1 && arr[ans[i][1]] == arr[i]) {
				ans[i][1] = ans[ans[i][1]][1];
			}
		}
	}

}
package class052;

// 每日温度
// 给定一个整数数组 temperatures ,表示每天的温度,返回一个数组 answer
// 其中 answer[i] 是指对于第 i 天,下一个更高温度出现在几天后
// 如果气温在这之后都不会升高,请在该位置用 0 来代替。
// 测试链接 : https://leetcode.cn/problems/daily-temperatures/
public class Code02_DailyTemperatures {

	public static int MAXN = 100001;

	public static int[] stack = new int[MAXN];

	public static int r;

	public static int[] dailyTemperatures(int[] nums) {
		int n = nums.length;
		int[] ans = new int[n];
		r = 0;
		for (int i = 0, cur; i < n; i++) {
			// 相等时候的处理,相等也加入单调栈
			while (r > 0 && nums[stack[r - 1]] < nums[i]) {
				cur = stack[--r];
				ans[cur] = i - cur;
			}
			stack[r++] = i;
		}
		return ans;
	}

}
package class052;

// 子数组的最小值之和
// 给定一个整数数组 arr,找到 min(b) 的总和,其中 b 的范围为 arr 的每个(连续)子数组。
// 由于答案可能很大,因此 返回答案模 10^9 + 7
// 测试链接 : https://leetcode.cn/problems/sum-of-subarray-minimums/
public class Code03_SumOfSubarrayMinimums {

	public static int MOD = 1000000007;

	public static int MAXN = 30001;

	public static int[] stack = new int[MAXN];

	public static int r;

	public static int sumSubarrayMins(int[] arr) {
		long ans = 0;
		r = 0;
		// 注意课上讲的相等情况的修正
		for (int i = 0; i < arr.length; i++) {
			while (r > 0 && arr[stack[r - 1]] >= arr[i]) {
				int cur = stack[--r];
				int left = r == 0 ? -1 : stack[r - 1];
				ans = (ans + (long) (cur - left) * (i - cur) * arr[cur]) % MOD;
			}
			stack[r++] = i;
		}
		while (r > 0) {
			int cur = stack[--r];
			int left = r == 0 ? -1 : stack[r - 1];
			ans = (ans + (long) (cur - left) * (arr.length - cur) * arr[cur]) % MOD;
		}
		return (int) ans;
	}

}
package class052;

// 柱状图中最大的矩形
// 给定 n 个非负整数,用来表示柱状图中各个柱子的高度
// 每个柱子彼此相邻,且宽度为 1 。求在该柱状图中,能够勾勒出来的矩形的最大面积
// 测试链接:https://leetcode.cn/problems/largest-rectangle-in-histogram
public class Code04_LargestRectangleInHistogram {

	public static int MAXN = 100001;

	public static int[] stack = new int[MAXN];

	public static int r;

	public static int largestRectangleArea(int[] height) {
		int n = height.length;
		r = 0;
		int ans = 0, cur, left;
		for (int i = 0; i < n; i++) {
			// i -> arr[i]
			while (r > 0 && height[stack[r - 1]] >= height[i]) {
				cur = stack[--r];
				left = r == 0 ? -1 : stack[r - 1];
				ans = Math.max(ans, height[cur] * (i - left - 1));
			}
			stack[r++] = i;
		}
		while (r > 0) {
			cur = stack[--r];
			left = r == 0 ? -1 : stack[r - 1];
			ans = Math.max(ans, height[cur] * (n - left - 1));
		}
		return ans;
	}

}
package class052;

import java.util.Arrays;

// 最大矩形
// 给定一个仅包含 0 和 1 、大小为 rows * cols 的二维二进制矩阵
// 找出只包含 1 的最大矩形,并返回其面积
// 测试链接:https://leetcode.cn/problems/maximal-rectangle/
public class Code05_MaximalRectangle {

	public static int MAXN = 201;

	public static int[] height = new int[MAXN];

	public static int[] stack = new int[MAXN];

	public static int r;

	public static int maximalRectangle(char[][] grid) {
		int n = grid.length;
		int m = grid[0].length;
		Arrays.fill(height, 0, m, 0);
		int ans = 0;
		for (int i = 0; i < n; i++) {
			// 来到i行,长方形一定要以i行做底!
			// 加工高度数组(压缩数组)
			for (int j = 0; j < m; j++) {
				height[j] = grid[i][j] == '0' ? 0 : height[j] + 1;
			}
			ans = Math.max(largestRectangleArea(m), ans);
		}
		return ans;
	}

	public static int largestRectangleArea(int m) {
		r = 0;
		int ans = 0, cur, left;
		for (int i = 0; i < m; i++) {
			// i -> arr[i]
			while (r > 0 && height[stack[r - 1]] >= height[i]) {
				cur = stack[--r];
				left = r == 0 ? -1 : stack[r - 1];
				ans = Math.max(ans, height[cur] * (i - left - 1));
			}
			stack[r++] = i;
		}
		while (r > 0) {
			cur = stack[--r];
			left = r == 0 ? -1 : stack[r - 1];
			ans = Math.max(ans, height[cur] * (m - left - 1));
		}
		return ans;
	}

}
package class052;

// 课上没讲的代码,单调栈在洛谷上的测试,原理是一样的
// 洛谷上这道题对java特别不友好,不这么写通过不了,注意看注释,非常极限
// 建议看看就好,现在的笔试和比赛时,不会这么极限的
// 给定一个长度为n的数组,打印每个位置的右侧,大于该位置数字的最近位置
// 测试链接 : https://www.luogu.com.cn/problem/P5788
// 提交以下的code,提交时请把类名改成"Main",可以通过所有用例

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;

public class Code06_MonotonicStackLuogu {

	public static void main(String[] args) throws IOException {
		int n = nextInt();
		int[] arr = new int[n + 1];
		for (int i = 1; i <= n; i++) {
			arr[i] = nextInt();
		}
		// 单调栈中保证 : 左 >= 右
		int[] stack = new int[n + 1];
		int r = 0;
		// 注意,这里为了省空间,直接复用了arr
		// 比如一个位置x,如果从stack中弹出,并且是当前的i位置让其弹出的
		// 那么令arr[x] = i,也就是代码36行
		// 此时arr[x]不再表示原始数组x位置的值
		// 而去表示,原始数组中,x的右边,大于arr[x],最近的位置
		// 也就是说,重新复用arr,让其变成答案数组
		// 为啥这么节省?为啥不单独弄一个答案数组
		// 没办法,不这么节省通过不了测试,空间卡的非常极限
		for (int i = 1; i <= n; i++) {
			while (r > 0 && arr[stack[r - 1]] < arr[i]) {
				arr[stack[--r]] = i;
			}
			stack[r++] = i;
		}
		while (r > 0) {
			arr[stack[--r]] = 0;
		}
		out.print(arr[1]);
		for (int i = 2; i <= n; i++) {
			out.print(" " + arr[i]);
		}
		out.println();
		out.flush();
	}

	// 用如下的方式读数据其实并不推荐
	// 但是这道题特别卡空间
	// 需要这么读数据让内存开销最小
	// 一般笔试、比赛时不需要这么写
	public static InputStream in = new BufferedInputStream(System.in);

	public static PrintWriter out = new PrintWriter(System.out);

	public static int nextInt() throws IOException {
		int ch, sign = 1, ans = 0;
		while (!Character.isDigit(ch = in.read())) {
			if (ch == '-')
				sign = -1;
		}
		do {
			ans = ans * 10 + ch - '0';
		} while (Character.isDigit(ch = in.read()));
		return (ans * sign);
	}

}

53. (必备)单调栈下

package class053;

// 最大宽度坡
// 给定一个整数数组 A,坡是元组 (i, j),其中  i < j 且 A[i] <= A[j]
// 这样的坡的宽度为 j - i,找出 A 中的坡的最大宽度,如果不存在,返回 0
// 测试链接 : https://leetcode.cn/problems/maximum-width-ramp/
public class Code01_MaximumWidthRamp {

	public static int MAXN = 50001;

	public static int[] stack = new int[MAXN];

	public static int r;

	public static int maxWidthRamp(int[] arr) {
		// 令r=1相当于0位置进栈了
		// stack[0] = 0,然后栈的大小变成1
		r = 1;
		int n = arr.length;
		for (int i = 1; i < n; i++) {
			if (arr[stack[r - 1]] > arr[i]) {
				stack[r++] = i;
			}
		}
		int ans = 0;
		for (int j = n - 1; j >= 0; j--) {
			while (r > 0 && arr[stack[r - 1]] <= arr[j]) {
				ans = Math.max(ans, j - stack[--r]);
			}
		}
		return ans;
	}

}
package class053;

import java.util.Arrays;

// 去除重复字母保证剩余字符串的字典序最小
// 给你一个字符串 s ,请你去除字符串中重复的字母,使得每个字母只出现一次
// 需保证 返回结果的字典序最小
// 要求不能打乱其他字符的相对位置
// 测试链接 : https://leetcode.cn/problems/remove-duplicate-letters/
public class Code02_RemoveDuplicateLetters {

	public static int MAXN = 26;

	// 每种字符词频
	public static int[] cnts = new int[MAXN];

	// 每种字符目前有没有进栈
	public static boolean[] enter = new boolean[MAXN];

	// 单调栈
	public static char[] stack = new char[MAXN];

	public static int r;

	public static String removeDuplicateLetters(String str) {
		r = 0;
		Arrays.fill(cnts, 0);
		Arrays.fill(enter, false);
		char[] s = str.toCharArray();
		for (char cha : s) {
			cnts[cha - 'a']++;
		}
		for (char cur : s) {
			// 从左往右依次遍历字符,a -> 0 b -> 1 ... z -> 25
			// cur -> cur - 'a'
			if (!enter[cur - 'a']) {
				while (r > 0 && stack[r - 1] > cur && cnts[stack[r - 1] - 'a'] > 0) {
					enter[stack[r - 1] - 'a'] = false;
					r--;
				}
				stack[r++] = cur;
				enter[cur - 'a'] = true;
			}
			cnts[cur - 'a']--;
		}
		return String.valueOf(stack, 0, r);
	}

}
package class053;

// 大鱼吃小鱼问题
// 给定一个数组arr,每个值代表鱼的体重
// 每一轮每条鱼都会吃掉右边离自己最近比自己体重小的鱼,每条鱼向右找只吃一条
// 但是吃鱼这件事是同时发生的,也就是同一轮在A吃掉B的同时,A也可能被别的鱼吃掉
// 如果有多条鱼在当前轮找到的是同一条小鱼,那么在这一轮,这条小鱼同时被这些大鱼吃
// 请问多少轮后,鱼的数量就固定了
// 比如 : 8 3 1 5 6 7 2 4
// 第一轮 : 8吃3;3吃1;5、6、7吃2;4没有被吃。数组剩下 8 5 6 7 4
// 第二轮 : 8吃5;5、6、7吃4。数组剩下 8 6 7
// 第三轮 : 8吃6。数组剩下 8 7
// 第四轮 : 8吃7。数组剩下 8。
// 过程结束,返回4
// 测试链接 : https://www.nowcoder.com/practice/77199defc4b74b24b8ebf6244e1793de
// 测试链接 : https://leetcode.cn/problems/steps-to-make-array-non-decreasing/

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StreamTokenizer;

public class Code03_BigFishEatSmallFish {

	public static int MAXN = 100001;

	public static int[] arr = new int[MAXN];

	public static int n;

	public static int[][] stack = new int[MAXN][2];

	public static int r;

	public static void main(String[] args) throws IOException {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		StreamTokenizer in = new StreamTokenizer(br);
		PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
		while (in.nextToken() != StreamTokenizer.TT_EOF) {
			n = (int) in.nval;
			for (int i = 0; i < n; i++) {
				in.nextToken();
				arr[i] = (int) in.nval;
			}
			out.println(turns());
		}
		out.flush();
		out.close();
		br.close();
	}

	// arr[0...n-1]鱼的体重
	// stack[...]随便用
	public static int turns() {
		r = 0;
		int ans = 0;
		for (int i = n - 1, curTurns; i >= 0; i--) {
			// i号鱼,arr[i]
			// 0轮是初始
			curTurns = 0;
			while (r > 0 && stack[r - 1][0] < arr[i]) {
				curTurns = Math.max(curTurns + 1, stack[--r][1]);
			}
			stack[r][0] = arr[i];
			stack[r++][1] = curTurns;
			ans = Math.max(ans, curTurns);
		}
		return ans;
	}

	// 也找到了leetcode测试链接
	// 测试链接 : https://leetcode.cn/problems/steps-to-make-array-non-decreasing/
	// 提交如下代码,可以直接通过
	public static int MAXM = 100001;

	public static int[][] s = new int[MAXM][2];

	public static int size;

	public static int totalSteps(int[] arr) {
		size = 0;
		int ans = 0;
		for (int i = arr.length - 1, curTurns; i >= 0; i--) {
			curTurns = 0;
			while (size > 0 && s[size - 1][0] < arr[i]) {
				curTurns = Math.max(curTurns + 1, s[--size][1]);
			}
			s[size][0] = arr[i];
			s[size++][1] = curTurns;
			ans = Math.max(ans, curTurns);
		}
		return ans;
	}

}
package class053;

import java.util.Arrays;

// 统计全1子矩形的数量
// 给你一个 m * n 的矩阵 mat,其中只有0和1两种值
// 请你返回有多少个 子矩形 的元素全部都是1
// 测试链接 : https://leetcode.cn/problems/count-submatrices-with-all-ones/
public class Code04_CountSubmatricesWithAllOnes {

	public static int MAXM = 151;

	public static int[] height = new int[MAXM];

	public static int[] stack = new int[MAXM];

	public static int r;

	public static int numSubmat(int[][] mat) {
		int n = mat.length;
		int m = mat[0].length;
		int ans = 0;
		Arrays.fill(height, 0, m, 0);
		for (int i = 0; i < n; i++) {
			for (int j = 0; j < m; j++) {
				height[j] = mat[i][j] == 0 ? 0 : height[j] + 1;
			}
			ans += countFromBottom(m);
		}
		return ans;
	}

	// 比如
	//              1
	//              1
	//              1         1
	//    1         1         1
	//    1         1         1
	//    1         1         1
	//             
	//    3  ....   6   ....  8
	//   left      cur        i
	// 如上图,假设6位置从栈中弹出,6位置的高度为6(上面6个1)
	// 6位置的左边、离6位置最近、且小于高度6的是3位置(left),3位置的高度是3
	// 6位置的右边、离6位置最近、且小于高度6的是8位置(i),8位置的高度是4
	// 此时我们求什么?
	// 1) 求在4~7范围上必须以高度6作为高的矩形有几个?
	// 2) 求在4~7范围上必须以高度5作为高的矩形有几个?
	// 也就是说,<=4的高度一律不求,>6的高度一律不求!
	// 其他位置也会从栈里弹出,等其他位置弹出的时候去求!
	// 那么在4~7范围上必须以高度6作为高的矩形有几个?如下:
	// 4..4  4..5  4..6  4..7
	// 5..5  5..6  5..7
	// 6..6  6..7
	// 7..7
	// 10个!什么公式?
	// 4...7范围的长度为4,那么数量就是 : 4*5/2
	// 同理在4~7范围上,必须以高度5作为高的矩形也是这么多
	// 所以cur从栈里弹出时产生的数量 : 
	// (cur位置的高度-Max{left位置的高度,i位置的高度}) * ((i-left-1)*(i-left)/2)
	public static int countFromBottom(int m) {
		// height[0...m-1]
		r = 0;
		int ans = 0;
		for (int i = 0, left, len, bottom; i < m; i++) {
			while (r > 0 && height[stack[r - 1]] >= height[i]) {
				int cur = stack[--r];
				if (height[cur] > height[i]) {
					// 只有height[cur] > height[i]才结算
					// 如果是因为height[cur]==height[i]导致cur位置从栈中弹出
					// 那么不结算!等i位置弹出的时候再说!
					// 上一节课讲了很多这种相等时候的处理,比如"柱状图中最大的矩形"问题
					left = r == 0 ? -1 : stack[r - 1];
					len = i - left - 1;
					bottom = Math.max(left == -1 ? 0 : height[left], height[i]);
					ans += (height[cur] - bottom) * len * (len + 1) / 2;
				}
			}
			stack[r++] = i;
		}
		while (r > 0) {
			int cur = stack[--r];
			int left = r == 0 ? -1 : stack[r - 1];
			int len = m - left - 1;
			int down = left == -1 ? 0 : height[left];
			ans += (height[cur] - down) * len * (len + 1) / 2;
		}
		return ans;
	}

}

54. (必备)单调队列上

package class054;

// 滑动窗口最大值(单调队列经典用法模版)
// 给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧
// 你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
// 返回 滑动窗口中的最大值 。
// 测试链接 : https://leetcode.cn/problems/sliding-window-maximum/
public class Code01_SlidingWindowMaximum {

	public static int MAXN = 100001;

	public static int[] deque = new int[MAXN];

	public static int h, t;

	public static int[] maxSlidingWindow(int[] arr, int k) {
		h = t = 0;
		int n = arr.length;
		// 先形成长度为k-1的窗口
		for (int i = 0; i < k - 1; i++) {
			// 大 -> 小
			while (h < t && arr[deque[t - 1]] <= arr[i]) {
				t--;
			}
			deque[t++] = i;
		}
		int m = n - k + 1;
		int[] ans = new int[m];
		// 当前窗口k-1长度
		for (int l = 0, r = k - 1; l < m; l++, r++) {
			// 少一个,要让r位置的数进来
			while (h < t && arr[deque[t - 1]] <= arr[r]) {
				t--;
			}
			deque[t++] = r;
			// 收集答案
			ans[l] = arr[deque[h]];
			// l位置的数出去
			if (deque[h] == l) {
				h++;
			}
		}
		return ans;
	}

}
package class054;

// 绝对差不超过限制的最长连续子数组
// 给你一个整数数组 nums ,和一个表示限制的整数 limit
// 请你返回最长连续子数组的长度
// 该子数组中的任意两个元素之间的绝对差必须小于或者等于 limit
// 如果不存在满足条件的子数组,则返回 0
// 测试链接 : https://leetcode.cn/problems/longest-continuous-subarray-with-absolute-diff-less-than-or-equal-to-limit/
public class Code02_LongestSubarrayAbsoluteLimit {

	public static int MAXN = 100001;

	// 窗口内最大值的更新结构(单调队列)
	public static int[] maxDeque = new int[MAXN];

	// 窗口内最小值的更新结构(单调队列)
	public static int[] minDeque = new int[MAXN];

	public static int maxh, maxt, minh, mint;

	public static int[] arr;

	public static int longestSubarray(int[] nums, int limit) {
		maxh = maxt = minh = mint = 0;
		arr = nums;
		int n = arr.length;
		int ans = 0;
		for (int l = 0, r = 0; l < n; l++) {
			// [l,r),r永远是没有进入窗口的、下一个数所在的位置
			while (r < n && ok(limit, nums[r])) {
				push(r++);
			}
			// 从while出来的时候,[l,r)是l开头的子数组能向右延伸的最大范围
			ans = Math.max(ans, r - l);
			pop(l);
		}
		return ans;
	}

	// 判断如果加入数字number,窗口最大值 - 窗口最小值是否依然 <= limit
	// 依然 <= limit,返回true
	// 不再 <= limit,返回false
	public static boolean ok(int limit, int number) {
		// max : 如果number进来,新窗口的最大值
		int max = maxh < maxt ? Math.max(arr[maxDeque[maxh]], number) : number;
		// min : 如果number进来,新窗口的最小值
		int min = minh < mint ? Math.min(arr[minDeque[minh]], number) : number;
		return max - min <= limit;
	}

	// r位置的数字进入窗口,修改窗口内最大值的更新结构、修改窗口内最小值的更新结构
	public static void push(int r) {
		while (maxh < maxt && arr[maxDeque[maxt - 1]] <= arr[r]) {
			maxt--;
		}
		maxDeque[maxt++] = r;
		while (minh < mint && arr[minDeque[mint - 1]] >= arr[r]) {
			mint--;
		}
		minDeque[mint++] = r;
	}

	// 窗口要吐出l位置的数了!检查过期!
	public static void pop(int l) {
		if (maxh < maxt && maxDeque[maxh] == l) {
			maxh++;
		}
		if (minh < mint && minDeque[minh] == l) {
			minh++;
		}
	}

}
package class054;

// 接取落水的最小花盆
// 老板需要你帮忙浇花。给出 N 滴水的坐标,y 表示水滴的高度,x 表示它下落到 x 轴的位置
// 每滴水以每秒1个单位长度的速度下落。你需要把花盆放在 x 轴上的某个位置
// 使得从被花盆接着的第 1 滴水开始,到被花盆接着的最后 1 滴水结束,之间的时间差至少为 D
// 我们认为,只要水滴落到 x 轴上,与花盆的边沿对齐,就认为被接住
// 给出 N 滴水的坐标和 D 的大小,请算出最小的花盆的宽度 W
// 测试链接 : https://www.luogu.com.cn/problem/P2698
// 请同学们务必参考如下代码中关于输入、输出的处理
// 这是输入输出处理效率很高的写法
// 提交以下的code,提交时请把类名改成"Main",可以直接通过

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StreamTokenizer;
import java.util.Arrays;

public class Code03_FallingWaterSmallestFlowerPot {

	public static int MAXN = 100005;

	public static int[][] arr = new int[MAXN][2];

	public static int n, d;

	public static int[] maxDeque = new int[MAXN];

	public static int[] minDeque = new int[MAXN];

	public static int maxh, maxt, minh, mint;

	public static void main(String[] args) throws IOException {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		StreamTokenizer in = new StreamTokenizer(br);
		PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
		while (in.nextToken() != StreamTokenizer.TT_EOF) {
			n = (int) in.nval;
			in.nextToken();
			d = (int) in.nval;
			for (int i = 0; i < n; i++) {
				in.nextToken();
				arr[i][0] = (int) in.nval;
				in.nextToken();
				arr[i][1] = (int) in.nval;
			}
			int ans = compute();
			out.println(ans == Integer.MAX_VALUE ? -1 : ans);
		}
		out.flush();
		out.close();
		br.close();
	}

	public static int compute() {
		// arr[0...n-1][2]: x(0), 高度(1)
		// 所有水滴根据x排序,谁小谁在前
		Arrays.sort(arr, 0, n, (a, b) -> a[0] - b[0]);
		maxh = maxt = minh = mint = 0;
		int ans = Integer.MAX_VALUE;
		for (int l = 0, r = 0; l < n; l++) {
			// [l,r) : 水滴的编号
			// l : 当前花盘的左边界,arr[l][0]
			while (!ok() && r < n) {
				push(r++);
			}
			if (ok()) {
				ans = Math.min(ans, arr[r - 1][0] - arr[l][0]);
			}
			pop(l);
		}
		return ans;
	}

	// 当前窗口 最大值 - 最小值 是不是>=d
	public static boolean ok() {
		int max = maxh < maxt ? arr[maxDeque[maxh]][1] : 0;
		int min = minh < mint ? arr[minDeque[minh]][1] : 0;
		return max - min >= d;
	}

	public static void push(int r) {
		while (maxh < maxt && arr[maxDeque[maxt - 1]][1] <= arr[r][1]) {
			maxt--;
		}
		maxDeque[maxt++] = r;
		while (minh < mint && arr[minDeque[mint - 1]][1] >= arr[r][1]) {
			mint--;
		}
		minDeque[mint++] = r;
	}

	public static void pop(int l) {
		if (maxh < maxt && maxDeque[maxh] == l) {
			maxh++;
		}
		if (minh < mint && minDeque[minh] == l) {
			minh++;
		}
	}

}

55. (必备)单调队列下

package class055;

// 和至少为K的最短子数组
// 给定一个数组arr,其中的值有可能正、负、0
// 给定一个正数k
// 返回累加和>=k的所有子数组中,最短的子数组长度
// 测试链接 : https://leetcode.cn/problems/shortest-subarray-with-sum-at-least-k/
public class Code01_ShortestSubarrayWithSumAtLeastK {

	public static int MAXN = 100001;

	// sum[0] : 前0个数的前缀和
	// sum[i] : 前i个数的前缀和
	public static long[] sum = new long[MAXN];

	public static int[] deque = new int[MAXN];

	public static int h, t;

	public static int shortestSubarray(int[] arr, int K) {
		int n = arr.length;
		for (int i = 0; i < n; i++) {
			// [3,4,5]
			//  0 1 2
			// sum[0] = 0
			// sum[1] = 3
			// sum[2] = 7
			// sum[3] = 12
			sum[i + 1] = sum[i] + arr[i];
		}
		h = t = 0;
		int ans = Integer.MAX_VALUE;
		for (int i = 0; i <= n; i++) {
			// 前0个数前缀和
			// 前1个数前缀和
			// 前2个数前缀和
			// ...
			// 前n个数前缀和
			while (h != t && sum[i] - sum[deque[h]] >= K) {
				// 如果当前的前缀和 - 头前缀和,达标!
				ans = Math.min(ans, i - deque[h++]);
			}
			// 前i个数前缀和,从尾部加入
			// 小 大
			while (h != t && sum[deque[t - 1]] >= sum[i]) {
				t--;
			}
			deque[t++] = i;
		}
		return ans != Integer.MAX_VALUE ? ans : -1;
	}

}
package class055;

// 满足不等式的最大值
// 给你一个数组 points 和一个整数 k
// 数组中每个元素都表示二维平面上的点的坐标,并按照横坐标 x 的值从小到大排序
// 也就是说 points[i] = [xi, yi]
// 并且在 1 <= i < j <= points.length 的前提下,xi < xj 总成立
// 请你找出 yi + yj + |xi - xj| 的 最大值,
// 其中 |xi - xj| <= k 且 1 <= i < j <= points.length
// 题目测试数据保证至少存在一对能够满足 |xi - xj| <= k 的点。
// 测试链接 : https://leetcode.cn/problems/max-value-of-equation/
public class Code02_MaxValueOfEquation {

	public static int MAXN = 100001;

	// [、i号点[x,y]、]
	//  h、t
	public static int[][] deque = new int[MAXN][2];

	public static int h, t;

	// 已知所有的点都是根据x值排序的!
	// 任何两个点,组成指标,要求 : 后x - 前x <= k
	// 返回最大指标
	public static int findMaxValueOfEquation(int[][] points, int k) {
		h = t = 0;
		int n = points.length;
		int ans = Integer.MIN_VALUE;
		for (int i = 0, x, y; i < n; i++) {
			// i号点是此时的点,当前的后面点,看之前哪个点的y-x值最大,x距离又不能超过k
			x = points[i][0];
			y = points[i][1];
			while (h < t && deque[h][0] + k < x) {
				// 单调队列头部的可能性过期了,头部点的x与当前点x的距离超过了k
				h++;
			}
			if (h < t) {
				ans = Math.max(ans, x + y + deque[h][1] - deque[h][0]);
			}
			// i号点的x和y,该从尾部进入单调队列
			// 大 -> 小
			while (h < t && deque[t - 1][1] - deque[t - 1][0] <= y - x) {
				t--;
			}
			deque[t][0] = x;
			deque[t++][1] = y;
		}
		return ans;
	}

}
package class055;

import java.util.Arrays;

// 你可以安排的最多任务数目
// 给你 n 个任务和 m 个工人。每个任务需要一定的力量值才能完成
// 需要的力量值保存在下标从 0 开始的整数数组 tasks 中,
// 第i个任务需要 tasks[i] 的力量才能完成
// 每个工人的力量值保存在下标从 0 开始的整数数组workers中,
// 第j个工人的力量值为 workers[j]
// 每个工人只能完成一个任务,且力量值需要大于等于该任务的力量要求值,即workers[j]>=tasks[i]
// 除此以外,你还有 pills 个神奇药丸,可以给 一个工人的力量值 增加 strength
// 你可以决定给哪些工人使用药丸,但每个工人 最多 只能使用 一片 药丸
// 给你下标从 0 开始的整数数组tasks 和 workers 以及两个整数 pills 和 strength
// 请你返回 最多 有多少个任务可以被完成。
// 测试链接 : https://leetcode.cn/problems/maximum-number-of-tasks-you-can-assign/
public class Code03_MaximumNumberOfTasksYouCanAssign {

	public static int[] tasks;

	public static int[] workers;

	public static int MAXN = 50001;

	public static int[] deque = new int[MAXN];

	public static int h, t;

	// 两个数组排序 : O(n * logn) + O(m * logm)
	// 二分答案的过程,每次二分都用一下双端队列 : O((n和m最小值)*log(n和m最小值))
	// 最复杂的反而是排序的过程了,所以时间复杂度O(n * logn) + O(m * logm)
	public static int maxTaskAssign(int[] ts, int[] ws, int pills, int strength) {
		tasks = ts;
		workers = ws;
		Arrays.sort(tasks);
		Arrays.sort(workers);
		int tsize = tasks.length;
		int wsize = workers.length;
		int ans = 0;
		// [0, Math.min(tsize, wsize)]
		for (int l = 0, r = Math.min(tsize, wsize), m; l <= r;) {
			// m中点,一定要完成的任务数量
			m = (l + r) / 2;
			if (f(0, m - 1, wsize - m, wsize - 1, strength, pills)) {
				ans = m;
				l = m + 1;
			} else {
				r = m - 1;
			}
		}
		return ans;
	}

	// tasks[tl....tr]需要力量最小的几个任务
	// workers[wl....wr]力量值最大的几个工人
	// 药效是s,一共有的药pills个
	// 在药的数量不超情况下,能不能每个工人都做一个任务
	public static boolean f(int tl, int tr, int wl, int wr, int s, int pills) {
		h = t = 0;
		// 已经使用的药的数量
		int cnt = 0;
		for (int i = wl, j = tl; i <= wr; i++) {
			// i : 工人的编号
			// j : 任务的编号
			for (; j <= tr && tasks[j] <= workers[i]; j++) {
				// 工人不吃药的情况下,去解锁任务
				deque[t++] = j;
			}
			if (h < t && tasks[deque[h]] <= workers[i]) {
				h++;
			} else {
				// 吃药之后的逻辑
				for (; j <= tr && tasks[j] <= workers[i] + s; j++) {
					deque[t++] = j;
				}
				if (h < t) {
					cnt++;
					t--;
				} else {
					return false;
				}
			}
		}
		return cnt <= pills;
	}

}

56. (必备)并查集上

package class056;

// 并查集模版(牛客)
// 路径压缩 + 小挂大
// 测试链接 : https://www.nowcoder.com/practice/e7ed657974934a30b2010046536a5372
// 请同学们务必参考如下代码中关于输入、输出的处理
// 这是输入输出处理效率很高的写法
// 提交以下的code,提交时请把类名改成"Main",可以直接通过

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StreamTokenizer;

public class Code01_UnionFindNowCoder {

	public static int MAXN = 1000001;

	public static int[] father = new int[MAXN];

	public static int[] size = new int[MAXN];

	public static int[] stack = new int[MAXN];

	public static int n;

	public static void build() {
		for (int i = 0; i <= n; i++) {
			father[i] = i;
			size[i] = 1;
		}
	}

	// i号节点,往上一直找,找到代表节点返回!
	public static int find(int i) {
		// 沿途收集了几个点
		int size = 0;
		while (i != father[i]) {
			stack[size++] = i;
			i = father[i];
		}
		// 沿途节点收集好了,i已经跳到代表节点了
		while (size > 0) {
			father[stack[--size]] = i;
		}
		return i;
	}

	public static boolean isSameSet(int x, int y) {
		return find(x) == find(y);
	}

	public static void union(int x, int y) {
		int fx = find(x);
		int fy = find(y);
		if (fx != fy) {
			// fx是集合的代表:拿大小
			// fy是集合的代表:拿大小
			if (size[fx] >= size[fy]) {
				size[fx] += size[fy];
				father[fy] = fx;
			} else {
				size[fy] += size[fx];
				father[fx] = fy;
			}
		}
	}

	public static void main(String[] args) throws IOException {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		StreamTokenizer in = new StreamTokenizer(br);
		PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
		while (in.nextToken() != StreamTokenizer.TT_EOF) {
			n = (int) in.nval;
			build();
			in.nextToken();
			int m = (int) in.nval;
			for (int i = 0; i < m; i++) {
				in.nextToken();
				int op = (int) in.nval;
				in.nextToken();
				int x = (int) in.nval;
				in.nextToken();
				int y = (int) in.nval;
				if (op == 1) {
					out.println(isSameSet(x, y) ? "Yes" : "No");
				} else {
					union(x, y);
				}
			}
		}
		out.flush();
		out.close();
		br.close();
	}

}
package class056;

// 并查集模版(洛谷)
// 本实现用递归函数实现路径压缩,而且省掉了小挂大的优化,一般情况下可以省略
// 测试链接 : https://www.luogu.com.cn/problem/P3367
// 请同学们务必参考如下代码中关于输入、输出的处理
// 这是输入输出处理效率很高的写法
// 提交以下的code,提交时请把类名改成"Main",可以直接通过

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StreamTokenizer;

public class Code02_UnionFindLuogu {

	public static int MAXN = 10001;

	public static int[] father = new int[MAXN];

	public static int n;

	public static void build() {
		for (int i = 0; i <= n; i++) {
			father[i] = i;
		}
	}

	public static int find(int i) {
		if (i != father[i]) {
			father[i] = find(father[i]);
		}
		return father[i];
	}

	public static boolean isSameSet(int x, int y) {
		return find(x) == find(y);
	}

	public static void union(int x, int y) {
		father[find(x)] = find(y);
	}

	public static void main(String[] args) throws IOException {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		StreamTokenizer in = new StreamTokenizer(br);
		PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
		while (in.nextToken() != StreamTokenizer.TT_EOF) {
			n = (int) in.nval;
			build();
			in.nextToken();
			int m = (int) in.nval;
			for (int i = 0; i < m; i++) {
				in.nextToken();
				int z = (int) in.nval;
				in.nextToken();
				int x = (int) in.nval;
				in.nextToken();
				int y = (int) in.nval;
				if (z == 1) {
					union(x, y);
				} else {
					out.println(isSameSet(x, y) ? "Y" : "N");
				}
			}
		}
		out.flush();
		out.close();
		br.close();
	}

}
package class056;

// 情侣牵手
// n对情侣坐在连续排列的 2n 个座位上,想要牵到对方的手
// 人和座位由一个整数数组 row 表示,其中 row[i] 是坐在第 i 个座位上的人的ID
// 情侣们按顺序编号,第一对是 (0, 1),第二对是 (2, 3),以此类推,最后一对是 (2n-2, 2n-1)
// 返回 最少交换座位的次数,以便每对情侣可以并肩坐在一起
// 每次交换可选择任意两人,让他们站起来交换座位
// 测试链接 : https://leetcode.cn/problems/couples-holding-hands/
public class Code03_CouplesHoldingHands {

	public static int minSwapsCouples(int[] row) {
		int n = row.length;
		build(n / 2);
		for (int i = 0; i < n; i += 2) {
			union(row[i] / 2, row[i + 1] / 2);
		}
		return n / 2 - sets;
	}

	public static int MAXN = 31;

	public static int[] father = new int[MAXN];

	public static int sets;

	public static void build(int m) {
		for (int i = 0; i < m; i++) {
			father[i] = i;
		}
		sets = m;
	}

	public static int find(int i) {
		if (i != father[i]) {
			father[i] = find(father[i]);
		}
		return father[i];
	}

	public static void union(int x, int y) {
		int fx = find(x);
		int fy = find(y);
		if (fx != fy) {
			father[fx] = fy;
			sets--;
		}
	}

}
package class056;

// 相似字符串组
// 如果交换字符串 X 中的两个不同位置的字母,使得它和字符串 Y 相等
// 那么称 X 和 Y 两个字符串相似
// 如果这两个字符串本身是相等的,那它们也是相似的
// 例如,"tars" 和 "rats" 是相似的 (交换 0 与 2 的位置);
// "rats" 和 "arts" 也是相似的,但是 "star" 不与 "tars","rats",或 "arts" 相似
// 总之,它们通过相似性形成了两个关联组:{"tars", "rats", "arts"} 和 {"star"}
// 注意,"tars" 和 "arts" 是在同一组中,即使它们并不相似
// 形式上,对每个组而言,要确定一个单词在组中,只需要这个词和该组中至少一个单词相似。
// 给你一个字符串列表 strs列表中的每个字符串都是 strs 中其它所有字符串的一个字母异位词。
// 返回 strs 中有多少字符串组
// 测试链接 : https://leetcode.cn/problems/similar-string-groups/
public class Code04_SimilarStringGroups {

	public static int MAXN = 301;

	public static int[] father = new int[MAXN];

	public static int sets;

	public static void build(int n) {
		for (int i = 0; i < n; i++) {
			father[i] = i;
		}
		sets = n;
	}

	public static int find(int i) {
		if (i != father[i]) {
			father[i] = find(father[i]);
		}
		return father[i];
	}

	public static void union(int x, int y) {
		int fx = find(x);
		int fy = find(y);
		if (fx != fy) {
			father[fx] = fy;
			sets--;
		}
	}

	public static int numSimilarGroups(String[] strs) {
		int n = strs.length;
		int m = strs[0].length();
		build(n);
		for (int i = 0; i < n; i++) {
			for (int j = i + 1; j < n; j++) {
				if (find(i) != find(j)) {
					int diff = 0;
					for (int k = 0; k < m && diff < 3; k++) {
						if (strs[i].charAt(k) != strs[j].charAt(k)) {
							diff++;
						}
					}
					if (diff == 0 || diff == 2) {
						union(i, j);
					}
				}
			}
		}
		return sets;
	}

}
package class056;

// 岛屿数量
// 给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量
// 岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成
// 此外,你可以假设该网格的四条边均被水包围
// 测试链接 : https://leetcode.cn/problems/number-of-islands/
public class Code05_NumberOfIslands {

	// 并查集的做法
	public static int numIslands(char[][] board) {
		int n = board.length;
		int m = board[0].length;
		build(n, m, board);
		for (int i = 0; i < n; i++) {
			for (int j = 0; j < m; j++) {
				if (board[i][j] == '1') {
					if (j > 0 && board[i][j - 1] == '1') {
						union(i, j, i, j - 1);
					}
					if (i > 0 && board[i - 1][j] == '1') {
						union(i, j, i - 1, j);
					}
				}
			}
		}
		return sets;
	}

	public static int MAXSIZE = 100001;

	public static int[] father = new int[MAXSIZE];

	public static int cols;

	public static int sets;

	public static void build(int n, int m, char[][] board) {
		cols = m;
		sets = 0;
		for (int a = 0; a < n; a++) {
			for (int b = 0, index; b < m; b++) {
				if (board[a][b] == '1') {
					index = index(a, b);
					father[index] = index;
					sets++;
				}
			}
		}
	}

	public static int index(int a, int b) {
		return a * cols + b;
	}

	public static int find(int i) {
		if (i != father[i]) {
			father[i] = find(father[i]);
		}
		return father[i];
	}

	public static void union(int a, int b, int c, int d) {
		int fx = find(index(a, b));
		int fy = find(index(c, d));
		if (fx != fy) {
			father[fx] = fy;
			sets--;
		}
	}

}

57. (必备)并查集下

package class057;

import java.util.HashMap;

// 移除最多的同行或同列石头
// n 块石头放置在二维平面中的一些整数坐标点上。每个坐标点上最多只能有一块石头
// 如果一块石头的 同行或者同列 上有其他石头存在,那么就可以移除这块石头
// 给你一个长度为 n 的数组 stones ,其中 stones[i] = [xi, yi] 表示第 i 块石头的位置
// 返回 可以移除的石子 的最大数量。
// 测试链接 : https://leetcode.cn/problems/most-stones-removed-with-same-row-or-column/
public class Code01_MostStonesRemovedWithSameRowOrColumn {

	// key : 某行
	// value : 第一次遇到的石头编号
	public static HashMap<Integer, Integer> rowFirst = new HashMap<Integer, Integer>();

	public static HashMap<Integer, Integer> colFirst = new HashMap<Integer, Integer>();

	public static int MAXN = 1001;

	public static int[] father = new int[MAXN];

	public static int sets;

	public static void build(int n) {
		rowFirst.clear();
		colFirst.clear();
		for (int i = 0; i < n; i++) {
			father[i] = i;
		}
		sets = n;
	}

	public static int find(int i) {
		if (i != father[i]) {
			father[i] = find(father[i]);
		}
		return father[i];
	}

	public static void union(int x, int y) {
		int fx = find(x);
		int fy = find(y);
		if (fx != fy) {
			father[fx] = fy;
			sets--;
		}
	}

	public static int removeStones(int[][] stones) {
		int n = stones.length;
		build(n);
		for (int i = 0; i < n; i++) {
			int row = stones[i][0];
			int col = stones[i][1];
			if (!rowFirst.containsKey(row)) {
				rowFirst.put(row, i);
			} else {
				union(i, rowFirst.get(row));
			}
			if (!colFirst.containsKey(col)) {
				colFirst.put(col, i);
			} else {
				union(i, colFirst.get(col));
			}
		}
		return n - sets;
	}

}
package class057;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

// 找出知晓秘密的所有专家
// 给你一个整数 n ,表示有 n 个专家从 0 到 n - 1 编号
// 另外给你一个下标从 0 开始的二维整数数组 meetings
// 其中 meetings[i] = [xi, yi, timei] 表示专家 xi 和专家 yi 在时间 timei 要开一场会
// 一个专家可以同时参加 多场会议 。最后,给你一个整数 firstPerson
// 专家 0 有一个 秘密 ,最初,他在时间 0 将这个秘密分享给了专家 firstPerson
// 接着,这个秘密会在每次有知晓这个秘密的专家参加会议时进行传播
// 更正式的表达是,每次会议,如果专家 xi 在时间 timei 时知晓这个秘密
// 那么他将会与专家 yi 分享这个秘密,反之亦然。秘密共享是 瞬时发生 的
// 也就是说,在同一时间,一个专家不光可以接收到秘密,还能在其他会议上与其他专家分享
// 在所有会议都结束之后,返回所有知晓这个秘密的专家列表
// 你可以按 任何顺序 返回答案
// 链接测试 : https://leetcode.cn/problems/find-all-people-with-secret/
public class Code02_FindAllPeopleWithSecret {

	public static int MAXN = 100001;

	public static int[] father = new int[MAXN];

	// 集合的标签信息 : 设置集合的一些属性
	// 属性在哪?secret[代表元素] 代表集合的属性
	public static boolean[] secret = new boolean[MAXN];

	public static void build(int n, int first) {
		for (int i = 0; i < n; i++) {
			father[i] = i;
			secret[i] = false;
		}
		father[first] = 0;
		secret[0] = true;
	}

	public static int find(int i) {
		if (i != father[i]) {
			father[i] = find(father[i]);
		}
		return father[i];
	}

	public static void union(int x, int y) {
		int fx = find(x);
		int fy = find(y);
		if (fx != fy) {
			father[fx] = fy;
			secret[fy] |= secret[fx];
		}
	}

	// 会议排序 : m * log m
	// 处理过程 : O(m)
	// 收集答案 : O(n)
	public static List<Integer> findAllPeople(int n, int[][] meetings, int first) {
		build(n, first);
		// {0 : 专家   1 : 专家编号   2 : 时刻}
		Arrays.sort(meetings, (a, b) -> a[2] - b[2]);
		int m = meetings.length;
		for (int l = 0, r; l < m;) {
			r = l;
			while (r + 1 < m && meetings[l][2] == meetings[r + 1][2]) {
				r++;
			}
			// l....r这些会议,一定是一个时刻
			for (int i = l; i <= r; i++) {
				union(meetings[i][0], meetings[i][1]);
			}
			// 有小的撤销行为,但这不是可撤销并查集
			// 只是每一批没有知道秘密的专家重新建立集合而已
			for (int i = l, a, b; i <= r; i++) {
				a = meetings[i][0];
				b = meetings[i][1];
				if (!secret[find(a)]) {
					father[a] = a;
				}
				if (!secret[find(b)]) {
					father[b] = b;
				}
			}
			l = r + 1;
		}
		List<Integer> ans = new ArrayList<>();
		for (int i = 0; i < n; i++) {
			if (secret[find(i)]) {
				ans.add(i);
			}
		}
		return ans;
	}

}
package class057;

import java.util.Arrays;

// 好路径的数目
// 给你一棵 n 个节点的树(连通无向无环的图)
// 节点编号从0到n-1,且恰好有n-1条边
// 给你一个长度为 n 下标从 0 开始的整数数组 vals
// 分别表示每个节点的值。同时给你一个二维整数数组 edges
// 其中 edges[i] = [ai, bi] 表示节点 ai 和 bi 之间有一条 无向 边
// 好路径需要满足以下条件:开始和结束节点的值相同、 路径中所有值都小于等于开始的值
// 请你返回不同好路径的数目
// 注意,一条路径和它反向的路径算作 同一 路径
// 比方说, 0 -> 1 与 1 -> 0 视为同一条路径。单个节点也视为一条合法路径
// 测试链接 : https://leetcode.cn/problems/number-of-good-paths/
public class Code03_NumberOfGoodPaths {

	public static int MAXN = 30001;

	// 需要保证集合中,代表节点的值,一定是整个集合的最大值
	public static int[] father = new int[MAXN];

	// 集合中最大值的次数,也就是 集合中代表节点的值有几个
	public static int[] maxcnt = new int[MAXN];

	public static void build(int n) {
		for (int i = 0; i < n; i++) {
			father[i] = i;
			maxcnt[i] = 1;
		}
	}

	// 这个并查集的优化只来自扁平化
	public static int find(int i) {
		if (i != father[i]) {
			father[i] = find(father[i]);
		}
		return father[i];
	}

	// 核心!
	// 注意以下的写法!
	// 谁的值大,谁做代表节点
	// 同时注意 maxcnt 的更新
	public static int union(int x, int y, int[] vals) {
		// fx : x所在集团的代表节点,同时也是x所在集团的最大值下标
		int fx = find(x);
		// fy : y所在集团的代表节点,同时也是y所在集团的最大值下标
		int fy = find(y);
		int path = 0;
		if (vals[fx] > vals[fy]) {
			father[fy] = fx;
		} else if (vals[fx] < vals[fy]) {
			father[fx] = fy;
		} else {
			// 两个集团最大值一样!
			path = maxcnt[fx] * maxcnt[fy];
			father[fy] = fx;
			maxcnt[fx] += maxcnt[fy];
		}
		return path;
	}

	public static int numberOfGoodPaths(int[] vals, int[][] edges) {
		int n = vals.length;
		build(n);
		int ans = n;
		// 课上重点讲这个核心排序!
		// 处理边的时候,依次从小节点往大节点处理
		Arrays.sort(edges, (e1, e2) -> (Math.max(vals[e1[0]], vals[e1[1]]) - Math.max(vals[e2[0]], vals[e2[1]])));
		for (int[] edge : edges) {
			ans += union(edge[0], edge[1], vals);
		}
		return ans;
	}

	// 课上讲解的例子1和例子2
	public static void main(String[] args) {
		// 课上例子1
		//              0  1  2  3  4  5  6  7
		int[] vals1 = { 2, 1, 1, 2, 2, 1, 1, 2 };
		int[][] edges1 = { 
				{ 0, 1 },
				{ 0, 2 },
				{ 1, 3 },
				{ 2, 4 },
				{ 2, 5 },
				{ 5, 6 },
				{ 6, 7 } };
		System.out.println(numberOfGoodPaths(vals1, edges1));

		// 课上例子2
		//              0  1  2  3  4  5  6  7  8  9 10 11 12
		int[] vals2 = { 1, 2, 2, 3, 1, 2, 2, 1, 1, 3, 3, 3, 3 };
		int[][] edges2 = {
				{ 0, 1 },
				{ 0, 2 },
				{ 0, 3 },
				{ 1, 4 },
				{ 4, 7 },
				{ 4, 8 },
				{ 3, 5 },
				{ 3, 6 },
				{ 6, 9 },
				{ 6, 10 },
				{ 6, 11 },
				{ 9, 12 } };
		System.out.println(numberOfGoodPaths(vals2, edges2));
	}

}
package class057;

import java.util.Arrays;

// 尽量减少恶意软件的传播 II
// 给定一个由 n 个节点组成的网络,用 n x n 个邻接矩阵 graph 表示
// 在节点网络中,只有当 graph[i][j] = 1 时,节点 i 能够直接连接到另一个节点 j。
// 一些节点 initial 最初被恶意软件感染。只要两个节点直接连接,
// 且其中至少一个节点受到恶意软件的感染,那么两个节点都将被恶意软件感染。
// 这种恶意软件的传播将继续,直到没有更多的节点可以被这种方式感染。
// 假设 M(initial) 是在恶意软件停止传播之后,整个网络中感染恶意软件的最终节点数。
// 我们可以从 initial 中删除一个节点,
// 并完全移除该节点以及从该节点到任何其他节点的任何连接。
// 请返回移除后能够使 M(initial) 最小化的节点。
// 如果有多个节点满足条件,返回索引 最小的节点 。
// initial 中每个整数都不同
// 测试链接 : https://leetcode.cn/problems/minimize-malware-spread-ii/
public class Code04_MinimizeMalwareSpreadII {

	// 如果测试数据变大,就改变这个值
	public static int MAXN = 301;

	// [3,6,103]
	// virus[3] = true;
	// virus[103] = true;
	// 方便查询
	public static boolean[] virus = new boolean[MAXN];

	// 每个源头点删掉的话,能拯救多少点的数据
	public static int[] cnts = new int[MAXN];

	// 集合的标签 : 集合的感染点是什么点
	// a : 代表点,整个集合源头是 infect[a]
	// infect[a] == -1,目前这个集合没有发现源头
	// infect[a] >= 0,目前这个集合源头是 infect[a]
	// infect[a] == -2,目前这个集合源头不止一个,已经无法拯救了!
	public static int[] infect = new int[MAXN];

	// 并查集固有信息
	public static int[] father = new int[MAXN];

	// 集合的标签 : 集合的大小是多少
	public static int[] size = new int[MAXN];

	// 集合一定只放普通点,源头点根本不参与集合,也不是元素!

	public static void build(int n, int[] initial) {
		for (int i = 0; i < n; i++) {
			virus[i] = false;
			cnts[i] = 0;
			infect[i] = -1;
			size[i] = 1;
			father[i] = i;
		}
		for (int i : initial) {
			virus[i] = true;
		}
	}

	public static int find(int i) {
		if (i != father[i]) {
			father[i] = find(father[i]);
		}
		return father[i];
	}

	public static void union(int x, int y) {
		int fx = find(x);
		int fy = find(y);
		if (fx != fy) {
			father[fx] = fy;
			size[fy] += size[fx];
		}
	}

	public static int minMalwareSpread(int[][] graph, int[] initial) {
		int n = graph.length;
		build(n, initial);
		// 不是病毒的点,普通点合并!
		for (int i = 0; i < n; i++) {
			for (int j = 0; j < n; j++) {
				if (graph[i][j] == 1 && !virus[i] && !virus[j]) {
					union(i, j);
				}
			}
		}
		// 病毒周围的普通点(集合 )去设置源头!
		for (int sick : initial) {
			for (int neighbor = 0; neighbor < n; neighbor++) {
				if (sick != neighbor && !virus[neighbor] && graph[sick][neighbor] == 1) {
					int fn = find(neighbor);
					if (infect[fn] == -1) {
						infect[fn] = sick;
					} else if (infect[fn] != -2 && infect[fn] != sick) {
						infect[fn] = -2;
					}
				}
			}
		}
		// 统计拯救数据
		for (int i = 0; i < n; i++) {
			// 不是代表点,不看
			if (i == find(i) && infect[i] >= 0) {
				cnts[infect[i]] += size[i];
			}
		}
		Arrays.sort(initial);
		int ans = initial[0];
		int max = cnts[ans];
		for (int i : initial) {
			if (cnts[i] > max) {
				ans = i;
				max = cnts[i];
			}
		}
		return ans;
	}

}
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值