「考研算法」
前言
本系列文章涉及的算法内容,针对的是哈尔滨工业大学854科目。在本文中通过具体的算法题进行讲解相应算法。
今天涉及的算法主要有:
- 线段树,
- 归并排序衍生的逆序对的数量的问题
- 蓝桥杯中阶乘的正约数个数。
一、线段树算法的使用—掉落的方块(LeetCode 699题)
算法题目:
在二维平面上的 x 轴上,放置着一些方块。
给你一个二维整数数组 positions ,其中 positions[i] = [lefti, sideLengthi] 表示:第 i 个方块边长为 sideLengthi ,其左侧边与 x 轴上坐标点 lefti 对齐。
每个方块都从一个比目前所有的落地方块更高的高度掉落而下。方块沿 y 轴负方向下落,直到着陆到 另一个正方形的顶边 或者是 x 轴上 。一个方块仅仅是擦过另一个方块的左侧边或右侧边不算着陆。一旦着陆,它就会固定在原地,无法移动。
在每个方块掉落后,你必须记录目前所有已经落稳的 方块堆叠的最高高度 。
返回一个整数数组 ans ,其中 ans[i] 表示在第 i 块方块掉落后堆叠的最高高度。
示例 1:
输入:positions = [[1,2],[2,3],[6,1]]
输出:[2,5,5]
解释:
第 1 个方块掉落后,最高的堆叠由方块 1 组成,堆叠的最高高度为 2 。
第 2 个方块掉落后,最高的堆叠由方块 1 和 2 组成,堆叠的最高高度为 5 。
第 3 个方块掉落后,最高的堆叠仍然由方块 1 和 2 组成,堆叠的最高高度为 5 。
因此,返回 [2, 5, 5] 作为答案。
示例 2:
输入:positions = [[100,100],[200,100]]
输出:[100,100]
解释:
第 1 个方块掉落后,最高的堆叠由方块 1 组成,堆叠的最高高度为 100 。
第 2 个方块掉落后,最高的堆叠可以由方块 1 组成也可以由方块 2 组成,堆叠的最高高度为 100 。
因此,返回 [100, 100] 作为答案。
注意,方块 2 擦过方块 1 的右侧边,但不会算作在方块 1 上着陆。
数据范围:
1 <= positions.length <= 1000
1 <= lefti <= 10^8
1 <= sideLengthi <= 10^6
算法代码:
将在线段树的完整代码中进行讲解!
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.TreeSet;
public class FallingSquares {
public static class SegmentTree {
private int[] max;
private int[] change;
private boolean[] update;
public SegmentTree(int size) {
int N = size + 1;
max = new int[N << 2];
change = new int[N << 2];
update = new boolean[N << 2];
}
private void pushUp(int rt) {
max[rt] = Math.max(max[rt << 1], max[rt << 1 | 1]);
}
// ln表示左子树元素结点个数,rn表示右子树结点个数
private void pushDown(int rt, int ln, int rn) {
if (update[rt]) {
update[rt << 1] = true;
update[rt << 1 | 1] = true;
change[rt << 1] = change[rt];
change[rt << 1 | 1] = change[rt];
max[rt << 1] = change[rt];
max[rt << 1 | 1] = change[rt];
update[rt] = false;
}
}
public void update(int L, int R, int C, int l, int r, int rt) {
if (L <= l && r <= R) {
update[rt] = true;
change[rt] = C;
max[rt] = C;
return;
}
int mid = (l + r) >> 1;
pushDown(rt, mid - l + 1, r - mid);
if (L <= mid) {
update(L, R, C, l, mid, rt << 1);
}
if (R > mid) {
update(L, R, C, mid + 1, r, rt << 1 | 1);
}
pushUp(rt);
}
public int query(int L, int R, int l, int r, int rt) {
if (L <= l && r <= R) {
return max[rt];
}
int mid = (l + r) >> 1;
pushDown(rt, mid - l + 1, r - mid);
int left = 0;
int right = 0;
if (L <= mid) {
left = query(L, R, l, mid, rt << 1);
}
if (R > mid) {
right = query(L, R, mid + 1, r, rt << 1 | 1);
}
return Math.max(left, right);
}
}
public HashMap<Integer, Integer> index(int[][] positions) {
TreeSet<Integer> pos = new TreeSet<>();
for (int[] arr : positions) {
pos.add(arr[0]);
pos.add(arr[0] + arr[1] - 1);
}
HashMap<Integer, Integer> map = new HashMap<>();
int count = 0;
for (Integer index : pos) {
map.put(index, ++count);
}
return map;
}
public List<Integer> fallingSquares(int[][] positions) {
HashMap<Integer, Integer> map = index(positions);
int N = map.size();
SegmentTree segmentTree = new SegmentTree(N);
int max = 0;
List<Integer> res = new ArrayList<>();
// 每落一个正方形,收集一下,所有东西组成的图像,最高高度是什么
for (int[] arr : positions) {
int L = map.get(arr[0]);
int R = map.get(arr[0] + arr[1] - 1);
int height = segmentTree.query(L, R, 1, N, 1) + arr[1];
max = Math.max(max, height);
res.add(max);
segmentTree.update(L, R, height, 1, N, 1);
}
return res;
}
}
线段树完整代码:
public class SegmentTree {
public static class SegmentTree {
// arr[]为原序列的信息从0开始,但在arr里是从1开始的
// sum[]模拟线段树维护区间和
// lazy[]为累加和标记
// change[]为更新的值
// update[]为更新标记
private int MAXN;
private int[] arr;
private int[] sum;
private int[] lazy;
private int[] change;
private boolean[] update;
public SegmentTree(int[] origin) {
MAXN = origin.length + 1;
arr = new int[MAXN]; // arr[0] 不用 从1开始使用
for (int i = 1; i < MAXN; i++) {
arr[i] = origin[i - 1];
}
sum = new int[MAXN << 2]; // 某一个范围的累加和信息
lazy = new int[MAXN << 2]; // 某一个范围沒有往下传下来的累加任务
change = new int[MAXN << 2]; // 某一个范围有没有更新操作的任务
update = new boolean[MAXN << 2]; // 某一个范围更新任务
}
private void pushUp(int rt) {
sum[rt] = sum[rt << 1] + sum[rt << 1 | 1];
}
// 之前的,所有累加以及更新,从父范围,发给左右两个子范围
// 分发策略是什么
// ln表示左子树元素结点个数,rn表示右子树结点个数
private void pushDown(int rt, int ln, int rn) {
if (update[rt]) {
update[rt << 1] = true;
update[rt << 1 | 1] = true;
change[rt << 1] = change[rt];
change[rt << 1 | 1] = change[rt];
lazy[rt << 1] = 0;
lazy[rt << 1 | 1] = 0;
sum[rt << 1] = change[rt] * ln;
sum[rt << 1 | 1] = change[rt] * rn;
update[rt] = false;
}
if (lazy[rt] != 0) {
lazy[rt << 1] += lazy[rt];
sum[rt << 1] += lazy[rt] * ln;
lazy[rt << 1 | 1] += lazy[rt];
sum[rt << 1 | 1] += lazy[rt] * rn;
lazy[rt] = 0;
}
}
// 在初始化阶段,先把sum数组,填好
// 在arr[l~r]范围上,去build,1~N,
// rt : 这个范围在sum中的下标
public void build(int l, int r, int rt) {
if (l == r) {
sum[rt] = arr[l];
return;
}
int mid = (l + r) >> 1;
build(l, mid, rt << 1);
build(mid + 1, r, rt << 1 | 1);
pushUp(rt);
}
// L~R 所有的值变成C
// l~r rt
public void update(int L, int R, int C, int l, int r, int rt) {
if (L <= l && r <= R) {
update[rt] = true;
change[rt] = C;
sum[rt] = C * (r - l + 1);
lazy[rt] = 0;
return;
}
// 线段树更新前有累加任务要往下发
int mid = (l + r) >> 1;
pushDown(rt, mid - l + 1, r - mid);
if (L <= mid) {
update(L, R, C, l, mid, rt << 1);
}
if (R > mid) {
update(L, R, C, mid + 1, r, rt << 1 | 1);
}
pushUp(rt);
}
// L~R, C 任务!
// rt,l~r
public void add(int L, int R, int C, int l, int r, int rt) {
// 任务包括当前范围!
if (L <= l && r <= R) {
sum[rt] += C * (r - l + 1);
lazy[rt] += C;
return;
}
// 任务没有包括当前整个范围!
// l r mid = (l+r)/2
int mid = (l + r) >> 1;
pushDown(rt, mid - l + 1, r - mid);
// L~R
if (L <= mid) {
add(L, R, C, l, mid, rt << 1);
}
if (R > mid) {
add(L, R, C, mid + 1, r, rt << 1 | 1);
}
pushUp(rt);
}
// 查询某一段范围的累加和
public long query(int L, int R, int l, int r, int rt) {
if (L <= l && r <= R) {
return sum[rt];
}
int mid = (l + r) >> 1;
pushDown(rt, mid - l + 1, r - mid);
long ans = 0;
if (L <= mid) {
ans += query(L, R, l, mid, rt << 1);
}
if (R > mid) {
ans += query(L, R, mid + 1, r, rt << 1 | 1);
}
return ans;
}
}
}
二、正约数个数
算法题目:
给定n = 100,求n的阶乘的正约数个数。
输入格式
输入n = 100.
输出格式
输出一个整数,表示n的阶乘的正约数的个数。
解题思路如下:
首先将100的阶乘求出来,然后进行质因数分解,也就是每次除以质数,在判断该质数用了几次。然后套用公式:n=(p₁^ a₁) * (p₂^ a₂) * (p₃^ a₃)* (p₄ ^ a₄)...
对于一个大于1正整数n可以分解质因数:n=(p₁^ a₁) * (p₂^ a₂) * (p₃^ a₃)* (p₄ ^ a₄)... 则n的正约数的个数就是(1+a₁)(1+a₂)(1+a₃)(1+a₄)... 假设自然数N等于P的a次乘以q的b次乘以r的C次,P、q、r为不同的质数,则N的约数个数等于(a+1)*(b+1)*(C+1)。
假设自然数N等于P的a次乘以q的b次乘以r的C次,P、q、r为不同的质数,则N的约数个数等于(a+1)*(b+1)*(C+1)。
因数和约数: 约数和因数既有联系,又有区别,这主要表现在以下三个方面。 (1) 约数必须在整除的前提下才存在,而因数是从乘积的角度来提出的。如果数a与数b相乘的积是数c,a与b都是c的因数。 (2) 约数只能对在整数范围内而言,而因数就不限于整数的范围。 例如:6×8=48。既可以说6和8都是48的因数,也可以说6和8都是48的约数。 又如:0.9×8=7.2。虽然可以说0.9和8都是7.2的因数,却不能说0.9和8是7.2的约数。
代码如下:
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
long[] ans = new long[10001000];
long n = sc.nextLong();
ans[0] = 1;
String str = largeIntegerFactorial(n, ans);
int t = 2;
String mm = "";
int z = 0;
ArrayList chuCun = new ArrayList();
BigInteger a = new BigInteger(str);
while (1 == 1) {
if (zhiShu(t)) {
mm = mm + t;
if (a.equals(new BigInteger("1"))) {
break;
}
a = a.divide(new BigInteger(mm));
z++;
if (!a.mod(new BigInteger(mm)).equals(new BigInteger("0"))) {
chuCun.add(z + 1);//将调用这个质数的次数赋值给集合
z = 0;
t++;
}
mm = "";
}
if (zhiShu(t) == false) {
t++;
}
}
BigInteger kk = new BigInteger("1");
String gg = "";
for (int i = 0; i < chuCun.size(); i++) {
int sss = (int) chuCun.get(i);
gg = gg + sss;
kk = kk.multiply(new BigInteger((gg)));
gg = "";
}
System.out.println(kk);
}
public static boolean zhiShu(int a) {
for (int i = 2; i <= a; i++) {
if (i == a) {
return true;
}
if (a % i == 0) {
break;
}
}
return false;
}
public static String largeIntegerFactorial(long n, long[] ans) {
int l = 0;
long num = 0;
for (int i = 1; i <= n; ++i) {
num = 0;
for (int j = 0; j <= l; j++) {
num = num + ans[j] * i;
ans[j] = num % 10;
num /= 10;
}
while (num != 0) {
ans[++l] = num % 10;
num /= 10;
}
}
StringBuilder sb = new StringBuilder();
for (int i = l; i >= 0; --i) {
sb.append(ans[i]);
}
return sb.toString();
}
}
三、逆序对的数量
算法题目:
给定一个长度为 n 的整数数列,请你计算数列中的逆序对的数量。
逆序对的定义如下:对于数列的第 i 个和第 j 个元素,如果满足 i<j 且 a[i]>a[j],则其为一个逆序对;否则不是。
输入格式:
第一行包含整数 n,表示数列的长度。
第二行包含 n 个整数,表示整个数列。
输出格式:
输出一个整数,表示逆序对的个数。
数据范围:
1 ≤ n ≤ 100000,
数列中的元素的取值范围 [1,10^9]。
输入样例:
6
2 3 4 5 6 1
输出样例:
5
算法核心:
其实在本算法中最主要的就是如下两句代码:
res += arr[p1] > arr[p2] ? (mid - p1 + 1) : 0;
help[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
其实就是一句话:
当左边指针p1指向的数arr[p1]和右边指针p2指向的数arr[p2]相等时,要先拿左边的数。
算法代码:
C++代码:
#include <iostream>
using namespace std;
long long res = 0;
void mergeSort(int arr[], int l, int r){
if(l >= r){
return;
}
int mid = l + ((r - l) >> 1);
mergeSort(arr, l, mid);
mergeSort(arr, mid + 1, r);
int i = 0;
int help[r - l + 1];
int p1 = l;
int p2 = mid + 1;
while(p1 <= mid && p2 <= r){
res += arr[p1] > arr[p2] ? (mid - p1 + 1) : 0;
help[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
}
while(p1 <= mid){
help[i++] = arr[p1++];
}
while(p2 <= r){
help[i++] = arr[p2++];
}
for(p1 = l, p2 = 0; p1 <= r; ++p1, ++p2){
arr[p1] = help[p2];
}
}
int main(){
int n;
scanf("%d", &n);
int arr[n];
for(int i = 0; i < n; i++){
scanf("%d", &arr[i]);
}
mergeSort(arr, 0, n - 1);
cout << res;
return 0;
}
Java代码:
import java.util.Scanner;
public class Main {
public static long x;
public static void mergeSort(long[] arr){
mergeSort(arr, 0, arr.length - 1);
}
public static void mergeSort(long[] arr, int l, int r){
if(l >= r){
return;
}
int mid = l + ((r - l) >> 1);
mergeSort(arr, l, mid);
mergeSort(arr, mid + 1, r);
long[] help = new long[r - l + 1];
int i = 0;
int p1 = l;
int p2 = mid + 1;
while(p1 <= mid && p2 <= r){
x += arr[p1] > arr[p2] ? (mid - p1 + 1) : 0;
help[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
}
while(p1 <= mid){
help[i++] = arr[p1++];
}
while(p2 <= r){
help[i++] = arr[p2++];
}
for(i = 0; i < help.length; ++i){
arr[l + i] = help[i];
}
}
public static void main(String[] args) throws IOException {
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(System.out));
String[] str1 = in.readLine().split(" ");
int n = Integer.parseInt(str1[0]);
long[] arr = new long[n];
String[] str2 = in.readLine().split(" ");
for(int i = 0; i < n; i++){
arr[i] = Integer.parseInt(str2[i]);
}
mergeSort(arr, 0, n - 1);
System.out.println(x);
out.flush();
out.close();
in.close();
}
}