NOIP 2022 题解

T1:种花

计数题,先考虑 dp,发现不好设计。

再想,C、F 一定是由竖着的线段和横着的线段组成,把它们找到然后计算即可,计算的式子很好推。

时间复杂度最坏 O ( n 4 ) O(n^4) O(n4),也许是数据水了,能拿 75   p t s 75\space pts 75 pts

考虑优化,发现这些横着的线段的长度可以前缀和 O ( 1 ) O(1) O(1) 计算,我们枚举每一列,按行从上到下计算即可。

时间复杂度 O ( T n m ) O(Tnm) O(Tnm)

#include<bits/stdc++.h>
using namespace std;
#define rd read()
#define ll long long
#define FOR(i,j,k) for(int i=j;i<=k;i++)
#define ROF(i,j,k) for(int i=j;i>=k;i--)
int read(){
	int x=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)) x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
	return x*f;
}
const int N=1010,mod=998244353;
int T,id,n,m,c,ff,a[N][N],f[N][N];
int main(){
	T=rd,id=rd;
	while(T--){
		memset(f,0,sizeof(f));
		n=rd,m=rd,c=rd,ff=rd;
		FOR(i,1,n) FOR(j,1,m) scanf("%1d",&a[i][j]);
		int C=0,F=0;
		FOR(i,1,n) ROF(j,m-1,1){
			if(a[i][j]) f[i][j]=-1;
			else if(a[i][j+1]==0) f[i][j]=f[i][j+1]+1;
		}
		FOR(j,1,m-1){
			int cntc=0,cntf=0;
			FOR(i,1,n){
				if(f[i][j]==-1){cntc=cntf=0;continue;}
				C=(1ll*C+1ll*cntc*f[i][j])%mod;
				F=(1ll*F+cntf)%mod;
				cntf=(cntf+1ll*f[i][j]*cntc%mod)%mod;
				cntc+=max(0,f[i-1][j]);
			}
		}
		printf("%d %d\n",c*C,ff*F);
	}
	return 0;
}

T2:P8866 [NOIP2022] 喵了个喵

k k k 只有 2 n − 1 , 2 n − 2 2n-1,2n-2 2n1,2n2 两种取值,如果 k = 2 n − 2 k=2n-2 k=2n2,即 2 ( n − 1 ) 2(n-1) 2(n1),那么我们可以拿出 n − 1 n-1 n1 个栈来放数,最后留一个栈(称其为特殊栈)来消除底部的数,容易证明这样肯定可以得到合法方案。

再看 k = 2 n − 1 k=2n-1 k=2n1,这样会出现 2 n − 2 2n-2 2n2 个数放完了,还有一个多出来的数,考虑这个数的归宿。

设多出来的数为 w w w,我们找到在它之后的最先出现在某个栈的底部的数 u u u,设 u u u 所在栈为 x x x,该栈栈顶为 v v v,进行分讨。

  • w w w u u u 先出现,那么我们将 w w w 放入特殊栈,因为之后用不到 2 2 2 操作。
  • 否则比较 u , v u,v u,v,若 u u u v v v 先出现,那么之后需要借用特殊栈消除 u u u,所以 w w w 放到 x x x 栈顶。
  • 否则就是 v < u < w v<u<w v<u<w,我们将 w w w 放入特殊栈,之后会出现 v , u v,u v,u 依次被消除的情况,此时 x x x 又空了,所以我们将 x x x 设为新的特殊栈。

本题实现细节较多,简单说一下:

  • 记录每个数所在栈。
  • 用队列来维护当前有哪些空栈,注意某个数出栈时,该栈有空余位置后要重新入队。
  • 1 , 2 1,2 1,2 操作分别写成函数,方便维护。

最后还有一个时间复杂度的问题,我们找 u , v u,v u,v 的出现顺序时,能否暴力往后找?答案是可以的,因为只有出现所有非特殊栈放满后,才会去找,而下一次再出现这种情况的位置,一定不会和之前重合,所以最多只会循环 m m m 次。

时间复杂度 O ( ∑ m ) O(\sum m) O(m)

