2022-6-23 #4 CF1558F & CF1693F
今天好颓啊😴。
007 CF1558F Strange Sort
这种排序的题 ,有一个套路:“钦定一个 \(x\),将大于等于 \(x\) 的数字设为 \(1\),否则设为 \(0\)”。
这样我们就不关心 \(0\) 之间的顺序,那么第 \(i\) 个 \(0\) 最后会到达位置 \(i\)。
于是我们从小到大扫描 \(x\),每次将一个 \(1\) 变成 \(0\),只需维护每个 \(0\) 到达其最后位置的时间,求出每个 \(x\) 的全局最大答案再取 \(\max\) 即可。
对于第 \(i\) 个 \(0\)(令它的位置为 \(z_i\)),令 \(p_i\) 为它对应的答案,那么可以列出:
也就是说,答案为:(令 \(zs\) 为 \(0\) 的数量)
变一下形,每个为零的位置维护一下:
注意如果一个点所在的前缀都是 \(0\),我们不能计算它的贡献,我们维护一个指针就好了,剩下的部分线段树维护即可,复杂度 \(O(n\log n)\)。
#include<stdio.h>
#define lc(x) (x<<1)
#define rc(x) (x<<1|1)
#define mid (l+r>>1)
#define inf 1000000000
const int maxn=200005,maxt=maxn<<2;
int T,n,ans;
int p[maxn],q[maxn],mx[maxt],lazy[maxt],vis[maxn];
inline int max(int a,int b){
return a>b? a:b;
}
inline void pushup(int now){
mx[now]=max(mx[lc(now)],mx[rc(now)]);
}
inline void getlazy(int now,int v){
lazy[now]+=v,mx[now]+=v;
}
inline void pushdown(int now){
if(lazy[now])
getlazy(lc(now),lazy[now]),getlazy(rc(now),lazy[now]),lazy[now]=0;
}
void build(int l,int r,int now){
lazy[now]=0;
if(l==r){
mx[now]=l+(l&1)-inf;
return ;
}
build(l,mid,lc(now)),build(mid+1,r,rc(now)),pushup(now);
}
void update(int l,int r,int now,int L,int R,int v){
if(L>R)
return ;
if(L<=l&&r<=R){
getlazy(now,v);
return ;
}
pushdown(now);
if(L<=mid)
update(l,mid,lc(now),L,R,v);
if(mid<R)
update(mid+1,r,rc(now),L,R,v);
pushup(now);
}
int main(){
scanf("%d",&T);
while(T--){
scanf("%d",&n),ans=0;
for(int i=1;i<=n;i++)
scanf("%d",&p[i]),q[p[i]]=i;
build(1,n,1);
for(int i=1,j=1;i<=n;i++){
int x=q[i];
update(1,n,1,x,x,inf),update(1,n,1,x,n,-2);
vis[x]=1;
while(j<=n&&vis[j])
update(1,n,1,j,j,-inf),j++;
ans=max(ans,mx[1]+i);
}
printf("%d\n",ans);
for(int i=1;i<=n;i++)
vis[i]=0;
}
return 0;
}
008 CF1693F I Might Be Wrong
很厉害的题啊!
有一个结论是操作一定会满足 \(cnt_0=cnt_1\),我们只需证明任意一个代价为 \(x\) 的操作都可以用不超过 \(x\) 次这样的操作代替即可。
将这个子段提取出来,将 \(0\) 看成 \(-1\),\(1\) 看成 \(1\),考察其前缀和对应格路,我们从高到低扫,若当前高度有大于一个位置,就将最前面的位置和最后面的位置进行一次操作。正确性感性理解一下就好了!!!而操作次数就是整个段的和的绝对值加一,不超过其操作一次的代价。
一个做法是继续观察性质:(不妨令序列总和小于 \(0\),如果不满足就可以将 \(01\) swap,然后给串 reverse)
- 操作的高度必须在起点与终点之间。(如果大于,则可以找到一个高度等于起点、终点高度最大值的区间包含这个操作的区间,反之亦然。)
- 如果操作的时候,操作左端点后面是一个下步,则可以省略这个操作。
哈哈,破防了,看不懂这个做法。
另一个做法是直接对这个这个东西贪心,我们对于每个 \(x\) 维护前缀和为 \(x\) 的最长前缀,然后贪心地向后跳,如果跳不了就补一些 \(0\) 进去。
这样是 \(O(n)\) 的。
#include<stdio.h>
#include<iostream>
#include<algorithm>
using namespace std;
const int maxn=200005;
int T,n,ans;
int sum[maxn],rec[maxn];
string s;
void calc(){
for(int i=1;i<=n;i++){
sum[i]=sum[i-1]+(s[i-1]=='0'? 1:-1);
if(sum[i]>=0)
rec[sum[i]]=i;
}
}
int main(){
scanf("%d",&T);
while(T--){
scanf("%d",&n),cin>>s;
int flg=0;
for(int i=1;i<n;i++)
if(s[i]<s[i-1])
flg=1;
if(flg==0){
puts("0");
continue;
}
calc();
if(sum[n]<0){
reverse(s.begin(),s.end());
for(int i=0;i<n;i++)
s[i]^=1;
calc();
}
int now=0;
while(now<n&&s[now]=='0')
now++;
ans=0;
while(now<sum[n])
ans++,now+=(rec[now]-now)/2;
printf("%d\n",ans+1);
for(int i=0;i<=n;i++)
rec[i]=0;
}
return 0;
}
009 CF1693E Outermost Maximums
ecnerwala 提供了一种很好写的解法。
可以发现,对于一个最大值位置,我们令 \(l_i,r_i\) 分别代表它前面的小于最大值的最大值,后面的小于最大值的最大值,那么这个位置一定会贪心地变为 \(\min\{l_i,r_i\}\)。
由于 \(l,r\) 单调,我们总能找到一个分割点使得其前面都有 \(l_i\leqslant r_i\),分割点前从前往后操作,分割点后从后往前即可使每个位置都到达下界。
现在我们要加速这个过程,我们考虑延迟维护这个 \(l,r\):
从大到小加入每个数,当一个数被加入时我们先不决策它选择 \(l\) 还是 \(r\)。在加入完一个值后,令这个值在序列上形成的区间为 \([l,r]\),我们将其左侧没有决策的数决策为选择 \(l\),并将决策了 \(r\) 的数加入这个区间,右侧同理。
上述算法正确性显然,而且没有决策的位置的位置中间一定不会夹着决策了的位置,我们只需维护这一区间,再用个树状数组记录哪些点被加入了就好了。
复杂度 \(O(n\log n)\)。
#include<stdio.h>
#include<vector>
#define lowbit(x) (x&-x)
using namespace std;
const int maxn=200005;
int n;
int a[maxn],t[maxn];
vector<int>v[maxn];
long long ans;
void update(int x,int v){
for(int i=x;i<=n;i+=lowbit(i))
t[i]+=v;
}
int query(int x){
int res=0;
for(int i=x;i;i-=lowbit(i))
res+=t[i];
return res;
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]),v[a[i]].push_back(i);
int lstL=n+1,lstR=0;
for(int i=n;i>=1;i--)
if(v[i].size()){
for(int j=0;j<v[i].size();j++)
update(v[i][j],1);
int L=v[i][0],R=v[i].back();
if(lstR<L)
L=lstR+1;
if(lstL>R)
R=lstL-1;
ans+=query(R)-query(L-1);
lstL=L,lstR=R;
}
printf("%lld\n",ans);
return 0;
}