NOIP2022 sol

20231215

NOIP2022 sol + 4道杂题

A. [NOIP2022] 种花

[NOIP2022] 种花

小 C 决定在他的花园里种出 \(\texttt{CCF}\) 字样的图案,因此他想知道 \(\texttt C\)\(\texttt F\) 两个字母各自有多少种种花的方案;不幸的是,花园中有一些土坑,这些位置无法种花,因此他希望你能帮助他解决这个问题。

花园可以看作有 \(n\times m\) 个位置的网格图,从上到下分别为第 \(1\) 到第 \(n\) 行,从左到右分别为第 \(1\) 列到第 \(m\) 列,其中每个位置有可能是土坑,也有可能不是,可以用 \(a_{i,j} = 1\) 表示第 \(i\) 行第 \(j\) 列这个位置有土坑,否则用 \(a_{i,j} = 0\) 表示这个位置没土坑。

一种种花方案被称为 \(\texttt{C-}\) 的,如果存在 \(x_1, x_2 \in [1, n]\),以及 \(y_0, y_1, y_2 \in [1, m]\),满足 \(x_1 + 1 < x_2\),并且 \(y_0 < y_1, y_2 \leq m\),使得第 \(x_1\) 的第 \(y_0\) 到第 \(y_1\) 、第 \(x_2\) 的第 \(y_0\) 到第 \(y_2\) 以及第 \(y_0\) 的第 \(x_1\) 到第 \(x_2\) 不为土坑,且只在上述这些位置上种花。

一种种花方案被称为 \(\texttt{F-}\) 的,如果存在 \(x_1, x_2, x_3 \in [1, n]\),以及 \(y_0, y_1, y_2 \in [1, m]\),满足 \(x_1 + 1 < x_2 < x_3\),并且 \(y_0 < y_1, y_2 \leq m\),使得第 \(x_1\) 的第 \(y_0\) 到第 \(y_1\) 、第 \(x_2\) 的第 \(y_0\) 到第 \(y_2\) 以及第 \(y_0\) 的第 \(x_1\) 到第 \(x_3\) 不为土坑,且只在上述这些位置上种花。

样例一解释中给出了 \(\texttt{C-}\) 形和 \(\texttt{F-}\) 形种花方案的图案示例。

现在小 C 想知道,给定 \(n, m\) 以及表示每个位置是否为土坑的值 \(\{a_{i,j}\}\)\(\texttt{C-}\) 形和 \(\texttt{F-}\) 形种花方案分别有多少种可能?由于答案可能非常之大,你只需要输出其对 \(998244353\) 取模的结果即可,具体输出结果请看输出格式部分。

\(1 \leq T \leq 5\)\(1 \leq n, m \leq 10^3\)\(0 \leq c, f \leq 1\)\(a_{i,j} \in \{0, 1\}\)

直接模拟即可。

#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] 喵了个喵

[NOIP2022] 喵了个喵

小 E 喜欢上了一款叫做《喵了个喵》的游戏。这个游戏有一个牌堆和 \(n\) 个可以从栈底删除元素的栈,任务是要通过游戏规则将所有的卡牌消去。开始时牌堆中有 \(m\) 张卡牌,从上到下的图案分别是 \(a_1, a_2,\dots, a_m\)。所有的卡牌一共有 \(k\) 种图案,从 \(1\)\(k\) 编号。牌堆中每一种图案的卡牌都有偶数张。开始时所有的栈都是空的。这个游戏有两种操作:

  • 选择一个栈,将牌堆顶上的卡牌放入栈的顶部。如果这么操作后,这个栈最上方的两张牌有相同的图案,则会自动将这两张牌消去。
  • 选择两个不同的栈,如果这两个栈栈的卡牌有相同的图案,则可以将这两张牌消去,原来在栈底上方的卡牌会成为新的栈底。如果不同,则什么也不会做。

这个游戏一共有 \(T\) 关,小 E 一直无法通关。请你帮小 E 设计一下游戏方案,即对于游戏的每一关,给出相应的操作序列使得小 E 可以把所有的卡牌消去。

测试点 \(T=\) \(n\) \(k=\) \(m \leq\)
\(1\sim 3\) \(1001\) \(\leq 300\) \(2n-2\) 无限制
\(4\sim 6\) \(1002\) \(=2\) \(2n-1\) 无限制
\(7\sim 10\) \(3\) \(=3\) \(2n-1\) \(14\)
\(11\sim 14\) \(1004\) \(=3\) \(2n-1\) 无限制
\(15\sim 20\) \(1005\) \(\leq 300\) \(2n-1\) 无限制

这个表挺有意思的啊。好一道构造题。


首先,容易发现我们的 \(k\) 只有两种取值,为什么呢?

因为小了太简单了,而大了根本做不了,

所以我们不妨从 \(k=2n-2\) 这个最小的入手。


发现这种情况是简单的,也就是我们将前 \(n-1\) 个栈,一个栈放两个,

上面一个下面一个,容易证明,每一种只有一个。

那么这样我们会多出来一个栈,我们把它叫做 特殊栈

发现有了这个栈之后就很好处理了。

假设当前进来一个 \(i\) 种类的,

如果它之前没有出现过,那么直接放到一个有空位的栈里面去就可以了。

而如果它在一个栈的上面,我们直接放上去抵消掉即可,

反之我们就把它放在 特殊栈 里面,利用特殊栈把它消掉。

