题解 P7619 [COCI2011-2012#2] RASPORED
Posted Dragon_Skies
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了题解 P7619 [COCI2011-2012#2] RASPORED相关的知识,希望对你有一定的参考价值。
题目描述
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\\) 个居民,能得到的小费的式子是:
如果 \\(P_i\\) 是负数,就代表要支付费用。
可以根据题目来理解:前面的 \\(L_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\\) 与顺序无关,所以只用考虑后面这个部分。
又因为我们要让结果最大,所以就要让后面这一部分最小,因为 \\(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_i\\) 这个数字,再次观察一下新的 \\(ans2\\) 的式子:
这个式子比较难理解,大概是这么来的:
- 前面的 \\(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;
}
以上是关于题解 P7619 [COCI2011-2012#2] RASPORED的主要内容,如果未能解决你的问题,请参考以下文章
[SinGuLaRiTy] COCI 2011~2012 #2
[COCI2011-2012#5] POPLOCAVANJE
P4596 [COCI2011-2012#5] RAZBIBRIGA