hihocoder 1053 居民迁移(二分搜索+贪心)
描述
公元2411年,人类开始在地球以外的行星建立居住点。在第1326号殖民星上,N个居住点分布在一条直线上。为了方便描述,我们设第i个居住点的位置是Xi,其中居住着Yi位居民。随着冬季的到来,一些人口较多的居住点的生态循环系统已经开始超负荷运转。为了顺利度过严冬,殖民星上的居民一致同意通过转移到人口较少的居住点来减轻人口众多的居住点的负荷。
遗憾的是,1326殖民星的环境非常恶劣。在冬季到来前,每个居民点的居民最远能迁移到距离不超过R的居民点。1326殖民星的居民希望知道,如何安排迁移才能使完成迁移后人口最多的居民点人口最少?
注意有可能存在多个居民点位置相同。
输入
第一行包含一个整数T(1 <= T <= 10),代表测试数据的组数。
每组数据的第一行包含2个整数N(1 <= N <= 100000)和R(0 <= R <= 10^9)。
以下N行每行包含两个整数,Xi和Yi(0 <= Xi, Yi, <= 10^9)。
输出
对于每组数据输出迁移后人口最多的居民点人口最少可能的数目。
样例输入
3
5 1
10 80
20 20
30 100
40 30
50 10
5 10
10 80
20 20
30 100
40 30
50 10
5 20
10 80
50 10
20 20
30 100
40 30
样例输出
100
50
48
分析:
典型的最小化最大值问题,需要用到二分搜索。
二分搜索(二分枚举):在答案可能的范围 [L, R] 内二分查找答案,检查当前答案是否满足题目的条件要求,根据判断结果更新查找区间。
主要用来解决四类问题:
① 最小化最大值
② 最大化最小值
③ 满足条件的最大(小)值
④ 最靠近的一个值
(跟二分查找一样要求满足条件的答案区间单调,速度比 for 枚举快)
那么这道题要怎么进行处理呢?
首先我们确定一个范围 [L, R],最理想的情况是所有居民点的居民人数都是一样的,平均分摊。
这样人口最多的居民点的人数一定是最小的,因此我们确定了左边界 L 。
而最差的情况是居民都没有进行移动或者移动完各个居民地的人口分配没有变化,
这样人口最多的居民点的人数就是原始数据中人口最多的居民地的人数,因此我们确定了右边界 R 。
更新查找区间的部分没有什么重点,重点在于如何进行检查 check 。
在 check 部分我们要检查能不能满足在所有居民点人数不超过 maxValue 的情况下实现分配人口。
把一个个的居民点从一个点变成一个区间,因为每个居民点的人能够移动的距离是有限制的,
区间的左边界就是能移动到的最左边,右边界就是能移动的最右边。
这里我们对这些区间进行排序,运用了贪心的思想,左边界较小的较先处理,右边界较大的较后处理。
因为左边界较小的紧急度比较高,而右边界较大的居民点可以放在后面的位置再进行处理也不迟,
先让紧急度高的居民点得到安置。
依据这样的处理标准,如果遇到了需要安置的居民的右边界 < 当前可以容纳居民的居民点位置,
这就说明调度失败,前面的居民没有办法得到安置。
如果需要安置的居民的左边界 > 当前可以容纳居民的居民点位置,说明需要到下一个居民点来安置居民。
(坑点:可能会出现第一个居民点的人数为 0 的情况)
参考代码:
// 记录! 大战了一个下午。。。 二分+贪心
#include<iostream>
#include<algorithm>
#include<cmath>
using namespace std;
// 用来保存输入 也可以认为是居住地的结构体
struct NODE {
int num;
int pos;
bool operator < (NODE other) const{
return pos < other.pos;
}
}*nodes;
// 居民的结构体
struct SEG {
int left;
int right;
int num;
// 这里的排序规则为左端点小的在前 右端点远的可以后面再处理 体现贪心 因此左端点一致时右端点较近的靠前
bool operator < (SEG other) const{
if(left != other.left) return left < other.left;
else return right < other.right;
}
}*segs;
int len; // len不同于n,是居住地的数量
// O(n)?
int check(long long mid, long long n, long long R){
int preSegPos, preSegNum;
// 坑点 有可能出现第一个居住地就是0的情况 因此要找到第一个非0的居民处
for(long long i = 0; i < n; i++){
if(segs[i].num != 0){
preSegPos = i;
preSegNum = segs[i].num;
break;
}
if(i == n-1) return 1;
}
// currentPos为当前居住地的位置 maxValue为check允许的最大居民数
int currentPos, maxValue;
for(int i = 0; i < len; i++){
currentPos = nodes[i].pos;
maxValue = mid;
// 注意这里是j <= n 因为j == n的判断语句在第一行 这样才能保证处理j == n-1的情况
for(int j = preSegPos; j <= n; j++){
if(j == n) return 1;
// 当前需要处理的居民的右端点已经无法触及时 表示调度失败
if(segs[j].right < currentPos) return 0;
// 当需要处理的居民的左端点已经越过了当前的居住地 进行处理下一个居住地
if(segs[j].left > currentPos){
preSegPos = j;
preSegNum = segs[j].num;
break;
}
else{
// 使用cnt 才能正确处理上一个居民地无法完整容纳当前居民的情况
int cnt = (j == preSegPos)?preSegNum:segs[j].num;
// 如果当前居民地可以完整容纳 继续处理下一个居民体
if(cnt <= maxValue){
maxValue -= cnt;
}
// 否则容纳一部分 处理下一个居住地
else{
preSegPos = j;
preSegNum = cnt-maxValue;
maxValue = 0;
break;
}
}
}
}
return 0;
}
// O(n^2)
int getResult(int n, int R){
long long sum = 0;
int maxValue = 0;
for(int i = 0; i < len; i++){
sum += nodes[i].num;
maxValue = max(maxValue, nodes[i].num);
}
// 左边界是平均值 这是最理想的
int left = sum/n;
// 右边界是居民的最大值
int right = maxValue;
// 二分枚举 这是最大值最小化问题
while(left < right){
long long mid = (left+right)/2;
if(check(mid, n, R)){
right = mid;
}
else{
left = mid+1;
}
}
return right;
}
int main(){
int T;
cin >> T;
while(T--){
len = 0;
int n, R;
cin >> n >> R;
nodes = new NODE[n];
segs = new SEG[n];
int pos, num;
for(int i = 0; i < n; i++){
cin >> pos >> num;
// 可能会出现多个输入表示同一个居住地 需要进行整合
if(pos == nodes[len-1].pos){
nodes[len-1].num += num;
}
else{
nodes[len].pos = pos;
nodes[len].num = num;
len++;
}
// 但是居民体就需要分开统计
segs[i].left = pos-R;
segs[i].right = pos+R;
segs[i].num = num;
}
sort(nodes, nodes+len);
sort(segs, segs+n);
int ans = getResult(n, R);
cout << ans << endl;
delete[] nodes;
delete[] segs;
}
return 0;
}
【END】感谢观看