0. 前言
PS:本人并不是集训队的成员,因此代码写的烂轻点喷。。。本专题一方面是巩固自己的算法知识,另一方面是给NBU学弟学妹们参考解题思路(切勿直接搬运抄袭提交作业!!!)最后,该系列博客AC代码均以Java语言提交,C/C++的可以参考思路编写代码
1. 题目详情
1.1 题目一:第K小的数
1.1.1 题目信息
题目描述:
输入n个数,求其中第k小的数。(要求采用分治法完成,不建议采用完整的排序)
输入要求:
第一行包含两个整数n和k;n<1000,1<=K<=n
第二行包含n个整数。
输出要求:
输出第k小的那个整数_。_
输入样例:
15 1
1 3 7 2 4 6 -1 0 9 88 2 5 17 6 1
输出样例:
-1
来源:
NBUOJ
1.1.2 算法思路(分治法)
本题的一般思路有如下几种:
- 借助堆/优先级队列实现的TopK问题
- 基于完整的排序算法
- 分治策略
由于题目限制使用分治思想,因此这里介绍最后一种方法
分治思想:简单来说就是分而治之,化繁为简,将大问题分解成子问题逐一求解
算法步骤(重要):
- 题目要求找到n个数中第k小的元素,我们首先在当前数组中随意选中一个基准元素
pivot
- 然后使用双指针 区域划分 的思想将数组划分为两部分,其中左半区域的元素比
pivot
小,右半区域的元素比pivot
大 - 此时假设
pivot
基准元素的下标是pivotIndex
,pivot
元素就是整个数组中第 pivotIndex + 1 小的元素 - 此时令左半区域的元素个数为
leftNum
,如果 k==leftNum + 1 那么直接返回基准元素,如果 k < leftNum + 1 那么就递归左半部分区域继续找到第k小的元素,如果 k > leftNum + 1 ,那么就递归右半部分区域递归找第k - (leftNum + 1)小的元素
PS:如果不清楚数组区域划分算法的小伙伴可以自行搜索或者直接看下面的代码实现部分
示例:我们使用上述给出的测试用例画图讲解:
1.1.2 AC代码(Java实现)
NBUOJ上面Java带中文注释会报错!因此这里放了两个版本的代码,前面不带注释的可以直接跑OJ通过,后面的方便读者阅读
1.1.2.1 不带注释版本
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt();
int k = scanner.nextInt();
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = scanner.nextInt();
}
System.out.println(partition(arr, 0, n - 1, k));
}
private static int partition(int[] arr, int left, int right, int k) {
int pivot = arr[right];
int prev = left - 1;
int cur = left;
while (cur <= right) {
if (arr[cur] <= pivot) {
int tmp = arr[prev + 1];
arr[prev + 1] = arr[cur];
arr[cur] = tmp;
cur++;
prev++;
} else {
cur++;
}
}
int leftNum = prev - left;
if (leftNum + 1 == k) {
return pivot;
} else if (leftNum + 1 > k) {
return partition(arr, left, prev - 1, k);
} else {
return partition(arr, prev + 1, right, k - (leftNum + 1));
}
}
}
1.1.2.2 带注释版本
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt();
int k = scanner.nextInt();
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = scanner.nextInt();
}
System.out.println(partition(arr, 0, n - 1, k));
}
/**
* 在数组arr的[left, right]区域找到第k小的数并返回
* @return 第k小的数
*/
private static int partition(int[] arr, int left, int right, int k) {
// 选基准元素
int pivot = arr[right];
int prev = left - 1; // 指向小于pivot的最后元素
int cur = left; // 遍历数组
while (cur <= right) {
if (arr[cur] <= pivot) {
// 将小于等于pivot的元素移至前面
int tmp = arr[prev + 1];
arr[prev + 1] = arr[cur];
arr[cur] = tmp;
cur++;
prev++;
} else {
// 大于pivot不用移动
cur++;
}
}
int leftNum = prev - left; // 左半区域元素个数
if (leftNum + 1 == k) {
return pivot;
} else if (leftNum + 1 > k) {
// 递归左半区域
return partition(arr, left, prev - 1, k);
} else {
// 递归右半区域
return partition(arr, prev + 1, right, k - (leftNum + 1));
}
}
}
1.1.3 扩展题
这里放一些跟本题类似的OJ题(读者可自行尝试解题):
- LeetCode数组中的第K大元素:https://leetcode.cn/problems/xx4gT2/description/
- LeetCode最小k个数:https://leetcode.cn/problems/smallest-k-lcci/description/
1.2 题目二:棋盘覆盖
1.2.1 题目信息
题目描述:
在一个2k×2k 个方格组成的棋盘中,恰有一个方格与其他方格不同,称该方格为一特殊方格,且称该棋盘为一特殊棋盘。在棋盘覆盖问题中,要用图示的4种不同形态的L型骨牌覆盖给定的特殊棋盘上除特殊方格以外的所有方格,且任何2个L型骨牌不得重叠覆盖。
输入要求:
输入一个整数k,k<=5;
输入特殊格子的坐标x,y。
输出要求:
输出一个由数值表示的二维矩阵。填充规则如下:
(1)用数值填充方格;
(2)特殊方格数值为0;
(3)从中心点开始;然后左上、右上、左下、右下的计数顺序填数;同一块用相同数值表示;
(4)每个数值占4个位置空间;右对齐,左补空格。
输入样例:
3
1 2
输出样例:
来源:
NBUOJ
1.2.2 算法思路(分治法)
分治策略:首先我们需要明确填充的顺序,根据题目描述填充规则为,从中心点开始,然后按照左上,右上,左下,右下的顺序进行填充
算法步骤:
- 首先定义出口条件:只有一个元素时不处理
- 找到当前区域的中心点,另四个点位坐标分别是(midRow, midCol),(midRow, midCol + 1),(midRow + 1, midCol),(midRow + 1, midCol + 1),分别对应四个方位(左上、右上、左下、右下)按照顺序进行处理,**以左上方位举例:**如果说左上存在特殊点,那么特殊点不变继续递归填充
dfs(左上, 原特殊点)
,但是如果左上方位内部没有特殊点,那么就将当前中心区域的点位作为特殊点继续递归dfs(左上,新特殊点)
,此时新特殊点为(midRow, midCol)
示例:也许上面的文字描述有点抽象,还是以画图进行举例:
输入:2 0 3
注意:如果我们使用成员变量(或者C++中的全局变量),我们需要先维护好中间位置的填充值,然后在递归四个方位,不然有可能出现在递归完左上角后准备填充中间元素的右上区域值时(填充值已经被改变),不过也可以事先全部填充完中间元素再进行递归(看个人习惯)
1.2.3 AC代码(Java实现)
NBUOJ上面Java带中文注释会报错!因此这里放了两个版本的代码,前面不带注释的可以直接跑OJ通过,后面的方便读者阅读
1.2.3.1 不带注释版本
import java.util.Scanner;
public class Main {
private static int curValue = 1;
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextInt()) {
curValue = 1;
int k = scanner.nextInt();
int specialRow = scanner.nextInt();
int specialCol = scanner.nextInt();
int edges = (int) Math.pow(2, k);
int[][] matrix = new int[edges][edges];
matrix[specialRow][specialCol] = 0;
partition(matrix, 0, edges - 1, 0, edges - 1, specialRow, specialCol);
for (int i = 0; i < matrix.length; i++) {
for (int j = 0; j < matrix[i].length - 1; j++) {
System.out.printf("%4s", matrix[i][j]);
}
System.out.printf("%4s\n", matrix[i][matrix[i].length - 1]);
}
}
}
private static void partition(int[][] matrix, int upRow, int downRow, int leftCol, int rightCol, int specialRow, int specialCol) {
if (upRow == downRow && leftCol == rightCol) {
return;
}
int midValue = curValue;
curValue++;
int midLeftUpRow = (upRow + downRow) / 2;
int midLeftUpCol = (leftCol + rightCol) / 2;
if (upRow <= specialRow && specialRow <= midLeftUpRow && leftCol <= specialCol && specialCol <= midLeftUpCol) {
partition(matrix, upRow, midLeftUpRow, leftCol, midLeftUpCol, specialRow, specialCol);
} else {
matrix[midLeftUpRow][midLeftUpCol] = midValue;
partition(matrix, upRow, midLeftUpRow, leftCol, midLeftUpCol, midLeftUpRow, midLeftUpCol);
}
int midRightUpRow = midLeftUpRow;
int midRightUpCol = midLeftUpCol + 1;
if (upRow <= specialRow && specialRow <= midRightUpRow && midRightUpCol <= specialCol && specialCol <= rightCol) {
partition(matrix, upRow, midRightUpRow, midRightUpCol, rightCol, specialRow, specialCol);
} else {
matrix[midRightUpRow][midRightUpCol] = midValue;
partition(matrix, upRow, midRightUpRow, midRightUpCol, rightCol, midRightUpRow, midRightUpCol);
}
int midLeftDownRow = midLeftUpRow + 1;
int midLeftDownCol = midLeftUpCol;
if (midLeftDownRow <= specialRow && specialRow <= downRow && leftCol <= specialCol && specialCol <= midLeftDownCol) {
partition(matrix, midLeftDownRow, downRow, leftCol, midLeftDownCol, specialRow, specialCol);
} else {
matrix[midLeftDownRow][midLeftDownCol] = midValue;
partition(matrix, midLeftDownRow, downRow, leftCol, midLeftDownCol, midLeftDownRow, midLeftDownCol);
}
int midRightDownRow = midLeftUpRow + 1;
int midRightDownCol = midLeftUpCol + 1;
if (midRightDownRow <= specialRow && specialRow <= downRow && midRightDownCol <= specialCol && specialCol <= rightCol) {
partition(matrix, midRightDownRow, downRow, midRightDownCol, rightCol, specialRow, specialCol);
} else {
matrix[midRightDownRow][midRightDownCol] = midValue;
partition(matrix, midRightDownRow, downRow, midRightDownCol, rightCol, midRightDownRow, midRightDownCol);
}
}
}
1.2.3.2 带注释版本
import java.util.Scanner;
public class Main {
private static int curValue = 1;
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextInt()) {
// 读取k值
curValue = 1;
int k = scanner.nextInt();
int specialRow = scanner.nextInt(); // 特殊点横坐标
int specialCol = scanner.nextInt(); // 特殊点纵坐标
int edges = (int) Math.pow(2, k);
int[][] matrix = new int[edges][edges];
matrix[specialRow][specialCol] = 0;
partition(matrix, 0, edges - 1, 0, edges - 1, specialRow, specialCol);
for (int i = 0; i < matrix.length; i++) {
for (int j = 0; j < matrix[i].length - 1; j++) {
System.out.printf("%4s", matrix[i][j]);
}
System.out.printf("%4s\n", matrix[i][matrix[i].length - 1]);
}
}
}
/**
*
* @param matrix 矩阵
* @param upRow 行的上界
* @param downRow 行的下界
* @param leftCol 列的左边界
* @param rightCol 列的右边界
* @param specialRow 特殊点横坐标
* @param specialCol 特殊点纵坐标
*/
private static void partition(int[][] matrix, int upRow, int downRow, int leftCol, int rightCol, int specialRow, int specialCol) {
if (upRow == downRow && leftCol == rightCol) {
// 只有一个元素,直接返回
return;
}
// 计算中心区域
// 维护中间区域填充值
int midValue = curValue;
curValue++;
int midLeftUpRow = (upRow + downRow) / 2;
int midLeftUpCol = (leftCol + rightCol) / 2;
if (upRow <= specialRow && specialRow <= midLeftUpRow && leftCol <= specialCol && specialCol <= midLeftUpCol) {
// 特殊点在左上区域
partition(matrix, upRow, midLeftUpRow, leftCol, midLeftUpCol, specialRow, specialCol);
} else {
matrix[midLeftUpRow][midLeftUpCol] = midValue;
partition(matrix, upRow, midLeftUpRow, leftCol, midLeftUpCol, midLeftUpRow, midLeftUpCol);
}
int midRightUpRow = midLeftUpRow;
int midRightUpCol = midLeftUpCol + 1;
if (upRow <= specialRow && specialRow <= midRightUpRow && midRightUpCol <= specialCol && specialCol <= rightCol) {
// 特殊点在右上区域
partition(matrix, upRow, midRightUpRow, midRightUpCol, rightCol, specialRow, specialCol);
} else {
matrix[midRightUpRow][midRightUpCol] = midValue;
partition(matrix, upRow, midRightUpRow, midRightUpCol, rightCol, midRightUpRow, midRightUpCol);
}
int midLeftDownRow = midLeftUpRow + 1;
int midLeftDownCol = midLeftUpCol;
if (midLeftDownRow <= specialRow && specialRow <= downRow && leftCol <= specialCol && specialCol <= midLeftDownCol) {
// 特殊点在左下区域
partition(matrix, midLeftDownRow, downRow, leftCol, midLeftDownCol, specialRow, specialCol);
} else {
matrix[midLeftDownRow][midLeftDownCol] = midValue;
partition(matrix, midLeftDownRow, downRow, leftCol, midLeftDownCol, midLeftDownRow, midLeftDownCol);
}
int midRightDownRow = midLeftUpRow + 1;
int midRightDownCol = midLeftUpCol + 1;
if (midRightDownRow <= specialRow && specialRow <= downRow && midRightDownCol <= specialCol && specialCol <= rightCol) {
// 特殊点在右下区域
partition(matrix, midRightDownRow, downRow, midRightDownCol, rightCol, specialRow, specialCol);
} else {
matrix[midRightDownRow][midRightDownCol] = midValue;
partition(matrix, midRightDownRow, downRow, midRightDownCol, rightCol, midRightDownRow, midRightDownCol);
}
}
}
1.3 题目三:求逆序对
1.3.1 题目信息
题目描述:
给定一个序列a1,a2,…,an,如果存在i<j并且ai>aj,那么我们称之为逆序对,求逆序对的数目。
要求用分治法求解。
输入要求:
第一行为n,表示序列长度;
接下来的n行,第i+1行表示序列中的第i个数。/或者第2行有n个数。
输出要求:
所有逆序对总数
输入样例:
4
3
2
3
2
输出样例:
3
来源:
NBUOJ
1.3.2 算法思路(分治法)
这里直接给大家介绍算法思路了:
模板:归并排序
算法步骤(重要):
- 我们设计一个递归函数
partition(int[] arr, int left, int right)
求解区间内逆序对的个数并且完成排序功能 - 然后我们将数组划分为两个区间[left, mid],[mid + 1, right]
- 递归调用
partition(arr, left, mid)
此时将左半部分区间逆序对统计完毕并且排序完成,递归调用partition(arr, mid + 1, right)
此时将右半部分区间逆序对统计完毕并且排序完成 - 此时我们只需要统计排序完毕的左区间和排序完毕后右区间之间的逆序对个数了
PS:上述代码基于归并排序的模板实现,下面着重讲解如何统计两个排序完毕区间之间的逆序对个数
示例:测试用例:[1, 5, 2, 7, 4, 3, 0, 6] 画图讲解:
上图所示是递归与回溯的示意图:下面我们专门讲解两个有序区间内部如何统计逆序对的问题
PS:与上图示意图有所不一致的是,统计逆序对的问题在归并排序的时候最好使用降序排序,至于为什么,大家下来自己尝试一下嘻嘻!
此时相比聪明的大家已经可以想明白过程了(事实上就是在二路归并中进行逆序对的统计,并且只有左区间的当前元素大于右区间当前元素时才会统计),当左区间全部遍历完毕后,两个区间之间的逆序对个数就统计完毕了!
1.3.3 AC代码(Java实现)
NBUOJ上面Java带中文注释会报错!因此这里放了两个版本的代码,前面不带注释的可以直接跑OJ通过,后面的方便读者阅读
1.3.3.1 不带注释版本
import java.util.Scanner;
public class Main {
private static int reversePairCount = 0;
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextInt()) {
reversePairCount = 0;
int n = scanner.nextInt();
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = scanner.nextInt();
}
partition(arr, 0, n - 1);
System.out.println(reversePairCount);
}
}
private static void partition(int[] arr, int left, int right) {
if (left >= right) {
return;
}
int mid = (left + right) / 2;
partition(arr, left, mid);
partition(arr, mid + 1, right);
int[] tmp = new int[right - left + 1];
int tmpIndex = 0;
int left1 = left;
int right1 = mid;
int left2 = mid + 1;
int right2 = right;
while (left1 <= right1 && left2 <= right2) {
if (arr[left1] > arr[left2]) {
tmp[tmpIndex++] = arr[left1];
left1++;
reversePairCount += (right2 - left2 + 1);
} else {
tmp[tmpIndex++] = arr[left2];
left2++;
}
}
while (left1 <= right1) {
tmp[tmpIndex++] = arr[left1];
left1++;
}
while (left2 <= right2) {
tmp[tmpIndex++] = arr[left2];
left2++;
}
for (int i = 0; i < tmpIndex; i++) {
arr[left + i] = tmp[i];
}
}
}
1.3.3.2 带注释版本
import java.util.Scanner;
public class Main {
private static int reversePairCount = 0;
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextInt()) {
reversePairCount = 0;
int n = scanner.nextInt();
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = scanner.nextInt();
}
partition(arr, 0, n - 1);
System.out.println(reversePairCount);
}
}
/**
* 统计区间[left, right]内部逆序对的个数
* @param arr 数组元素
* @param left 左区间
* @param right 右区间
*/
private static void partition(int[] arr, int left, int right) {
if (left >= right) {
return;
}
// 计算中心下标
int mid = (left + right) / 2;
partition(arr, left, mid); // 统计左区间逆序对个数并排序
partition(arr, mid + 1, right); // 统计右区间逆序对个数并排序
int[] tmp = new int[right - left + 1]; // 临时数组
int tmpIndex = 0;
int left1 = left;
int right1 = mid;
int left2 = mid + 1;
int right2 = right;
while (left1 <= right1 && left2 <= right2) {
if (arr[left1] > arr[left2]) {
tmp[tmpIndex++] = arr[left1];
left1++;
reversePairCount += (right2 - left2 + 1);
} else {
tmp[tmpIndex++] = arr[left2];
left2++;
}
}
while (left1 <= right1) {
tmp[tmpIndex++] = arr[left1];
left1++;
}
while (left2 <= right2) {
tmp[tmpIndex++] = arr[left2];
left2++;
}
// 拷贝回原数组
for (int i = 0; i < tmpIndex; i++) {
arr[left + i] = tmp[i];
}
}
}
1.3.4 扩展题
这里放一些跟本题类似的OJ题(读者可自行尝试解题):
- LeetCode交易逆序对的总数:https://leetcode.cn/problems/shu-zu-zhong-de-ni-xu-dui-lcof/description/
- LeetCode翻转对:https://leetcode.cn/problems/reverse-pairs/description/