NOIP2023题解

T1:词典

有一个好猜的结论:对于一个字符串,若它当中的最小字符大于等于某其他字符串中的最大字符,那么这个字符串一定不可行。

证明也很简单,若最小字符大于最大字符,显然一定不可行。若最小字符等于最大字符,由于字符串长度相同,且字符串两两不同,所以次小的一定大于另一个次大的,依然不可行,由此得证。代码及其简单,就不贴了。

T2:三值逻辑

见到这个题第一眼就可以想到并查集了。对于 true 和 false,我们可以将它们视为正负关系,这样就极大简化操作了。初始化常量,令 T = − F , U = 0 T=-F,U=0 T=−F,U=0,然后我们处理完所有赋值操作后,再处理答案。

考虑一个数 x x x,在并查集中,若 x x x 祖先为 U U U,那么答案自然加 1 1 1;若它的祖先有 − x -x −x,说明是非关系起冲突了,那么 x x x 只能为 U U U。由此,我们处理的重点便是如何判断 x x x 是否存在这两个祖先。

若 x = T / F / U x=T/F/U x=T/F/U,可以直接判断;若之前访问过 − x -x −x,那么也可以判断。然后考虑 x x x 大于或小于 0 0 0 的情况,注意处理数组越界的情况,就可以了。代码不长,但细节还是比较多的。

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
#define rd read()
const int N=3e5+10,T=2e5+10,F=-T,U=0;
int read(){
	int x=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)) x=x*10+ch-'0',ch=getchar();
	return x*f;
}
int c,t,n,m,fa[N],vis[N];
int find(int x){
	if(x==T||x==F) return x;
	if(x==U||vis[-x+n]) return U;
	if(vis[x+n]) return T;
	int res;
	if(x<0){
		if(x==-fa[-x]) return x;//为根节点
		vis[x+n]=1,res=find(-fa[-x]),vis[x+n]=0;
	}
	else{
		if(x==fa[x]) return x;
		vis[x+n]=1,res=fa[x]=find(fa[x]),vis[x+n]=0;
	}
	return res;
}
int main(){
	c=rd,t=rd;
	while(t--){
		n=rd,m=rd;memset(fa,0,sizeof(fa)),memset(vis,0,sizeof(is));
		for(int i=1;i<=n;i++) fa[i]=i;
		while(m--){
			char op;cin>>op;int x=rd,y;
			if(op=='T') fa[x]=T;
			else if(op=='F') fa[x]=F;
			else if(op=='U') fa[x]=U;
			else if(op=='+') y=rd,fa[x]=fa[y];
			else y=rd,fa[x]=-fa[y];
		}
		int ans=0;for(int i=1;i<=n;i++){
			//cout<<find(i)<<":::"<<endl;
			if(find(i)==U) ans++;
		}
		printf("%d\n",ans);
	}
	return 0;
}

T3:双序列拓展

感觉比 T4 难。

神仙思维题,依然是由特殊性质启发正解的题。

我们先看两个序列满足的条件,实际上就是对于任意位置 i i i,要么 f i > g i f_i>g_i fi>gi,要么 f i < g i f_i<g_i fi<gi,且这种大小关系只能满足一个。我们可以规定 f f f 恒小于 g g g,两种情况处理方法几乎是一样的。

先看 Subtask 1~7,可以用比较暴力的 O ( n 2 ) O(n^2) O(n2) 做法。 f i , g j f_i,g_j fi,gj 可以分别由 ( X i − 1 , Y j ) , ( X i , Y j − 1 ) , ( X i − 1 , Y j − 1 ) (X_{i-1},Y_j),(X_i,Y_{j-1}),(X_{i-1},Y_{j-1}) (Xi−1,Yj),(Xi,Yj−1),(Xi−1,Yj−1) 三种情况拓展过来。然后暴力枚举 i , j i,j i,j 即可,时间复杂度 O ( T n m ) O(Tnm) O(Tnm)。

