NOIP2023(惨烈)做题记(泪奔::>_<::)

P9868 [NOIP2023] 词典

1.这道题倒是做出来了,大概思路如下:

对于每一个字符串,可以存储一个 k 和 k2​ 分别表示这个字符串包含的字符中的字典序最小字符与字典序最大字符,这一步可以初始就处理好。

然后判断每一个字符串是否成立时,我们可以直接判断该字符串的 k 是否绝对小于任意其他字符串的 k2​(注意这里不能等于,注意到题目中 wi 的字典序比 wj​ 都要,而不是小于等于,如果当前 k=k2​,那只可能当前字符串的字典序大于等于另外的字符串,不符合题意)。

2.代码实现如下:

#include<bits/stdc++.h>
using namespace std;
const int N=3010;
int n,m;
char c[N];
int k[N],k2[N];
int main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		scanf("%s",c);
		for(int j=0;j<m;j++){	
			if(j==0)
				k[i]=k2[i]=c[j]-'a';
			else{
				k[i]=min(k[i],c[j]-'a');
				k2[i]=max(k2[i],c[j]-'a');
			}
		}
	}
	for(int i=1;i<=n;i++){
		int flag=1;
		for(int j=1;j<=n;j++){
			if(i==j) continue;
			if(k[i]<k2[j]) continue;
			else{
				flag=0;
				printf("0");
				break;
			}
		}
		if(flag==1) printf("1");
	}
	return 0;
}

P9869 [NOIP2023] 三值逻辑

1.这道题寄了(祭)。

2.并查集做法:按照题意模拟,给所有输入的操作的相关变量用并查集标记,在并查集查询操作前可以知道哪些变量的值是确定的,哪些是不确定的,这时我们的任务就是最小化不确定的变量中 U 的个数。变量的值一定是 U 的情况总的来说有两种:

  1. x 的祖先是 −x。
  2. x 的祖先是 U。

把这两种情况查询出来,再统计个数,就是最终答案了。由于 x 的值可能标记成负数了,所以并查集查询时遇到负数记得要取相反数。

由于要查询的 x 的祖先有可能是 −x,取相反数后又是 x,这样就会陷入死循环。于是就可以用 book 数组记录是否到过这个点的相反数。由于 book 记录的值也有负数,所以给记录的值统一加上一个 n 把其全变成正数,(所以注意book的大小需要开到2n)。

3.再简单说说细节,可以把三值逻辑分别赋予三个值,再把这三个值赋予三个常量,常量就以 T、F、U 命名,方便在代码中编写。规定逻辑非运算就是把值的正负取反,那么 U 常量的值就应该为 0(因为 0=−0)。而 T 常量和 F 常量的值呢?为了把常量值和变量值区别开,所以取一个 n 的上限加一的值,即 100001 和 −100001,(再大点应该也没问题吧)。

4.然后贴上修改后的代码(大功告成):

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+10;
const int T=100001,F=-100001,U=0;
int c,t,n,m,a,b,fa[N];
char ch[N];
bool book[N*2];
int find(int x){
	int res;
	if(x==T||x==F) res=x;
	else if(book[n-x]||x==U) res=U;
	else if(book[x+n]) res=T;
	else if(x<0){
		if(x==-fa[-x]) res=x;
		else{
			book[x+n]=1;
			res=find(-fa[-x]);
			book[x+n]=0;
		}
	}
	else{
		if(x==fa[x]) res=x;
		else{
			book[x+n]=1;
			res=fa[x]=find(fa[x]);
			book[x+n]=0;
		}
	}
	return res;
}
signed main(){
	cin>>c>>t;
	while(t--){
		cin>>n>>m;
		for(int i=1;i<=n;i++)fa[i]=i;
		for(int i=1;i<=m;i++){
			cin>>ch[i];
			if(ch[i]=='T'){
				cin>>a;
				fa[a]=T;
			}
			else if(ch[i]=='F'){
				cin>>a;
				fa[a]=F;
			}
			else if(ch[i]=='U'){
				cin>>a;
				fa[a]=U;
			}
			else{
				cin>>a>>b;
				if(ch[i]=='+') fa[a]=fa[b];
				else fa[a]=-fa[b];
			}
		}
		int ans=0;
		for(int i=1;i<=n;i++){
			if(find(i)==U) ans++;
		}
		cout<<ans<<endl;
	}
	return 0;
}

毋庸置疑,剩下两道题都没来得及看 (逃~)。

P9871 [NOIP2023] 天天爱打卡

