B站左神【算法课程】学习笔记- 【算法讲解109【扩展】树状数组相关题目】
文章目录
- B站左神【算法课程】学习笔记- 【算法讲解109【扩展】树状数组相关题目】
- 前置知识
- 一、PPT内容
- 二、关键内容笔记
- 三、代码
- 3.1 题目1:[P1908 逆序对](https://www.luogu.com.cn/problem/P1908)
- 3.2 题目2:[P1637 三元上升子序列](https://www.luogu.com.cn/problem/P1637)
- 3.3 题目3:[673. 最长递增子序列的个数](https://leetcode.cn/problems/number-of-longest-increasing-subsequence/description/)
- 3.4 题目4:[P1972 [SDOI2009] HH的项链](https://www.luogu.com.cn/problem/P1972)
- 3.5 题目5:[2193. 得到回文串的最少操作次数](https://leetcode.cn/problems/minimum-number-of-moves-to-make-palindrome/description/)
- 总结
前置知识
讲解022 - 归并分治,本节课题目1、题目5需要
讲解059 - 链式前向星建图,本节课题目5需要
讲解108 - 树状数组
一、PPT内容
上节课讲述:
树状数组原理、扩展、代码详解
本节课讲述:
树状数组相关题目,进一步练习树状数组的使用
最经典的用法:一维数组上的单点增加、范围查询
本节课所有题目都和这个用法有关,这是树状数组最常考的用法
其他用法的相关题目要么比较冷门,要么可以被线段树解决,讲解110会开始讲述线段树的内容
二、关键内容笔记
略
三、代码
3.1 题目1:P1908 逆序对
(洛谷界面说句真心话,真的太难用了,这题可以用Acwing或力扣)
类似练习题目:
逆序对力扣练习题目: LCR 170. 交易逆序对的总数
树状数组力扣练习题目:3072. 将元素分配到两个数组中 II
逆序对Acwing题目:788. 逆序对的数量
题目描述:
给定一个长度为n的数组arr
如果 i < j 且 arr[i] > arr[j]
那么(i,j)就是一个逆序对
求arr中逆序对的数量
1 <= n <= 5 * 10^5
1 <= arr[i] <= 10^9
归并分治实现 :
无需离散化代码、使用空间少、常数时间优良。不能实时查询,只能是离线的批量过程。
树状数组实现 :
需要离散化代码、使用空间多、常数时间稍慢。可以实时查询,也就是在线的查询
两种方法都要掌握,树状数组可以方便的在每个位置进行查询(在线),很多题目都需要这种类型的查询,两个方法的时间复杂度都是O(n * logn)。
方法1:归并分治(其实就是利用归并排序统计逆序对)代码:
package class109;
// 逆序对数量(归并分治)
// 给定一个长度为n的数组arr
// 如果 i < j 且 arr[i] > arr[j]
// 那么(i,j)就是一个逆序对
// 求arr中逆序对的数量
// 1 <= n <= 5 * 10^5
// 1 <= arr[i] <= 10^9
// 测试链接 : https://www.luogu.com.cn/problem/P1908
// 请同学们务必参考如下代码中关于输入、输出的处理
// 这是输入输出处理效率很高的写法
// 提交以下的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_NumberOfReversePair1 {
public static int MAXN = 500001;
public static int[] arr = new int[MAXN];
public static int[] help = 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));
in.nextToken();
n = (int) in.nval;
for (int i = 1; i <= n; i++) {
in.nextToken();
arr[i] = (int) in.nval;
}
out.println(compute());
out.flush();
out.close();
br.close();
}
public static long compute() {
return f(1, n);
}
// 归并分治
// 1) 统计i、j来自l~r范围的情况下,逆序对数量
// 2) 统计完成后,让arr[l...r]变成有序的
public static long f(int l, int r) {
if (l == r) {
return 0;
}
int m = (l + r) / 2;
return f(l, m) + f(m + 1, r) + merge(l, m, r);
}
public static long merge(int l, int m, int r) {
// i来自l.....m
// j来自m+1...r
// 统计有多少逆序对
long ans = 0;
for (int i = m, j = r; i >= l; i--) {
while (j >= m + 1 && arr[i] <= arr[j]) {
j--;
}
ans += j - m;
}
// 左右部分合并,整体变有序,归并排序的过程
int i = l;
int a = l;
int b = m + 1;
while (a <= m && b <= r) {
help[i++] = arr[a] <= arr[b] ? arr[a++] : arr[b++];
}
while (a <= m) {
help[i++] = arr[a++];
}
while (b <= r) {
help[i++] = arr[b++];
}
for (i = l; i <= r; i++) {
arr[i] = help[i];
}
return ans;
}
}
方法2:树状数组代码:
package class109;
// 逆序对数量(值域树状数组)
// 给定一个长度为n的数组arr
// 如果 i < j 且 arr[i] > arr[j]
// 那么(i,j)就是一个逆序对
// 求arr中逆序对的数量
// 1 <= n <= 5 * 10^5
// 1 <= arr[i] <= 10^9
// 测试链接 : https://www.luogu.com.cn/problem/P1908
// 请同学们务必参考如下代码中关于输入、输出的处理
// 这是输入输出处理效率很高的写法
// 提交以下的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 Code01_NumberOfReversePair2 {
public static int MAXN = 500001;
public static int[] arr = new int[MAXN];
public static int[] sort = new int[MAXN];
public static int[] tree = new int[MAXN];
public static int n, m;
public static int lowbit(int i) {
return i & -i;
}
public static void add(int i, int v) {
while (i <= m) {
tree[i] += v;
i += lowbit(i);
}
}
// 1~i范围的累加和
public static long sum(int i) {
long ans = 0;
while (i > 0) {
ans += tree[i];
i -= lowbit(i);
}
return ans;
}
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));
in.nextToken();
n = (int) in.nval;
for (int i = 1; i <= n; i++) {
in.nextToken();
arr[i] = (int) in.nval;
sort[i] = arr[i];
}
out.println(compute());
out.flush();
out.close();
br.close();
}
public static long compute() {
Arrays.sort(sort, 1, n + 1);
m = 1;
for (int i = 2; i <= n; i++) {
if (sort[m] != sort[i]) {
sort[++m] = sort[i];
}
}
for (int i = 1; i <= n; i++) {
arr[i] = rank(arr[i]);
}
long ans = 0;
for (int i = n; i >= 1; i--) {
// 右边有多少数字是 <= 当前数值 - 1
ans += sum(arr[i] - 1);
// 增加当前数字的词频
add(arr[i], 1);
}
return ans;
}
// 给定原始值v
// 返回排名值(排序部分1~m中的下标)
public static int rank(int v) {
int l = 1, r = m, mid;
int ans = 0;
while (l <= r) {
mid = (l + r) / 2;
if (sort[mid] >= v) {
ans = mid;
r = mid - 1;
} else {
l = mid + 1;
}
}
return ans;
}
}
3.2 题目2:P1637 三元上升子序列
题目描述:
升序三元组数量
给定一个数组arr,如果i < j < k且arr[i] < arr[j] < arr[k]
那么称(i, j, k)为一个升序三元组
返回arr中升序三元组的数量
代码:
package class109;
// 升序三元组数量
// 给定一个数组arr,如果i < j < k且arr[i] < arr[j] < arr[k]
// 那么称(i, j, k)为一个升序三元组
// 返回arr中升序三元组的数量
// 测试链接 : https://www.luogu.com.cn/problem/P1637
// 请同学们务必参考如下代码中关于输入、输出的处理
// 这是输入输出处理效率很高的写法
// 提交以下的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 Code02_IncreasingTriples {
public static int MAXN = 30001;
public static int[] arr = new int[MAXN];
public static int[] sort = new int[MAXN];
// 维护信息 : 课上讲的up1数组
// tree1不是up1数组,是up1数组的树状数组
public static long[] tree1 = new long[MAXN];
// 维护信息 : 课上讲的up2数组
// tree2不是up2数组,是up2数组的树状数组
public static long[] tree2 = new long[MAXN];
public static int n, m;
public static int lowbit(int i) {
return i & -i;
}
public static void add(long[] tree, int i, long c) {
while (i <= m) {
tree[i] += c;
i += lowbit(i);
}
}
public static long sum(long[] tree, int i) {
long ans = 0;
while (i > 0) {
ans += tree[i];
i -= lowbit(i);
}
return ans;
}
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));
in.nextToken();
n = (int) in.nval;
for (int i = 1; i <= n; i++) {
in.nextToken();
arr[i] = (int) in.nval;
sort[i] = arr[i];
}
out.println(compute());
out.flush();
out.close();
br.close();
}
// 时间复杂度O(n * logn)
public static long compute() {
Arrays.sort(sort, 1, n + 1);
m = 1;
for (int i = 2; i <= n; i++) {
if (sort[m] != sort[i]) {
sort[++m] = sort[i];
}
}
for (int i = 1; i <= n; i++) {
arr[i] = rank(arr[i]);
}
long ans = 0;
for (int i = 1; i <= n; i++) {
// 查询以当前值做结尾的升序三元组数量
ans += sum(tree2, arr[i] - 1);
// 更新以当前值做结尾的升序一元组数量
add(tree1, arr[i], 1);
// 更新以当前值做结尾的升序二元组数量
add(tree2, arr[i], sum(tree1, arr[i] - 1));
}
return ans;
}
public static int rank(int v) {
int l = 1, r = m, mid;
int ans = 0;
while (l <= r) {
mid = (l + r) / 2;
if (sort[mid] >= v) {
ans = mid;
r = mid - 1;
} else {
l = mid + 1;
}
}
return ans;
}
}
3.3 题目3:673. 最长递增子序列的个数
题目描述:
最长递增子序列的个数
给定一个未排序的整数数组nums,返回最长递增子序列的个数
代码:
package class109;
import java.util.Arrays;
// 最长递增子序列的个数
// 给定一个未排序的整数数组nums,返回最长递增子序列的个数
// 测试链接 : https://leetcode.cn/problems/number-of-longest-increasing-subsequence/
// 本题在讲解072,最长递增子序列问题与扩展,就做出过预告
// 具体可以看讲解072视频最后的部分
// 用树状数组实现时间复杂度O(n * logn)
public class Code03_NumberOfLIS {
public static int MAXN = 2001;
public static int[] sort = new int[MAXN];
// 维护信息 : 以数值i结尾的最长递增子序列,长度是多少
// 维护的信息以树状数组组织
public static int[] treeMaxLen = new int[MAXN];
// 维护信息 : 以数值i结尾的最长递增子序列,个数是多少
// 维护的信息以树状数组组织
public static int[] treeMaxLenCnt = new int[MAXN];
public static int m;
public static int lowbit(int i) {
return i & -i;
}
// 查询结尾数值<=i的最长递增子序列的长度,赋值给maxLen
// 查询结尾数值<=i的最长递增子序列的个数,赋值给maxLenCnt
public static int maxLen, maxLenCnt;
public static void query(int i) {
maxLen = maxLenCnt = 0;
while (i > 0) {
if (maxLen == treeMaxLen[i]) {
maxLenCnt += treeMaxLenCnt[i];
} else if (maxLen < treeMaxLen[i]) {
maxLen = treeMaxLen[i];
maxLenCnt = treeMaxLenCnt[i];
}
i -= lowbit(i);
}
}
// 以数值i结尾的最长递增子序列,长度达到了len,个数增加了cnt
// 更新树状数组
public static void add(int i, int len, int cnt) {
while (i <= m) {
if (treeMaxLen[i] == len) {
treeMaxLenCnt[i] += cnt;
} else if (treeMaxLen[i] < len) {
treeMaxLen[i] = len;
treeMaxLenCnt[i] = cnt;
}
i += lowbit(i);
}
}
public static int findNumberOfLIS(int[] nums) {
int n = nums.length;
for (int i = 1; i <= n; i++) {
sort[i] = nums[i - 1];
}
Arrays.sort(sort, 1, n + 1);
m = 1;
for (int i = 2; i <= n; i++) {
if (sort[m] != sort[i]) {
sort[++m] = sort[i];
}
}
Arrays.fill(treeMaxLen, 1, m + 1, 0);
Arrays.fill(treeMaxLenCnt, 1, m + 1, 0);
int i;
for (int num : nums) {
i = rank(num);
query(i - 1);
if (maxLen == 0) {
// 如果查出数值<=i-1结尾的最长递增子序列长度为0
// 那么说明,以值i结尾的最长递增子序列长度就是1,计数增加1
add(i, 1, 1);
} else {
// 如果查出数值<=i-1结尾的最长递增子序列长度为maxLen != 0
// 那么说明,以值i结尾的最长递增子序列长度就是maxLen + 1,计数增加maxLenCnt
add(i, maxLen + 1, maxLenCnt);
}
}
query(m);
return maxLenCnt;
}
public static int rank(int v) {
int ans = 0;
int l = 1, r = m, mid;
while (l <= r) {
mid = (l + r) / 2;
if (sort[mid] >= v) {
ans = mid;
r = mid - 1;
} else {
l = mid + 1;
}
}
return ans;
}
}
3.4 题目4:P1972 [SDOI2009] HH的项链
题目描述:
一共有n个位置,每个位置颜色给定,i位置的颜色是arr[i]
一共有m个查询,question[i] = {li, ri}
表示第i条查询想查arr[li…ri]范围上一共有多少种不同颜色
返回每条查询的答案
1 <= n、m、arr[i] <= 10^6
1 <= li <= ri <= n
代码:
package class109;
// HH的项链
// 一共有n个位置,每个位置颜色给定,i位置的颜色是arr[i]
// 一共有m个查询,question[i] = {li, ri}
// 表示第i条查询想查arr[li..ri]范围上一共有多少种不同颜色
// 返回每条查询的答案
// 1 <= n、m、arr[i] <= 10^6
// 1 <= li <= ri <= n
// 测试链接 : https://www.luogu.com.cn/problem/P1972
// 请同学们务必参考如下代码中关于输入、输出的处理
// 这是输入输出处理效率很高的写法
// 提交以下的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 Code04_DifferentColors {
public static int MAXN = 1000010;
public static int[] arr = new int[MAXN];
public static int[][] query = new int[MAXN][3];
public static int[] ans = new int[MAXN];
public static int[] map = new int[MAXN];
public static int[] tree = new int[MAXN];
public static int n, m;
public static int lowbit(int i) {
return i & -i;
}
public static void add(int i, int v) {
while (i <= n) {
tree[i] += v;
i += lowbit(i);
}
}
public static int sum(int i) {
int ans = 0;
while (i > 0) {
ans += tree[i];
i -= lowbit(i);
}
return ans;
}
public static int range(int l, int r) {
return sum(r) - sum(l - 1);
}
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));
in.nextToken();
n = (int) in.nval;
for (int i = 1; i <= n; i++) {
in.nextToken();
arr[i] = (int) in.nval;
}
in.nextToken();
m = (int) in.nval;
for (int i = 1; i <= m; i++) {
in.nextToken();
query[i][0] = (int) in.nval;
in.nextToken();
query[i][1] = (int) in.nval;
query[i][2] = i;
}
compute();
for (int i = 1; i <= m; i++) {
out.println(ans[i]);
}
out.flush();
out.close();
br.close();
}
public static void compute() {
Arrays.sort(query, 1, m + 1, (a, b) -> a[1] - b[1]);
for (int s = 1, q = 1, l, r, i; q <= m; q++) {
r = query[q][1];
for (; s <= r; s++) {
int color = arr[s];
if (map[color] != 0) {
add(map[color], -1);
}
add(s, 1);
map[color] = s;
}
l = query[q][0];
i = query[q][2];
ans[i] = range(l, r);
}
}
}
3.5 题目5:2193. 得到回文串的最少操作次数
题目描述:
给你一个只包含小写英文字母的字符串s
每一次操作可以选择s中两个相邻的字符进行交换
返回将s变成回文串的最少操作次数
输入数据会确保s一定能变成一个回文串
代码:
package class109;
import java.util.Arrays;
// 得到回文串的最少操作次数
// 给你一个只包含小写英文字母的字符串s
// 每一次操作可以选择s中两个相邻的字符进行交换
// 返回将s变成回文串的最少操作次数
// 输入数据会确保s一定能变成一个回文串
// 测试链接 : https://leetcode.cn/problems/minimum-number-of-moves-to-make-palindrome/
public class Code05_MinimumNumberOfMovesToMakePalindrome {
public static int MAXN = 2001;
public static int MAXV = 26;
public static int n;
public static char[] s;
// 所有字符的位置列表
public static int[] end = new int[MAXV];
public static int[] pre = new int[MAXN];
// 树状数组
public static int[] tree = new int[MAXN];
// 归并分治
public static int[] arr = new int[MAXN];
public static int[] help = new int[MAXN];
public static void build() {
Arrays.fill(end, 0, MAXV, 0);
Arrays.fill(arr, 1, n + 1, 0);
Arrays.fill(tree, 1, n + 1, 0);
for (int i = 1; i <= n; i++) {
add(i, 1);
}
}
// 字符v把下标j加入列表
public static void push(int v, int j) {
pre[j] = end[v];
end[v] = j;
}
// 弹出当前v字符最后的下标
public static int pop(int v) {
int ans = end[v];
end[v] = pre[end[v]];
return ans;
}
public static int lowbit(int i) {
return i & -i;
}
public static void add(int i, int v) {
while (i <= n) {
tree[i] += v;
i += lowbit(i);
}
}
public static int sum(int i) {
int ans = 0;
while (i > 0) {
ans += tree[i];
i -= lowbit(i);
}
return ans;
}
// 时间复杂度O(n * logn)
public static int minMovesToMakePalindrome(String str) {
s = str.toCharArray();
n = s.length;
build();
for (int i = 0, j = 1; i < n; i++, j++) {
push(s[i] - 'a', j);
}
// arr[i]记录每个位置的字符最终要去哪
for (int i = 0, l = 1, r, k; i < n; i++, l++) {
if (arr[l] == 0) {
r = pop(s[i] - 'a');
if (l < r) {
k = sum(l);
arr[l] = k;
arr[r] = n - k + 1;
} else {
arr[l] = (1 + n) / 2;
}
add(r, -1);
}
}
return number(1, n);
}
public static int number(int l, int r) {
if (l >= r) {
return 0;
}
int m = (l + r) / 2;
return number(l, m) + number(m + 1, r) + merge(l, m, r);
}
public static int merge(int l, int m, int r) {
int ans = 0;
for (int i = m, j = r; i >= l; i--) {
while (j >= m + 1 && arr[i] <= arr[j]) {
j--;
}
ans += j - m;
}
int i = l;
int a = l;
int b = m + 1;
while (a <= m && b <= r) {
help[i++] = arr[a] <= arr[b] ? arr[a++] : arr[b++];
}
while (a <= m) {
help[i++] = arr[a++];
}
while (b <= r) {
help[i++] = arr[b++];
}
for (i = l; i <= r; i++) {
arr[i] = help[i];
}
return ans;
}
}
总结
本节主要是讲了树状数组的习题课,主要讲了5道题,建议大家看懂之后,自己敲一下,复制粘贴是学习的拦路虎。