CSP-J 模拟赛8 — 讲评

CSP-J 模拟赛8 — 讲评


目录


T1 团结(unite)

题意回顾

给定长度为 \(n\) 的序列 \(A\),每次操作:选位置 \(i\),将 \(A_i\) 变为 \(\gcd(A_i,i)\),代价为 \(n-i+1\)。求使 \(\gcd(A_1,\dots,A_n)=1\) 的最小代价。

关键结论:答案不超过 3

核心观察\(\gcd(n-1,n)=1\) 永远成立。

  • 最坏情况:操作位置 \(n-1\)(代价 \(2\))和 \(n\)(代价 \(1\)),总代价 \(2+1=3\)。操作后 \(A_{n-1}=\gcd(A_{n-1},n-1)\)\(A_n=\gcd(A_n,n)\),这两个数的 \(\gcd\) 能够"吸收" \(n-1\)\(n\) 的因子,使得整个序列的 \(\gcd\) 变为 \(1\)

因此答案只能是 \(\{0,1,2,3\}\) 之一。只需分类讨论:

情况 条件 答案
不需操作 原序列 \(\gcd=1\) \(0\)
操作位置 \(n\)(代价 \(1\) \(\gcd(A_1,\dots,A_{n-1},\gcd(A_n,n))=1\) \(1\)
操作位置 \(n-1\)(代价 \(2\) \(\gcd(A_1,\dots,A_{n-2},\gcd(A_{n-1},n-1),A_n)=1\) \(2\)
两个都操作 以上都不满足 \(3\)
  • 时间复杂度\(\mathcal{O}(n\log V)\),其中 \(V\) 是值域
  • 空间复杂度\(\mathcal{O}(n)\)

AC 代码

#include <bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int n;
int a[N];
//求最大公约数
int gcd(int a,int b)
{
    return b==0?a:gcd(b,a%b);
}
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++) cin>>a[i];
    //n=1 特判:操作位置1花费1,gcd(a[1],1)=1 必然团结
    if(n==1)
    {
        if(a[1]==1) cout<<0<<endl;
        else cout<<1<<endl;
        return 0;
    }
    //n=2 特判
    if(n==2)
    {
        if(gcd(a[1],a[2])==1) cout<<0<<endl;
        //操作位置2,花费1:a[2]=gcd(a[2],2)
        else if(gcd(a[1],gcd(a[2],2))==1) cout<<1<<endl;
        //否则操作位置1,花费2(必然成功,因为gcd(a[1],1)=1)
        else cout<<2<<endl;
        return 0;
    }
    //n>=3:答案只有0,1,2,3四种可能
    //因为gcd(n-1,n)=1,最坏情况操作位置n-1和n,花费2+1=3
    int g=0;
    for(int i=1;i<=n-2;i++) g=gcd(g,a[i]);
    int gn=gcd(a[n],n);      //操作位置n后的值
    int gn1=gcd(a[n-1],n-1); //操作位置n-1后的值
    //0次操作:全部gcd=1
    if(gcd(g,gcd(a[n-1],a[n]))==1) cout<<0<<endl;
    //1次操作:操作位置n,花费1
    else if(gcd(g,gcd(a[n-1],gn))==1) cout<<1<<endl;
    //1次操作:操作位置n-1,花费2
    else if(gcd(g,gcd(gn1,a[n]))==1) cout<<2<<endl;
    //否则需要操作两个位置,花费1+2=3
    else cout<<3<<endl;
    return 0;
}

易错点

  • 需要特判 \(n=1\)\(n=2\) 的情况
  • 答案上限为 \(3\) 这个结论需要大胆猜想并证明

T2 染色(color)

题意回顾

给定一棵以 \(1\) 为根的有根树,初始无色。每次操作:选节点 \(u\) 和颜色 \(x\),将子树 \(u\) 全部染成颜色 \(x\)。求达到目标颜色 \(c_i\) 的最少操作次数。

正解

核心观察:每次操作影响的是整棵子树。从上往下染色一定最优(先染根再染子树),因为如果先染子树再染根,子树的染色会被根的染色覆盖掉,造成浪费。

