NOIP2021 sol
20231201-20231221
NOIP2021 sol
A. [NOIP2021] 报数
设
表示 的十进制表示中是否含有数字 ,若含有则 ,否则 。则一个正整数 不能被报出,当且仅当存在正整数 和 ,使得 且 。 现在小 r 的上一个数报出了
,小 z 想快速算出他下一个数要报多少,不过他很快就发现这个游戏可比原版的游戏难算多了,于是他需要你的帮助。当然,如果小 r 报出的 x 本身是不能报出的,你也要快速反应过来小 r 输了才行。 由于小 r 和小 z 玩了很长时间游戏,你也需要回答小 z 的很多个问题。
, 。
看完题愣了一下,一直一维直接筛过不了。
但事实证明跑得飞快,这种情况一定要去试一下,
于是直接类似于埃氏筛法去筛就可以了。
#include <bits/stdc++.h>
using namespace std;
const int N=1e7+5;
int n,cnt=0,a[N];
bool vis[N];
bool chk(int x){
if(vis[x]) return true;
int nw=0;
while(x){
nw=x%10,x/=10;
if(nw==7) return true;
}
return false;
}
void init(){
for(int i=1;i<N;i++){
if(chk(i)) vis[i]|=1;
if(!vis[i]) a[++cnt]=i;
else for(int j=2;i*j<N;j++) vis[i*j]=true;
}
}
int main(){
/*2023.12.20 H_W_Y P7960 [NOIP2021] 报数 模拟*/
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
init();
int _;cin>>_;
while(_--){
int x;cin>>x;
if(vis[x]) cout<<"-1\n";
else cout<<a[(upper_bound(a+1,a+cnt+1,x)-a)]<<'\n';
}
return 0;
}
B. [NOIP2021] 数列
给定整数
,和一个长度为 的正整数数组 。 对于一个长度为
,下标从 开始且每个元素均不超过 的非负整数序列 ,我们定义它的权值为 。 当这样的序列
满足整数 的二进制表示中 的个数不超过 时,我们认为 是一个合法序列。 计算所有合法序列
的权值和对 取模的结果。
, , 。
首先容易想到我们按照
但是可能因为前几天做过一道类似的题目,它用的算组合数的方法在这道题上面不能用,我卡了挺久。。。
而对于这种二进制进位问题,很容易想到常见的套路,维护一下进位数和低位
于是我们就可以很容易得到一个 dp,设
这样只要我们枚举一下当前的
那么
于是就做完了,算答案就枚举一下合法的
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll mod=998244353;
const int N=35,M=105;
int n,m,K;
ll c[N][N],a[M][N],f[M][N][N][N],ans=0;
void add(ll &x,ll y){x=(x+y)%mod;}
int chk(int x){
int res=0;
while(x) res+=x%2,x/=2;
return res;
}
int main(){
/*2023.12.20 H_W_Y P7961 [NOIP2021] 数列 dp*/
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n>>m>>K;
for(int i=0;i<=m;i++){
cin>>a[i][1];a[i][0]=1ll;
for(int j=2;j<=n;j++) a[i][j]=a[i][j-1]*a[i][1]%mod;
}
for(int i=0;i<=n;i++){
c[i][0]=1ll;
for(int j=1;j<=i;j++) c[i][j]=(c[i-1][j]+c[i-1][j-1])%mod;
}
f[0][0][0][0]=1ll;
for(int u=0;u<=m;u++)
for(int i=0;i<=n;i++)
for(int j=0;j<=i;j++)
for(int k=0;k<=n;k++)
for(int x=0;i+x<=n;x++)
add(f[u+1][i+x][(j+x)/2][k+(j+x)%2],f[u][i][j][k]*c[n-i][x]%mod*a[u][x]%mod);
for(int j=0;j<=n;j++)
for(int k=0;k<=n;k++)
if(chk(j)+k<=K) add(ans,f[m+1][n][j][k]);
cout<<ans<<'\n';
return 0;
}
C. [NOIP2021] 方差
给定长度为
的非严格递增正整数数列 。每次可以进行的操作是:任意选择一个正整数 ,将 变为 。求在若干次操作之后,该数列的方差最小值是多少。请输出最小值乘以 的结果。 其中方差的定义为:数列中每个数与平均值的差的平方的平均值。更形式化地说,方差的定义为
,其中 。
, 。
首先答案可以转化成
然后在考虑模拟一下每一次的操作,
发现这其实就是差分数组的交换位置,那么问题就变成了如何排列这个差分数组使得答案最小。(由于求的是方差,首先
通过打表可以发现(或者推式子),取得最小值时的
感觉还是比较好理解,感性理解也很显然。
那么问题现在就变成了我们如何确定一个数实在这个谷的左边还是右边?
于是考虑 dp,这种分成了两部分求最小值的东西我们可以考虑把它们其中一个变成状态,另外一个求最小/大值。
这样就是设
发现这样的 dp 是极其好转移的,我们就只需要讨论是放在左边还是右边:
- 放左边,
,因为 对后面谷中的 个都有贡献。 - 放右边,
,其中 。
于是直接做就可以了,还可以用滚动数组优化一下,从而做到空间
这样的复杂度是
但是当
于是我们对于
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll inf=1e18;
const int N=1e4+5,M=5e5+5;
int a[N],d[N],s[N],n,mx;
ll f[M],ans;
void chkmn(ll &x,ll y){if(y<x) x=y;}
int main(){
/*2023.12.21 H_W_Y P7962 [NOIP2021] 方差 dp*/
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n>>a[1];mx=a[1];
for(int i=2;i<=n;i++) cin>>a[i],d[i-1]=a[i]-a[i-1],mx=max(mx,a[i]);
for(int i=0;i<=n*mx;i++) f[i]=inf;
f[0]=s[0]=mx=0;
sort(d+1,d+n);
for(int i=1;i<=n;i++){
s[i]=s[i-1]+d[i];
if(d[i]==0) continue;
for(int x=mx;x>=0;x--){
if(f[x]==inf) continue;
chkmn(f[x+i*d[i]],f[x]+1ll*i*d[i]*d[i]+2ll*x*d[i]);
chkmn(f[x+s[i]],f[x]+1ll*s[i]*s[i]);
mx=max(mx,max(x+i*d[i],x+s[i]));
f[x]=inf;
}
}
ans=inf;
for(int x=0;x<=mx;x++) if(f[x]<inf) chkmn(ans,1ll*n*f[x]-1ll*x*x);
cout<<ans<<'\n';
return 0;
}
D. [NOIP2021] 棋局
前三题的代码为什么那么短?因为第四题代码长啊。(CCF 还怪会安排的呢)
给定
的棋盘,棋盘上有 种类型的边,分别代表不可通行、只能走一步、只能一直沿一个方向往前走和可以任意走。棋子有两种颜色和等级,棋子间可以吃子,规定只能吃颜色不同且等级不高于自己的棋子,且吃完子后不能继续向前走。同时规定每次走子时经过的边类型必须相同。初始棋盘是空的,有 次操作,每次往棋盘上放一个棋子,问这个棋子能走到多少个格子。
, , , , , , , 。
题目读起来就挺复杂的,但属实有点意思。
首先直接写一个 bfs 会得到
我只写了
#include <bits/stdc++.h>
using namespace std;
#define pii pair<int,int>
#define fi first
#define se second
int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
while(isdigit(ch)){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
return x*f;
}
void prt(int x){
int p[15],tmp=0;
if(x==0) putchar('0');
if(x<0) putchar('-'),x=-x;
while(x) p[++tmp]=x%10,x/=10;
for(int i=tmp;i>=1;i--) putchar(p[i]+'0');
putchar('\n');
}
const int N=5e5+5,fx[4]={-1,0,1,0},fy[4]={0,1,0,-1};
int n,m,q,d[N][4],ans=0;
struct node{int col,lv;}v[N],nw;
bool vis[N],b[N];
int id(int i,int j){return (i-1)*m+j;}
bool chk(node i,node j){
if(j.col==-1) return true;
return i.col!=j.col&&i.lv>=j.lv;
}
int nxt(int x,int y,int i){
if(i==0) return id(x-1,y);
if(i==1) return id(x,y+1);
if(i==2) return id(x+1,y);
return id(x,y-1);
}
void init(){
memset(d,0,sizeof(d));
for(int i=1;i<=n*m;i++) v[i]={-1,0};
}
queue<pii> Q;
void bfs(int x,int y){
Q.push({x,y});b[id(x,y)]=true;
while(!Q.empty()){
x=Q.front().fi,y=Q.front().se;Q.pop();
int num=id(x,y);
if(!vis[num]) vis[num]=true,++ans;
if(v[num].col!=-1) continue;
for(int i=0;i<4;i++){
if(d[num][i]!=3||b[nxt(x,y,i)]||!chk(nw,v[nxt(x,y,i)])) continue;
Q.push({x+fx[i],y+fy[i]});b[nxt(x,y,i)]=true;
}
}
}
int main(){
int _=read();
while(_--){
n=read();m=read();q=read();
init();
for(int i=1;i<=n;i++)
for(int j=1;j<m;j++){
char ch=getchar();
while(!isdigit(ch)) ch=getchar();
d[id(i,j)][1]=d[id(i,j+1)][3]=(int)(ch-'0');
}
for(int i=1;i<n;i++)
for(int j=1;j<=m;j++){
char ch=getchar();
while(!isdigit(ch)) ch=getchar();
d[id(i,j)][2]=d[id(i+1,j)][0]=(int)(ch-'0');
}
while(q--){
nw.col=read(),nw.lv=read();
int x=read(),y=read();ans=0;
if(n*m<=5000){for(int i=0;i<=n*m;i++) vis[i]=b[i]=false;}
int num=id(x,y);vis[num]=true;
for(int i=0;i<4;i++){
if(!d[num][i]||!chk(nw,v[nxt(x,y,i)])) continue;
if(d[num][i]==1) ++ans,vis[nxt(x,y,i)]=true;
if(d[num][i]==2){
int xx=x,yy=y;
while(d[id(xx,yy)][i]==2&&chk(nw,v[nxt(xx,yy,i)])){
xx+=fx[i];yy+=fy[i];
vis[id(xx,yy)]=true;++ans;
if(v[id(xx,yy)].col!=-1) break;
}
}
}
bfs(x,y);v[num]=nw;
prt(ans);
}
}
return 0;
}
现在再来考虑正解。
发现
但是依次加点取分裂它很明显不可做。
这个时候就用到了一个挺套路的东西,离线下来倒着删点。
于是这样其实就是只用维护合并了,就变得没有什么思维难度了。
具体来说,每一个块往外走到有棋子的位置的权值是肯定要维护的,
容易发现合并的时候有可能有重复,所以我们需要先提前处理一下,即让每一个棋子的
我们用两棵线段树分别维护每一个连动块为
这样直接做其实就是上面所说的
现在问题就在于
容易发现,我们这些点都是连续的,用两个并查集分别维护
于是我们只需要减去
做法就呼之欲出,我们维护两棵线段树分别记录一个连动块内部的
那么上面就变成了一个线段树上面的区间查询。
而对于
这样一共会用到
细节不多,就是有点长,写的时候一定要专注,以免犯下那种小错误一直调不出来。
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define pii pair<int,int>
#define fi first
#define se second
int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
while(isdigit(ch)){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
return x*f;
}
void prt(int x){
int P[15],tmp=0;
if(x==0) putchar('0');
if(x<0) putchar('-'),x=-x;
while(x) P[++tmp]=x%10,x/=10;
for(int i=tmp;i>=1;i--) putchar(P[i]+'0');
putchar('\n');
}
const int N=2e5+5,fx[4]={-1,0,1,0},fy[4]={0,1,0,-1};
int n,m,q,d[N][4],val[N],ans[N];
struct node{int col,lv,x,y,id;}a[N];
bool cmp1(node x,node y){return (x.lv==y.lv)?x.id<y.id:x.lv<y.lv;}
bool cmp2(node x,node y){return x.id<y.id;}
int id(int x,int y){return (x-1)*m+y;}
int id2(int x,int y){return (y-1)*n+x;}
pii rev(int x){return {(x-1)/m+1,(x-1)%m+1};}
struct SGT{
struct sgt{int lc,rc,v;}tr[N*40];
int rt[N],idx=0;
#define lc(p) tr[p].lc
#define rc(p) tr[p].rc
#define mid ((l+r)>>1)
void init(){
memset(rt,0,sizeof(rt));idx=0;
for(int i=0;i<N*40;i++) tr[i].lc=tr[i].rc=tr[i].v=0;
}
void pu(int p){tr[p].v=tr[lc(p)].v+tr[rc(p)].v;}
int upd(int l,int r,int p,int x,int fl=1){
if(!p) p=++idx;
if(l==r){
if(fl==-1) return 0;
if(!tr[p].v) ++tr[p].v;
return p;
}
if(x<=mid) tr[p].lc=upd(l,mid,lc(p),x,fl);
else tr[p].rc=upd(mid+1,r,rc(p),x,fl);
pu(p);return (!tr[p].v)?0:p;
}
int merge(int p1,int p2,int l,int r){
if(!p1||!p2) return p1+p2;
if(l==r) return tr[p1].v=min(tr[p1].v+tr[p2].v,1),p1;
tr[p1].lc=merge(lc(p1),lc(p2),l,mid);
tr[p1].rc=merge(rc(p1),rc(p2),mid+1,r);
pu(p1);return p1;
}
int qle(int l,int r,int p,int x){//query_le
if(!p||!x) return 0;
if(l==r) return tr[p].v;
return (x<=mid)?qle(l,mid,lc(p),x):(tr[lc(p)].v+qle(mid+1,r,rc(p),x));
}
int qin(int l,int r,int p,int x){//query_in?
if(!p||!x) return 0;
if(l==r) return tr[p].v;
return (x<=mid)?qin(l,mid,lc(p),x):qin(mid+1,r,rc(p),x);
}
int qlr(int p,int l,int r){return qle(1,n*m,rt[p],r)-qle(1,n*m,rt[p],l-1);}
}T[4];
//T[0/1] 有关颜色,T[2] 是列,T[3] 是行
void mg(int u,int v){
for(int i=0;i<2;i++) T[i].rt[u]=T[i].merge(T[i].rt[u],T[i].rt[v],1,q);
for(int i=2;i<4;i++) T[i].rt[u]=T[i].merge(T[i].rt[u],T[i].rt[v],1,n*m);
}
struct DSU{
int fa[N],sz[N],mx[N],mn[N];
int find(int x){return (fa[x]==x)?x:fa[x]=find(fa[x]);}
void merge(int u,int v,int fl=0){
u=find(u);v=find(v);
if(u==v) return;
if(sz[u]<=sz[v]) swap(u,v);
fa[v]=u;sz[u]+=sz[v];
mx[u]=max(mx[u],mx[v]);
mn[u]=min(mn[u],mn[v]);
if(fl) mg(u,v);
}
void init(int lim){for(int i=1;i<=lim;i++) fa[i]=mx[i]=mn[i]=i,sz[i]=1;}
int gmx(int x){return mx[find(x)];}
int gmn(int x){return mn[find(x)];}
int gsz(int x){return sz[find(x)];}
}t[3];
//t[0] 是 3 的,t[1] 是列,t[2] 是行
bool chk(int x,int y){return y&&(x>y)&&a[x].col!=a[y].col&&a[x].lv>=a[y].lv;}
void init(){
for(int i=0;i<4;++i) T[i].init();
for(int i=0;i<3;++i) t[i].init(n*m);
memset(d,0,sizeof(d));
memset(val,0,sizeof(val));
memset(ans,0,sizeof(ans));
}
int main(){
/*2023.12.21 H_W_Y P7963 [NOIP2021] 棋局 线段树合并+ds*/
int _=read();
while(_--){
n=read();m=read();q=read();
init();
for(int i=1;i<=n;i++)
for(int j=1;j<m;j++){
char ch=getchar();
while(!isdigit(ch)) ch=getchar();
d[id(i,j)][1]=d[id(i,j+1)][3]=(int)(ch-'0');
}
for(int i=1;i<n;i++)
for(int j=1;j<=m;j++){
char ch=getchar();
while(!isdigit(ch)) ch=getchar();
d[id(i,j)][2]=d[id(i+1,j)][0]=(int)(ch-'0');
}
for(int i=1;i<=q;i++) a[i].col=read(),a[i].lv=read(),a[i].x=read(),a[i].y=read(),a[i].id=i;
sort(a+1,a+q+1,cmp1);
for(int i=1;i<=q;i++) a[i].lv=i;
sort(a+1,a+q+1,cmp2);
for(int i=1;i<=q;i++) val[id(a[i].x,a[i].y)]=i;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++){
int nw=id(i,j);
for(int k=0;k<4;k++){
if(d[nw][k]<=1) continue;
int nxt=id(i+fx[k],j+fy[k]);
if(!val[nw]&&!val[nxt]) t[(d[nw][k]==3)?0:(k%2+1)].merge(nw,nxt);
}
}
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++){
int nw=id(i,j),fa=t[0].find(nw);
T[2].rt[fa]=T[2].upd(1,n*m,T[2].rt[fa],id2(i,j));
T[3].rt[fa]=T[3].upd(1,n*m,T[3].rt[fa],nw);
for(int k=0;k<4;k++) if(d[nw][k]==3){
int nxt=id(i+fx[k],j+fy[k]);
if(val[nxt]){
int col=a[val[nxt]].col;
T[col].rt[fa]=T[col].upd(1,q,T[col].rt[fa],a[val[nxt]].lv);
}
}
}
for(int i=q;i>=1;i--){
int num=id(a[i].x,a[i].y),col=a[i].col;
for(int j=0;j<4;j++) if(d[num][j]==3){//删除
int nxt=id(a[i].x+fx[j],a[i].y+fy[j]),fa=t[0].find(nxt);
T[col].rt[fa]=T[col].upd(1,q,T[col].rt[fa],a[i].lv,-1);
}
for(int j=0;j<4;j++) if(d[num][j]==3){//合并3
int nxt=id(a[i].x+fx[j],a[i].y+fy[j]);
if(val[nxt]&&val[nxt]<i) continue;
t[0].merge(num,nxt,1);
}
int idfa=t[0].find(num);
ans[i]=t[0].gsz(num)+T[col^1].qle(1,q,T[col^1].rt[idfa],a[i].lv);
for(int j=0;j<4;j++) if(d[num][j]==2){
int nxt=id(a[i].x+fx[j],a[i].y+fy[j]);
if(val[nxt]&&val[nxt]<i) continue;
t[j%2+1].merge(num,nxt);
}
int mx1=t[1].gmx(num),mx2=t[2].gmx(num),mn1=t[1].gmn(num),mn2=t[2].gmn(num);
ans[i]+=mx2-mn2+1+(mx1-mn1)/m+1;
int mx=id2(rev(mx1).fi,rev(mx1).se),mn=id2(rev(mn1).fi,rev(mn1).se);
ans[i]-=T[2].qlr(idfa,mn,mx)+T[3].qlr(idfa,mn2,mx2);
if(d[mn1][0]==2&&chk(i,val[mn1-m])&&!T[col^1].qin(1,q,T[col^1].rt[idfa],a[val[mn1-m]].lv)) ++ans[i];
if(d[mx2][1]==2&&chk(i,val[mx2+1])&&!T[col^1].qin(1,q,T[col^1].rt[idfa],a[val[mx2+1]].lv)) ++ans[i];
if(d[mx1][2]==2&&chk(i,val[mx1+m])&&!T[col^1].qin(1,q,T[col^1].rt[idfa],a[val[mx1+m]].lv)) ++ans[i];
if(d[mn2][3]==2&&chk(i,val[mn2-1])&&!T[col^1].qin(1,q,T[col^1].rt[idfa],a[val[mn2-1]].lv)) ++ans[i];
for(int j=0;j<4;j++) if(d[num][j]==1){
int nxt=id(a[i].x+fx[j],a[i].y+fy[j]);
if(val[nxt]&&val[nxt]<i){
if(chk(i,val[nxt])&&!T[col^1].qin(1,q,T[col^1].rt[idfa],a[val[nxt]].lv)) ++ans[i];
}else if(t[0].find(nxt)!=t[0].find(num)) ++ans[i];
}
--ans[i];
}
for(int i=1;i<=q;i++) prt(ans[i]);
}
return 0;
}
Conclusion
- 做题时一定要 手玩样例,遇到取最优情况的时候可以考虑 打表 找规律。(C. [NOIP2021] 方差)
- 求两个值相减的最值时,我们可以将一个值设成 dp 状态,从而维护另外一个值的最值。(C. [NOIP2021] 方差)
本文作者:H_W_Y
本文链接:https://www.cnblogs.com/H-W-Y/p/noip2021.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步