P17153 [ICPC 2017 Xi'an R] Arrangement for Contests
题目大意
有一个长度为 \(n\) 的整数序列 \(a\),要从里面选出长度等于 \(k\) 的连续的子段,将该子段每一位减去 \(1\)。保证在任意时刻序列 \(a\) 的每一元素非负。询问最多能进行的操作数。
分析
假设当处理完左端点为 \(i\) 的子段 \([i, i+k-1]\) 后,原数组的第 \(i\) 个位置,从此以后就再也不会被覆盖到了,所以如果我们不去取这段区间,那么就会造成第 \(i\) 个位置的浪费,所以我们选取的区间越靠左就越优,也就是从左往右遍历时能取就取,容易发现这是符合贪心性质的。所以我们的算法就变成了:遍历左端点 \(l\),计算区间的最小值,将区间每一个元素都减去这个最小值,记录答案。
注意到 \(n\le10^5\),我们不能用朴素的方式去维护这个序列每段区间的最小值,必须要用到线段树来维护。
实现
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e5+10;
int T,a[N];
struct SegmentTree{//线段树
int l,r,add,val;
#define l(x) tree[x].l
#define r(x) tree[x].r
#define add(x) tree[x].add
#define val(x) tree[x].val
}tree[N*5];
void pushup(int u){
val(u)=min(val(u*2),val(u*2+1));
}
void pushdown(int u){
if(add(u)){
val(u*2)+=add(u);
val(u*2+1)+=add(u);
add(u*2)+=add(u);
add(u*2+1)+=add(u);
add(u)=0;
}
}
void modify(int u,int l,int r,int k){//修改操作
if(l<=l(u)&&r>=r(u)){
val(u)+=k;
add(u)+=k;
return;
}
pushdown(u);
int mid=(l(u)+r(u))/2;
if(l<=mid)modify(u*2,l,r,k);
if(r>mid)modify(u*2+1,l,r,k);
pushup(u);
}
int query(int u,int l,int r){//查询操作
if(l<=l(u)&&r>=r(u))return val(u);
pushdown(u);
int mid=(l(u)+r(u))/2;
int temp=1e9;
if(l<=mid)temp=min(temp,query(u*2,l,r));
if(r>mid)temp=min(temp,query(u*2+1,l,r));
return temp;
}
void build(int u,int l,int r){
l(u)=l;r(u)=r;
if(l==r){
val(u)=a[l];
return;
}
int mid=(l(u)+r(u))/2;
build(u*2,l,mid);
build(u*2+1,mid+1,r);
pushup(u);
}
signed main(){
cin>>T;
while(T--){
memset(tree,0,sizeof tree);
int n,k,ans=0;
cin>>n>>k;
for(int i=1;i<=n;i++){
cin>>a[i];
}
build(1,1,n);//线段树一定要有棵树
for(int L=1;L<=n-k+1;L++){//遍历区间的左端点
int val=query(1,L,L+k-1);//查询区间的最小值
ans+=val;//记录答案
modify(1,L,L+k-1,-val);//将区间的每个元素减去这个最小值
}
cout<<ans<<'\n';
}
}

浙公网安备 33010602011771号