于是从根出发 DFS:

  • 维护"当前颜色" col(继承自父亲节点的最后一次染色)
  • 如果节点 \(u\) 的目标颜色 \(c_u\)col 不同,则需要一次新操作,并将 col 更新为 \(c_u\)
  • 继续 DFS 子节点

实际答案 = 满足 \(c_u \neq c_{p_u}\) 的节点数(根节点若 \(c_1 \neq 0\) 也算一次)。

  • 时间复杂度\(\mathcal{O}(n)\)
  • 空间复杂度\(\mathcal{O}(n)\)

与部分分的关系

  • \(p_i=i-1\)):相当于染后缀,从左往右,\(c_i \neq c_{i-1}\) 就答案 \(+1\)
  • 菊花\(p_i=1\)):先染根,然后每个与根颜色不同的叶子各需一次
  • 正解:链和菊花的思想推广到一般树——只需比较父子颜色

AC 代码

#include <bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int n;
int p[N],c[N];
vector<int> tree[N];
int ans;
//递归DFS——现在比赛内存512兆,栈空间也是512兆,不会溢出,递归写法更优雅
void dfs(int u,int col)
{
    if(c[u]!=col)
    {
        ans++;     //需要一次新的染色操作
        col=c[u];  //当前子树颜色更新
    }
    for(auto v:tree[u]) dfs(v,col);
}
int main()
{
    cin>>n;
    for(int i=2;i<=n;i++)
    {
        cin>>p[i];
        tree[p[i]].push_back(i);
    }
    for(int i=1;i<=n;i++) cin>>c[i];
    dfs(1,0);
    cout<<ans<<endl;
    return 0;
}

易错点

  • 根节点颜色从 \(0\)(无色)开始比较
  • 递归 DFS 代码简洁清晰,现在比赛内存和栈空间都是 512 兆,\(10^5\) 级别的递归不会溢出

T3 三元组(triple)

题意回顾

给定序列 \(A\),求满足 \(i\neq j, i\neq k, j\neq k\)\(\frac{A_j}{A_i}=\frac{A_k}{A_j}=b\)\(b\) 为正整数)的三元组 \((i,j,k)\) 的数量。

等价条件:\(A_j^2 = A_i \times A_k\),且 \(A_j\)\(A_i\) 的倍数。

20 分做法(\(n \le 500\)

思路:直接三重循环枚举 \((i,j,k)\),检查条件。

  • 时间复杂度\(\mathcal{O}(n^3)\)
  • 空间复杂度\(\mathcal{O}(n)\)
#include <bits/stdc++.h>
using namespace std;
const int N=505;
int n;
int a[N];
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++) cin>>a[i];
    long long ans=0;
    //暴力枚举三个不同的位置
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=n;j++)
        {
            if(i==j) continue;
            if(a[j]%a[i]!=0) continue;  //b必须是整数
            int b=a[j]/a[i];
            long long target=1ll*a[j]*b; //A_k = A_j * b
            for(int k=1;k<=n;k++)
            {
                if(k==i||k==j) continue;
                if(a[k]==target) ans++;
            }
        }
    }
    cout<<ans<<endl;
    return 0;
}

40 分做法(\(n \le 5000\)

思路:减少一层循环。枚举 \((i,j)\) 后,\(A_k\) 的值就确定了(\(A_k=A_j \times b\)),用哈希表直接查询有多少个位置等于该值,再减去与 \(i,j\) 冲突的情况。

  • 时间复杂度\(\mathcal{O}(n^2)\)
  • 空间复杂度\(\mathcal{O}(n)\)
#include <bits/stdc++.h>
using namespace std;
const int N=5005;
int n;
int a[N];
unordered_map<long long,int> cnt;  //值->出现次数
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        cin>>a[i];
        cnt[a[i]]++;
    }
    long long ans=0;
    //枚举(i,j),计算b和target
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=n;j++)
        {
            if(i==j) continue;
            if(a[j]%a[i]!=0) continue;
            int b=a[j]/a[i];
            long long target=1ll*a[j]*b;
            if(cnt.count(target))
            {
                int add=cnt[target];
                if(a[i]==target) add--;  //k不能和i同位置
                if(a[j]==target) add--;  //k不能和j同位置
                ans+=add;
            }
        }
    }
    cout<<ans<<endl;
    return 0;
}