暴力的 dp 做法似乎没有什么优化的余地了,对正解也没什么启发,但我们将其抽象为一个网格图,发现了什么?

我们实际上就是从左上角的 ( 1 , 1 ) (1,1) (1,1),每次可以往右、往下、往右下移动,最终只要能到右下角的 ( n , m ) (n,m) (n,m) 即为成功。而设 ( x , y ) (x,y) (x,y) 分别表示 X , Y X,Y X,Y 的下标,我们能到这个格子,当且仅当 X x < Y y X_x<Y_y Xx<Yy。

这样的转化看似影响不大,但我们再去看 Subtask 8~14,由于其特殊性质,发现只要 X m i n < Y m i n , X m a x < Y m a x X_{min}<Y_{min},X_{max}<Y_{max} Xmin<Ymin,Xmax<Ymax,那么这个网格就会像这样:

其中红格都是我们能到的地方,如果不理解的话一定要手动画图去体会一下,这是最关键的部分。

所以我们就可以通过找最大最小值位置,来判断是否合法并不断缩小范围了,最终只要 x = 1 x=1 x=1 或 y = 1 y=1 y=1,我们就可以构造出 f f f 和 g g g 了。

但是若没有这个性质呢?实际上我们发现,改变的只有 X X X 和 Y Y Y 的最值所在位置,所以网格图就会变为这种:

这个时候我们只要能从 ( 1 , 1 ) (1,1) (1,1) 到红格,并从红格到 ( n , m ) (n,m) (n,m) 即可。相比之前,我们需要多判断右下角。

至此,我们解决了这个问题。总结一下思路:预处理出 X X X 和 Y Y Y 的前缀及后缀最值所在位置,然后两个 check 函数分别判断左上角和右下角的可行情况,不断缩小边界。

代码并不长,大多都是在预处理最值位置,关键部分就是两个 check 函数。

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
#define rd read()
const int N=5e5+10;
int read(){
	int x=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)) x=x*10+ch-'0',ch=getchar();
	return x*f;
}
int c,n,m,T,a[N],b[N],ta[N],tb[N],f[N],g[N];
struct node{int mnpos,mxpos;}prex[N],prey[N],sufx[N],sufy[N];
bool check1(int x,int y,int n,int m){
	if(x==1||y==1) return true;
	node A=prex[x-1],B=prey[y-1];
	if(f[A.mnpos]<g[B.mnpos]) return check1(A.mnpos,y,n,m);
	if(f[A.mxpos]<g[B.mxpos]) return check1(x,B.mxpos,n,m);
	return false;
}
bool check2(int x,int y,int n,int m){
	if(x==n||y==m) return true;
	node A=sufx[x+1],B=sufy[y+1];
	if(f[A.mnpos]<g[B.mnpos]) return check2(A.mnpos,y,n,m);
	if(f[A.mxpos]<g[B.mxpos]) return check2(x,B.mxpos,n,m);
	return false;
}
bool solve(int aa[],int bb[],int n,int m){
	if(aa[1]>=bb[1]) return false;
	memcpy(f,aa,sizeof(f)),memcpy(g,bb,sizeof(g));
	for(int i=1;i<=n;i++){
		if(i==1) prex[i]={1,1};
		else{
			prex[i].mnpos=prex[i-1].mnpos,prex[i].mxpos=prex[i-1].mxpos;
			if(f[i]<f[prex[i].mnpos]) prex[i].mnpos=i;
			if(f[i]>f[prex[i].mxpos]) prex[i].mxpos=i;
		}
	}
	for(int i=1;i<=m;i++){
		if(i==1) prey[i]={1,1};
		else{
			prey[i].mnpos=prey[i-1].mnpos,prey[i].mxpos=prey[i-1].mxpos;
			if(g[i]<g[prey[i].mnpos]) prey[i].mnpos=i;
			if(g[i]>g[prey[i].mxpos]) prey[i].mxpos=i;
		}
	}
	for(int i=n;i>=1;i--){
		if(i==n) sufx[i]={n,n};
		else{
			sufx[i].mnpos=sufx[i+1].mnpos,sufx[i].mxpos=sufx[i+1].mxpos;
			if(f[i]<f[sufx[i].mnpos]) sufx[i].mnpos=i;
			if(f[i]>f[sufx[i].mxpos]) sufx[i].mxpos=i;
		}
	}
	for(int i=m;i>=1;i--){
		if(i==m) sufy[i]={m,m};
		else{
			sufy[i].mnpos=sufy[i+1].mnpos,sufy[i].mxpos=sufy[i+1].mxpos;
			if(g[i]<g[sufy[i].mnpos]) sufy[i].mnpos=i;
			if(g[i]>g[sufy[i].mxpos]) sufy[i].mxpos=i;
		}
	}
	node A=prex[n],B=prey[m];
	if(f[A.mnpos]>=g[B.mnpos]||f[A.mxpos]>=g[B.mxpos]) return false;
	return check1(A.mnpos,B.mxpos,n,m)&&check2(A.mnpos,B.mxpos,n,m);
}
int main(){
	c=rd,n=rd,m=rd,T=rd;
	for(int i=1;i<=n;i++) a[i]=rd;
	for(int i=1;i<=m;i++) b[i]=rd;
	if(solve(a,b,n,m)||solve(b,a,m,n)) cout<<1;
	else cout<<0;
	while(T--){
		int kx=rd,ky=rd;
		memcpy(ta,a,sizeof(a)),memcpy(tb,b,sizeof(b));
		for(int i=1;i<=kx;i++){int x=rd,y=rd;ta[x]=y;}
		for(int i=1;i<=ky;i++){int x=rd,y=rd;tb[x]=y;}
		if(solve(ta,tb,n,m)||solve(tb,ta,m,n)) cout<<1;
		else cout<<0;
	}
	return 0;
}

