peiwenjun's blog 没有知识的荒原

CF1693E Outermost Maximums 题解

题目描述

给定一个长为 \(n\) 的序列 \(a\) ,边界 \(a_0=a_{n+1}=0\)

你可以进行如下两种操作:

  • \(x\) 为最靠左的全局最大值所在位置,令 \(a_x\gets\max\limits_{0\le i\lt x}a_i\)
  • \(y\) 为最靠右的全局最大值所在位置,令 \(a_y\gets\max\limits_{y\lt i\le n+1}a_i\)

求让序列 \(a\) 变成全零的最小总操作次数。

数据范围

  • \(1\le n\le 2\cdot 10^5,0\le a_i\le n\)

时间限制 \(\texttt{2s}\) ,空间限制 \(\texttt{256MB}\)

分析

按值域从大到小考虑每个数,记当前最大值为 \(x\) ,出现位置为 \(p_1,\cdots,p_k\)

\(l_i=\max\limits_{0\le j\lt p_i}a_j,r_i=\max\limits_{p_i\lt j\le n+1}a_j\) ,即 \(p_i\) 两侧的最大值。

贪心地让 \(a_i\) 变成 \(\min(l_i,r_i)\) ,由于 \(l_i,r_i\) 都具有单调性,因此可以做到。

直接模拟上述贪心,我们获得了一个 \(\mathcal O(n^2)\) 的做法。


我们需要加快计数的过程。

首先 \(a_i\) 变化的过程与具体位置无关,仅与左右两侧出现过的数的集合有关。

\(S=\{a_j\mid j\lt i\},T=\{a_j\mid j\gt i\}\) ,考虑在值域上从 \(a_i-1\) 走到零的过程。

如果遇到 \(S\) 则收集一个 \(\leftarrow\) 标记,如果遇到 \(T\) 则收集一个 \(\rightarrow\) 标记。

如果同时拥有两种标记,清空标记同时贡献 \(1\) 的代价。

容易发现这和前面的贪心等价,考虑用矩乘刻画。

维护初始状态为无标记、有 \(\leftarrow\) 标记、有 \(\rightarrow\) 标记时走到的状态和代价,转移就是矩阵乘法。

注意到 \(i\to i+1\) 的过程会修改 \(\mathcal O(1)\)\(S\)\(T\) 中的位置,用线段树维护单点修改和区间乘积即可。

具体的,用一个 \(1\times 3\) 的向量表示当前状态,将当前代价放在对于状态的位置上,其余位置为 \(\infty\) ,转移用 \((\min,+)\) 矩阵乘法实现。

当然你也可以不写矩阵乘法,由于每种状态只有一条出边,手动维护每种状态转移到的位置和代价即可。

时间复杂度 \(\mathcal O(3^3n\log n)\)