#include<bits/stdc++.h>
using namespace std;
#define rd read()
#define FOR(i,j,k) for(int i=j;i<=k;i++)
#define ROF(i,j,k) for(int i=j;i>=k;i--)
#define debug cout<<"FUCKCCF!"<<endl;
int read(){
	int x=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)) x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
	return x*f;
}
const int N=2e6+10;
int T,n,m,k,a[N],cnt,spe,belong[N];
struct node{int op,x,y;}ans[N<<2];
vector<deque<int> > s(310);
queue<int> q;
void opera1(int x,int t){//1操作
	ans[++cnt]={1,x};
	if(s[x].size()>0&&s[x].back()==t){
		s[x].pop_back();
		if(x!=spe&&s[x].size()<2) q.push(x);
		belong[t]=0;
	}
	else{
		s[x].push_back(t);
		belong[t]=x;
	}
}
void opera2(int x,int y){//2操作
	ans[++cnt]={2,x,y};
	belong[s[x].front()]=0;
	s[x].pop_front(),s[y].pop_front();
	if(s[x].size()<2) q.push(x);
}
void init(){
	while(!q.empty()) q.pop();
	cnt=0;s.clear();s.resize(310);
	FOR(i,0,k) belong[i]=0;
}
int main(){
	T=rd;
	while(T--){
		init();n=rd,m=rd,k=rd;
		FOR(i,1,m) a[i]=rd;
		FOR(i,1,n-1) q.push(i),q.push(i);
		spe=n;
		FOR(i,1,m){
			int x=belong[a[i]];
			if(x){//之前出现过
				if(s[x].size()==1) opera1(x,a[i]);
				else if(s[x].back()==a[i]) opera1(x,a[i]);
				else opera1(spe,a[i]),opera2(x,spe);
			}
			else if(q.size()>0){//有空栈
				x=q.front();q.pop();
				opera1(x,a[i]);
			}
			else{
				for(int j=i+1;;j++){
					if(a[j]==a[i]) break;
					if(s[belong[a[j]]].front()==a[j]){x=belong[a[j]];break;}
				}
				if(!x) opera1(spe,a[i]);//w<u
				else{
					int u=s[x].front(),v=s[x].back();
					for(int j=i+1;;j++){
						if(a[j]==u){//w>v>u
							opera1(x,a[i]);
							break;
						}
						else if(a[j]==v){//w>u>v
							opera1(spe,a[i]),q.push(spe),spe=x;
							break;
						}
					}
				} 
			}
		}
		printf("%d\n",cnt);
		FOR(i,1,cnt){
			if(ans[i].op==1) printf("%d %d\n",ans[i].op,ans[i].x);
			else printf("%d %d %d\n",ans[i].op,ans[i].x,ans[i].y);
		}
	}
	return 0;
}

T3:P8867 [NOIP2022] 建造军营

不难发现只有割边才是关键的,所以先跑边双缩点,然后转化为树后考虑树形 dp。

f x , 0 / 1 f_{x,0/1} fx,0/1 表示 x x x 节点是否建造军营的方案数,发现很难转移,所含的状态太多了。

改变含义,令 f x , 0 / 1 f_{x,0/1} fx,0/1 表示以 x x x 为根的子树内是否有军营,若有,则都通过某些边与 x x x 连通,并且不选 x x x f a x fa_x fax 的连边的方案数。

为方便统计,强制令 x x x 子树外不建军营,则答案为 ∑ f x , 1 × x \sum f_{x,1}\times x fx,1×x 子树外边数,注意特判 x x x 为根节点情况。

边双缩完点后,设 V x V_x Vx 表示 x x x 代表边双内点数, E x E_x Ex 表示边数,初始化显然: f x , 1 = 2 V x , f x , 0 = 2 V x + E x − f x , 1 f_{x,1}=2^{V_x},f_{x,0}=2^{V_x+E_x}-f_{x,1} fx,1=2Vx,fx,0=2Vx+Exfx,1。设边 ( x , y ) (x,y) (x,y),转移时 f x , 0 f_{x,0} fx,0 较为显然,只能是 f y , 0 f_{y,0} fy,0,边可以选和不选。转移 f x , 1 f_{x,1} fx,1 时考虑之前没有建军营,在 y y y 才建,以及之前建过,此时 y y y 建不建都行。

