构造题做
-
找出题中特殊限制(前提)
-
找出答案的限制(求出的方法?取值范围?性质?若需判断能否构造,条件是什么?...)
-
贪心(关键):逐一解决限制
CF1712D Empty Graph
-
个点的完全图, 之间的边权为 。
-
由于是完全图,则任意两点都存在边,两点之间的最短路只有两种情况:直接走;通过 的点走,即 。
-
注意到 ,这告诉我们只用考虑编号相邻两点即可。于是整个直径就是 。看到 相互嵌套果断二分答案。首先解决 的限制,若 显然要把 变大。然后解决 ,如果存在 的那没事了,若没有,则考虑是否有 ,有则随便修改一条它相邻的边(点),无则要修改两个点。判断总修改次数是否 即可。
点击查看代码
const int N=1e5+10,inf=1e9;
int n,k,a[N],tmp[N];
bool check(int mid){
int tot=0,qwq=-1;bool flag=0;
for(int i=1;i<=n;++i){
if(2*a[i]<mid)tmp[i]=inf,++tot;
else tmp[i]=a[i];
}
for(int i=1;i<n;++i){
if(min(tmp[i],tmp[i+1])>=mid){flag=1;break;}
qwq=max(qwq,tmp[i]);
}qwq=max(qwq,tmp[n]);
if(flag)return tot<=k;
return (qwq>=mid?tot+1:tot+2)<=k;
}
void solve(){
read(n),read(k);
for(int i=1;i<=n;++i)read(a[i]);
int l=1,r=1e9,mid,ans;
while(l<=r){
mid=(l+r)>>1;
if(check(mid))ans=mid,l=mid+1;
else r=mid-1;
}
printf("%d\n",ans);
}
int main(){
int t;read(t);
while(t--)solve();
return 0;
}
CF1717D Madoka and The Corruption Scheme
-
比赛是二叉树的形式,可以钦定比赛对手,可以钦定赢家,但是某场比赛的赢家会被改变(只有 次)。
-
在会被改变的情况下,求能保证的编号最小。
-
既然我们能钦定一开始的排列顺序以及谁赢,不如我们把他直接确定下来,
红边表示钦定它赢,用 0 表示该边赢,1 表示该边输,则叶子节点都对应一个唯一的 01 串。全为 0 的那位就是最后的赢家。
对于赞助商单次改变结果的操作相当于将某个节点的 1 变成 0。对于一个人,赞助商想要他赢,它的编号不能有多于 个 1。
于是我们可以算出赞助商能钦定的赢家个数 ,我们从小到大安排编号,那么最终的答案就是 啦。
怎么算?编号的二进制位有 位,其中 位是 的人就有 个,答案就是 。
点击查看代码
const int N=1e5+10,mod=1e9+7;
int n,k;
long long fac[N],ifac[N],ans=0;
long long qpow(long long a,int b){
long long res=1;
while(b){
if(b&1)res=res*a%mod;
b>>=1;a=a*a%mod;
}
return res;
}
long long C(int a,int b){
return fac[a]*ifac[b]%mod*ifac[a-b]%mod;
}
int main(){
read(n),read(k);fac[0]=1;
for(int i=1;i<=n;++i)fac[i]=1ll*i*fac[i-1]%mod;
ifac[n]=qpow(fac[n],mod-2);
for(int i=n-1;~i;--i)ifac[i]=1ll*(i+1)*ifac[i+1]%mod;
for(int i=0;i<=min(n,k);++i)ans=(ans+C(n,i))%mod;
printf("%lld\n",ans);
return 0;
}
CF1381A2 Prefix Flip (Hard Version)
-
对于一次操作,是将前缀取反后反转。
-
最多操作 次,使 变成 。
-
注意到操作并不会影响到后缀,考虑从后往前逐位确定。对于一位 ,则考虑能否通过反转前缀 得到,若 ,则先反转第一个数再反转 。故最多操作 次,符合要求。记录一下前缀区间的 和反转情况就能很好维护了。
点击查看代码
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
#include<vector>
using namespace std;
#define pb push_back
const int N=1e5+10;
int n,a[N],b[N];
vector<int>ans;
void solve(){
scanf("%d",&n);ans.clear();
for(int i=1;i<=n;++i)scanf("%1d",&a[i]);
for(int i=1;i<=n;++i)scanf("%1d",&b[i]);
int l=1,r=n,rev=0;
for(int i=n;i;--i){
if((a[r]^rev)==b[i]){
l<r?--r:++r;
continue;
}
if((a[l]^rev)==b[i])ans.pb(1);
ans.pb(i);
swap(l,r),rev^=1;
l<r?--r:++r;
}
printf("%d ",ans.size());
for(int i:ans)printf("%d ",i);printf("\n");
}
int main(){
#ifdef LOCAL
freopen("std.in","r",stdin);
freopen("my.out","w",stdout);
#endif
int t;scanf("%d",&t);
while(t--)solve();
return 0;
}
CF1706D2 Chopping Carrots (Hard Version)
-
结果的式子中含有下取整,有 和 ,。
-
求 的最小值
-
可以考虑枚举最小值 (注意 是递增给出的, 时取得上界 ,下界显然是 ),对于 计算 的最小值,其中 尽量大,这可以通过 得到。时间复杂度是 的,只能通过弱化版本。
这个做法的瓶颈就是对于枚举的每一个 都要 算一遍能取到的最大值,考虑如何通过预处理优化这个过程。
记 表示当取 为最小值时,取到的最大值,也就是上面 算的东西。考虑每个 对其有什么影响。
注意到 的取值只有 种(整除分块的结论),记为 ,显然对于 , 至少为 。于是我们可以用 去更新 ,最后计算答案的时候扫描一遍就可以了。总共时间复杂度 。
点击查看代码
int n,k,a[N],maxn[N];
void solve(){
read(n),read(k);
memset(maxn,0,sizeof maxn);
for(int i=1;i<=n;++i){
read(a[i]);int pre=-1,cur;
//注意这里枚举取值是递减的,所以是用 pre 更新 cur+1
for(int j=1;j<=min(a[i],k);j=(a[i]/(a[i]/j))+1){
cur=a[i]/j;
maxn[cur+1]=max(maxn[cur+1],pre);
pre=cur;
}maxn[0]=max(maxn[0],pre);
}
int qwq=0,ans=1e9;
for(int i=0;i<=a[1];++i){
qwq=max(qwq,maxn[i]);
ans=min(ans,qwq-i);
}printf("%d\n",ans);
}
CF1738D Permutation Addicts
-
的构造与 和 存在大小关系,且 所代表的值在 前面,或为特殊值。
-
给出 ,构造合法的 和 。
-
写的太好了遂搬过来
点击查看代码
int n;
vector<int>e[N],ans;
void dfs(int u){
ans.pb(u);
for(int v:e[u])if(e[v].empty())ans.pb(v);
for(int v:e[u])if(!e[v].empty())dfs(v);
}
void solve(){
read(n);ans.clear();
for(int i=0;i<=n+1;++i)e[i].clear();
int k=N;
for(int i=1,b;i<=n;++i){
read(b);e[b].pb(i);
i<b?k=min(k,b-1):k=min(k,i-1);
}printf("%d\n",k);
if(!e[0].empty())dfs(0);
else dfs(n+1);
for(int i=1;i<=n;++i)printf("%d ",ans[i]);printf("\n");
}
CF1658C Shinju and the Lost Permutation
-
排列 的 循环置换表示将后 个数移到最前
-
定义 循环置换的权值为 的不同元素个数,给出 表示 循环置换的权值,是否存在排列 满足 。
-
注意到若最大数 在最前面,则 ,这样的 有且仅有一个。事实上 循环置换到 只是将最后一个数移到前面,于是我们从这个 的位置开始往后循环判断,可以分讨得出相邻两次置换一定得满足 。
点击查看代码
const int N=1e5+10;
int n,c[N];
void solve(){
read(n);int flag=0,pos;
for(int i=1;i<=n;++i){
read(c[i]);
if(c[i]==1)pos=i,++flag;
}
if(flag>1||!flag)return printf("NO\n"),void();
for(int i=1;i<=n;++i){
int nxt=pos+1>n?1:pos+1;
if(c[nxt]-c[pos]>1){flag=0;break;}
pos=nxt;
}
if(!flag)printf("NO\n");
else printf("YES\n");
}
int main(){
#ifdef LOCAL
freopen("std.in","r",stdin);
freopen("my.out","w",stdout);
#endif
int t;read(t);
while(t--)solve();
return 0;
}
CF1691D Max GEQ Sum
1、2. 给出 询问对于所有的 都满足 。
- 一种很自然的思路就是钦定某个数为最大值,向左右拓展判断最大的和是否小于等于它。向左右拓展可以用双向链表维护,从小到大钦定数之后删掉就可以了。
点击查看代码
const int N=2e5+10;
#define int long long
int n,a[N],sum[N],p[N];
int pre[N],nxt[N];
int lg[N],mn[20][N],mx[20][N];
void build(){
for(int i=1;i<=lg[n];++i){
for(int j=1;j+(1<<i)-1<=n;++j){
mn[i][j]=min(mn[i-1][j],mn[i-1][j+(1<<(i-1))]);
mx[i][j]=max(mx[i-1][j],mx[i-1][j+(1<<(i-1))]);
}
}
}
int query(int l,int mid,int r){
int p=lg[mid-l+1],q=lg[r-mid+1];
return max(mx[q][mid],mx[q][r-(1<<q)+1])-min(mn[p][l],mn[p][mid-(1<<p)+1]);
}
void solve(){
read(n);
for(int i=1;i<=n;++i){
read(a[i]);pre[i]=i-1,nxt[i]=i+1,p[i]=i;
sum[i]=sum[i-1]+a[i],mn[0][i]=sum[i-1],mx[0][i]=sum[i];
}build();
sort(p+1,p+n+1,[](const int &x,const int &y){return a[x]<a[y];});
for(int i=1;i<=n;++i){
int id=p[i];
if(a[id]<query(pre[id]+1,id,nxt[id]-1))return printf("NO\n"),void();
pre[nxt[id]]=pre[id];nxt[pre[id]]=nxt[id];
}printf("YES\n");
}
signed main(){
for(int i=2;i<N;++i)lg[i]=lg[i>>1]+1;
int t;read(t);
while(t--)solve();
return 0;
}
CF1659D Reverse Sort Sum
-
操作 得到的序列就是将 前 个数排序后的序列。 表示 次操作每列的和。
-
给出 求 ,保证有解。
-
一个显然的结论就是 里面每个 对 都贡献了 次,于是 的个数就是 。由于每次操作都是和前缀有关,所以考虑从后往前推。
注意到,若序列存在 ,则 要么是 要么是 ,这分别对应 和 。那我们可不可以用类似的结论推出 呢?当然是可以的。
若 ,则它一定会在前 次操作内给 贡献 次。设当前在位置 ,剩余 个 没填。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】