60 分做法(\(\max A_i \le 5000\)

思路:值域很小的时候,可以枚举数值而非位置。对于每个可能的中间值 \(v\),枚举 \(v\) 的所有因子 \(d\) 作为 \(A_i\),则 \(A_k=v^2/d\)。用桶统计每种值的出现次数。

注意:当 \(d=v\) 时,\(A_i=A_j=A_k\),三元组三个位置的值相同,需用组合数计算。

  • 时间复杂度\(\mathcal{O}(V\log V)\),其中 \(V=\max A_i\le 5000\)
  • 空间复杂度\(\mathcal{O}(V)\)
#include <bits/stdc++.h>
using namespace std;
const int V=5005;
int n;
int cnt[V];  //值->出现次数,值域很小直接用数组
int main()
{
    cin>>n;
    int maxv=0;
    for(int i=1;i<=n;i++)
    {
        int x;cin>>x;
        cnt[x]++;
        maxv=max(maxv,x);
    }
    long long ans=0;
    //枚举中间值v
    for(int v=1;v<=maxv;v++)
    {
        if(!cnt[v]) continue;
        //枚举v的因子d作为A_i
        for(int d=1;d*d<=v;d++)
        {
            if(v%d!=0) continue;
            int d1=d,d2=v/d;
            //因子d1
            long long target1=1ll*v*v/d1;
            if(target1<=maxv&&cnt[target1])
            {
                if(d1==v) ans+=1ll*cnt[v]*(cnt[v]-1)*(cnt[v]-2);  //三者相同
                else ans+=1ll*cnt[d1]*cnt[v]*cnt[target1];
            }
            //因子d2(避免重复计算平方因子)
            if(d1!=d2)
            {
                long long target2=1ll*v*v/d2;
                if(target2<=maxv&&cnt[target2])
                {
                    if(d2==v) ans+=1ll*cnt[v]*(cnt[v]-1)*(cnt[v]-2);
                    else ans+=1ll*cnt[d2]*cnt[v]*cnt[target2];
                }
            }
        }
    }
    cout<<ans<<endl;
    return 0;
}

80 分做法(\(\max A_i \le 10^6\)

思路:对于数组中实际出现的每个不同值 \(v\),枚举其因子 \(d\)\(\sqrt{v}\le 1000\))。用哈希表存储出现次数。注意 \(A_k=v^2/d\) 可能超过 int 范围,需要先判断是否 \(\le 10^9\) 再查哈希表。

  • 时间复杂度\(\mathcal{O}(n\sqrt{V})\)\(V\le 10^6\)\(\sqrt{V}\le 1000\),实际约为 \(\mathcal{O}(n \cdot d(V))\),其中 \(d(V)\) 是平均因子数
  • 空间复杂度\(\mathcal{O}(n)\)
#include <bits/stdc++.h>
using namespace std;
const int V=1e9;  //全局值域上限
int n;
unordered_map<int,int> cnt;  //值->出现次数
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        int x;cin>>x;
        cnt[x]++;
    }
    long long ans=0;
    //对每个不同的值,枚举其因子
    for(auto &[v,c]:cnt)
    {
        if(c>=3) ans+=1ll*c*(c-1)*(c-2);  //三者相同的特殊情况(A_i=A_j=A_k, b=1)
        //枚举v的因子d作为A_i
        for(int d=1;d*d<=v;d++)
        {
            if(v%d!=0) continue;
            int d1=d,d2=v/d;
            //因子d1:A_i=d1, A_j=v, A_k=v^2/d1
            if(d1!=v)
            {
                long long target=1ll*v*v/d1;
                if(target<=V&&cnt.count(target))
                    ans+=1ll*cnt[d1]*c*cnt[target];
            }
            //因子d2
            if(d1!=d2&&d2!=v)
            {
                long long target=1ll*v*v/d2;
                if(target<=V&&cnt.count(target))
                    ans+=1ll*cnt[d2]*c*cnt[target];
            }
        }
    }
    cout<<ans<<endl;
    return 0;
}

