NOIP2022 sol
20231215
NOIP2022 sol + 4道杂题
A. [NOIP2022] 种花
小 C 决定在他的花园里种出
字样的图案,因此他想知道 和 两个字母各自有多少种种花的方案;不幸的是,花园中有一些土坑,这些位置无法种花,因此他希望你能帮助他解决这个问题。 花园可以看作有
个位置的网格图,从上到下分别为第 到第 行,从左到右分别为第 列到第 列,其中每个位置有可能是土坑,也有可能不是,可以用 表示第 行第 列这个位置有土坑,否则用 表示这个位置没土坑。 一种种花方案被称为
形的,如果存在 ,以及 ,满足 ,并且 ,使得第 行的第 到第 列、第 行的第 到第 列以及第 列的第 到第 行都不为土坑,且只在上述这些位置上种花。 一种种花方案被称为
形的,如果存在 ,以及 ,满足 ,并且 ,使得第 行的第 到第 列、第 行的第 到第 列以及第 列的第 到第 行都不为土坑,且只在上述这些位置上种花。 样例一解释中给出了
形和 形种花方案的图案示例。 现在小 C 想知道,给定
以及表示每个位置是否为土坑的值 , 形和 形种花方案分别有多少种可能?由于答案可能非常之大,你只需要输出其对 取模的结果即可,具体输出结果请看输出格式部分。
, , , 。
直接模拟即可。
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N=1e3+5;
const ll mod=998244353;
int nw,n,m,c,f,s[N][N],sc[N],sf[N],T,id;
ll ansc=0,ansf=0;
char a[N][N];
int main(){
/*2023.12.15 H_W_Y P8865 [NOIP2022] 种花 乱搞*/
scanf("%d%d",&T,&id);
while(T--){
scanf("%d%d%d%d",&n,&m,&c,&f);
memset(s,0,sizeof(s));
memset(sc,0,sizeof(sc));
memset(sf,0,sizeof(sf));
ansc=ansf=0;
for(int i=1;i<=n;i++){
scanf("%s",a[i]+1);
for(int j=m;j>=1;j--){
if(a[i][j]=='0') s[i][j]=s[i][j+1]+1;
else s[i][j]=0;
}
}
for(int j=1;j<=m;nw=0,j++){
for(int i=n;i>=1;i--){
if(a[i][j]=='0') sc[i]=sc[i+1]+s[i][j]-1,sf[i]=sf[i+1]+(s[i][j]-1)*nw,++nw;
else nw=0,sc[i]=sf[i]=0;
}
for(int i=1;i+2<=n;i++){
if(a[i][j]=='1'||a[i+1][j]=='1') continue;
ansc=(ansc+1ll*(s[i][j]-1)*sc[i+2]%mod)%mod;
ansf=(ansf+1ll*(s[i][j]-1)*sf[i+2]%mod)%mod;
}
}
ansc*=c;ansf*=f;
printf("%lld %lld\n",ansc,ansf);
}
return 0;
}
B. [NOIP2022] 喵了个喵
小 E 喜欢上了一款叫做《喵了个喵》的游戏。这个游戏有一个牌堆和
个可以从栈底删除元素的栈,任务是要通过游戏规则将所有的卡牌消去。开始时牌堆中有 张卡牌,从上到下的图案分别是 。所有的卡牌一共有 种图案,从 到 编号。牌堆中每一种图案的卡牌都有偶数张。开始时所有的栈都是空的。这个游戏有两种操作:
- 选择一个栈,将牌堆顶上的卡牌放入栈的顶部。如果这么操作后,这个栈最上方的两张牌有相同的图案,则会自动将这两张牌消去。
- 选择两个不同的栈,如果这两个栈栈底的卡牌有相同的图案,则可以将这两张牌消去,原来在栈底上方的卡牌会成为新的栈底。如果不同,则什么也不会做。
这个游戏一共有
关,小 E 一直无法通关。请你帮小 E 设计一下游戏方案,即对于游戏的每一关,给出相应的操作序列使得小 E 可以把所有的卡牌消去。
测试点 无限制 无限制 无限制 无限制
这个表挺有意思的啊。好一道构造题。
首先,容易发现我们的
因为小了太简单了,而大了根本做不了,
所以我们不妨从
发现这种情况是简单的,也就是我们将前
上面一个下面一个,容易证明,每一种只有一个。
那么这样我们会多出来一个栈,我们把它叫做 特殊栈,
发现有了这个栈之后就很好处理了。
假设当前进来一个
如果它之前没有出现过,那么直接放到一个有空位的栈里面去就可以了。
而如果它在一个栈的上面,我们直接放上去抵消掉即可,
反之我们就把它放在 特殊栈 里面,利用特殊栈把它消掉。
这样问题就可以轻松解决。
现在再来考虑多一个的情况,
那么它也一定是把所有的栈都满了的时候才会出现。
假设现在已经放满了,当前又进来了一个新的种类
首先需要找到下一个栈底元素进来的位置,因为这个时候消掉那个栈底元素会用到特殊栈,
我们假设这个栈是
于是有以下几种情况:
- 下一个
在下一个 之前,那么这个特殊栈随便用,之间把 放到特殊栈里面去。 ,即下一个 最先出现(这里的 是下一个的出现时间),那么我们直接把 叠在 上面,因为 很快就会走了。 ,那么我们不能把 放到 上面,因为 很快就要走了,而在 出现之前我们又不会用到特殊栈,所以我们直接把 放到特殊栈里面。于是这个时候就出现了一个 换栈 的过程,即全局的特殊栈现在变成了 所在的栈,容易发现这样不会错。
这样我们就分析完了,具体实现的时候也就是直接模拟,
找下一个出现位置也可以直接找,因为我们一定不会重复找一段区间。
我们直接维护一下特殊栈是哪个栈,那些栈有空位即可。
#include <bits/stdc++.h>
using namespace std;
#define pb push_back
const int M=2e6+5;
int n,m,k,a[M],h[605],pos,T;
vector<deque<int> > s(305);
struct node{int op,x,y;};
vector<node> ans;
queue<int> q;
void init(){
for(int i=1;i<=k;i++) h[i]=0;
while(!q.empty()) q.pop();
ans.clear();pos=0;
}
void oper1(int x,int v){
ans.pb((node){1,x,0});
if(s[x].size()&&s[x].back()==v){
s[x].pop_back();
if(x!=pos&&s[x].size()<2) q.push(x);
h[v]=0;
}else s[x].pb(v),h[v]=x;
}
void oper2(int x,int y){
ans.pb((node){2,x,y});
h[s[x][0]]=0;
s[x].pop_front();s[y].pop_front();
if(x!=pos&&s[x].size()<2) q.push(x);
}
int main(){
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
/*2023.12.15 H_W_Y P8866 [NOIP2022] 喵了个喵 构造*/
cin>>T;
while(T--){
cin>>n>>m>>k;
init();
for(int i=1;i<=m;i++) cin>>a[i];
pos=n;
for(int i=1;i<n;i++) q.push(i),q.push(i);
for(int i=1;i<=m;i++){
int x=h[a[i]];
if(x){
if(s[x].back()==a[i]) oper1(x,a[i]);
else oper1(pos,a[i]),oper2(x,pos);
continue;
}
if(!q.empty()){
x=q.front();q.pop();
oper1(x,a[i]);
continue;
}
for(int j=i+1;j<=m;j++){
if(a[j]==a[i]) break;
else if(h[a[j]]&&s[h[a[j]]][0]==a[j]){x=h[a[j]];break;}
}
if(x==0) oper1(pos,a[i]);
else{
int fi=s[x].front(),se=s[x].back();
for(int j=i+1;j<=m;j++){
if(a[j]==fi){oper1(x,a[i]);break;}
else if(a[j]==se){
oper1(pos,a[i]);q.push(pos);
pos=x;break;
}
}
}
}
cout<<ans.size()<<'\n';
for(auto i:ans){
if(i.op==1) cout<<1<<" "<<i.x<<'\n';
else cout<<2<<' '<<i.x<<' '<<i.y<<'\n';
}
}
return 0;
}
C. [NOIP2022] 建造军营
A 国与 B 国正在激烈交战中,A 国打算在自己的国土上建造一些军营。
A 国的国土由
座城市组成, 条双向道路连接这些城市,使得任意两座城市均可通过道路直接或间接到达。A 国打算选择一座或多座城市(至少一座),并在这些城市上各建造一座军营。 众所周知,军营之间的联络是十分重要的。然而此时 A 国接到情报,B 国将会于不久后袭击 A 国的一条道路,但具体的袭击目标却无从得知。如果 B 国袭击成功,这条道路将被切断,可能会造成 A 国某两个军营无法互相到达,这是 A 国极力避免的。因此 A 国决定派兵看守若干条道路(可以是一条或多条,也可以一条也不看守),A 国有信心保证被派兵看守的道路能够抵御 B 国的袭击而不被切断。
A 国希望制定一个建造军营和看守道路的方案,使得 B 国袭击的无论是 A 国的哪条道路,都不会造成某两座军营无法互相到达。现在,请你帮 A 国计算一下可能的建造军营和看守道路的方案数共有多少。由于方案数可能会很多,你只需要输出其对
取模的值即可。两个方案被认为是不同的,当且仅当存在至少一 座城市在一个方案中建造了军营而在另一个方案中没有,或者存在至少一条道路在一个 方案中被派兵看守而在另一个方案中没有。
, , , 。
首先,很容易发现,这个东西就是让你先缩点,留下来桥的。
于是我们就得到了一棵树,
这些树边的选择是有要求的,而每一个边双里面的边是可选可不选的,也就是有
而这里的
剩下来的这个计数问题就变成了一个树形 dp 了,
发现选不选桥其实只是和你选没选这个子树中的节点有关,于是我们记
记录答案的时候就是你选的这些点的 LCA 是
而这个转移是平凡的,
直接枚举一下
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define pb push_back
#define pii pair<int,int>
#define fi first
#define se second
const ll mod=1e9+7;
const int N=1e6+5;
int n,m,sz[N],a[N],bl[N],scc=0,st[N<<2],tp=0,dfn[N],low[N],idx=0;
ll f[N],t[N],ans=0;
vector<int> G[N];
vector<pii> g[N];
void tarjan(int u,int k){
dfn[u]=low[u]=++idx;st[++tp]=u;
for(auto i:g[u]){
int v=i.fi;
if(!dfn[v]) tarjan(v,i.se),low[u]=min(low[u],low[v]);
else if(i.se!=k) low[u]=min(low[u],dfn[v]);
}
if(low[u]==dfn[u]){
++scc;int x;
while((x=st[tp])){
bl[x]=scc;--tp;
if(x==u) break;
}
}
}
void init(){
t[0]=1ll;
for(int i=1;i<N;i++) t[i]=(t[i-1]+t[i-1])%mod;
}
void dfs(int u,int fa){
sz[u]=1;f[u]=t[a[u]];
for(auto v:G[u]){
if(v==fa) continue;
dfs(v,u);
sz[u]+=sz[v];
f[u]=f[u]*(t[sz[v]]+f[v])%mod;
}
f[u]=(f[u]-t[sz[u]-1]+mod)%mod;
ll s=f[u];
for(auto v:G[u]){
if(v==fa) continue;
s=(s-f[v]*t[sz[u]-sz[v]-1]%mod+mod)%mod;
}
ans=(ans+s*t[scc-sz[u]]%mod)%mod;
}
int main(){
/*2023.12.20 H_W_Y P8867 [NOIP2022] 建造军营 Tarjan + 树形 dp*/
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n>>m;init();
for(int i=1,u,v;i<=m;i++) cin>>u>>v,g[u].pb({v,i}),g[v].pb({u,i});
for(int i=1;i<=n;i++) if(!dfn[i]) tarjan(i,-1);
for(int u=1;u<=n;u++){
++a[bl[u]];
for(auto i:g[u]){
int v=i.fi;
if(bl[v]!=bl[u]) G[bl[u]].pb(bl[v]);
}
}
dfs(1,0);
ans=ans*t[m-scc+1]%mod;
cout<<ans<<'\n';
return 0;
}
D. [NOIP2022] 比赛
给定
,以及 个询问,每次询问给出 ,求
。
区间子区间问题典。
区间子区间问题的常规处理方法已经在 lxlds 中讲的很清楚了,
这道题的
其实题解部分已经讲得很清楚了
主要的思想就是先将同类的标记合并之后,我们对于交错的标记再进行一个讨论就可以了,
也就是分组,累加时分类讨论。具体看代码吧。
代码中我们钦定一个标记组是先历史值累加再赋值。
#include <bits/stdc++.h>
using namespace std;
#define ull unsigned long long
#define pii pair<int,int>
#define fi first
#define se second
#define pb push_back
const int N=1e6+5;
int n,m,a[N],b[N],st[N],tp=0,fa[N],fb[N];
ull ans[N];
vector<pii> g[N];
struct tag{ull cx,cy,xy,x,y,c;}tg[N<<2];
struct sgt{
ull s,xy,x,y;
sgt operator +(const sgt &t)const{return (sgt){s+t.s,xy+t.xy,x+t.x,y+t.y};}
}tr[N<<2];
#define mid ((l+r)>>1)
#define lson l,mid,p<<1
#define rson mid+1,r,p<<1|1
#define lc p<<1
#define rc p<<1|1
void pu(int p){tr[p]=tr[lc]+tr[rc];}
/*先历史值累计再更新*/
void change(int l,int r,int p,tag t){
ull &cx=tg[p].cx,&cy=tg[p].cy,&xy=tg[p].xy,&x=tg[p].x,&y=tg[p].y,&c=tg[p].c;
int len=r-l+1;
if(cx&&cy) c+=t.xy*cx*cy+t.x*cx+t.y*cy+t.c;
else if(cx) y+=t.y+cx*t.xy,c+=cx*t.x+t.c;
else if(cy) x+=t.x+cy*t.xy,c+=cy*t.y+t.c;
else x+=t.x,y+=t.y,xy+=t.xy,c+=t.c;
if(t.cx) cx=t.cx;
if(t.cy) cy=t.cy;
ull &s=tr[p].s,&sxy=tr[p].xy,&sx=tr[p].x,&sy=tr[p].y;
s+=t.xy*sxy+t.x*sx+t.y*sy+1ull*t.c*len;
if(t.cx&&t.cy) sxy=t.cx*t.cy*len,sx=t.cx*len,sy=t.cy*len;
else if(t.cx) sxy=t.cx*sy,sx=t.cx*len;
else if(t.cy) sxy=t.cy*sx,sy=t.cy*len;
}
void pd(int l,int r,int p){
if(tg[p].cx||tg[p].cy||tg[p].x||tg[p].xy||tg[p].y||tg[p].c)
change(lson,tg[p]),change(rson,tg[p]),tg[p]=(tag){0,0,0,0,0,0};
}
void upd(int l,int r,int p,int x,int y,tag t){
if(x<=l&&y>=r) return change(l,r,p,t);pd(l,r,p);
if(x<=mid) upd(lson,x,y,t);
if(y>mid) upd(rson,x,y,t);pu(p);
}
ull qry(int l,int r,int p,int x,int y){
if(x<=l&&y>=r) return tr[p].s;pd(l,r,p);
if(y<=mid) return qry(lson,x,y);
if(x>mid) return qry(rson,x,y);
return qry(lson,x,y)+qry(rson,x,y);
}
int main(){
/*2023.11.30 H_W_Y P8868 [NOIP2022] 比赛 SGT*/
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n;cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
while(tp>0&&a[st[tp]]<a[i]) tp--;
fa[i]=st[tp]+1;st[++tp]=i;
}tp=0;
for(int i=1;i<=n;i++){
cin>>b[i];
while(tp>0&&b[st[tp]]<b[i]) tp--;
fb[i]=st[tp]+1;st[++tp]=i;
}
cin>>m;
for(int i=1,l,r;i<=m;i++) cin>>l>>r,g[r].pb({l,i});
for(int r=1;r<=n;r++){
upd(1,n,1,fa[r],r,(tag){1ull*a[r],0,0,0,0,0});
upd(1,n,1,fb[r],r,(tag){0,1ull*b[r],0,0,0,0});
upd(1,n,1,1,r,(tag){0,0,1,0,0,0});
for(auto j:g[r]) ans[j.se]=qry(1,n,1,j.fi,r);
}
for(int i=1;i<=m;i++) cout<<ans[i]<<'\n';
return 0;
}
Conclusion
- c++ 20 之后就不能用
char s;cin>>s+1;
了。(A. [NOIP2022] 种花) - 树形 dp 的状态不只是可以维护子树中的状态,还可以记录
到根节点的状态。(G. 石老板告老还乡)
本文作者:H_W_Y
本文链接:https://www.cnblogs.com/H-W-Y/p/noip2022.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步