#include<bits/stdc++.h>
using namespace std;
#define rd read()
#define ll long long
#define FOR(i,j,k) for(int i=j;i<=k;i++)
#define ROF(i,j,k) for(int i=j;i>=k;i--)
#define graph(t,edge,head) for(int i=head[t];i;i=edge[i].nxt)
int read(){
	int x=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)) x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
	return x*f;
}
const int N=5e5+10,mod=1e9+7;
int n,m,head[N],tot=1,he[N],tot2,f[N][2],V[N],E[N],ecnt[N],ans;
int dfn[N],low[N],tim,cnt,dcc[N],stk[N],top,bri[N<<2];
struct node{int from,to,nxt;}edge[N<<2],e[N<<2];
void add(int x,int y){edge[++tot]={x,y,head[x]},head[x]=tot;}
void ADD(int x,int y){e[++tot2]={x,y,he[x]},he[x]=tot2;}
int qpow(int a,int b){
	int res=1;
	while(b){if(b&1)res=1ll*res*a%mod;a=1ll*a*a%mod,b>>=1;}
	return res;
}
void tarjan(int x,int in_edge){
	dfn[x]=low[x]=++tim,stk[++top]=x;
	graph(x,edge,head){
		int y=edge[i].to;
		if(!dfn[y]){
			tarjan(y,i);
			low[x]=min(low[x],low[y]);
			if(low[y]>dfn[x]) bri[i]=bri[i^1]=1;
		}
		else if((in_edge^1)!=i) low[x]=min(low[x],dfn[y]);
	}
	if(dfn[x]==low[x]){
		int y;cnt++;do{
			y=stk[top--],dcc[y]=cnt,V[cnt]++;
		}while(x!=y);
	}
}
void dfs(int x,int fa){
	ecnt[x]=E[x];
	graph(x,e,he){
		int y=e[i].to;if(y==fa) continue;
		dfs(y,x);
		ecnt[x]+=ecnt[y]+1;
	}
}
void dp(int x,int fa){
	graph(x,e,he){
		int y=e[i].to;if(y==fa) continue;
		dp(y,x);
		f[x][1]=(1ll*f[x][0]*f[y][1]%mod+1ll*f[x][1]*(2ll*f[y][0]+f[y][1])%mod)%mod;
		f[x][0]=2ll*f[x][0]*f[y][0]%mod;
	}
	if(x==1) ans=(1ll*ans+f[x][1])%mod;
	else ans=(1ll*ans+1ll*f[x][1]*qpow(2,ecnt[1]-ecnt[x]-1))%mod;
}
int main(){
	n=rd,m=rd;
	FOR(i,1,m){int x=rd,y=rd;add(x,y),add(y,x);}
	FOR(i,1,n) if(!dfn[i]) tarjan(i,0);
	FOR(i,2,tot) if(bri[i]) ADD(dcc[edge[i].from],dcc[edge[i].to]);
	FOR(i,2,tot) if(!bri[i]) E[dcc[edge[i].from]]++;
	FOR(i,1,cnt){
		E[i]>>=1;
		f[i][0]=qpow(2,E[i]),f[i][1]=qpow(2,V[i]+E[i])-f[i][0];
	}
	dfs(1,0),dp(1,0);
	printf("%d\n",ans);
	return 0;
}

T4:比赛

暴力做是 8   p t s 8\space pts 8 pts 的,考虑优化。

把询问离线下来,扩展右端点,设右端点为 r r r x j = max ⁡ k = j r { a k } , y j = max ⁡ k = j r { b k } , f i = ∑ j = i r x j y j x_j=\max_{k=j}^{r}\{a_k\},y_j=\max_{k=j}^{r}\{b_k\},f_i=\sum_{j=i}^{r}x_jy_j xj=maxk=jr{ak},yj=maxk=jr{bk},fi=j=irxjyj,则对于右端点为 r r r 的询问,其答案为 ∑ i = l r f i \sum_{i=l}^{r}f_i i=lrfi,可以达到 20   p t s 20\space pts 20 pts