100 分做法(\(n\le 10^5\)\(A_i\le 10^9\)

思路(根号分治):

设阈值 \(B = V^{2/3} = 10^6\)\(V=10^9\))。

  • 小值\(A_j \le B\)):枚举 \(A_j\) 的所有因子,\(\sqrt{A_j}\le 1000\)
  • 大值\(A_j > B\)):枚举 \(b\)\(b=A_j/A_i\)),由于 \(A_j>10^6\)\(A_k=A_j\cdot b\le 10^9\),有 \(b\le 10^3\),枚举 \(b\) 即可

两者的复杂度均摊为 \(\mathcal{O}(n\cdot V^{1/3})\)

  • 时间复杂度\(\mathcal{O}(n\cdot V^{1/3})\)\(V^{1/3}=1000\)
  • 空间复杂度\(\mathcal{O}(n)\)
#include <bits/stdc++.h>
using namespace std;
const int B=1e6;      //阈值 V^{2/3}
const int V=1e9;      //值域上限
int n;
unordered_map<int,int> cnt;  //值->出现次数
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        int x;cin>>x;
        cnt[x]++;
    }
    long long ans=0;
    for(auto &[x,c]:cnt)
    {
        if(c>=3) ans+=1ll*c*(c-1)*(c-2);  //三者相同: A_i=A_j=A_k
        if(x<=B)
        {
            //小值:枚举因子d
            for(int d=1;d*d<=x;d++)
            {
                if(x%d!=0) continue;
                int d1=d,d2=x/d;
                //因子d1
                if(d1!=x)
                {
                    long long target=1ll*x*x/d1;
                    if(target<=V&&cnt.count(target))
                        ans+=1ll*cnt[d1]*c*cnt[target];
                }
                //因子d2
                if(d1!=d2&&d2!=x)
                {
                    long long target=1ll*x*x/d2;
                    if(target<=V&&cnt.count(target))
                        ans+=1ll*cnt[d2]*c*cnt[target];
                }
            }
        }
        else
        {
            //大值:枚举b (b = A_j/A_i, A_i = x/b, A_k = x*b)
            //b ≥ 2 且 x*b ≤ V
            for(int b=2;1ll*x*b<=V;b++)
            {
                if(x%b==0&&cnt.count(x/b)&&cnt.count(x*b))
                    ans+=1ll*cnt[x/b]*c*cnt[x*b];
            }
        }
    }
    cout<<ans<<endl;
    return 0;
}

评分标准对应

分数 测试点 约束 算法
20 1~2 \(n\le 500\) \(\mathcal{O}(n^3)\) 暴力
40 1~4 \(n\le 5000\) \(\mathcal{O}(n^2)\) 枚举
60 1~6 \(\max A_i\le 5000\) \(\mathcal{O}(V\log V)\) 枚举因子
80 1~8 \(\max A_i\le 10^6\) \(\mathcal{O}(n\sqrt{V})\) 枚举因子
100 1~10 \(n\le 10^5\)\(A_i\le 10^9\) \(\mathcal{O}(n V^{1/3})\) 根号分治

T4 排序(sort)

题意回顾

给定元素两两不同的序列 \(A\)。每次操作:选区间 \([l,r]\),代价 \(r-l\),将该区间内的数从小到大排序。对每个子区间 \([l,r]\) 求最小代价 \(f_{l,r}\),输出 \(\sum_{l\le r} f_{l,r}\)

核心结论:操作区间不交

最优解中,操作的区间两两不交。 若有交集,对它们的并集排序代价更小。

因此,排序方案等价于将 \([l,r]\) 划分为若干段,每段内部排序。一个分割点 \(j\) 合法当且仅当 \(\max[l..j] < \min[j+1..r]\)

若选择了 \(k\) 个合法分割点,则 \(f_{l,r}=(r-l)-k\)