1.本题是线段树优化dp:有一些 DP 的初始化和转移操作可以转化为序列上 / 值域上的区间操作 / 区间查询问题,可以用线段树加速这些操作。

2.先理解下题意:即求出选择若干个互不相交且互不相邻的区间,每个区间的大小不超过 kk,每个区间覆盖到的挑战的权值和 减去\sum(r−l+1)*d 的最大值。

3.部分分思路:8分:爆搜即可   56分:线段树优化dp   100分:离散化+线段树优化dp

8分代码:

#include <bits/stdc++.h>
using namespace std;
int n,m,k,d,c,t;
long long maxn;
const int N=1e5+5;
int x[N],y[N],v[N],ch[N];
bool check(){
	int s=0;
	for(int i=1;i<=n;i++){
		if(ch[i]==0) s=0;
		else s++;
		if(s>k) return false;
	}
	return true;
}
void dfs(int dep){
	if(dep>n){
		if(!check()) return ;
		long long s=0;
		for(int i=1;i<=m;i++){
			bool pd=1;
			for(int j=x[i]-y[i]+1;j<=x[i];j++){
				if(ch[j]!=1){
					pd=0;
					break;
				}
			}
			if(pd) s+=v[i];
		}
		for(int i=1;i<=n;i++) if(ch[i]) s-=d;
		maxn=max(maxn,s);
	}
	else{
		for(int i=0;i<=1;i++){
			ch[dep]=i;
			dfs(dep+1);
			ch[dep]=0;
		}
	}
}
int main(){
	cin>>c>>t;
	while(t--){
		memset(x,0,sizeof(x));
		memset(y,0,sizeof(y));
		memset(v,0,sizeof(v));
		memset(ch,0,sizeof(ch));
		maxn=0;
		cin>>n>>m>>k>>d;
		for(int i=1;i<=m;i++) cin>>x[i]>>y[i]>>v[i];
		dfs(1);
		cout<<maxn<<endl;
	}
	return 0;
}

56分代码:

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e5+10;
vector<pair<int,int> >g[N];
struct node{
	int mx,add,zero;
}st[N*4];
inline void jia(int p, int x){
	st[p].add+=x;
	st[p].mx+=x;
}
inline void Zero(int p){
	st[p].mx=-1e18;
	st[p].add=0;
	st[p].zero=1;
}
inline void upd(int p){
	st[p].mx=max(st[p*2].mx,st[p*2+1].mx);
}
inline void spd(int p){
	if(st[p].zero) Zero(p*2),Zero(p*2+1);
	st[p].zero=0;
	jia(p*2,st[p].add);
	jia(p*2+1,st[p].add);
	st[p].add=0;
}
void build(int p,int L,int R){
	st[p].mx=-1e18;
	st[p].add=st[p].zero=0;
	if(L==R) return;
	int mid=L+R>>1;
	build(p*2,L,mid);
	build(p*2+1,mid+1,R); 
}
void add(int p,int l,int r,int L,int R,int x){
	if(l<=L&&R<=r) return jia(p,x),void();
	int mid=L+R>>1;
	spd(p);
	if(l<=mid) add(p*2,l,r,L,mid,x);
	if(mid<r) add(p*2+1,l,r,mid+1,R,x);
	upd(p);
} 
void ze(int p,int l,int r,int L,int R){
	if(l<=L&&R<=r) return Zero(p),void();
	int mid=L+R>>1;
	spd(p);
	if(l<=mid) ze(p*2,l,r,L,mid);
	if(mid<r) ze(p*2+1,l,r,mid+1,R);
	upd(p);
} 
int ask(int p,int l,int r,int L,int R){
	if(l<=L&&R<=r) return st[p].mx;
	int mid=L+R>>1;
	spd(p);
	if(l>mid) return ask(p*2+1,l,r,mid+1,R);
	if(mid>=r) return ask(p*2,l,r,L,mid);
	return max(ask(p*2,l,r,L,mid),ask(p*2+1,l,r,mid+1,R));
}
int solve(){
	int n,m,k,d,i;
	cin>>n>>m>>k>>d;
	for(i=1;i<=n;i++) g[i].clear();
	while(m--){
		int x,y,v;
		cin>>x>>y>>v;
		g[x].emplace_back(y,v);
	}
	build(1,1,n+1);
	add(1,1,1,1,n+1,1e18);
	for(i=1;i<=n;i++){
		int x=ask(1,1,i,1,n+1);
		add(1,i+1,i+1,1,n+1,(int)1e18+x);
		add(1,1,i,1,n+1,-d);
		for(auto j:g[i])
			add(1,1,i-j.first+1,1,n+1,j.second);
		if(i>k) ze(1,1,i-k,1,n+1);
	}
	cout<<ask(1,1,n+1,1,n+1)<<'\n';
	return 0;
}
signed main(){
	int c, t;
	cin>>c>>t;
	while(t--) solve();
	return 0;
}

