算法 {区间选点的最大匹配}
区间选点的最大匹配
定義
给定区间集合A(每个区间[l,r]
都是整数), 你需要从實數域裡選擇一個點集P 使得存在一個A的子集SA, 滿足: 存在雙射P<->SA
使得P中的點x包含於SA的區域[l,r]
裡 (即l<=x<=r
);
求點集P的大小的最大值; (簡之 讓點集P盡可能的大 使得P裡的每個點都能A裡的區間進行一一匹配)
.
比如, A: {[1,1], [2,2], [1,2]}
, 那麼答案為2, 選擇P={1,2}, SA={[1,1] [2,2]} (SA不唯一)
;
算法
{ 錯誤
將A進行左端點排序, 維護一個全局變量pos
假如l,r >= 0
那麼讓pos=-1
(表示已經選擇的P集合 都是<=pos
的點), 對於當前區間[l,r]
, 如果r >= max(pos+1,l)
則選擇max(pos+1,l)
這個點與當前區間匹配;
long long L = -1;
int ANS = 0;
for( auto & i : A){
auto lef = i.first, rig = i.second;
if( rig >= max( L+1, lef)){
L = max(L+1,lef); // 選擇L與當前區間`i`匹配;
++ ANS;
break;
}
}
這個貪心策略 其實是錯誤的…
比如[1,1], [1,3], [2,2]
, 你的答案是2, 實際答案是3;
}
正確的做法 也是貪心, 但沒上面的貪心 那麼簡單…
假如0<=l/r<=1e18
, 遍歷for( pos : [0,1,...,1e18])
:
.
1:[把A中的所有的右端點<pos
的區間 給刪除掉];
.
2:[在A中的所有的左端點<=pos
的區間裡 選擇一個右端點最小的區間T];
.
(此時T一定包含pos
) 那麼pos-T
就是一個匹配 當然將T從A中刪除掉;
雖然也是貪心 但要比上面的做法複雜很多;
這裡就不證明了 畢竟貪心題…; 簡單證明下, 此時[0,...pos-1]
不用管 他們已經匹配了 因此1:
是合理的, 對於2:
選擇出來的這個T 比如他是[l,r]
, 他的[l,pos]
子區間不用管 因為是在pos
的左側, 關鍵是看[pos+1, r]
這個子區間 由於r
是滿足(覆蓋pos
的最小的右端點) 也就是意味著 在當前所有滿足(覆蓋pos
)的區間裡 T這個區間的[pos+1, r]
子區間 是最小的, 也就是意味著 他給接下來的點[pos+1, pos+2,...]
餘出了更大的空間和可能性 因為剩下的區間 他們的[pos+1, R]
中的R>r
都很大 即選擇性就大, 也就是選擇的這個T 是最劣的, 而剩下的區間 都是優秀的;
這是超時的 要把這個pos
的for循環給優化掉, 如果你把他優化為: 讓pos
遞增的遍歷A的交集, 那麼也是超時的 因為A的區間可以是[0,1e18]
;
因為最終答案 pos
只有N個, 因此嘗試讓每一次的pos
遍歷的 就是答案, 即最多遍歷N次;
1:
裡面的操作 是要找A裡面的右端點, 而2:
的操作 找的是A裡面的左端點, 他倆是無法協調的 因為你如果對A進行排序 要麼是對左端點 要麼是對右端點, 是很矛盾 互相排斥的邏輯;
.
此時 一個重要的跳躍性思維來了, 我們把1:, 2:
給合併掉 因為沒辦法了 他倆本身是水火不容的, 由於1:
操作很簡單(就是刪除一些特定的區間) 於是把1:
操作 融合到2:
裡面去, 組成一個新的操作:[在A裡所有的左端點<=pos
的區間集合S裡 把S中的所有(右端點<pos
的區間)都刪除掉 得到S1
集合, 在S1集合裡 獲取右端點最小的區間T];
集合S只是個中介 關鍵是要得到S1集合; 此時有性質: 令SS
為當前pos
的集合S, 令SSS
為任意>pos
的集合S, 令SS1
為當前pos
的集合S1, 令SSS1
為任意>pos
的集合S1, 那麼會有
S
S
⊂
S
S
S
,
S
S
1
⊂
S
S
S
1
SS \subset SSS, SS1 \subset SSS1
SS⊂SSS,SS1⊂SSS1, 而又有
S
1
⊂
S
S1 \subset S
S1⊂S, 因此 我們可以直接 全局的來同時維護{S,S1}集合;
令heap
為一個pair<右端點,左端點>
的遞增排序的堆, 將區間A按照左端點排序, 令ind=0
, 對於當前pos
, 他要滿足: 所有的A[>=ind].左端點 > pos
意味著pos
所匹配的區間 一定是[0...ind-1]
範圍裡的; heap存儲的是區間 都是A[0...ind-1]
裡面的區間 且滿足 A[0...ind-1]
裡的所有(右端點>=pos
的)區間 一定是在heap裡面的;
1: 將heap裡 (右端點<pos
的區間(一定在堆頂)) 刪除;
2: 此時heap
的堆頂元素 如果存在 他就是和pos
所匹配的區間, 將他刪除 記錄答案;
3: 更新pos
;
.
IF(heap
不是空的): 執行++pos
; 比如A= [ [1,2], [1,3]]
當pos=1
時 heap裡面此時為[1,3]
此時ind=2
, 那麼就繼續讓pos ++
;
.
@ELSE: @IF(ind == N
):[算法結束]; @ELSE: 令pos = A[ind].左端點
; 比如A= [ [1,1], [5,5]]
, 當pos=1
此時heap為空, 讓pos=2
沒有必要 讓他直接跳到5
;
4: 更新heap; 因為此時pos
更新了 因此heap也需要更新, 遍歷while( ind < N)
只要A[ind].左端點 <= pos
把A[ind ++]
放到heap裡;
代碼
{
__A: 區間的集合;
__N: __A的長度;
對`__A`進行*左端點*的排序;
int64_t __pos = 0; // 點的坐標
priority_queue< pair<int64_t,int64_t>, vector<pair<int64_t,int64_t>>, greater<>> __heap; // 他的元素是`[右端點,左端點]`;
for( int __ind = 0;;){
//>< `A[ind].first(左端點) > pos`;
while( __heap.empty()==false && __heap.top().first<__pos){ // 堆頂元素的區間*右端點* 為`heap.top().first`;
__heap.pop();
}
if( false == __heap.empty()){
//>< 點`pos` 與 `Q.top()`所對應的區間 匹配;
__heap.pop();
}
if( __heap.empty()){
if( __ind == __N){ break;}
__pos = A[__ind].first;
}
else{
++ __pos;
}
while( __ind<__N && A[__ind].first<=__pos){
__heap.push( {A[__ind].second, A[__ind].first}); ++ __ind;
}
}
}
例題
@LINK: https://atcoder.jp/contests/abc325/tasks/abc325_d
;