20 分做法(\(n \le 5\)

思路:对每个子区间,枚举所有可能的分割点组合(\(2^{len-1}\) 种),检查每种组合是否合法,取最大分割数。

  • 时间复杂度\(\mathcal{O}(n^2\cdot 2^n)\)\(n\le 5\) 时约 240 次检查
  • 空间复杂度\(\mathcal{O}(n)\)
#include <bits/stdc++.h>
using namespace std;
const int N=10;
int n;
int a[N];
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++) cin>>a[i];
    long long ans=0;
    //枚举所有子区间[l,r]
    for(int l=1;l<=n;l++)
    {
        for(int r=l;r<=n;r++)
        {
            int len=r-l+1;
            int maxsplits=0;
            //枚举所有可能的分割点集合(2^{len-1}种)
            for(int mask=0;mask<(1<<(len-1));mask++)
            {
                int splits=0;
                bool ok=1;
                int curmax=0;
                //扫描区间,验证每个分割是否合法
                for(int p=l;p<=r;p++)
                {
                    curmax=max(curmax,a[p]);
                    //检查是否在p处分割
                    if(p<r&&(mask>>(p-l)&1))
                    {
                        //p处分割,需要curmax < 右侧最小值
                        int rightmin=2e9;
                        for(int q=p+1;q<=r;q++) rightmin=min(rightmin,a[q]);
                        if(curmax<rightmin) splits++;
                        else {ok=0;break;}
                    }
                }
                if(ok) maxsplits=max(maxsplits,splits);
            }
            //f[l][r] = (r-l) - 最多合法分割点数
            ans+=(r-l)-maxsplits;
        }
    }
    cout<<ans<<endl;
    return 0;
}

40 分做法(\(n \le 16\)

思路:使用公式 \(f_{l,r}=(r-l)-\text{合法分割点数}\)。对每个子区间,预处理后缀最小值,然后从左到右扫描统计合法分割点。

  • 时间复杂度\(\mathcal{O}(n^3)\)\(n\le 16\) 时约 2000 次操作
  • 空间复杂度\(\mathcal{O}(n)\)
#include <bits/stdc++.h>
using namespace std;
const int N=20;
int n;
int a[N];
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++) cin>>a[i];
    long long ans=0;
    //枚举所有子区间
    for(int l=1;l<=n;l++)
    {
        for(int r=l;r<=n;r++)
        {
            int splits=0;
            int premax=0;
            //预处理后缀最小值
            vector<int> sufmin(r+5);
            sufmin[r+1]=2e9;
            for(int k=r;k>=l;k--) sufmin[k]=min(sufmin[k+1],a[k]);
            //从左往右扫描,统计合法分割点
            for(int j=l;j<r;j++)
            {
                premax=max(premax,a[j]);
                if(premax<sufmin[j+1]) splits++;
            }
            ans+=(r-l)-splits;
        }
    }
    cout<<ans<<endl;
    return 0;
}

60 分做法(\(n \le 500\)

思路:与 40 分做法完全相同的 \(\mathcal{O}(n^3)\) 公式法,直接适用于 \(n\le 500\)。总操作数约 \(\frac{n^3}{6}\approx 2\times 10^7\),在 C++ 中可以通过。

  • 时间复杂度\(\mathcal{O}(n^3)\)
  • 空间复杂度\(\mathcal{O}(n)\)
#include <bits/stdc++.h>
using namespace std;
const int N=505;
int n;
int a[N];
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++) cin>>a[i];
    long long ans=0;
    for(int l=1;l<=n;l++)
    {
        for(int r=l;r<=n;r++)
        {
            //计算后缀最小值 sufmin[k] = min(a[k..r])
            int sufmin[N];
            sufmin[r+1]=2e9;
            for(int k=r;k>=l;k--) sufmin[k]=min(sufmin[k+1],a[k]);
            //统计合法分割点
            int premax=0,splits=0;
            for(int j=l;j<r;j++)
            {
                premax=max(premax,a[j]);
                if(premax<sufmin[j+1]) splits++;
            }
            ans+=(r-l)-splits;
        }
    }
    cout<<ans<<endl;
    return 0;
}

80 分做法(\(n \le 5000\)

思路:将问题转化为统计三元组 \((i,j,k)\) 满足 \(i\le j<k\)\(\max[i..j]<\min[j+1..k]\)

先计算所有子区间的 \((r-l)\) 之和(\(\sum_{len=2}^{n}(len-1)(n-len+1)\)),再枚举分割点 \(j\),用双指针统计以 \(j\) 为分割点的合法三元组个数,从答案中减去。

对于固定的 \(j\),定义 \(g[i]=\max[i..j]\)\(i\le j\)\(g[i]\) 关于 \(i\) 递减时单调不减)和 \(h[k]=\min[j+1..k]\)\(k>j\)\(h[k]\) 关于 \(k\) 递增时单调不减)。对所有 \(i\),用双指针找到满足 \(h[k]>g[i]\) 的最大 \(k\)

  • 时间复杂度\(\mathcal{O}(n^2)\)
  • 空间复杂度\(\mathcal{O}(n)\)