正解思路:直接 dp,令 fn 表示前 n 天的答案,枚举一个 r=n 的区间的左端点对当前状态进行更新,最后再与 fn−1​ 取个最大值。时间复杂度为O(t*n*n),若使用离散化复杂度就为O(t*m*m),再加上线段树优化dp的话可达(t*m*\log {m}),显然我并不会(—_—)。

正解代码:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e5+10;
struct node{
    int x,y,z;
}a[N];
int C,T,n,m,k,d,rt,tot,f[N<<1];
ll t[N<<3],dp[N<<1],ad[N<<3];
bool cmp(node a,node b){
	return a.y<b.y;
}
inline void push_down(int p){
    if(ad[p]){
        ad[p<<1]+=ad[p],t[p<<1]+=ad[p];
        ad[p<<1|1]+=ad[p],t[p<<1|1]+=ad[p];
        ad[p]=0;
    }
}
void build(int p,int l,int r){
    ad[p]=0;
    if(l==r){
        t[p]=(ll)f[l+1]*d;
        return;
    }
    int mid=(l+r)>>1;
    build(p<<1,l,mid);
    build(p<<1|1,mid+1,r);
    t[p]=max(t[p<<1],t[p<<1|1]);
}
void add(int p,int l,int r,int x,int L,int R){
    if(l>r)return;
    if(l<=L&&R<=r){
        ad[p]+=x;
		t[p]+=x;
        return;
    }
    push_down(p);
    int mid=(L+R)>>1;
    if(l<=mid) add(p<<1,l,r,x,L,mid);
    if(r>mid) add(p<<1|1,l,r,x,mid+1,R);
    t[p]=max(t[p<<1],t[p<<1|1]);
}
ll ask(int p,int l,int r,int L,int R){
    if(l>r) return -1e18;
    if(l<=L&&R<=r) return t[p];
    push_down(p);
    int mid=(L+R)>>1;
    if(l<=mid&&r>mid) return max(ask(p<<1,l,r,L,mid),ask(p<<1|1,l,r,mid+1,R));
    if(l<=mid) return ask(p<<1,l,r,L,mid);
    return ask(p<<1|1,l,r,mid+1,R);
}
void change(int p,int x,ll y,int l,int r){
    if(l==r){
		t[p]=y;
		return;
	}
    push_down(p);
    int mid=(l+r)>>1;
    if(x<=mid) change(p<<1,x,y,l,mid);
    else change(p<<1|1,x,y,mid+1,r);
    t[p]=max(t[p<<1],t[p<<1|1]);
}
signed main(){
    scanf("%d%d",&C,&T);
    while(T--){
        scanf("%d%d%d%d",&n,&m,&k,&d);
        f[tot=1]=0;
        for(int i=1;i<=m;i++){
            scanf("%d%d%d",&a[i].x,&a[i].y,&a[i].z);
            swap(a[i].x,a[i].y);
            a[i].x=a[i].y-a[i].x+1;
            f[++tot]=a[i].x,f[++tot]=a[i].y;
        }
        sort(f+1,f+tot+1);
        tot=unique(f+1,f+tot+1)-f-1;
        build(1,1,tot);
        rt=1;
        sort(a+1,a+m+1,cmp);
        for(int i=2;i<=tot;i++){
            while(rt<=m&&a[rt].y<=f[i]){
                add(1,1,lower_bound(f+1,f+tot+1,a[rt].x)-f-1,a[rt].z,1,tot);
                rt++;
            }
            dp[i]=max(ask(1,lower_bound(f+1,f+tot+1,f[i]-k+1)-f-1,i-1,1,tot)-(ll)(f[i]+1)*d,dp[i-1]);
            if(f[i]+1==f[i+1]) change(1,i,dp[i-1]+(ll)f[i+1]*d,1,tot);
            else change(1,i,dp[i]+(ll)f[i+1]*d,1,tot);
        }
        printf("%lld\n",dp[tot]);
    }
    return 0;
}

P9870 [NOIP2023] 双序列拓展

1.之前做过这道题,但现在还是没做对(悲)

2.题目理解:

