23 真题前两题

密码锁

没什么好说的,要么暴力判断,要么手推。


消消乐

我们观察一下这个能消除的结构,因为是每次消除相邻两个,所以不难想到很像括号匹配,或者说可消除的是有对称性的,所以我们一定可以找到一个节点开始反转左右手性,所以我们就可以用栈来保持前半部分的手性,后半部分遇到转折点就是可以消除,可以消除就是可以消除。

先考虑部分分,就是 \(n^2\) 枚举每个子串,再对每个子串进行入栈模拟,是 \(n^3\) 的,35pts。

考虑怎么优化这个暴力。就是考虑大的串必然包含小的串,所以对大的串进行模拟的时候我们是一定将小的串也进行了一次模拟的。考虑对于每个小的串,如果前面是全部可以消除的,就是到它的起点位置时栈为空,那么到它的末尾时看其栈为不为空就已经判断了它是不是可消除的。如果起点不为空,那么我们就不算进去,后面总有一次会起点是空的。所以我们不用再重复模拟,只需要枚举起点然后看栈为空的次数就好了。但是有人就要问了,你这样为什么不会重呢,有可能这个区间是可消除的几个拼接在一起,那你这样算出来栈为空的次数就多了吧。其实不然。你考虑如果两段是合法的,那么拼起来还是合法的,这样就会比单纯的单个合法多了一些排列组合,而如果有不合法的显然对答案没有影响,所以是少想了而不是算多了。

所以我们就有了 \(n^2\) 暴力,可以获得额外的15pts。

考虑继续优化,既然我们入栈的模拟不能优化,那我们就对于枚举头端点进行一个优化,考虑我们后面的计算能不能用上前面的数值呢?你考虑其实我们的字符串对于每次出栈入栈以后都是有一个状态的,我们如果维护栈内的字符串组成的状态,那么是不是就可以避免重复对这一个状态进行计算了。发现在从 \(1\)\(n\) 维护栈序列的时候,若对于某个时刻 \(l\) 和某个时刻 \(r\),两种时刻的栈序列完全相同,那么说明子串 \(l+1,r\) 一定是可消除的。所以我们可以采用字符串哈希来维护每个时刻的栈序列,那么栈序列相同说明该情况下哈希值完全相同。维护每种哈希值出现了多少次,假设一种哈希值出现了 \(k\) 次,那么其对答案的贡献就是 \(k\choose 2\) 。即 \(k\) 个相同的时刻,每次取两个时刻 \(l\)\(r\) 构成的子串 \(𝑙+1,𝑟\) 是可消除的。

对于每种哈希值对答案的贡献求和,即为最终答案。

这种解法的代码

#include<bits/stdc++.h>
#define ull unsigned long long
using namespace std;
const int N=2e6+5;
int n,top;
char ch[N],st[N];
map<ull,int> cnt;
long long ans;
ull now,base=1145141,wei[N];
void init(){
	wei[0]=1;cnt[0]=1;
	for(int i=1;i<=n;++i) wei[i]=base*wei[i-1];
}
int main(){
	scanf("%d",&n);
	scanf("%s",ch+1);
	init();
	for(int i=1;i<=n;++i){
		if(top&&st[top]==ch[i]){
			now-=st[top]*wei[top],--top;
		}
		else{
			st[++top]=ch[i],now+=st[top]*wei[top];
		}
		ans+=cnt[now];
		++cnt[now];
	}
	printf("%lld",ans);
	return 0;
}

上面是在状态基础上思考怎么转移,下面我们尝试从子结构思考怎么转移。

直接考虑 \(dp_i\) 表示到了第 \(i\) 位单纯有多少个可以消除。我们考虑上一个可以消除的地方到当前位之间是可以消除的,那么我们就要对当前位置进行更新。所以我们就要找到上一个可以转移过来的位置,设为 \(lst_i\),则答案就是所有dp值的累加。然后你就考虑暴力向前跳 \(lst\) 找到第一个和当前字母相等的(有点像并查集),最多跳26次,所以复杂度 \(O( |\sum| n)\)。考虑还是可以优化,你考虑将 \(lst_i\) 向前推一位,会发现上一位的链尾和这一位的链头重合了,所以每次计算只需要找链尾,优化掉了 字符集大小。

这部分代码

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N=2e6+5;
int n,dp[N],a[N][26],to[N];
char s[N];
ll ans;
int main()
{
    scanf("%d%s",&n,s+1);
    for(int i=1;i<=n;i++)
    {
        to[i]=i;
        int x=a[to[i-1]][s[i]-'a'];
        if(x) to[i]=to[x-1],dp[i]=dp[x-1]+1;
        a[to[i]][s[i]-'a']=i,ans+=dp[i];
    }
    printf("%lld\n",ans);
    return 0;
}

词典

贪心,只要存在一个的最小值大于等于另一个的最大值就不行,否则可以。


三值逻辑

考虑这种关系类的题,我们第一时间就是想到并查集。

首先考虑什么情况下这个元素的初值需要被赋成 \(U\) ,当且仅当这个值的终值是 \(U\) (即过程中被赋为 \(U\) ) 或 存在一个形如 \(fa_x=¬fa_x\) 的条件。

然后注意一下细节:

  1. 全部处理完以后再统计。

  2. 负数要取相反,形式见代码。

  3. 如果一个元素的祖先是负数,取了相反,如果祖先还是这个数,就会陷入死循环,所以要标记一下有没有访问过相反数。

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int c,t,n,m,a,b,ans,x,y;
char id;
int fa[N],book[N<<1];
void clear(){
	ans=0;
	for(int i=0;i<=N;++i) fa[i]=i;
	for(int i=0;i<=2e5+5;++i) book[i]=0;
}
int find(int st,int x){
	if(book[x]&&st==0) st=x; 
	book[x]=1;
	if(fa[x]==-(1e5+1)) return -(1e5+1);
	if(fa[x]==0) return 0;
	if(fa[x]==1e5+1) return 1e5+1;
	if(fa[x]==x) return fa[x]=1e5+1;
	if(fa[x]==st) return fa[x]=1e5+1;
	if(fa[x]==-st) return fa[x]=0;
	if(fa[x]<0) return fa[x]=-find(-st,-fa[x]);
	else return fa[x]=find(st,fa[x]);
}
int main(){
	scanf("%d %d",&c,&t);
	while(t--){
		clear();
		scanf("%d %d",&n,&m);
		for(int i=1;i<=m;++i){
			scanf("\n%c",&id);
			if(id=='T'){
				scanf("%d",&a);
				fa[a]=1e5+1;
			}
			else if(id=='F'){
				scanf("%d",&a);
				fa[a]=-(1e5+1);
			}
			else if(id=='U'){
				scanf("%d",&a);
				fa[a]=0;
			}
			else if(id=='+'){
				scanf("%d %d",&a,&b);
				fa[a]=fa[b];
			}
			else{
				scanf("%d %d",&a,&b);
				fa[a]=-fa[b];
			}
		}
		for(int i=1;i<=n;++i){
			if(find(0,i)==0) ++ans;
		}
		printf("%d\n",ans);
	}
	return 0;
}
posted @ 2024-10-11 11:04  mountzhu  阅读(15)  评论(0)    收藏  举报