1. 程式人生 > 其它 >題解 P7619 [COCI2011-2012#2] RASPORED

題解 P7619 [COCI2011-2012#2] RASPORED

題目描述

Link

Mirko 的比薩店是城裡最好的,鎮上所有的居民每天午餐都吃比薩餅。而且 Mirko 的送貨服務很快,送貨時間可以忽略不計。但是 Mirko 只有一個小烤箱,一次只能烤一個比薩餅。

我們將城裡的 \(N\) 個居民從 \(1\)\(N\) 編號,他們計劃吃午餐的時間為 \(L_i\),Mirko 需要為他們烘焙比薩的所需時間為 \(T_i\)

如果一個居民在他計劃吃午餐時間的前 \(K\) 個時間單位收到了他的比薩餅,那麼 Mirko 會得到 \(K\) 元小費。相應地,如果一個居民在他計劃吃午餐時間的後 \(K\) 個時間單位才收到了他的比薩餅,那麼 Mirko 必須向居民付款 \(K\)

元。如果比薩餅準時送到,Mirko 不會得到小費,但是也不用付任何費用。

請你幫助 Mirko 安排一天的比薩烘焙順序,使得他一天賺取的總小費最大

\(1 \leq N,C \leq 2 \times 10^5 ,0 \leq L_i \leq 10^5 ,1 \leq T_i \leq 10^5\)

Solution

首先考慮如果沒有修改,就一次詢問怎麼做。先推推式子:

假設我們已經通過某種方法給這 \(N\) 個居民排好了順序,我們按照順序給第 \(1,2,3,\cdots N-1,N\) 個居民烤披薩。

因為我們要讓小費最多,所以肯定不會存在烤了一會又休息一會什麼的。

對於第 \(i\) 個居民,能得到的小費的式子是:

\[L_i-\sum_{j=1}^i T_i \]

如果 \(P_i\) 是負數,就代表要支付費用。

可以根據題目來理解:前面的 \(L_i\) 就是預定時間,後面一項就是烤好的時間,減法是根據"提前就是正,延遲就是負"來定方向的。

那麼最後總共獲得的小費是:

\[\sum_{i=1}^n (L_i-\sum_{j=1}^i T_i) \]

然後把兩項分開來:

\[\sum_{i=1}^nL_i - \sum_{i=1}^n\sum_{j=1}^i T_i \]

對於後面這個兩重求和,可以考慮每個 \(T_i\) 出現的次數,可以發現: \(T_j\) 會在 \(\forall i \geq j\)

\(i\) 中減一次,這樣的 \(i\)\(n-j+1\) 個,就相當於一共減了 \(T_j \times (n-j+1)\)

所以最終的式子就是:

\[\sum_{i=1}^n L_i - \sum_{i=1}^n T_i \times(n-i+1) \]

我們再回過頭來看怎麼確定烤的順序,注意到前面 \(\sum_{i=1}^n L_i\) 與順序無關,所以只用考慮後面這個部分。

又因為我們要讓結果最大,所以就要讓後面這一部分最小,因為 \(n-i+1\) 隨著 \(i\) 的增大而減小,所以對於越小的 \(i\) ,應該讓 \(T_i\) 儘可能小。

於是就可以按照 \(T_i\) 從小到大排序,就可以得到最大小費了。


下面設 \(ans1=\sum_{i=1}^n L_i,ans2=\sum_{i=1}^nT_i\times(n-i+1)\) 。最終要輸出的值就是 \(ans1-ans2\)

但是這一題有修改,怎麼辦?

修改 \(L_i\) 很好辦,直接讓 \(ans1\) 減掉原來的,再加上現在的就行。

修改 \(T_i\) 稍微麻煩一點:首先把修改變成刪掉一個數,再插入一個數。

觀察一下式子的變化,原來的 \(ans2\) 是:

\[T_1\times n+\cdots+T_{i-1}\times(n-i+2)+T_{i}\times(n-i+1)+T_{i+1}\times(n-i)+\cdots++T_n \]

假如說這時要刪掉 \(T_i\) 這個數字,再次觀察一下新的 \(ans2\) 的式子:

\[T_1\times (n-1)+\cdots+T_{i-1}\times(n-i+1)+T_{i+1}\times(n-i)+\cdots+T_n \]

這個式子比較難理解,大概是這麼來的:

  • 前面的 \(n\) 全部變成了 \(n-1\) 是因為刪除一個數以後數字個數減去了 \(1\)
  • 對於 \(i\) 後面的數字,這裡以 \(i+1\) 為例,原本是 \(n-(i+1)+1=n-i-1+1=n-i\) ,現在 \(n\) 減去了 \(1\) ,比 \(T_{i+1}\) 小的數字也少了一個,式子就變成了 \((n-1)-(i+1-1)+1=n-1-i+1=n-i\) ,所以和原來一樣。

發現這裡對於 \(i \in [1,i-1]\) 每個 \(T_i\) 乘的數字都減去了 \(1\) ,對於 \(i \in [i+1,n]\) ,每個 \(T_i\) 乘的數字不變。同時 \(ans2\) 還要減去 \(T_i \times (n-i+1)\)

如果是插入的話是一樣的,只不過從下面的式子推到上面這個式子。

插入的結論是:對於 \(i\in[1,i-1]\) ,每個 \(T_i\) 乘的數字都加上了 \(1\) ,對於 \(i\in[i+1,n]\) ,每個 \(T_i\) 乘的數字不變。同時 \(ans2\) 還要加上 \(T_i \times (n-i+1)\)

當然對於插入和刪除操作,我們都需要"動態排序",即實時更新下標。

於是我們就需要一個數據結構,支援:

  • 插入一個數。
  • 刪除一個數。
  • 查詢比一個數小的數的個數。
  • 查詢比一個數小的數的和。

可以用平衡樹或者樹狀陣列,這裡用的 Splay 來實現。

幾個細節:

  • 在求和的時候要開 long long ,不然可能會炸。
  • 記得修改的時候同時更新原來的陣列(方便下次操作)
  • 如果要插入/刪除的數有很多個,那麼直接插入/刪除最靠左的一個,可以用幾個資料算一算,會發現這是對的,證明我就不寫了。

程式碼如下( Splay 是真的慢):

#include <cstdio>
#include <cstring>
#include <cctype>
#include <algorithm>
#include <iostream>
typedef long long LL;
using namespace std;
inline int read() {
    int num = 0 ,f = 1; char c = getchar();
    while (!isdigit(c)) f = c == '-' ? -1 : f ,c = getchar();
    while (isdigit(c)) num = (num << 1) + (num << 3) + (c ^ 48) ,c = getchar();
    return num * f;
}
const int N = 2e5 + 5 ,INF = 1e6;
struct Splay {
    struct node {
        int ch[2] ,fa ,val ,siz ,cnt; LL sum;
        node () : fa(0) ,val(0) ,siz(0) ,cnt(0) ,sum(0) {ch[0] = ch[1] = 0;}
    }t[N]; int root ,tot;
    Splay () : root(0) ,tot(0) {}
    inline void update(int now) {
        t[now].siz = t[t[now].ch[0]].siz + t[t[now].ch[1]].siz + t[now].cnt;
        t[now].sum = t[t[now].ch[0]].sum + t[t[now].ch[1]].sum + (LL)t[now].cnt * t[now].val;
    }
    inline void rotate(int x) {
        int y = t[x].fa ,z = t[y].fa; bool k = t[y].ch[1] == x;
        t[x].fa = z; t[z].ch[t[z].ch[1] == y] = x;
        t[y].ch[k] = t[x].ch[k ^ 1]; t[t[x].ch[k ^ 1]].fa = y;
        t[x].ch[k ^ 1] = y; t[y].fa = x;
        update(y); update(x);
    }
    inline void splay(int x ,int goal) {
        while (t[x].fa != goal) {
            int y = t[x].fa ,z = t[y].fa;
            if (z != goal) {
                if ((t[z].ch[1] == y) == (t[y].ch[1] == x)) rotate(y);
                else rotate(x);
            }
            rotate(x);
        }
        if (goal == 0) root = x;
    }
    inline void insert(int val) {
        int now = root ,fa = 0;
        while (now && t[now].val != val) fa = now ,now = t[now].ch[val > t[now].val];
        if (now) t[now].cnt++;
        else {
            now = ++tot;
            t[now].val = t[now].sum = val;
            t[now].siz = t[now].cnt = 1;
            t[now].fa = fa;
            t[fa].ch[val > t[fa].val] = now;
        }
        splay(now ,0);
    }
    inline void build() {insert(-INF); insert(INF);}
    inline void find(int val) {
        int now = root;
        while (t[now].val != val && t[now].ch[val > t[now].val]) now = t[now].ch[val > t[now].val];
        splay(now ,0);
    }
    inline int findnext(int val ,int k) {
        find(val);
        if (t[root].val < val && k == 0) return root;
        if (t[root].val > val && k == 1) return root;
        int now = t[root].ch[k];
        while (t[now].ch[k ^ 1]) now = t[now].ch[k ^ 1];
        return now;
    }
    inline void remove(int val) {
        int l = findnext(val ,0) ,r = findnext(val ,1);
        splay(l ,0) ,splay(r ,l);
        int now = t[r].ch[0];
        if (t[now].cnt >= 2) t[now].cnt-- ,splay(now ,0);
        else t[r].ch[0] = t[now].fa = 0 ,update(r) ,update(l);
    }
    node& operator [] (int index) {return t[index];}
}t;
int n ,a[N] ,nums[N] ,L[N] ,C;
LL ans1 ,ans2;
signed main() {
    n = read(); C = read(); t.build();
    for (int i = 1; i <= n; i++) {
        L[i] = read(); a[i] = read();
        nums[i] = a[i]; ans1 += L[i]; t.insert(a[i]);
    }
    sort(nums + 1 ,nums + n + 1);
    for (int i = 1; i <= n; i++) ans2 += (LL)nums[i] * (n - i + 1);
    printf("%lld\n" ,ans1 - ans2);
    while (C--) {
        int x = read() ,l = read() ,T = read();
        ans1 = ans1 - L[x] + l; L[x] = l;
        t.find(a[x]);
        int now = t.root;
        ans2 -= t[t[now].ch[0]].sum + INF;
        //這裡加上正無窮的原因是在平衡樹裡面我插入了負無窮,所以比一個數小的數一定含有負無窮
        //所以 ans2 就要加上正無窮。
        ans2 -= LL(n - t[t[now].ch[0]].siz + 1) * a[x];
        t.remove(a[x]);
        a[x] = T;
        t.insert(T);
        now = t.root;
        ans2 += t[t[now].ch[0]].sum + INF;
        ans2 += LL(n - t[t[now].ch[0]].siz + 1) * a[x];
        printf("%lld\n" ,ans1 - ans2);
    }
    return 0;
}