定义一个序列 a1,…n 是一个序列 b1,…m 的拓展当且仅当可以将 a 划分成 m 个连续的非空段,使得按顺序的第 i 段中的所有元素都等于 bi​。

给序列 a1,…n​,b1,…m​,求是否存在长度为 10^{100} 的序列 s,t,使得 s 是 a 的拓展,t 是 b 的拓展,且 ∀(i,j)∈[1,10^{_{100}}]^{2},(si−ti)(sj-tj)>0。

有 q 次询问,每次会基于原序列改 k 个地方并要求输出答案。

3.每次询问可能基本无关。所以相当于要求 O(n+m) 解决每次询问。

考虑两个序列一起拓展的意义。它相当于在 n+m 个点的二分图上连至多 n+m−1 条边,要求边不能相交,每个点至少连一条边,且对于所有边 (i,j),满足 ai​<bj​。(经过一系列的推导)所以需要求最小值及其位置。发现每个求最小值的位置都只要求前缀最小值,所以可以每次询问分别预处理。单次询问时间复杂度 O(n+m)。唯一一处不是求最小值位置的地方是求 u。由于 u 只会向左移动,所以这部分也不难做到 O(n+m)。

4.还是直接看代码吧(╮(╯▽╰)╭):

#include<bits/stdc++.h>
using namespace std;
const int N=5e5+10;
int f,n,m,q;
int A[N],B[N],a[N],b[N];
int amx[N],amn[N],bmx[N],bmn[N];
string ans;
bool slove(int p,int q){
	if(a[1]==b[1]) return false;
	if(p==1||q==1) return true;
	amx[1]=amn[1]=bmx[1]=bmn[1]=1;
	for(int i=2;i<=n;i++){
		amx[i]=amx[i-1];
		amn[i]=amn[i-1];
		if(a[i]>a[amx[i]]) amx[i]=i;
		if(a[i]<a[amn[i]]) amn[i]=i;
	}
	for(int i=2;i<=m;i++){
		bmx[i]=bmx[i-1];
		bmn[i]=bmn[i-1];
		if(b[i]>b[bmx[i]]) bmx[i]=i;
		if(b[i]<b[bmn[i]]) bmn[i]=i;
	}
	while(p>1&&q>1){
		bool s=0;
		int r=bmn[p];
		int u=q;
		while(u&&a[amn[u]]<b[r]){
			u--;
			s=1;
		}
		u++;
		if(!s) return false;
		int v=bmx[r];
		if(a[amx[u]]>=b[v]) return false;
		p=v;
		q=u;
	}
	return true;
}
void rev(){
	for(int i=1;i+i<=n;i++) swap(a[i],a[n-i+1]);
	for(int i=1;i+i<=m;i++) swap(b[i],b[m-i+1]);
}
bool solve(){
	int R=0;
	if(a[1]>b[1]){
		R=1;
		for(int i=1;i<=max(n,m);i++) swap(a[i],b[i]);
		swap(n,m);
	}
	int p=1,q=1;
	for(int i=1;i<=n;i++) if(a[i]<a[q]) q=i;
	for(int i=1;i<=m;i++) if(b[i]>b[p]) p=i;
	int s=0;
	for(int i=1;i<=n;i++) if(a[i]>=b[p]) s=1;
	for(int i=1;i<=m;i++) if(b[i]<=a[q]) s=1;
	if(s){
		if(R) swap(n,m);
		return false;
	}
	if(slove(p,q)){
		rev();
		bool t=slove(m-p+1,n-q+1);
		if(R) swap(n,m);
		return t;
	}
	if(R) swap(n,m);
	return false;
}
int main(){
	cin>>f>>n>>m>>q;
	for(int i=1;i<=n;i++) scanf("%d",&A[i]),a[i]=A[i];
	for(int i=1;i<=m;i++) scanf("%d",&B[i]),b[i]=B[i];
	ans+=solve()+'0';
	for(int i=1,j,k,l,o;i<=q;i++){
		for(int p=1;p<=n;p++) a[p]=A[p];
		for(int p=1;p<=m;p++) b[p]=B[p];
		scanf("%d%d",&j,&k);
		for(;j--;a[l]=o) scanf("%d%d",&l,&o);
		for(;k--;b[l]=o) scanf("%d%d",&l,&o);
		ans+=solve()+'0';
	}
	cout<<ans;
	return 0;
}

PS

1.:如果之前做过的题现在又不会了,只能说还没真正理解,建议多总结。

2.做题时头脑一定要清楚,不清楚的话建议不要做题,更不要上手写代码,否则会很惨(QAQ)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值