题解:P12280 [蓝桥杯 2024 国 Python A] 特别的数组
guoshengyu1231 · · 题解
题解:P12280 [蓝桥杯 2024 国 Python A] 特别的数组
方法一
思路
考虑到答案具备单调性,首先考虑二分答案。
其实通俗的讲,如果说暴力枚举是一个一个猜答案的话,那二分答案就是将答案用二分查找的方式找出来。
那这就不得不提到二分答案的使用条件了。
-
解空间有序:问题的答案必须存在于一个有序的范围内(如数值区间),且存在明确的单调性特征。也就是:
- 当求解最大值时,若某个值
x 满足条件,则所有比x 小的值也满足条件。 - 当求解最小值时,若某个值
x 满足条件,则所有比x 大的值也满足条件。
例如,问题中出现“最大值最小化”或“最小值最大化”等双最值描述时,通常适用二分答案。
- 当求解最大值时,若某个值
- 存在高效的判定函数:对于给定的候选答案
mid ,需要能在多项式时间(通常为O(n) 或O(n \log n) )内验证其是否满足题目要求。 - 判定逻辑独立于答案生成:判定函数的设计应仅依赖当前候选答案
mid ,无需预先知道其他可能的解。这种独立性使得每次二分迭代只需关注当前mid 的可行性。\\
其实说了一大堆,都只是前置知识。
实现
代码的大致框架有了,接下来需要思考如何去实现这个算法。首先二分答案部分比较简单,我们可以先不管。重点是如何去实现
代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+5;
int n,a[maxn];
bool check(int x)
{
int cnt[maxn],s=0;
memset(cnt,0,sizeof cnt);
for(int i=x+1;i<=n;i++)//覆盖了第一个区间,故从x+1开始计数
{
cnt[a[i]]++;
if(cnt[a[i]]==2) s++;
}
if(s==0) return true;
for(int i=1;i+x<=n;i++)
{
cnt[a[i+x]]--;
if(cnt[a[i+x]]==1) s--;
cnt[a[i]]++;
if(cnt[a[i]]==2) s++;
//滑动窗口
if(s==0) return true;
}
return false;
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
int l=0,r=n;
while(l<r)
{
int mid=l+r>>1;
if(check(mid)) r=mid;
else l=mid+1;
}
cout<<n-l;
return 0;
}
方法二
其实还有更简单且更高效的办法。
思路
我们可以从方法一的思想开始想,方法一,也就是二分答案,是通过二分区间长度来求解的。那我们可不可以根本不用枚举区间长度,或者说可以快速算出区间长度,然后直接枚举左端点求解。那既然是通过枚举左端点
代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+5;
int n,a[maxn],ans=1e9;
bool vis[maxn];//记录每个数字是否出现再原序列中(没被删除)
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
int l=1,r=n;
while(l<=n&&!vis[a[l]]) vis[a[l++]]=true;
while(r&&!vis[a[r]]) vis[a[r--]]=true;
//初始化l,r
while(l)
{
ans=min(ans,r-l+1);//更新答案
vis[a[--l]]=false;//扩大左边界l
while(r&&!vis[a[r]]) vis[a[r--]]=true;//缩小有边界r
}
cout<<n-ans;
return 0;
}