贪心问题
[NOIP2004 提高组] 合并果子 / [USACO06NOV] Fence Repair G
题目描述
在一个果园里,多多已经将所有的果子打了下来,而且按果子的不同种类分成了不同的堆。多多决定把所有的果子合成一堆。
每一次合并,多多可以把两堆果子合并到一起,消耗的体力等于两堆果子的重量之和。可以看出,所有的果子经过 n − 1 n-1 n−1 次合并之后, 就只剩下一堆了。多多在合并果子时总共消耗的体力等于每次合并所耗体力之和。
因为还要花大力气把这些果子搬回家,所以多多在合并果子时要尽可能地节省体力。假定每个果子重量都为 1 1 1 ,并且已知果子的种类 数和每种果子的数目,你的任务是设计出合并的次序方案,使多多耗费的体力最少,并输出这个最小的体力耗费值。
例如有 3 3 3 种果子,数目依次为 1 1 1 , 2 2 2 , 9 9 9 。可以先将 1 1 1 、 2 2 2 堆合并,新堆数目为 3 3 3 ,耗费体力为 3 3 3 。接着,将新堆与原先的第三堆合并,又得到新的堆,数目为 12 12 12 ,耗费体力为 12 12 12 。所以多多总共耗费体力 = 3 + 12 = 15 =3+12=15 =3+12=15 。可以证明 15 15 15 为最小的体力耗费值。
输入格式
共两行。
第一行是一个整数
n
(
1
≤
n
≤
10000
)
n(1\leq n\leq 10000)
n(1≤n≤10000) ,表示果子的种类数。
第二行包含 n n n 个整数,用空格分隔,第 i i i 个整数 a i ( 1 ≤ a i ≤ 20000 ) a_i(1\leq a_i\leq 20000) ai(1≤ai≤20000) 是第 i i i 种果子的数目。
输出格式
一个整数,也就是最小的体力耗费值。输入数据保证这个值小于 2 31 2^{31} 231 。
样例 #1
样例输入 #1
3
1 2 9
样例输出 #1
15
提示
对于 30 % 30\% 30% 的数据,保证有 n ≤ 1000 n \le 1000 n≤1000:
对于 50 % 50\% 50% 的数据,保证有 n ≤ 5000 n \le 5000 n≤5000;
对于全部的数据,保证有 n ≤ 10000 n \le 10000 n≤10000。
代码如下
1.优先队列
package exercise.luogu.greedy;
import java.util.Comparator;
import java.util.PriorityQueue;
import java.util.Scanner;
public class FruitMerging {
// 定义一个简单的类来表示果子堆和其重量
static class FruitHeap {
int weight;
FruitHeap(int weight) {
this.weight = weight;
}
}
// 比较器,用于优先队列,使得重量最小的堆优先出队
static class FruitHeapComparator implements Comparator<FruitHeap> {
public int compare(FruitHeap fh1, FruitHeap fh2) {
return fh1.weight - fh2.weight;
}
}
// 计算最小的体力耗费值
public static int minEnergy(int[] fruits) {
PriorityQueue<FruitHeap> pq = new PriorityQueue<>(new FruitHeapComparator());
// 将所有果子堆添加到优先队列中
for (int weight : fruits) {
pq.add(new FruitHeap(weight));
}
int totalEnergy = 0;
// 当队列中有多于一个堆时,持续合并
while (pq.size() > 1) {
// 弹出两个最小的堆
FruitHeap fh1 = pq.poll();
FruitHeap fh2 = pq.poll();
// 合并它们,并计算合并的体力消耗
int combinedWeight = fh1.weight + fh2.weight;
totalEnergy += combinedWeight;
// 将合并后的堆添加回队列
pq.add(new FruitHeap(combinedWeight));
}
return totalEnergy;
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int[] fruits = new int[n];
for (int i = 0; i < fruits.length; i++) {
fruits[i] = sc.nextInt();
}
System.out.println(minEnergy(fruits));
}
}
2.普通队列模拟优先队列
package exercise.luogu.greedy;
import java.util.Arrays;
import java.util.Scanner;
public class FruitMergingWithSortedArray {
// 使用插入排序对数组进行排序
public static int[] insertSort(int[] arr, int newValue) {
int[] newArr = Arrays.copyOf(arr, arr.length + 1);
newArr[arr.length] = newValue;
for (int i = 1; i < newArr.length; i++) {
int key = newArr[i];
int j = i - 1;
// 将小于 key 的元素向右移动
while (j >= 0 && newArr[j] > key) {
newArr[j + 1] = newArr[j];
j = j - 1;
}
newArr[j + 1] = key;
}
return newArr;
}
public static int minEnergyWithSortedArray(int[] fruits) {
int[] sortedFruits = new int[0]; // 初始化一个空数组
int totalEnergy = 0;
// 逐个添加果子到数组,并排序
for (int fruit : fruits) {
sortedFruits = insertSort(sortedFruits, fruit);
}
// 现在 sortedFruits 是一个包含所有果子的已排序数组
// 使用类似于之前 PriorityQueue 的逻辑来合并果子
while (sortedFruits.length > 1) {
int combinedWeight = sortedFruits[0] + sortedFruits[1];
totalEnergy += combinedWeight;
// 创建一个新数组,不包含已合并的两个果子
int[] newFruits = new int[sortedFruits.length - 2];
System.arraycopy(sortedFruits, 2, newFruits, 0, newFruits.length);
// 将合并后的果子添加到新数组,并排序
sortedFruits = insertSort(newFruits, combinedWeight);
}
return totalEnergy;
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int[] fruits = new int[n];
for (int i = 0; i < fruits.length; i++) {
fruits[i] = sc.nextInt();
}
System.out.println(minEnergyWithSortedArray(fruits));
}
}
注意:
其实,普通队列使用了排序算法在每次添加数据的时候都进行排序,其实就是在模拟优先队列,所以我称之为"普通队列模拟优先队列".
分治问题
逆序对
题目描述
猫猫 TOM 和小老鼠 JERRY 最近又较量上了,但是毕竟都是成年人,他们已经不喜欢再玩那种你追我赶的游戏,现在他们喜欢玩统计。
最近,TOM 老猫查阅到一个人类称之为“逆序对”的东西,这东西是这样定义的:对于给定的一段正整数序列,逆序对就是序列中 a i > a j a_i>a_j ai>aj 且 i < j i<j i<j 的有序对。知道这概念后,他们就比赛谁先算出给定的一段正整数序列中逆序对的数目。注意序列中可能有重复数字。
Update:数据已加强。
输入格式
第一行,一个数 n n n,表示序列中有 n n n个数。
第二行 n n n 个数,表示给定的序列。序列中每个数字不超过 1 0 9 10^9 109。
输出格式
输出序列中逆序对的数目。
样例 #1
样例输入 #1
6
5 4 2 6 3 1
样例输出 #1
11
提示
对于 25 % 25\% 25% 的数据, n ≤ 2500 n \leq 2500 n≤2500
对于 50 % 50\% 50% 的数据, n ≤ 4 × 1 0 4 n \leq 4 \times 10^4 n≤4×104。
对于所有数据, n ≤ 5 × 1 0 5 n \leq 5 \times 10^5 n≤5×105
请使用较快的输入输出
应该不会 O ( n 2 ) O(n^2) O(n2) 过 50 万吧 by chen_zhe
代码如下:
package exercise.luogu.graph;
import java.util.Scanner;
public class P1908 {
private static long cnt = 0;
private static int[] tmp; // 用于归并排序的辅助数组
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = sc.nextInt();
}
tmp = new int[n];
mergeSort(arr, 0, n - 1);
System.out.println(cnt);
}
public static void mergeSort(int[] arr, int l, int r) {
if (l >= r) {
return;
}
int mid = l + (r - l) / 2;
mergeSort(arr, l, mid);
mergeSort(arr, mid + 1, r);
merge(arr, l, mid, r);
}
public static void merge(int[] arr, int l, int mid, int r) {
int i = l, j = mid + 1, k = l;
while (i <= mid && j <= r) {
if (arr[i] <= arr[j]) {
tmp[k++] = arr[i++];
} else {
cnt += mid - i + 1; // 当前有序数组arr[i..mid]中,所有的元素都大于arr[j]
tmp[k++] = arr[j++];
}
}
while (i <= mid) {
tmp[k++] = arr[i++];
}
while (j <= r) {
tmp[k++] = arr[j++];
}
for (i = l, k = l; i <= r; ) {
arr[i++] = tmp[k++];
}
}
}
注意:
要优化这段代码,我们可以考虑减少不必要的比较次数。当前的实现中,对于每个元素(arr[j]),都会与后面的所有元素进行比较,这导致时间复杂度为(O(n^2))。
一个更高效的算法是使用归并排序的思想来计算逆序对的数量。
在这个优化后的代码中,mergeSort函数用于递归地将数组分成更小的部分,并在merge函数中将这些部分合并起来。在合并的过程中,我们计算了逆序对的数量,并将其累加到cnt变量中。最后,打印出cnt即可得到数组中逆序对的总数。
动态规划问题
[NOIP1996 提高组] 挖地雷
题目描述
在一个地图上有 N ( N ≤ 20 ) N\ (N \le 20) N (N≤20) 个地窖,每个地窖中埋有一定数量的地雷。同时,给出地窖之间的连接路径。当地窖及其连接的数据给出之后,某人可以从任一处开始挖地雷,然后可以沿着指出的连接往下挖(仅能选择一条路径),当无连接时挖地雷工作结束。设计一个挖地雷的方案,使某人能挖到最多的地雷。
输入格式
有若干行。
第 1 1 1 行只有一个数字,表示地窖的个数 N N N。
第 2 2 2 行有 N N N 个数,分别表示每个地窖中的地雷个数。
第 3 3 3 行至第 N + 1 N+1 N+1 行表示地窖之间的连接情况:
第 3 3 3 行有 n − 1 n-1 n−1 个数( 0 0 0 或 1 1 1),表示第一个地窖至第 2 2 2 个、第 3 3 3 个 … \dots … 第 n n n 个地窖有否路径连接。如第 3 3 3 行为 11000 ⋯ 0 11000\cdots 0 11000⋯0,则表示第 1 1 1 个地窖至第 2 2 2 个地窖有路径,至第 3 3 3 个地窖有路径,至第 4 4 4 个地窖、第 5 5 5 个 … \dots … 第 n n n 个地窖没有路径。
第 4 4 4 行有 n − 2 n-2 n−2 个数,表示第二个地窖至第 3 3 3 个、第 4 4 4 个 … \dots … 第 n n n 个地窖有否路径连接。
……
第 n + 1 n+1 n+1 行有 1 1 1 个数,表示第 n − 1 n-1 n−1 个地窖至第 n n n 个地窖有否路径连接。(为 0 0 0 表示没有路径,为 1 1 1 表示有路径)。
输出格式
第一行表示挖得最多地雷时的挖地雷的顺序,各地窖序号间以一个空格分隔,不得有多余的空格。
第二行只有一个数,表示能挖到的最多地雷数。
样例 #1
样例输入 #1
5
10 8 4 7 6
1 1 1 0
0 0 0
1 1
1
样例输出 #1
1 3 4 5
27
提示
【题目来源】
NOIP 1996 提高组第三题
代码如下:
由于此题我代码仍有问题,仅供参考
package exercise.luogu.dp;
import java.util.*;
public class MineDigging {
private static int maxMines = 0; // 记录挖到的最多地雷数
private static List<Integer>[] graph; // 地窖之间的连接图
private static int[] mines; // 每个地窖中的地雷数
private static boolean[] visited; // 记录地窖是否被访问过
public static void main(String[] args) {
// 示例输入,可以根据实际情况修改
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();// 地窖数量
mines = new int[n]; // 每个地窖中的地雷数
for (int i = 0; i < mines.length; i++) {
mines[i] = sc.nextInt();
}
Map<Integer, List<Integer>> edgesMap = new HashMap<>();
int j = n - 1;
while (j != 0) {
List<Integer> edges = new ArrayList<>();
for (int i = 0; i < j; i++) {
edges.add(sc.nextInt());
}
edgesMap.put(n - j, edges);
j--;
}
graph = new ArrayList[n]; // 初始化连接图
for (int i = 0; i < n; i++) {
graph[i] = new ArrayList<>();
}
// 添加连接路径,例如:0-1, 1-2, 1-3, 2-4(表示地窖0连接到地窖1,地窖1连接到地窖2和地窖3,等等)
int index = 0;
for (Map.Entry<Integer, List<Integer>> entry : edgesMap.entrySet()) {
if (index == n - 1) {
break;
}
for (int i = 0; i < entry.getValue().size(); i++) {
if (entry.getValue().get(i) == 1) {
graph[index].add(i + 1);
}
}
index++;
}
// 执行深度优先搜索来找到最多地雷的路径
visited = new boolean[n]; // 初始化访问数组
for (int i = 0; i < n; i++) { // 从每个地窖开始尝试一次DFS
dfs(i, 0); // 从地窖i开始,当前已经挖到的地雷数为0
}
// 输出最多地雷数
System.out.println(maxMines);
}
private static void dfs(int current, int totalMines) {
// 如果当前地窖已经被访问过,直接返回以避免重复计算
if (visited[current]) {
return;
}
// 标记当前地窖为已访问并累加地雷数
visited[current] = true;
totalMines += mines[current]; // 挖到当前地窖的地雷
// 更新最多地雷数
maxMines = Math.max(maxMines, totalMines);
// 继续深度优先搜索相邻的地窖
for (int neighbor : graph[current]) {
dfs(neighbor, totalMines); // 递归搜索相邻地窖
}
// 回溯,标记当前地窖为未访问,以便其他路径可以访问它
visited[current] = false;
}
}
二分查找
【深基13.例1】查找
题目描述
输入 n n n 个不超过 1 0 9 10^9 109 的单调不减的(就是后面的数字不小于前面的数字)非负整数 a 1 , a 2 , … , a n a_1,a_2,\dots,a_{n} a1,a2,…,an,然后进行 m m m 次询问。对于每次询问,给出一个整数 q q q,要求输出这个数字在序列中第一次出现的编号,如果没有找到的话输出 − 1 -1 −1 。
输入格式
第一行 2 2 2 个整数 n n n 和 m m m,表示数字个数和询问次数。
第二行 n n n 个整数,表示这些待查询的数字。
第三行 m m m 个整数,表示询问这些数字的编号,从 1 1 1 开始编号。
输出格式
输出一行, m m m 个整数,以空格隔开,表示答案。
样例 #1
样例输入 #1
11 3
1 3 3 3 5 7 9 11 13 15 15
1 3 6
样例输出 #1
1 2 -1
提示
数据保证, 1 ≤ n ≤ 1 0 6 1 \leq n \leq 10^6 1≤n≤106, 0 ≤ a i , q ≤ 1 0 9 0 \leq a_i,q \leq 10^9 0≤ai,q≤109, 1 ≤ m ≤ 1 0 5 1 \leq m \leq 10^5 1≤m≤105
本题输入输出量较大,请使用较快的 IO 方式。
代码如下:
由于此题同还有两个点超时没有通过,代码仅供参考
package exercise.luogu.binary;
import java.util.Scanner;
public class P2249 {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int m = sc.nextInt();
int[] arr = new int[n];
int[] crr = new int[m];
for (int i = 0; i < arr.length; i++) {
arr[i] = sc.nextInt();
}
for (int i = 0; i < crr.length; i++) {
crr[i] = sc.nextInt();
int index = binarySearch(arr, crr[i]);
if (index == -1) {
System.out.print(-1 + " ");
} else {
System.out.print((index + 1) + " ");
}
}
}
public static int binarySearch(int[] arr, int findValue) {
return binarySearchHelper(arr, 0, arr.length - 1, findValue, -1);
}
public static int binarySearchHelper(int[] arr, int left, int right, int findValue, int result) {
if (left > right) {
return result;
}
int mid = left + (right - left) / 2;
if (arr[mid] < findValue) {
return binarySearchHelper(arr, mid + 1, right, findValue, result);
} else if (arr[mid] > findValue) {
return binarySearchHelper(arr, left, mid - 1, findValue, result);
} else {
return binarySearchHelper(arr, left, mid - 1, findValue, mid);
}
}
}
求助
动态规划习题
二分查找习题
这两个题仍有问题,请看到的朋友们,尝试为我解答,谢谢!!