20   p t s 20\space pts 20 pts 代码见下:

#include<bits/stdc++.h>
using namespace std;
#define rd read()
#define PII pair<int,int>
#define pb push_back
#define mp make_pair
#define fi first
#define se second
#define ull unsigned long long
#define FOR(i,j,k) for(int i=j;i<=k;i++)
#define ROF(i,j,k) for(int i=j;i>=k;i--)
int read(){
	int x=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)) x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
	return x*f;
}
const int N=3e5+10;
int T,n,q,a[N],b[N],x[N],y[N];ull f[N],ans[N];
vector<PII> d[N];
int main(){
	T=rd,n=rd;
	FOR(i,1,n) a[i]=rd;FOR(i,1,n) b[i]=rd;
	q=rd;
	FOR(i,1,q){int l=rd,r=rd;d[r].pb(mp(l,i));}
	FOR(r,1,n){
		FOR(i,1,r) x[i]=max(x[i],a[r]),y[i]=max(y[i],b[r]),f[i]+=x[i]*y[i];
		for(auto i:d[r]){
			int l=i.fi,id=i.se;
			FOR(j,l,r) ans[id]+=f[j];
		}
	}
	FOR(i,1,q) printf("%llu\n",ans[i]);
	return 0;
}

继续优化求 f f f 的过程,很容易想到利用线段树。

线段树维护 f f f 值, x y xy xy 乘积之和, x x x 的和, y y y 的和。

先预处理出每个 a i , b i a_i,b_i ai,bi 可以影响到最远多远,即 i i i 左边第一个大于它的数之后,这是一段后缀,我们会进行区间赋值操作,然后将 [ 1 , r ] [1,r] [1,r] 内的所有 f i f_i fi 值加上 x i y i x_iy_i xiyi,最后区间求和即可,时间复杂度 O ( n l o g n ) O(nlogn) O(nlogn)

线段树查询,标记与信息的合并是好做的,关键是标记与标记的合并。我们首先会维护 6 6 6 个标记:

  • c x , c y , a d d x y , a d d x , a d d y , a d d c cx,cy,addxy,addx,addy,addc cx,cy,addxy,addx,addy,addc,表示区间赋值的标记,区间增量的标记。

规定区间增量的标记更新在赋值标记前。

对于只赋值 c x cx cx 的区间, c y , a d d y , a d d c cy,addy,addc cy,addy,addc 无影响, a d d x y ∑ x i y i addxy\sum x_iy_i addxyxiyi 会变为 a d d x y × c x ∑ y i addxy\times cx\sum y_i addxy×cxyi,所以 a d d x y × c x → a d d y addxy\times cx\to addy addxy×cxaddy

只赋值 c y cy cy 的区间同理。

对于 c x , c y cx,cy cx,cy 均不为 0 0 0 的区间, a d d x y ∑ x i y i = a d d x y × c x × c y × l e n addxy\sum x_iy_i=addxy\times cx\times cy\times len addxyxiyi=addxy×cx×cy×len
所以 a d d x y × c x × c y → a d d c addxy\times cx\times cy\to addc addxy×cx×cyaddc

然后合并即可,具体实现细节可见代码。