#include <bits/stdc++.h>
using namespace std;
const int N=5005;
int n;
int a[N];
int lmax[N];  //lmax[i]=max(a[i..j])
int rmin[N];  //rmin[k]=min(a[j+1..k])
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++) cin>>a[i];
    //ans_base = sum_{l≤r} (r-l)
    long long ans=0;
    for(int len=2;len<=n;len++) ans+=1ll*(len-1)*(n-len+1);
    //枚举分割点j,减去所有合法三元组(i,j,k)的贡献
    //三元组(i,j,k): i≤j<k, max[i..j] < min[j+1..k]
    for(int j=1;j<n;j++)
    {
        //计算左侧:lmax[i]=max(a[i..j]), i从j到1递减
        int cur=0;
        for(int i=j;i>=1;i--)
        {
            cur=max(cur,a[i]);
            lmax[i]=cur;
        }
        //计算右侧:rmin[k]=min(a[j+1..k]), k从j+1到n递增
        cur=2e9;
        for(int k=j+1;k<=n;k++)
        {
            cur=min(cur,a[k]);
            rmin[k]=cur;
        }
        //双指针:对每个i,找到最大的k使rmin[k]>lmax[i]
        //rmin是非递增的(k增大,最小值不增)
        //lmax是非递减的(i减小,最大值不减)
        int ptr=n;
        for(int i=j;i>=1;i--)
        {
            while(ptr>=j+1&&rmin[ptr]<=lmax[i]) ptr--;
            //k可取 j+1..ptr,共 ptr-j 个
            ans-=(ptr-j);
        }
    }
    cout<<ans<<endl;
    return 0;
}

100 分做法(\(n \le 3\times 10^5\)

思路:延续三元组 \((i,j,k)\) 的思路,换一个角度:枚举 \(\max[i..j]\) 的值 \(x\)

\(x\) 在位置 \(b\)。用 set 维护:

  • \(a\)\(b\) 左边第一个比 \(x\) 大的位置(前驱更大值)
  • \(c\)\(b\) 右边第一个比 \(x\) 大的位置(后继更大值)
  • \(d\)\(c\) 右边第一个比 \(x\) 小的位置(后继更小值)

则有:

  • \(i \in (a, b]\)(共 \(b-a\) 种选择)
  • \(j = c-1\)固定,因为 \(j+1\) 必须恰好是 \(c\) 才能保证 \(\min[j+1..k]>x\)
  • \(k \in [c, d)\)(共 \(d-c\) 种选择)
  • 贡献:\((b-a)\times(d-c)\)

按值从小到大处理,维护两个 sets1 存储未处理(值更大)的位置,s2 存储已处理(值更小)的位置。

  • 时间复杂度\(\mathcal{O}(n\log n)\)
  • 空间复杂度\(\mathcal{O}(n)\)
#include <bits/stdc++.h>
using namespace std;
const int N=3e5+5;
int n;
int a[N],id[N];
set<int> s1,s2;  //s1:未处理的位置(值更大的), s2:已处理的位置(值更小的)
//按值从小到大排序
bool cmp(int x,int y)
{
    return a[x]<a[y];
}
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++) cin>>a[i];
    //ans_base = sum_{l≤r} (r-l)
    long long ans=0;
    for(int len=2;len<=n;len++) ans+=1ll*(len-1)*(n-len+1);
    //初始化:s1包含所有位置+哨兵, s2包含哨兵
    for(int i=0;i<=n+1;i++) s1.insert(i);
    s2.insert(0);
    s2.insert(n+1);
    //按值排序
    for(int i=1;i<=n;i++) id[i]=i;
    sort(id+1,id+n+1,cmp);
    //从小到大处理每个值
    for(int t=1;t<=n;t++)
    {
        int b=id[t];  //当前最大值x的位置
        //在s1中找前驱和后继(值比x大的最近位置)
        int a=*(--s1.lower_bound(b));
        int c=*s1.upper_bound(b);
        if(c!=n+1)
        {
            //在s2中找c后面第一个值比x小的位置
            int d=*s2.upper_bound(c);
            ans-=1ll*(b-a)*(d-c);
        }
        //将b从s1移到s2
        s1.erase(b);
        s2.insert(b);
    }
    cout<<ans<<endl;
    return 0;
}

