B站左神【算法课程】学习笔记 - 【算法讲解109【扩展】树状数组相关题目】

B站左神【算法课程】学习笔记- 【算法讲解109【扩展】树状数组相关题目】


前置知识

讲解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道题,建议大家看懂之后,自己敲一下,复制粘贴是学习的拦路虎。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

CodePlayer大旭

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值