#include<bits/stdc++.h>
using namespace std;
#define rd read()
#define PII pair<int,int>
#define mp make_pair
#define ull unsigned long long
#define FOR(i,j,k) for(int i=j;i<=k;i++)
#define ROF(i,j,k) for(int i=j;i>=k;i--)
int read(){
	int x=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)) x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
	return x*f;
}
const int N=3e5+10;
int T,n,q,a[N],b[N],la[N],lb[N];ull ans[N];
vector<PII> d[N];
struct node{ull s,xy,x,y;}tr[N<<2];
struct TAG{ull cx,cy,addxy,addx,addy,addc;}tag[N<<2];
void change(int u,int l,int r,TAG t){
	if(tag[u].cx&&tag[u].cy){
		tag[u].addc+=t.addxy*tag[u].cx*tag[u].cy+t.addx*tag[u].cx+t.addy*tag[u].cy+t.addc;
	}
	else if(tag[u].cx){
		tag[u].addc+=t.addx*tag[u].cx+t.addc;
		tag[u].addy+=t.addxy*tag[u].cx+t.addy;
	}
	else if(tag[u].cy){
		tag[u].addc+=t.addy*tag[u].cy+t.addc;
		tag[u].addx+=t.addxy*tag[u].cy+t.addx;
	}
	else{
		tag[u].addxy+=t.addxy;
		tag[u].addx+=t.addx;
		tag[u].addy+=t.addy;
		tag[u].addc+=t.addc;
	}
	if(t.cx) tag[u].cx=t.cx;
	if(t.cy) tag[u].cy=t.cy;
	int len=r-l+1;
	tr[u].s+=t.addxy*tr[u].xy+t.addx*tr[u].x+t.addy*tr[u].y+t.addc*len;
	if(t.cx&&t.cy){
		tr[u].xy=t.cx*t.cy*len;
		tr[u].x=t.cx*len,tr[u].y=t.cy*len;
	}
	else if(t.cx){
		tr[u].xy=t.cx*tr[u].y;
		tr[u].x=t.cx*len;
	}
	else if(t.cy){
		tr[u].xy=t.cy*tr[u].x;
		tr[u].y=t.cy*len;
	}
}
void pushdown(int u,int l,int r){
	int mid=(l+r)>>1;
	change(u<<1,l,mid,tag[u]),change(u<<1|1,mid+1,r,tag[u]);
	tag[u]={0,0,0,0,0,0};
}
void pushup(int u){
	tr[u].s=tr[u<<1].s+tr[u<<1|1].s;
	tr[u].x=tr[u<<1].x+tr[u<<1|1].x;
	tr[u].y=tr[u<<1].y+tr[u<<1|1].y;
	tr[u].xy=tr[u<<1].xy+tr[u<<1|1].xy;
}
void modify(int u,int l,int r,int ql,int qr,TAG t){
	if(ql<=l&&r<=qr){
		change(u,l,r,t);
		return;
	}
	pushdown(u,l,r);int mid=(l+r)>>1;
	if(ql<=mid) modify(u<<1,l,mid,ql,qr,t);
	if(qr>mid) modify(u<<1|1,mid+1,r,ql,qr,t);
	pushup(u);
}
ull query(int u,int l,int r,int ql,int qr){
	if(ql<=l&&r<=qr) return tr[u].s;
	pushdown(u,l,r);int mid=(l+r)>>1;ull ans=0;
	if(ql<=mid) ans+=query(u<<1,l,mid,ql,qr);
	if(qr>mid) ans+=query(u<<1|1,mid+1,r,ql,qr);
	return ans;	
}
int main(){
	T=rd,n=rd;
	FOR(i,1,n) a[i]=rd;FOR(i,1,n) b[i]=rd;
	a[0]=b[0]=N;
	FOR(i,1,n){
		la[i]=i,lb[i]=i;
		while(a[i]>a[la[i]-1]) la[i]=la[la[i]-1];
		while(b[i]>b[lb[i]-1]) lb[i]=lb[lb[i]-1];
	}
	q=rd;
	FOR(i,1,q){int l=rd,r=rd;d[r].push_back(mp(l,i));}
	FOR(i,1,n){
		modify(1,1,n,la[i],i,(TAG){(ull)a[i],0,0,0,0,0});
		modify(1,1,n,lb[i],i,(TAG){0,(ull)b[i],0,0,0,0});
		modify(1,1,n,1,i,(TAG){0,0,1,0,0,0});
		for(auto j:d[i]){
			ans[j.second]=query(1,1,n,j.first,i);
		}
	}
	FOR(i,1,q) printf("%llu\n",ans[i]);
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值