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】感謝觀看