BZOJ 1150 - 資料備份Backup - [小頂堆][CTSC2007]
題目連結:https://www.lydsy.com/JudgeOnline/problem.php?id=1150
Time Limit: 10 Sec Memory Limit: 162 M
Description
你在一家 IT 公司為大型寫字樓或辦公樓(offices)的計算機資料做備份。然而資料備份的工作是枯燥乏味的,因此你想設計一個系統讓不同的辦公樓彼此之間互相備份,而你則坐在家中盡享計算機遊戲的樂趣。已知辦公樓都位於同一條街上。你決定給這些辦公樓配對(兩個一組)。每一對辦公樓可以通過在這兩個建築物之間鋪設網路電纜使得它們可以互相備份。然而,網路電纜的費用很高。當地電信公司僅能為你提供 K 條網路電纜,這意味著你僅能為 K 對辦公樓(或總計2K個辦公樓)安排備份。任一個辦公樓都屬於唯一的配對組(換句話說,這 2K 個辦公樓一定是相異的)。此外,電信公司需按網路電纜的長度(公里數)收費。因而,你需要選擇這 K 對辦公樓使得電纜的總長度儘可能短。換句話說,你需要選擇這 K 對辦公樓,使得每一對辦公樓之間的距離之和(總距離)儘可能小。下面給出一個示例,假定你有 5 個客戶,其辦公樓都在一條街上,如下圖所示。這 5 個辦公樓分別位於距離大街起點 1km, 3km, 4km, 6km 和 12km 處。電信公司僅為你提供 K=2 條電纜。
上例中最好的配對方案是將第 1 個和第 2 個辦公樓相連,第 3 個和第 4 個辦公樓相連。這樣可按要求使用K=2 條電纜。第 1 條電纜的長度是 3km-1km=2km ,第 2 條電纜的長度是 6km-4km=2km。這種配對方案需要總長4km 的網路電纜,滿足距離之和最小的要求。
Input
第一行包含整數n和k
其中n(2≤n≤100000)表示辦公樓的數目,k(1≤k≤n/2)表示可利用的網路電纜的數目。
接下來的n行每行僅包含一個整數(0≤s≤1000000000),表示每個辦公樓到大街起點處的距離。
這些整數將按照從小到大的順序依次出現。
Output
輸出應由一個正整陣列成,給出將2K個相異的辦公樓連成k對所需的網路電纜的最小總長度。
Sample Input
5 2
1
3
4
6
12
Sample Output
4
題解:
顯然肯定是選擇相鄰的兩棟樓架設電纜,那麼可以將 $1 \sim n$ 個辦公樓變成 $1 \sim n-1$ 個間距,並將其儲存在一個數組 $d$ 中。
問題轉化為,在 $n-1$ 個 $d_i$ 中,選擇 $k$ 個任意兩兩間均不相鄰的數字,使它們的和最小。
假設 $d[1:n-1]$ 中最小的數為 $d_k$,我們可以證明對於 $d_{k-1}$ 和 $d_{k+1}$ 這兩個數,要麼同時選,要麼都不選,
證明不存在只選 $d_{k-1}$ 和 $d_{k+1}$ 中某一個這種情況:若我只選擇了 $d_{k-1}$ 而沒有選擇 $d_{k+1}$,那麼我必然可以將 $d_{k-1}$ 改選為 $d_k$,既不會違反兩兩不相鄰的條件,同時又獲得了一個更優解,因此不可能存在只選 $d_{k-1}$ 和 $d_{k+1}$ 中某一個這種情況。
同時我們還可以知道,如果不選 $d_{k-1}$ 和 $d_{k+1}$,那麼必然會選擇 $d_k$,這是顯然的。
換句話說,現在就變成了一道二選一:1、選 $d_k$;2、選 $d_{k-1}$ 和 $d_{k+1}$。
因此,可以先選擇 $d_k$,然後將 $d_{k-1},d_k,d_{k+1}$ 從陣列 $d$ 中刪去,新增一個新元素 $d_{k-1} + d_{k+1} - d_k$,然後即變成從新的陣列 $d$ 中選擇 $k-1$ 個任意兩兩間均不相鄰的數字,使它們的和最小。這樣一來,一旦選到 $d_{k-1} + d_{k+1} - d_k$,就相當於拋棄原來的 $d_k$ 轉而選擇 $d_{k-1}$ 和 $d_{k+1}$。
至於具體實現,對於陣列 $d$ 可以構建一個連結串列結構,用 $pre$ 和 $nxt$ 記錄前後相鄰的元素。同時,建立一個小頂堆,堆記憶體儲的是指向連結串列某個位置的指標(記為“節點值”),堆內元素比較大小可以直接由指標 $O(1)$ 得到連結串列中實際儲存的值,即看做節點的鍵值(節點的鍵值不同於節點值),用該鍵值進行節點間的兩兩比較。
這樣一來,每次刪除連結串列內的某一節點可以 $O(1)$ 完成。而要刪除堆中對應的指向該節點的指標,由於我們手寫二叉堆只能完成刪除 $heap$ 陣列中指定位置的元素,也就是說,我們需要另開一個數組,用來儲存“節點值”到“節點位置”的對映關係,這也正是我們為什麼要在堆記憶體儲指標的原因,因為指向連結串列指定位置的指標是唯一的,方便構建對映關係。
AC程式碼:
#include<bits/stdc++.h> using namespace std; const int maxn=100000+10; int n,k; int d[maxn],pre[maxn],nxt[maxn]; struct Heap { int sz; int heap[maxn],pos[maxn]; void up(int now) { while(now>1) { int par=now>>1; if(d[heap[now]]<d[heap[par]]) { swap(heap[par],heap[now]); swap(pos[heap[par]],pos[heap[now]]); now=par; } else break; } } void push(int x) { heap[++sz]=x; pos[x]=sz; up(sz); } inline int top(){return heap[1];} void down(int now) { while((now<<1)<=sz) { int nxt=now<<1; if(nxt+1<=sz && d[heap[nxt+1]]<d[heap[nxt]]) nxt++; if(d[heap[now]]>d[heap[nxt]]) { swap(heap[now],heap[nxt]); swap(pos[heap[now]],pos[heap[nxt]]); now=nxt; } else break; } } void pop() { heap[1]=heap[sz--]; pos[heap[1]]=1; down(1); } void del(int p) //刪除儲存在陣列下標為p位置的節點 { heap[p]=heap[sz--]; pos[heap[p]]=p; up(p), down(p); } void delx(int x){del(pos[x]);} //刪除堆中值為x的節點 inline void clr(){sz=0;} }h; int main() { cin>>n>>k; for(int i=1;i<=n;i++) scanf("%d",&d[i]); h.clr(); for(int i=1;i<n;i++) { d[i]=d[i+1]-d[i]; pre[i]=i-1; nxt[i]=(i+1)%n; h.push(i); } int ans=0; while(k--) { int x=h.top(); h.pop(); ans+=d[x]; if(!pre[x] && !nxt[x]) break; if(!pre[x]) { h.delx(nxt[x]); pre[nxt[nxt[x]]]=0; } else if(!nxt[x]) { h.delx(pre[x]); nxt[pre[pre[x]]]=0; } else { h.delx(pre[x]); h.delx(nxt[x]); d[x]=d[pre[x]]+d[nxt[x]]-d[x]; h.push(x); pre[x]=pre[pre[x]]; nxt[pre[x]]=x; nxt[x]=nxt[nxt[x]]; pre[nxt[x]]=x; } } cout<<ans<<endl; }
本程式碼大量參考《演算法競賽進階指南》給出的標程(我自己敲…已經敲自閉了……)。
這種在堆中儲存唯一編碼的方式,類似於二叉堆優化Dijkstra中二叉堆的寫法,比直接在堆中儲存結構體要靈活。