评分标准对应

分数 测试点 约束 算法
20 1~2 \(n \le 5\) \(\mathcal{O}(n^2 2^n)\) 暴力枚举分割
40 1~4 \(n \le 16\) \(\mathcal{O}(n^3)\) 公式法
60 1~6 \(n \le 500\) \(\mathcal{O}(n^3)\) 公式法
80 1~8 \(n \le 5000\) \(\mathcal{O}(n^2)\) 双指针
100 1~10 \(n \le 3\times 10^5\) \(\mathcal{O}(n\log n)\) set + 单调性

赛场经验总结

1. 观察答案上界,大胆猜想

T1\(\gcd(n-1,n)=1\) 这个性质直接给出了答案 \(\le 3\) 的结论。在考场上遇到类似问题,先想想答案是否有一个很小的上界,往往可以大幅简化问题。

可复用技巧\(\gcd(x,x+1)=1\)\(\gcd(n-1,n)=1\) 这类关于相邻数互质的性质在数论题中反复出现。

2. 特殊形态推广到一般

T2 的解法来源于链和菊花两个特殊情况:

  • 链:\(c_i \neq c_{i-1}\) 就需要染色
  • 菊花:先染根,再看叶子

推广到一般树就是"父子颜色不同就需要染色"。先做部分分,从特殊形态中找规律,再推广到正解。

3. 值域分治(根号分治)

T3 的正解使用了经典的阈值分治技巧。当题目中涉及到因子枚举时,设置 \(B=V^{2/3}\) 的阈值可以平衡"枚举因子"和"枚举倍数"两种策略的复杂度。

可复用技巧

  • 枚举因子:\(\mathcal{O}(\sqrt{V})\)
  • 枚举倍数:\(\mathcal{O}(V/x)\)
  • 阈值取 \(V^{2/3}\) 时两者平衡,总复杂度 \(\mathcal{O}(V^{1/3})\)

4. 操作区间不交 → 分割点模型

T4 的核心转化:"排序操作的区间不交" → "用分割点划分区间"。这个结论在很多区间操作题中都成立——如果有重叠操作,合并它们一定不劣。

可复用技巧:区间操作的最优解中,操作区间通常要么嵌套要么不交(本题是不交)。先证明这个性质,再 DP 或贪心。

5. 正向难求,反向转化

T4 从直接求 \(f_{l,r}\) 转为求"合法分割点数量",再转化为"统计满足前缀最大值小于后缀最小值的三元组"。枚举的对象从"子区间"变为"分割点 \(j\)"(80分)再变为"最大值 \(x\)"(100分),每次转化都让枚举范围变小。

可复用技巧:当 \(\sum_{l,r} f(l,r)\) 直接求困难时,考虑换枚举对象——换成分割点、最值、或其它关键元素。

6. 用 set 维护前驱后继

T4-100分 中,从小到大处理元素,用 set 维护"已处理"和"未处理"的位置集合,可以 \(\mathcal{O}(\log n)\) 查询前驱后继。这种"逐步加入 + 查询邻居"的技巧在单调栈/笛卡尔树相关的题目中非常常见。

7. 递归还是迭代?

T2 树可能是链(深度 \(10^5\))。以前比赛栈空间较小(如 8 兆),递归 DFS 容易栈溢出,需要用显式栈迭代。但现在比赛内存都是 512 兆,栈空间也是 512 兆,\(10^5\) 级别的递归完全不会溢出,递归代码更优雅简洁,推荐使用递归写法。

8. 溢出陷阱

T3\(A_k = A_j^2 / A_i\),即使 \(A_i,A_j\le 10^6\),结果也可能达到 \(10^{12}\),超出 int 范围。计算中间结果时务必使用 long long,并且在对哈希表查询前先判断是否超过全局值域上限。


讲评完毕,祝大家 CSP 取得好成绩!

posted @ 2026-07-17 08:09  surprise_ying  阅读(35)  评论(0)    收藏  举报