T4:天天爱打卡

比较典的数据结构优化 dp。

先设状态, f i f_i fi 表示前 i i i 天,第 i i i 天必跑步,最终能量最大值。我们枚举上次从哪一天开始跑,转移方程不难得出:
f i = max ⁡ { max ⁡ t = 0 j − 1 f t − ( j − i ) × d + ∑ [ l p , r p ] ∈ [ j , i ] v p } f_i=\max \left \{ \max_{t=0}^{j-1}f_t-(j-i)\times d+\sum_{[l_p,r_p]\in [j,i]}v_p \right \} fi=max{maxt=0j−1ft−(j−i)×d+∑[lp,rp]∈[j,i]vp}。

设 g i = max ⁡ j = 0 i f j g_i=\max_{j=0}^{i} f_j gi=maxj=0ifj,我们就有了 O ( n 2 ) O(n^2) O(n2) 的做法。发现这样的式子可以运用线段树优化,具体地:线段树叶子结点代表下标,维护相应位置最优值。对于 i → i + 1 i\to i+1 i→i+1, [ 0 , i ] [0,i] [0,i] 中的所有位置值都会减 d d d,然后对于区间右端点等于 i + 1 i+1 i+1 的区间,设左端点为 l l l,则 [ 0 , l ] [0,l] [0,l] 中的值都会增加相应的 v v v。做完修改后,由于我们不能跑超过连续 k k k 天,所以我们查询 [ i − k + 1 , i + 1 ] [i-k+1,i+1] [i−k+1,i+1] 中的最大值。此时最大值会改变,再令 i + 2 i+2 i+2 位置的值加上此时的答案。

