在现代计算中有一个非常核心和普遍的场景:资源调度与分配。
-
物理机 (
capacities) : 想象一个机房里有成百上千台物理服务器。这些服务器的配置各不相同,有的内存大,有的内存小。capacities数组代表了这个物理服务器资源池,每个元素是某台服务器当前可用的内存大小。 -
虚拟机请求 (
requests) : 不断有客户通过网站或API请求创建虚拟机(云主机)。每个请求都会指定需要的内存大小(例如 "我要一台8G内存的虚拟机")。requests数组代表了这一系列按时间顺序到达的客户请求。 -
调度系统的任务: 云平台的调度系统(也称“大脑”)需要为每一个新的虚拟机请求,从成千上万的物理机中挑选一台最合适的来部署。
-
为什么选择“可用内存最小”的物理机?(Best-Fit 策略) 这是一种经典的资源分配策略,称为最佳适应 (Best-Fit) 。它的主要目的是减少内存碎片化。
例子: 假设你有两台物理机,A机剩余 100GB 内存,B机剩余 20GB 内存。现在来了一个需要 16GB 内存的虚拟机请求。
-
如果把它放在 A机,A机剩下 84GB。
-
如果把它放在 B机,B机剩下 4GB。
-
选择 B机(可用内存最小的)显然是更好的策略。因为这样保留了 A机这一整块大的内存资源,未来如果有一个需要 90GB 内存的大型虚拟机请求,我们仍然可以满足它。如果当初用掉了 A机,那么这个 90GB 的请求就无法满足了。
-
“编号最小”的规则:这是一个 tie-breaker(决胜局规则),当有多台物理机的“匹配度”(最小可用内存)相同时,需要一个确定的规则来选择,以保证分配结果是可预测和唯一的。选择编号最小的是最简单的一种。
-
现在我们抽象一下问题。
给定:
- 一个包含
N个容器 (Bins) 的集合,每个容器i有一个初始的、可变的容量 (capacity)C_i。 - 一个包含
M个物品 (Items) 的有序序列,每个物品j有一个固定的尺寸 (size)S_j。
任务:
按顺序处理每一个物品 S_j,为它寻找一个可以容纳它的容器,并遵循以下分配规则:
- 资格筛选: 从所有容器中,筛选出那些当前剩余容量
C'_i大于或等于物品尺寸S_j的容器。 - 最优选择: 在所有符合资格的容器中,选择那个剩余容量
C'_i最小的容器。 - 决胜局规则: 如果有多个容器的剩余容量都同为最小,则选择那个原始编号(或索引)
i最小的容器。
操作与输出:
- 如果为物品
S_j找到了最佳容器i,则更新该容器的容量C'_i = C'_i - S_j,并记录下所选容器的编号i。 - 如果找不到任何符合资格的容器,则记录一个“失败”标识(例如 -1)。
- 最终,返回一个记录了每次分配结果(所选容器的编号或 -1)的序列。
import java.util.Scanner;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
// ACM模式下的输入处理
Scanner in = new Scanner(System.in);
// 示例输入格式:一行空格分隔的数字
String[] capacitiesStr = in.nextLine().split(" ");
int[] capacities = new int[capacitiesStr.length];
for (int i = 0; i < capacitiesStr.length; i++) {
capacities[i] = Integer.parseInt(capacitiesStr[i]);
}
String[] requestsStr = in.nextLine().split(" ");
int[] requests = new int[requestsStr.length];
for (int i = 0; i < requestsStr.length; i++) {
requests[i] = Integer.parseInt(requestsStr[i]);
}
in.close();
// 调用核心算法
int[] result = deployVMs(capacities, requests);
// 格式化输出
for (int i = 0; i < result.length; i++) {
System.out.print(result[i] + (i == result.length - 1 ? "" : " "));
}
System.out.println();
}
/**
* 核心算法:选择最匹配内存部署虚拟机
* @param capacities 物理机初始内存数组
* @param requests 虚拟机请求内存数组
* @return 每个请求的部署结果数组
*/
public static int[] deployVMs(int[] capacities, int[] requests) {
// Key: 可用内存, Value: 拥有该可用内存的物理机编号集合(使用TreeSet保证编号有序)
TreeMap<Integer, TreeSet<Integer>> availableMachines = new TreeMap<>();
// 1. 初始化,将所有物理机按内存分组放入TreeMap
for (int i = 0; i < capacities.length; i++) {
int memory = capacities[i];
// computeIfAbsent: 如果key不存在,则创建一个新的TreeSet并放入map
availableMachines.computeIfAbsent(memory, k -> new TreeSet<>()).add(i);
}
int[] results = new int[requests.length];
// 2. 按顺序处理每一个部署请求
for (int i = 0; i < requests.length; i++) {
int requestMemory = requests[i];
// 3. 查找最佳物理机
// ceilingKey(k): 返回大于或等于k的最小键。
Integer bestFitMemory = availableMachines.ceilingKey(requestMemory);
if (bestFitMemory == null) {
// 如果找不到,说明没有任何物理机满足条件
results[i] = -1;
} else {
// 找到了内存大小最合适的物理机分组
TreeSet<Integer> machineIds = availableMachines.get(bestFitMemory);
// first(): 由于TreeSet有序,第一个元素就是编号最小的。
int machineToDeploy = machineIds.first();
results[i] = machineToDeploy;
// 4. 更新状态
// a. 从旧的内存分组中移除这台物理机
machineIds.remove(machineToDeploy);
// b. 如果这个分组空了,就从TreeMap中移除这个内存键
if (machineIds.isEmpty()) {
availableMachines.remove(bestFitMemory);
}
// c. 计算新的可用内存,并将这台物理机加入新的分组
int remainingMemory = bestFitMemory - requestMemory;
availableMachines.computeIfAbsent(remainingMemory, k -> new TreeSet<>()).add(machineToDeploy);
}
}
return results;
}
}