题解:NFLSOI#31351. 小吃

题面:

Shor 小鸭已经准备了 \(n\) 个小吃盘子,准备一边看电影一边享用!第 \(i\) 个盘子最初装有一个美味值为 \(a[i]\) 的小吃。
你需要处理 \(q\) 个查询。在第 \(j\) 个查询中,Shor 将按顺序执行以下两个操作:

  • 1. 吃掉所有美味值在 \(l[j]\)\(r[j]\) (包含)之间的小吃。
  • 2. 然后,将每一个被吃掉的小吃替换为一个美味值为 \(x[j]\) 的新小吃。
    在处理任何查询之前,以及每次处理完查询之后,Shor 都希望你确定所有盘子中小吃的美味值总和。
    形式化地说,给定一个长度为 \(n\) 的数组 \(a\),你必须处理 \(q\) 个查询。在处理所有查询之前,打印 \(a\) 中所有元素的总和。在第 \(j\) 个查询中,将所有满足 \(l[j]\le a[i]\le r[j]\) 的元素 \(a[i]\) 更新为 \(x[j]\),然后打印更新后的 \(a\) 中所有元素的总和。

输入格式

输入的第一行包含两个用空格分隔的整数 \(n\)\(q\)
第二行包含 \(n\) 个用空格分隔的整数 \(a[1],a[2],\dots,a[n]\)
接下来的 \(q\) 行输入中,每一行包含三个用空格分隔的整数。第 \(j\) 行包含 \(l[j],r[j]\)\(x[j]\),描述了第 \(j\) 个查询。

输出格式

输出应包含 \(q+1\) 行。
输出的第一行应包含一个整数,表示在所有查询之前 \(a\) 中所有元素的总和。
接下来的 \(q\) 行中,第 \(i\) 行应包含一个整数,表示第 \(i\) 个查询后 \(a\) 中所有元素的总和。

对于所有测试用例,输入将满足以下约束条件:

\(1\le n\le200000\)
\(0\le q\le200000\)
\(0\le a[i]\le 10^9\)
对于所有 \(1\le i\le n\)\(0\le x[j]\le 10^9\)
对于所有 \(1\le j\le q\)\(0\le l[j]\le r[j]\le 10^9\)
对于所有 \(1\le j\le q\)

我们开始解题

首先我们考虑如何查找一个数组的满足值域的所有值,鉴于这道题的数值范围非常大,我们用普通的线段树去维护这个桶会爆炸,空间直接飞起来,我们就去考虑动态开点线段树,也就是动态分配当前线段树的子节点。

我这边简单地搓了一个,\(83pts\) 还是会 MLE。

我们发现这个动态开点的请求过多会造成冗余的点越来越多,但是内存并没有被释放,所以我们这里可以 delete 彻底一点。 delete 的我没有去调他(懒得调)所以就贴一个 \(83pts\) 的动开sgt的部分代码(而且还是伪代码)在这里。

struct Node{
    int ls, rs;
    int cnt;
    int sum;
    Node():ls(0),rs(0),cnt(0),sum(0){}
};
vector<Node>t;
int idx=0;
int n,q;
int sumall=0;
#define mid ((l+r)>>1)
#define lson t[u].ls
#define rson t[u].rs
void pushup(int u){
	t[u].cnt=(!lson?0:t[lson].cnt)+(!rson?0:t[rson].cnt);
	t[u].sum=(!lson?0:t[lson].sum)+(!rson?0:t[rson].sum);
}
void insert(int &u, int l, int r, int v, int k) {
    if(!u)u=++idx,t.emplace_back();
    if(l==r){
    	t[u].cnt+=k,t[u].sum+=v*k;
    	return;
    }
    if(v<=mid)insert(lson,l,mid,v,k);
    else      insert(rson,mid+1,r,v,k);
    pushup(u);
}
void query(int u, int l, int r, int L, int R, int &cnt, int &sum) {
	if(!u or R<l or L>r)return;
	if(L<=l and r<=R){
		cnt+=t[u].cnt;
		sum+=t[u].sum;
		return;
	}
	query(lson,l,mid,L,R,cnt,sum);
	query(rson,mid+1,r,L,R,cnt,sum);
}
void del(int &u, int l, int r, int L, int R) {
	if(!u or R<l or L>r)return;
	if(L<=l and r<=R){
		t[u].cnt=0,t[u].sum=0;
		lson=rson=0;
		return;
	}
	if(L<=mid)del(lson,l,mid,L,R);
	if(mid<R) del(rson,mid+1,r,L,R);
    pushup(u);
}
main(void){
    t.reserve(N);//预留空间
    t.emplace_back();//初始开空节点
	read();
    int root=0;
    for(int i=0;i<n;i++)
    	read_and_insert();
    write(sumall);
    while(q--)
    	read;
    	int acnt=0,asum=0;
    	query(root,0,MAX_VAL,l,r,acnt,asum);
    	if(acnt>0)
    		del(root,0,MAX_VAL,l,r);
    		insert(root,0,MAX_VAL,x,acnt);
			sumall=sumall-asum+x*acnt; 
    	write(sumall);

更可行的正解

\[\Large \textcolor{red}{暴力\mathbb{STL}什么东西维护不了?(暴论)} \]

骗骗人的,因为这里有区间值域查询我们可能需要 \(\log\) 的时间来找到左右端点,然后去修改它,但是这个修改特别奇妙,是把整个区间全部推平,我们不难想到珂朵莉树,开平衡树去把这个点越缩越小。这里我们直接生搬硬套 \(\mathbb{STL}\)map 来维护。

为什么平衡树直接暴力删点是可以的?,我们设这时候的数值种类\(n\) 种,要求改的区间大小为 \(len\)

\[单次查询需要 \mathcal {O(\log\ n)}\\ 单次修改需要 \mathcal {O(len)}\\ 但是下次查询+修改只需要 \mathcal {O(\log (n-len) + len^{'}}\\ 更神奇的是 \mathcal{\sum len = n} \]

然后非常非常地开心,(我绝对不会告诉你赛时我线段树调了一个多小时然后没AC),发现这个区间修改的时间总复杂度只有 \(\mathcal {O(n)}\) ,查询的时间复杂度 \(\mathcal {q\log n}\),这个 \(\log\) 还是收敛的。完全就是随便切这道题。

下面就可以给出这个代码了。我们一个 lower_boundupper_bound 来快速查询左右端点,然后指针暴力往下跳就好,最后不要忘了这个 mp[x]=cnt

for(int i=1,x;i<=n;i++)
   read_ans_sum()
   do_set();
write(sum);
for(int i=1;i<=q;i++)
   read();
   auto L=mp.lower_bound(l),R=mp.upper_bound(r);
   #define cont L->second
   #define val L->first
   while(L!=R){//暴力跳
   	cnt+=cont;//处理元素个数
   	sum-=val*cont;//减去该桶的和
   	L=mp.erase(L);
   }
   sum+=cnt*x,mp[x]+=cnt;
   write(sum);
posted @ 2025-11-21 15:01  Noivelist  阅读(0)  评论(0)    收藏  举报