#include<bits/stdc++.h>
#define ls p<<1
#define rs p<<1|1
using namespace std;
const int maxn=2e5+5,inf=1e9;
int n;
long long res;
int a[maxn],cur[maxn];
vector<int> vec[maxn];
struct mat
{
    int v[3][3];
}t[4]=
{
    {0,inf,inf,inf,0,inf,inf,inf,0},
    {inf,0,inf,inf,0,inf,1,inf,inf},
    {inf,inf,0,1,inf,inf,inf,inf,0},
    {1,inf,inf,1,inf,inf,1,inf,inf},
};
mat operator*(mat a,mat b)
{
    static mat c;
    for(int i=0;i<=2;i++)
        for(int j=0;j<=2;j++)
        {
            c.v[i][j]=inf;
            for(int k=0;k<=2;k++) c.v[i][j]=min(c.v[i][j],a.v[i][k]+b.v[k][j]);
        }
    return c;
}
struct node
{
    int l,r;
    mat mul;
}f[4*maxn];
inline void pushup(int p)
{
    f[p].mul=f[rs].mul*f[ls].mul;
}
void build(int p,int l,int r)
{
    f[p].l=l,f[p].r=r;
    if(l==r) return f[p].mul=t[cur[l]],void();
    int mid=(l+r)>>1;
    build(ls,l,mid);
    build(rs,mid+1,r);
    pushup(p);
}
void modify(int p,int x)
{
    if(f[p].l==f[p].r) return f[p].mul=t[cur[x]],void();
    int mid=(f[p].l+f[p].r)>>1;
    modify(x<=mid?ls:rs,x);
    pushup(p);
}
mat query(int p,int l,int r)
{
    if(l<=f[p].l&&f[p].r<=r) return f[p].mul;
    int mid=(f[p].l+f[p].r)>>1;
    if(r<=mid) return query(ls,l,r);
    if(l>=mid+1) return query(rs,l,r);
    return query(rs,l,r)*query(ls,l,r);
}
int get(int x)
{
    mat res=(mat){0,inf,inf}*query(1,0,x);
    for(int i=0;i<=2;i++) if(res.v[0][i]!=inf) return res.v[0][i];
    return -1;
}
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++) scanf("%d",&a[i]),cur[a[i]]|=2,vec[a[i]].push_back(i);
    cur[0]=3;
    build(1,0,n);
    for(int i=1;i<=n;i++)
    {
        int x=a[i];
        if(!x) continue;
        if(i==vec[x].back()) cur[x]^=2,modify(1,x);
        res+=get(x-1);
        if(i==vec[x].front()) cur[x]^=1,modify(1,x);
    }
    printf("%lld\n",res);
    return 0;
}

上面的做法快速算出了每个 \(a_i\) 的操作次数,不过我们还可以换一个角度考虑这个问题。

在值域上从大到小枚举 \(x\) ,显然任意时刻已经出现(大于 \(x\) )的所有数在序列上一定是一段前缀有 \(\leftarrow\) 标记,一段后缀有 \(\rightarrow\) 标记,中间没有标记。

我们希望求出哪些数中途会变成 \(x\)

\(x\) 在序列中出现的区间为 \([l,r]\) ,原本无标记的区间为 \([L,R]\)

我们需要给 \([0,l)\)\(\leftarrow\) 标记,给 \([l,r]\) 清空,给 \((r,n+1]\)\(\rightarrow\) 标记,分类讨论:

  • \([l,r]\cap[L,R]\neq\varnothing\) :只有 \([l,r]\) 会被清空,令 \(L'=l,R'=r\)
  • \(r\lt L\) :对于 \((r,L]\) 中的元素,原本就有一个 \(\leftarrow\) 标记,现在新加一个 \(\rightarrow\) 标记,因此也需要清空,令 \(L'=l,R'=L-1\)
  • \(l\gt R\) :和上一类情况类似,不再赘述。

代价为一段区间中已经出现的数的个数,树状数组可以轻松维护。

时间复杂度\(\mathcal O(n\log n)\)

#include<bits/stdc++.h>
using namespace std;
const int maxn=2e5+5;
int n;
long long res;
int a[maxn],c[maxn];
vector<int> vec[maxn];
inline void add(int x,int v)
{
    while(x<=n) c[x]+=v,x+=x&(-x);
}
inline int query(int x)
{
    int res=0;
    while(x) res+=c[x],x-=x&(-x);
    return res;
}
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++) scanf("%d",&a[i]),vec[a[i]].push_back(i);
    for(int i=n,L=1,R=n;i>=1;i--)
    {
        if(vec[i].empty()) continue;
        auto upd=[&](int _l,int _r)
        {
            L=_l,R=_r,res+=query(R)-(L?query(L-1):0);
        };
        int l=vec[i].front(),r=vec[i].back();
        if(l<=R&&r>=L) upd(l,r);
        else if(r<L) upd(l,L-1);
        else if(l>R) upd(R+1,r);
        if(i) for(auto p:vec[i]) add(p,1);
        res+=vec[i].size();
    }
    printf("%lld\n",res);
    return 0;
}

posted on 2023-07-06 16:27  peiwenjun  阅读(11)  评论(0)    收藏  举报

导航