题目描述
给你长度为 N 的正整数序列 A=(A_1,A_2,\\dots,A_N) 和 B=(B_1,B_2,\\dots,B_N) 。
给你 Q 个查询。
每个查询都属于以下两种类型之一。
- 1 i x :将 A_i 更改为 x 。
- 2 i x :将 B_i 更改为 x 。
处理完每个查询后,请解决以下问题。
- 高桥需要向 N 家公司分别发送一封邮件,并收到每家公司的回复。
- 写电子邮件发送给第 j 个公司需要 A_j 分钟,回复在发送后 B_j 分钟到达。
- 他从 0 开始写邮件。
- 他可以按照任意顺序写 N 封邮件,但不能同时写两封或多封邮件。
- 求他收到所有回复的最短时间?
- 假设发送电子邮件所花费的时间可以忽略不计。
数据范围
- 1 \\leq N \\leq 10\^5
- 1 \\leq Q \\leq 10\^5
- 1 \\leq A_j,B_j \\leq 10\^9
- 在每个查询中, 1 \\leq i \\leq N .
- 在每个查询中, 1 \\leq x \\leq 10\^9 。
- 所有输入值均为整数。
输入格式
N Q
A1 A2 ...... An
B1 B2 ...... Bn
query-1
query-2
.
.
.
.
.
.
query-Q
每个查询 \\mathrm{query}_q 、查询类型( 1 或 2 )、 i 和 x 依次给出,中间用空格隔开。
也就是说,每个查询以以下两种格式之一给出:
1 i x
2 i x
解题思路
一、题意转化与数学建模
1. 场景描述
有 N 个任务,任务 j:
- 书写耗时 \(A_j\),任务必须串行执行;
- 完成书写的时刻为 S,回复到达时刻为 \(S+B_j\); 最终全部收到回复的时间 = 所有回复到达时间的最大值。 设书写顺序为 \(p_1,p_2,\dots,p_N\),定义前缀和:
\(S_k = A_{p_1}+A_{p_2}+\dots+A_{p_k}\) 答案为:
\(\max_{1\le k\le N}(S_k+B_{p_k})\) 目标:排列任务顺序,让这个最大值尽可能小;同时支持单点修改 \(A_i/B_i\),每次修改后输出最优答案。
2. 数据范围约束
\(N,Q\le 10^5\),单次操作必须 \(O(\log N)\),暴力排序 \(O(N\log N)\) 无法通过。
二、贪心排序结论(交换论证证明)
最优排序规则
任务按 \(B_j\) 从大到小降序排列。
证明(两任务交换法)
仅两个任务 \(X(A_1,B_1),Y(A_2,B_2)\):
- 先 X 后 Y:结果 \(\max(A_1+B_1,\ A_1+A_2+B_2)\)
- 先 Y 后 X:结果 \(\max(A_2+B_2,\ A_1+A_2+B_1)\)
若 \(B_1>B_2\),交换后第二项 \(A_1+A_2+B_1\) 会变大,总答案只会变差; 推广到 N 个任务:任意相邻逆序(小 B 在前、大 B 在后)交换后答案不增,全局最优一定是 B 降序。
问题简化
现在所有元素固定按 B 降序排成有序序列,我们只需要动态维护:
- 序列有序(按 B 降序);
- 区间子树总 A 和(求前缀和);
- 区间内 \(\max(前缀A和+B)\); 每次单点修改后快速查询全局最大值。
三、排序关键字设计(适配平衡树)
平衡树需要唯一有序 key 实现二分分裂: pair 比较规则:先比 first,再比 second。 令 key = (-B_i, i):
- B 越大,\(-B_i\) 越小,会排在树左侧;
- 下标 i 作为第二关键字,保证 B 相同时 key 不重复,删除时能精准定位单个节点。
四、数据结构选择:FHQ-Treap(无旋 Treap)
选择理由
- split+merge 天然维护有序序列,插入 / 删除单点 \(O(\log N)\);
- 可在每个节点向上合并维护区间信息(总和、区间最大值);
- 相比旋转 Treap,区间信息合并(pushup/pull)写法更直观。
节点维护信息:
struct Node{
int l,r,pri; // 左右儿子、随机堆权维护堆性质
ll A,B; // 当前任务A、B值
ll sum; // 子树内所有A的总和
ll mx; // 子树区间内 max(前缀A和+B)
pii key; // 排序关键字 (-B_i,i)
}
核心合并函数 pull(pushup)
设当前节点 t,左子树 L,右子树 R:
- 子树总和更新:
\(trt.sum = trL.sum + trt.A + trR.sum\) 2. 区间最大值更新(三种候选取最大):
- 最大值出现在左子树内部:\(trL.mx\)
- 最大值恰好是当前节点:左子树全部 A + 当前 A + 当前 B
- 最大值出现在右子树内部:左子树全部 A + 当前 A + 右子树内部最优值
\(trt.mx = \max\big(trL.mx,\ trL.sum+trt.A+trt.B,\ trL.sum+trt.A+trR.mx\big)\) 空节点 \(tr0.mx\) 设为极小负无穷,避免干扰 max 计算。
五、FHQ 基础操作作用
split_less(root, key, a, b)将树拆成两棵:a 存所有 key < 目标 key(B 更大的元素),b 存剩余;用于插入。split_leq(root, key, a, b)拆出所有 key ≤ 目标 key,两次 split 精准分离单个目标节点,用于删除。merge(a,b)保证 a 内所有 key < b 内所有 key,按随机权小根堆合并,合并后自动 pull 更新信息。insert_node(id)分裂出左树,合并「左树 + 当前节点 + 右树」完成插入。erase_by_key(key)两次 split 取出唯一目标节点,合并剩余两棵树,返回被摘下的节点(复用修改)。
六、处理查询流程
每次修改操作统一流程:
- 摘出节点:用旧 key 删除该元素,拿到独立节点;
- 修改属性
- type1(改 A):仅更新节点 A,调用 pull 刷新 sum、mx;
- type2(改 B):更新节点 B,同时更新排序 key
(-新B,i),再 pull;
- 重新插入:把修改后的节点放回平衡树;
- 输出答案 :根节点的
tr[root].mx就是当前最优答案。
最后,输出trroot.mx即可。
完整代码
#include<bits/stdc++.h>
#define fr1(i,a,b) for(int (i)=(a);(i)<=(b);(i)++)
#define fr2(i,a,b) for(int (i)=(a);(i)>=(b);(i)--)
#define fv(i,p) for(auto (i):(p))
#define ll long long
#define ull unsigned ll
#define pii pair<int,int>
#define pll pair<ll,ll>
#define _1st first
#define _2nd second
#define elif else if
#define debug cout<<endl<<"-------------------------------------------------------------"<<endl
using namespace std;
int n,q;
const int MAXN=100000+5;
const ll NEG_INF=-(1LL<<60);
struct Node{
int l,r,pri;
ll A,B;
ll sum,mx;
pii key;
}tr[MAXN];
int root=0;
void pull(int t){
if(!t)return;
int l=tr[t].l,r=tr[t].r;
tr[t].sum=tr[l].sum+tr[t].A+tr[r].sum;
tr[t].mx=max({tr[l].mx,tr[l].sum+tr[t].A+tr[t].B,tr[l].sum+tr[t].A+tr[r].mx});
}
void split_less(int t,pii key,int &a,int &b){
if(!t){a=b=0;return;}
if(tr[t].key<key){
a=t;
split_less(tr[t].r,key,tr[t].r,b);
pull(t);
}else{
b=t;
split_less(tr[t].l,key,a,tr[t].l);
pull(t);
}
}
void split_leq(int t,pii key,int &a,int &b){
if(!t){a=b=0;return;}
if(tr[t].key<=key){
a=t;
split_leq(tr[t].r,key,tr[t].r,b);
pull(t);
}else{
b=t;
split_leq(tr[t].l,key,a,tr[t].l);
pull(t);
}
}
int merge(int a,int b){
if(!a||!b)return a?a:b;
if(tr[a].pri<tr[b].pri){
tr[a].r=merge(tr[a].r,b);
pull(a);
return a;
}else{
tr[b].l=merge(a,tr[b].l);
pull(b);
return b;
}
}
void insert_node(int id){
int a,b;
split_less(root,tr[id].key,a,b);
root=merge(merge(a,id),b);
}
int erase_by_key(pii key){
int a,b,c,d;
split_less(root,key,a,b);
split_leq(b,key,c,d);
root=merge(a,d);
if(c){
tr[c].l=tr[c].r=0;
pull(c);
}
return c;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(NULL);cout.tie(NULL);
cin>>n>>q;
vector<ll>a(n+1),b(n+1);
fr1(i,1,n)cin>>a[i];
fr1(i,1,n)cin>>b[i];
tr[0].mx=NEG_INF;
mt19937 rng(chrono::steady_clock::now().time_since_epoch().count());
fr1(i,1,n){
tr[i].l=tr[i].r=0;
tr[i].pri=(int)rng();
tr[i].A=a[i];
tr[i].B=b[i];
tr[i].key={-(int)b[i],i};
tr[i].sum=a[i];
tr[i].mx=a[i]+b[i];
insert_node(i);
}
while(q--){
int type,i;
ll x;
cin>>type>>i>>x;
if(type==1){
int node=erase_by_key(tr[i].key);
tr[node].A=x;
pull(node);
insert_node(node);
}else{
int node=erase_by_key(tr[i].key);
tr[node].B=x;
tr[node].key={-(int)x,i};
pull(node);
insert_node(node);
}
cout<<tr[root].mx<<'\n';
}
return 0;
}