P2240 【深基12.例1】部分背包问题
解题思路
定义和输入:
- 首先定义了一个
GoldCoin
类,用于存储每个金币的重量m
、价值v
和单位价值比ratio
(即每单位重量的价值)。- 然后从标准输入中读取金币的数量
n
和总时间t
。- 创建一个
GoldCoin
数组goldCoin
,用于存储所有金币的信息。读取和计算:
- 使用一个循环读取每个金币的重量
m
和价值v
,并计算单位价值比ratio
,存储在goldCoin
数组中。排序:
- 按照单位价值比
ratio
对金币数组进行排序,排序后的数组是按单位价值从小到大排列的。贪心算法求解:
- 初始化一个变量
ans
用于存储最终的最大价值。- 从排序后的数组的最后一个元素开始遍历(即从单位价值比最大的金币开始)。
- 如果剩余的时间
t
小于等于当前金币的重量m
,则只需要用剩余的时间t
乘以当前金币的单位价值比ratio
,并累加到ans
中,然后结束循环。- 如果剩余的时间
t
大于当前金币的重量m
,则直接累加当前金币的全部价值v
,并减去相应的重量m
,继续处理下一个金币。输出结果:
- 最后输出计算出的最大价值
ans
,保留两位小数。关键点
- 贪心选择性质: 每次选择单位价值比最高的金币,在有限的时间内获取最大价值。
- 排序策略: 按单位价值比进行排序确保每次都选择当前最优的金币。
- 边界条件处理: 如果剩余时间小于某个金币的重量,按比例取部分价值;否则,取整个金币的价值。
import java.util.Arrays;
import java.util.Comparator;
import java.util.Scanner;
public class Main {
public static class GoldCoin{
int m = 0;
int v = 0;
double ratio = 0.0;
}
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
int n = input.nextInt();
int t = input.nextInt();
GoldCoin[] goldCoin = new GoldCoin[n];
for (int i = 0; i < n; i++) {
goldCoin[i] = new GoldCoin();
goldCoin[i].m= input.nextInt();
goldCoin[i].v= input.nextInt();
goldCoin[i].ratio = (double) goldCoin[i].v / goldCoin[i].m;
}
// 从小到大排序
Arrays.sort(goldCoin, Comparator.comparingDouble(a -> a.ratio));
double ans = 0;
// 因为比值从小到大排序,所以从最后开始遍历
for (int i = n - 1; i >= 0; i--) {
if (t <= goldCoin[i].m) {
// 如果t小于等于当前金币的重量,则直接加上当前金币的价值
ans += t * goldCoin[i].ratio;
break;
}else {
// 如果t大于当前金币的重量,则加上当前金币的价值,并减去当前金币的重量
ans += goldCoin[i].v;
t -= goldCoin[i].m;
}
}
System.out.printf("%.2f%n", ans);
}
}
P1223 排队接水
解题思路
- 贪心策略: 将接水时间短的人排在前面,这样每个人的等待时间才会最小化。
- 排序: 按接水时间从小到大排序。
- 计算等待时间: 计算每个人的等待时间,然后求出平均等待时间。
import java.util.Arrays;
import java.util.Comparator;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
int n = input.nextInt();
int[] T = new int[n];
for (int i = 0; i < n; i++) {
T[i] = input.nextInt();
}
// 创建一个二维数组,第一列是接水时间,第二列是原始位置
int[][] persons = new int[n][2];
for (int i = 0; i < n; i++) {
persons[i][0] = T[i];
persons[i][1] = i + 1; // 原始位置从1开始
}
// 按接水时间排序
Arrays.sort(persons, Comparator.comparingInt(a -> a[0]));
// 计算总等待时间和输出顺序
double totalWaitTime = 0;
double currentWaitTime = 0;
StringBuilder order = new StringBuilder();
for (int i = 0; i < n; i++) {
order.append(persons[i][1]).append(" ");
if (i != n - 1) {
currentWaitTime += persons[i][0];
totalWaitTime += currentWaitTime;
}
}
double averageWaitTime = totalWaitTime / n;
// 输出顺序
System.out.println(order.toString().trim());
// 输出平均等待时间,保留两位小数
System.out.printf("%.2f%n", averageWaitTime);
}
}
P1803 凌乱的yyy / 线段覆盖
解题思路
- 贪心策略:遍历排序后的
time
列表,对于每个比赛,如果它的开始时间startTime
大于等于当前时间curTime
,说明可以参加这个比赛,将res
加1,并将curTime
更新为这个比赛的结束时间endTime
。- 排序: 按比赛结束时间从小到大排序。
- 计算最大参加比赛个数: 通过贪心策略进行比较,符合条件的res+1,最终输出结果。
time.sort(Comparator.comparingInt(o -> o.endTime));
sort
方法是集合类(如List)的一个方法,用于对集合中的元素进行排序。在这里,我们使用Comparator.comparingInt
方法来指定排序的规则。Comparator.comparingInt
方法接收一个整数类型的属性(在这里是endTime
)和一个比较器(在这里是o -> o.endTime
),用于比较两个对象的endTime
属性。
o -> o.endTime
是一个lambda表达式,它接收一个对象o
作为参数,并返回该对象的endTime
属性值。这样,Comparator.comparingInt
方法就可以根据对象的endTime
属性值对集合中的元素进行排序。
import java.io.*;
import java.util.ArrayList;
import java.util.Comparator;
// 设置类记录比赛起始时间
class Competition{
int startTime;
int endTime;
public Competition(int startTime, int endTime){
this.startTime = startTime;
this.endTime = endTime;
}
}
public class Main {
static StreamTokenizer st;
static int n;
static int read() throws IOException {
st.nextToken();
return (int) st.nval;
}
public static void main(String[] args) throws IOException {
// 提高读取数据效率
st = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
// 读取比赛数量
n = read();
// 记录比赛时间
ArrayList<Competition> time = new ArrayList<>();
for(int i = 0; i < n; i++){
int startTime = read();
int endTime = read();
Competition c = new Competition(startTime, endTime);
time.add(c);
}
time.sort(Comparator.comparingInt(o -> o.endTime));
int curTime = 0;
int res = 0;
for(int i = 0; i < n; i++){
Competition c = time.get(i);
if(c.startTime >= curTime){
res++;
curTime = c.endTime;
}
}
System.out.println(res);
}
}
P1090 [NOIP2004 提高组] 合并果子 / [USACO06NOV] Fence Repair G
解题思路
贪心策略:核心思想就是找出数组中最小的两个元素进行相加,然后继续寻找相加,最终累加结果即为答案。
贪心策略+冒泡
import java.util.Arrays;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
int n = input.nextInt();
long sum = 0;int index = 0;
int[] a = new int[n];
for (int i = 0; i < n; i++) {
a[i] = input.nextInt();
}
Arrays.sort(a);
// 当只剩下一堆时退出
while (index < n - 1) {
// 每次合并最小的两堆
sum += a[index] + a[index + 1];
// 合并后的一堆存储后下一堆
a[index + 1] += a[index];
// 移动数组
index++;
// 冒泡排序一次遍历完
// 快排会修改数组,改变之前的结果
for (int j = index + 1; j < n; j++) {
if (a[j - 1] > a[j]) {
int temp = a[j];
a[j] = a[j - 1];
a[j - 1] = temp;
}else break;
}
}
System.out.println(sum);
}
}
优先队列
import java.util.PriorityQueue;
import java.util.Queue;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
int n = input.nextInt();
long sum = 0;int index;
Queue<Integer> queue = new PriorityQueue<>();
for (int i = 1; i <= n; i++) {
index = input.nextInt();
queue.add(index);
}
// 当队列只有一个元素的时候退出
while (queue.size() >= 2) {
// 获取队列的最小元素
int a = queue.peek();
// 删除
queue.poll();
// 获取队列的最小元素
int b = queue.peek();
// 删除
queue.poll();
// 计算最小值
sum = a + b + sum;
// 将两个元素相加,并添加到队列中
queue.add(a + b);
}
System.out.println(sum);
}
}
哈夫曼树法
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
int n = input.nextInt();
long sum = 0;
int[] a = new int[n];
for (int i = 0; i < n; i++) {
a[i] = input.nextInt();
}
// 遍历数组累加
for (int i = 0; i < n - 1; i++) {
sum += func(n, a);
}
System.out.println(sum);
}
// 设计函数找到最小的两个堆,将其相加,累计结果
public static int func(int n, int[] a) {
// min1、min2存储数组中两个最小的元素
int min1 = Integer.MAX_VALUE;
int min2 = Integer.MAX_VALUE;
// tem1、tem2表示两个最小元素的下标
int tem1 = 0, tem2 = 0;
for (int i = 0; i < n; i++) {
// 从非0元素开始查找
if (a[i] != 0) {
if (a[i] < min1) {
min1 = a[i];
tem1 = i;
}
}
}
// 把第一个最小值设为0
a[tem1] = 0;
for (int i = 0; i < n; i++) {
// 从非0元素开始查找
if (a[i] != 0) {
if (a[i] < min2) {
min2 = a[i];
tem2 = i;
}
}
}
// min1在查找第一个最小元素时赋值为了第一个最小元素值
// 即将两个最小元素相加
a[tem1] = min1 + min2;
// 把第二个最小值设为0
a[tem2] = 0;
// 返回两个最小值之和
return a[tem1];
}
}
P3817 小A的糖果
解题思路
贪心准则:如果相邻两个盒子的糖果数大于x,就吃掉第二个盒子里超出的糖果。
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
int n = input.nextInt();
int x = input.nextInt();
int[] a = new int[n + 1];
long ans = 0;
for (int i = 1; i <= n; i++) {
a[i] = input.nextInt();
}
for (int i = 1; i <= n; i++) {
int tem = a[i - 1] + a[i];
// 如果相邻两个糖果数大于x,就吃掉右边盒子的糖果
/*
从第一个盒子开始,如果吃第一个盒子的糖果,那么只影响第一个盒子和第二个盒子的结果,
后面第二个盒子和第三个盒子的结果可能还是不符合题意,所以吃第二个盒子的糖果。此时第一个盒子和第二个盒子的组合已经符合题意,
只需要继续向后遍历,以此类推,只要糖果之和大于x,就吃第二个盒子的糖果
*/
if (tem > x) {
ans += tem - x;
a[i] -= tem - x;
}
}
System.out.println(ans);
}
}
P1106 删数问题
解题思路
贪心准则:每一步总是删除一个数字,使得剩下的数最小。
按高位到低位的顺序搜索,若各位数字递增,则删除最后一个数字,否则删除第一个递减区间的首字符,这样删一位便形成了一个新数字串。然后回到数字串首,按上述规则再删下一个数字。重复以上过程s次,剩下的数字串便是问题的解。
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
String n = input.next();
int k = input.nextInt();
// k = 0说明删除完毕,退出循环
while (k > 0) {
int i = 0;
/*
在循环内部,首先初始化一个i变量为0。然后使用另一个while循环,
当i小于字符串n的长度减1,并且n的第一个字符小于等于第二个字符时,
继续执行循环。这个循环的目的是找到一个从左到右第一个递减的数字。
*/
while (i < n.length() - 1 && n.charAt(i) <= n.charAt(i + 1)) {
i++;
}
// 删除字符
n = n.substring(0, i) + n.substring(i + 1);
k--;
}
// 处理前导零的情况
while (n.length() > 1 && n.charAt(0) == '0') {
// 删掉0
n = n.substring(1);
}
System.out.println(n);
}
}
P1478 陶陶摘苹果(升级版)
解题思路
贪心准则:根据所需力气大小对苹果进行递增排序,然后从所需力气最小的苹果开始摘。
import java.util.Arrays;
import java.util.Comparator;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
int n = input.nextInt();
int s = input.nextInt();
int a = input.nextInt();
int b = input.nextInt();
// 测试例子4:所有输入都是0,此时返回结果0
if (n == 0) {
System.out.println(0);
return;
}
// 存储苹果高度和需要力气
int[][] arr = new int[n][2];
for (int i = 0; i < n; i++) {
arr[i][0] = input.nextInt();
arr[i][1] = input.nextInt();
}
// 根据二维数组的第二个元素进行递增排序
Arrays.sort(arr, Comparator.comparingInt(array -> array[1]));
int max = a + b;
int count = 0;
int index = 0;
//System.out.println(Arrays.deepToString(arr));
// 根据贪心法则,从所需力气最小的苹果开始摘,一直摘到力气 <0 为止
while (s >= 0) {
if (arr[index][0] <= max) {
s -= arr[index][1];
count++;
}
index++;
}
// 结果-1是因为在循环里会先加到 s < 0 的情况再退出循环,所以需要-1,可以自己复制洛谷给出的例子debug看循环情况
System.out.println(count - 1);
}
}
P5019 [NOIP2018 提高组] 铺设道路
解题思路
贪心准则:在连续区间填充每块区域时,深度大的区域会顺带填充深度小的区域,这样深度小的区域被填充,深度大的区域也会减少a[i]-a[i-1]的深度。
具体过程:遍历下陷深度数组,如果当前区域的深度大于前一个区域的深度,那么计算从前一个区域的深度填充到当前区域的深度所需的操作次数,并累加到结果中。
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
// 读取道路的长度 n
int n = input.nextInt();
// 创建一个数组 a 来存储每个区域的下陷深度,数组大小为 n + 1 以方便使用1-based索引
int[] a = new int[n + 1];
// 用来存储最少需要的天数
long ans = 0;
// 读取每个区域的下陷深度并存储在数组 a 中,从索引1开始
for (int i = 1; i <= n; i++) {
a[i] = input.nextInt();
}
// 遍历从第2个区域到第 n 个区域
for (int i = 2; i <= n; i++) {
// 如果当前区域的深度大于前一个区域的深度
if (a[i] > a[i - 1]) {
// 计算从前一个区域的深度填充到当前区域的深度所需的操作次数,并累加到 ans 中
ans += a[i] - a[i - 1];
}
}
// 输出总的操作次数加上第一个区域的深度,因为第一个区域也需要填充到0
System.out.println(ans + a[1]);
}
}
P1208 [USACO1.3] 混合牛奶 Mixing Milk
解题思路
贪心准则:基本思路同P1478 陶陶摘苹果(升级版)
把所有农民给出的单价进行递增排序,然后再遍历数组,计算最小花销。
import java.util.Arrays;
import java.util.Comparator;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
int n = input.nextInt();
int m = input.nextInt();
int[][] a = new int[m][2];
int sum = 0;
for (int i = 0; i < m; i++) {
a[i][0] = input.nextInt();
a[i][1] = input.nextInt();
}
Arrays.sort(a, Comparator.comparingInt(arr -> arr[0]));
//System.out.println(Arrays.deepToString(a));
for (int i = 0; i < m; i++) {
if (a[i][1] <= n) {
sum += a[i][0] * a[i][1];
n -= a[i][1];
}else {
sum += a[i][0] * n;
break;
}
}
System.out.println(sum);
}
}
P1094 [NOIP2007 普及组] 纪念品分组
解题思路
贪心准则:将纪念品存入数组进行排序,最小值和最大值相加与上限比较,如果大于上限,说明最大值只能单独一组,反之,最大值和最小值就可以一组。
当left = right时,说明只剩下一个纪念品,此时无论它的值是多少,它都自成一组。
如果没有 = ,那么可能会存在遗漏的情况。
此题虽然简单,但是有兴趣的朋友可以尝试去写公式分情况去证明为什么这样贪心是最优的并且可行的。该题的贪心算法证明
import java.util.Arrays;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
int w = input.nextInt();
int n = input.nextInt();
int[] a = new int[n];
int count = 0;
for (int i = 0; i < n; i++) {
a[i] = input.nextInt();
}
Arrays.sort(a);
int left = 0, right = n - 1;
// 双指针
while (left <= right) {
if (a[left] + a[right] <= w) {
left++;
}
right--;
count++;
}
System.out.println(count);
}
}
P4995 跳跳!
解题思路
贪心准则:每一步都必须消耗当前情况的最大体力。(从地面开始)
首先第一步一定是跳到最高的石头,这样符合贪心准则,其次,第二步需要跳到最矮的石头上,这样确保这两次跳跃的高度差最大,从而保证第二步的体力消耗最大。之后以此类推。
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
int n = input.nextInt();
int[] h = new int[n];
// 由于是平方相加,数据会超出int
long ans;
for (int i = 0; i < n; i++) {
h[i] = input.nextInt();
}
Arrays.sort(h);
int left = 0, right = n - 1;
// 确定结果初始值
ans = (int) Math.pow(h[right], 2);
//
while (left < right) {
// 从最高跳到最低(每一步)
ans += Math.pow((h[right] - h[left]), 2);
// 最高跳过了,right-1即为次高
right--;
// 从最低跳到次高(每一步)
ans += Math.pow(h[left] - h[right], 2);
// 最低跳过了,left+1即为次低
left++;
}
System.out.println(ans);
}
}
P4447 [AHOI2018初中组] 分组
解题思路
- 排序与去重:首先对输入的实力值进行排序,并确保没有重复的实力值。
- 二分搜索与分组:通过二分搜索,将每个学生分配到合适的组中,并记录每个组的大小。
- 找到最小组的人数:遍历所有组,找到最小的组人数,并输出。
(该题可能不太好理解,可以参考洛谷里的题解)
import java.util.Arrays;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
int max = 1000001;
// 队员实力值
int[] a = new int[max];
// 每一组队员个数
int[] size = new int[max];
// 小组添加新成员的实力值
int[] q = new int[max];
// 读取队员数量
int n = input.nextInt();
// 读取每个队员的实力值
for (int i = 1; i <= n; i++) {
a[i] = input.nextInt();
}
// 递增排序
Arrays.sort(a, 1, n + 1);
// 初始化 q[0] 为一个非常大的值,确保不会被使用
q[0] = 1000000001;
// 用于记录当前组的数量
int top = 0;
// 初始化最小组人数的最大值为一个非常大的值
int ans = 1000000000;
// 遍历每个队员的实力值
for (int i = 1; i <= n; i++) {
int l = 0, r = top;
// 二分查找找到合适的组
while (l < r) {
int mid = (l + r + 1) >> 1;
if (a[i] >= q[mid]) {
l = mid;
} else {
r = mid - 1;
}
}
// 如果找到了新的组,将其加入
if (q[l] != a[i]) {
size[++top] = 1; // 新组初始人数为1
q[top] = a[i] + 1; // 更新该组的下一个需要的值
} else {
size[l]++; // 否则,增加现有组的大小
q[l]++; // 更新该组的下一个需要的值
}
}
// 找到最小组的人数
for (int i = 1; i <= top; i++) {
ans = Math.min(ans, size[i]);
}
// 输出结果
System.out.println(ans);
}
}
P1080 [NOIP2012 提高组] 国王游戏
解题思路
贪心策略:将大臣按左右手金币数的乘积进行递增排序。
该题数据较大需要使用高精度。
该证明过程来自题解 P1080 【国王游戏】。
import java.math.BigInteger;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Scanner;
// 创建coin类记录左右手的金币数
class coin{
BigInteger left;
BigInteger right;
}
public class Main {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
int n = input.nextInt();
int a = input.nextInt();
int b = input.nextInt();
// 存储每一位大臣的左右手金币数
coin[] coins = new coin[n];
// 读数据
for (int i = 0; i < n; i++) {
coins[i] = new coin();
coins[i].left = BigInteger.valueOf(input.nextInt());
coins[i].right = BigInteger.valueOf(input.nextInt());
}
// 根据大臣左右手金币数的乘积递增排序
Arrays.sort(coins, Comparator.comparingInt(o -> o.left.intValue() * o.right.intValue()));
// x是国王左手金币数
BigInteger x = BigInteger.valueOf(a);
// 从第一位大臣获得金币数就是:国王左手金币数除以第一位大臣的右手金币数
BigInteger ans = x.divide(coins[0].right);
// 再乘以第一位大臣的左手金币数,方便进行第二位大臣的计算
// 此处不需要输出x = x.multiply(coins[0].left); 该语句会导致下面的循环重复计算
x.multiply(coins[0].left);
// 第一位大臣结果以及得到,从第二位大臣开始
for (int i = 1; i < n; i++) {
x = x.multiply(coins[i - 1].left);
// 如果下一位大臣的金币数大于上一位大臣的金币数,就更新ans为下一位大臣的金币数
if (x.divide(coins[i].right).compareTo(ans) > 0) {
ans = x.divide(coins[i].right);
}
}
System.out.println(ans);
}
}