此时时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn),而 n n n 最大可达 1 0 9 10^9 109,考虑如何优化。发现 m m m 的数据范围很小,所以真正有用的位置并不多,只有一个区间的 l − 1 , r l-1,r l−1,r 两个位置。将这些位置存下来进行离散化,再进行上述操作,这道题就做完了。

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define rd read()
const int N=2e5+10;
int read(){
	int x=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)) x=x*10+ch-'0',ch=getchar();
	return x*f;
}
int c,T,n,m,k,d,lsh[N],tot,mx[N<<2],tag[N<<2];
struct node{int l,r,v;}a[N];
bool cmp(node a,node b){return a.r<b.r;}
void pushdown(int u){
	if(!tag[u]) return;
	tag[u<<1]+=tag[u],tag[u<<1|1]+=tag[u];
	mx[u<<1]+=tag[u],mx[u<<1|1]+=tag[u];
	tag[u]=0;
}
void pushup(int u){mx[u]=max(mx[u<<1],mx[u<<1|1]);}
void modify(int u,int l,int r,int ql,int qr,int v){
	if(ql<=l&&r<=qr){mx[u]+=v,tag[u]+=v;return;}
	pushdown(u);int mid=(l+r)>>1;
	if(ql<=mid) modify(u<<1,l,mid,ql,qr,v);
	if(qr>mid) modify(u<<1|1,mid+1,r,ql,qr,v);
	pushup(u);
}
int query(int u,int l,int r,int ql,int qr){
	if(ql<=l&&r<=qr) return mx[u];
	pushdown(u);int mid=(l+r)>>1,ans=0;
	if(ql<=mid) ans=max(ans,query(u<<1,l,mid,ql,qr));
	if(qr>mid) ans=max(ans,query(u<<1|1,mid+1,r,ql,qr));
	return ans;
}
signed main(){
	c=rd,T=rd;
	while(T--){
		n=rd,m=rd,k=rd,d=rd;tot=0;memset(mx,0,sizeof(mx)),memset(tag,0,sizeof(tag));
		for(int i=1;i<=m;i++){int x=rd,y=rd,v=rd;a[i]={x-y,x,v};}
		for(int i=1;i<=m;i++) lsh[++tot]=a[i].l,lsh[++tot]=a[i].r;
		sort(lsh+1,lsh+1+tot),tot=unique(lsh+1,lsh+1+tot)-lsh-1;
		for(int i=1;i<=m;i++) a[i].l=lower_bound(lsh+1,lsh+1+tot,a[i].l)-lsh,a[i].r=lower_bound(lsh+1,lsh+1+tot,a[i].r)-lsh;
		int ans=0,p=1;sort(a+1,a+1+m,cmp);
		for(int i=1;i<=tot;i++){
			modify(1,0,tot-1,0,i-1,-d*(lsh[i]-lsh[i-1]));
			while(p<=m&&a[p].r==i) modify(1,0,tot-1,0,a[p].l,a[p].v),p++;
			int t=lower_bound(lsh+1,lsh+1+tot,lsh[i]-k)-lsh;
			ans=max(ans,query(1,0,tot-1,t,i-1));
			if(i+1<tot) modify(1,0,tot-1,i+1,i+1,ans);
		}
		printf("%lld\n",ans);
	}
	return 0;
}
相关推荐
姜西西_4 分钟前
递归 算法专题
算法·深度优先
nurupo1235 分钟前
C++学习路线(二十五)
c++·学习·算法
wrx繁星点点31 分钟前
原型模式:高效的对象克隆解决方案
数据结构·spring·spring cloud·java-ee·maven·intellij-idea·原型模式
yava_free36 分钟前
OpenMV的无人驾驶智能小车模拟系统
c语言·c++·stm32
铭正41 分钟前
C++多线程应用
c++·多线程
LNTON羚通1 小时前
算法定制LiteAIServer视频智能分析平台裸土检测技术实现、应用场景与优势概览
大数据·算法·目标检测·音视频·监控
学习编程之路1 小时前
深入理解C++ Lambda表达式:语法、用法与原理及其包装器的使用
开发语言·c++
书鸢12362 小时前
力扣每日一题合集
java·算法·leetcode
何事驚慌2 小时前
2024/10/30 数据结构大题打卡
数据结构
qing_0406032 小时前
C++——string的模拟实现(上)
开发语言·c++·string