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)\)
按值从小到大处理,维护两个 set:s1 存储未处理(值更大)的位置,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 取得好成绩!
浙公网安备 33010602011771号