这样问题就可以轻松解决。


现在再来考虑多一个的情况,

那么它也一定是把所有的栈都满了的时候才会出现。

假设现在已经放满了,当前又进来了一个新的种类 \(w\),我们希望知道他能放在哪里?

首先需要找到下一个栈底元素进来的位置,因为这个时候消掉那个栈底元素会用到特殊栈,

我们假设这个栈是 \([v,u]\) 表示它上面是 \(v\),下面是 \(u\),那么这个 \(w\) 的位置只会影响到这一个栈,因为 \(u\) 下一个出现最早。

于是有以下几种情况:

  1. 下一个 \(w\) 在下一个 \(u\) 之前,那么这个特殊栈随便用,之间把 \(w\) 放到特殊栈里面去。
  2. \(u \lt v \lt w\),即下一个 \(u\) 最先出现(这里的 \(\lt\) 是下一个的出现时间),那么我们直接把 \(w\) 叠在 \(v\) 上面,因为 \(u\) 很快就会走了。
  3. \(v \lt u \lt w\),那么我们不能把 \(w\) 放到 \(v\) 上面,因为 \(v\) 很快就要走了,而在 \(u\) 出现之前我们又不会用到特殊栈,所以我们直接把 \(w\) 放到特殊栈里面。于是这个时候就出现了一个 换栈 的过程,即全局的特殊栈现在变成了 \([v,u]\) 所在的栈,容易发现这样不会错。

这样我们就分析完了,具体实现的时候也就是直接模拟,

找下一个出现位置也可以直接找,因为我们一定不会重复找一段区间。

我们直接维护一下特殊栈是哪个栈,那些栈有空位即可。

#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] 建造军营

[NOIP2022] 建造军营

A 国与 B 国正在激烈交战中,A 国打算在自己的国土上建造一些军营。

A 国的国土由 \(n\) 座城市组成,\(m\) 条双向道路连接这些城市,使得任意两座城市均可通过道路直接或间接到达。A 国打算选择一座或多座城市(至少一座),并在这些城市上各建造一座军营。

众所周知,军营之间的联络是十分重要的。然而此时 A 国接到情报,B 国将会于不久后袭击 A 国的一条道路,但具体的袭击目标却无从得知。如果 B 国袭击成功,这条道路将被切断,可能会造成 A 国某两个军营无法互相到达,这是 A 国极力避免的。因此 A 国决定派兵看守若干条道路(可以是一条或多条,也可以一条也不看守),A 国有信心保证被派兵看守的道路能够抵御 B 国的袭击而不被切断。

A 国希望制定一个建造军营和看守道路的方案,使得 B 国袭击的无论是 A 国的哪条道路,都不会造成某两座军营无法互相到达。现在,请你帮 A 国计算一下可能的建造军营和看守道路的方案数共有多少。由于方案数可能会很多,你只需要输出其对 \(1,000,000,007\left(10^{9}+7\right)\) 取模的值即可。两个方案被认为是不同的,当且仅当存在至少一 座城市在一个方案中建造了军营而在另一个方案中没有,或者存在至少一条道路在一个 方案中被派兵看守而在另一个方案中没有。

\(1 \leq n \leq 5 \times 10^5\)\(n - 1 \leq m \leq 10^6\)\(1 \leq u_i, v_i \leq n\)\(u_i \neq v_i\)

首先,很容易发现,这个东西就是让你先缩点,留下来桥的。


于是我们就得到了一棵树,

这些树边的选择是有要求的,而每一个边双里面的边是可选可不选的,也就是有 \(2^{a_u}\) 种情况,

而这里的 \(a_u\) 是一个边双的大小。


剩下来的这个计数问题就变成了一个树形 dp 了,

发现选不选桥其实只是和你选没选这个子树中的节点有关,于是我们记 \(f_u\) 表示选了 \(u\) 子树中的点的方案数,

记录答案的时候就是你选的这些点的 LCA 是 \(u\) 的情况对答案的贡献。

而这个转移是平凡的,

直接枚举一下 \(v\) 加上答案,而注意要排除一个都没选的情况,记录答案的时候要减去 LCA 不是 \(u\) 的贡献。

#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] 比赛

[NOIP2022] 比赛

给定 \(A_{1,2,\dots,n},B_{1,2,\dots,n}\),以及 \(m\) 个询问,每次询问给出 \(l,r\),求

\[\sum_{p=l}^r \sum_{q=p}^r (\max_{i=p}^q A_i)(\max_{i=p}^q B_i) \]

\(1 \le n,m \le 2.5 \times 10^5\)

区间子区间问题典。


区间子区间问题的常规处理方法已经在 lxlds 中讲的很清楚了,

这道题的 \(\max\) 我们同样可以用单调栈维护出来,分析一下去加信息累加时的标记。


其实题解部分已经讲得很清楚了

主要的思想就是先将同类的标记合并之后,我们对于交错的标记再进行一个讨论就可以了,

也就是分组,累加时分类讨论。具体看代码吧。


代码中我们钦定一个标记组是先历史值累加再赋值。

#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

  1. c++ 20 之后就不能用 char s;cin>>s+1; 了。(A. [NOIP2022] 种花)
  2. 树形 dp 的状态不只是可以维护子树中的状态,还可以记录 \(i\) 到根节点的状态。(G. 石老板告老还乡)
posted @ 2023-12-20 17:22  H_W_Y  阅读(36)  评论(0编辑  收藏  举报