1. 程式人生 > 其它 >bzoj 2151 種樹 題解

bzoj 2151 種樹 題解

傳送門


【分析】

反悔貪心

若沒有相鄰不可選的限制,則用優先佇列維護前 \(m\) 大,然後直接求和即可

現考慮對貪心如何反悔:

假設我們處於開局,什麼都沒選的狀態

當貪心選擇第 \(i\) 個時,由於相鄰不可選,需要刪除 \((i-1)\)\((i+1)\) 的權值,避免產生相鄰的選擇

但實際上有可能 \(a_{i-1}+a_{i+1}>a_i\) 從而反悔之前的貪心,故我們補回一個點,權值為 \(a_{i-1}+a_{i+1}-a_i\)

若後續過程中,我們選擇了該點,則代表我們反悔了選擇 \(a_i\) 的這次貪心;但同時,我們選擇的點數仍然+1

此時,我們刪除點 \((i-2)\)

\((i+2)\) 的權值,同樣是為了避免產生相鄰的選擇

但與上面相同,實際上可能 \(a_{i-2}+a_i+a_{i+2}>a_{i-1}+a_{i+1}>a_i\) 從而反悔之前的貪心,故我們同樣補回一個點,權值為 \(a_{i-2}+a_i+a_{i+2}-a_{i-1}-a_{i+1}=a_{i-2}+a_{i+2}-(a_{i-1}-a_{i+1}-a_i)\)

而這次過程等價於將上一步新產生的補充點看成同樣的一個點,再進行上一步的補充操作


因此我們不區分補充的點與原有的點,直接將原有的點塞進優先佇列中,並維護一個迴圈連結串列

當我們從優先佇列中取出一個未被打過標記的點時,將它與它前後,共三個點打上標記

之後,我們新建權值為 \(a_{i-1}+a_{i+1}-a_i\) 的點,並在迴圈佇列中頂替原來的三個點即可

執行 \(m\) 次得到答案


【程式碼】

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int, int> pii;
typedef double db;
#define fi first
#define se second
const int MAXN=4e5+10;
int n, m, a[MAXN], pre[MAXN], suf[MAXN], cntc;
bool used[MAXN];
priority_queue<pii> pq;
inline int maxc(){
    int val, pos;
    while(pq.size()){
        val=pq.top().fi;
        pos=pq.top().se;
        pq.pop();
        if(!used[pos])
            break;
    }
    if(used[pos]) return 0;

    used[pos]=used[ pre[pos] ]=used[ suf[pos] ]=1;
    a[++cntc]=a[ pre[pos] ]+a[ suf[pos] ]-a[pos];
    suf[ pre[pre[pos]] ]=cntc;
    pre[ suf[suf[pos]] ]=cntc;
    pre[cntc]=pre[pre[pos]];
    suf[cntc]=suf[suf[pos]];
    pq.push( pii(a[cntc], cntc) );
    return val;
}
inline void init(){
    cin>>n>>m;
    for(int i=1;i<=n;++i){
        cin>>a[i];
        pre[i]=i-1;
        suf[i]=i+1;
        pq.push( pii(a[i], i) );
    }
    suf[n]=1; pre[1]=n;
    cntc=n;
}
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0); cout.tie(0);
    init();
    if(n<m+m){
        cout<<"Error!";
    }
    else{
        int res=0;
        for(int i=1;i<=m;++i)
            res+=maxc();
        cout<<res;
    }
    cout.flush();
    return 0;
}