[CSP-S 2021] 回文

[CSP-S 2021] 回文

题目描述:

给定正整数 n 和整数序列 a1​,a2​,…,a2n​,在这 2n 个数中,1,2,…,n 分别各出现恰好 2 次。现在进行 2n 次操作,目标是创建一个长度同样为 2n 的序列 b1​,b2​,…,b2n​,初始时 b 为空序列,每次可以进行以下两种操作之一:

  1. 将序列 a 的开头元素加到 b 的末尾,并从 a 中移除。
  2. 将序列 a 的末尾元素加到 b 的末尾,并从 a 中移除。

我们的目的是让 b 成为一个回文数列,即令其满足对所有 1≤i≤n,有 bi​=b2n+1−i​。

请你判断该目的是否能达成,如果可以,请输出字典序最小的操作方案,具体在【输出格式】中说明。

输入格式:

每个测试点包含多组测试数据。

输入的第一行,包含一个整数 T,表示测试数据的组数。对于每组测试数据:

第一行,包含一个正整数 n。
第二行,包含 2n 个用空格隔开的整数 a1​,a2​,…,a2n​。

输出格式:

对每组测试数据输出一行答案。

如果无法生成出回文数列,输出一行 ‐1,否则输出一行一个长度为 2n 的、由字符 L 或 R 构成的字符串(不含空格),其中 L 表示移除开头元素的操作 1,R 表示操作 2。

你需要输出所有方案对应的字符串中字典序最小的一个。

字典序的比较规则如下:

长度均为 2n 的字符串 s1∼2n​ 比 t1∼2n​ 字典序小,当且仅当存在下标 1≤k≤2n 使得对于每个 1≤i<k 有 si​=ti​ 且 sk​<tk​。

输入输出样例

输入 #1:

2
5
4 1 2 4 5 3 1 2 3 5
3
3 2 1 2 1 3

输出 #1:

LRRLLRRRRL
-1

输入 #2:

见附件中的 palin/palin2.in

输出 #2:

见附件中的 palin/palin2.ans

说明/提示

【样例解释 #1】

在第一组数据中,生成的的 b 数列是 [4, 5, 3, 1, 2, 2, 1, 3, 5, 4],可以看出这是一个回文数列。

另一种可能的操作方案是 LRRLLRRRRR,但比答案方案的字典序要大。

【数据范围】

令 ∑n 表示所有 T 组测试数据中 n 的和。

对所有测试点保证 1≤T≤100,1≤n,∑n≤5×10^5。

测试点编号 T≤ n≤ ∑n≤ 特殊性质
1∼7 10 10 50
8∼10 100 20 1000
11∼12 100 100 1000
13∼15 100 1000 25000
16∼17 1 5×10^5 5×10^5
18∼20 100 5×10^5 5×10^5
21∼25 100 5×10^5 5×10^5

特殊性质:

如果我们每次删除 a 中两个相邻且相等的数,存在一种方式将序列删空(例如 a = [1, 2, 2, 1])。 

附件下载:

palin.zip 4.25KB

思路:

  这个题解思路清奇(就是暴力加上了一堆优化然后跑过了),相较于 std 可能会有所不同。

首先一看,回文,好耶, Hash —— 然而并不是。

众所周知,了解一道题的最好方法就是手推样例(瞎说的),所以我们来手推一下。

样例一的第一组数据的 a 和 b:

然后手模一下怎么生成 b 的:

 

嗯,连得密密麻麻的不想看,做 T4 去了。

这当然是不可以的,但是这样看确实看不出来什么东西。

想一下题上有什么提示……

b 是一个回文数列。

既然 b 是一个好观察的回文数列那就从 b 入手吧。

b 前后相等,那就把前后分开看看?

好像也没有什么。

不对,看一看后半段 b 。

这是由连续的一段 a 组成的。

想一想,不论什么 b 的后半段都是由 a 中一段连续的,长度为 n 的数列组成的。

就像这样:

(长度不准确请忽视)

有了这个,我们就可以开始我们优雅の暴力了。

1.暴力枚举每一个后半段区间位置。

2.判断是否合法。

3.输出。

然后这个时候有人就会跑上来说,你这个暴力是 O(n^3) 的啊不是 O(n^2) 的啊。

( n^3 : 枚举 n 个区间、记录每个区间 n 个数,判断是否合法要 n 次判断)

所以我们需要对暴力进行优化。

优化一:

首先我们想,一个后半段区间假若有机会成为解,那么必要条件是……

当然是这个区间里包含了 1,2,...,n 啦(不然你怎么回文)。

那么我们在枚举一个后半段区间时可以顺便来看看它是否包含了 1,2,...,n,没有则直接跳到下一个区间,否则来判断是否合法以及假如合法则方案是否是字典序最小的。

当然这个优化对于随机数据收益巨大,能大幅消减判断的 n 以至于能看成 O(n^2) ,因为减少了很多不必要的判断;但是对于构造的数据,比如说这个:

1,2,...,10000,1,2,...,10000

那这个优化就什么用都没有,直接退化 O(n^3)。

优化二

所以再来一个显而易见的优化:每个区间都是在上一个区间基础上挪了一个罢了,所以可以直接在上一个区间的基础上进行改动,这样就把记录每个区间的数的 n 就消掉了,这样就是一个完全的 O(n^2) 暴力了。加上前面的优化,对于随机数据能直接跑到近似于 O(n) 乘上一个大常数。

(民间数据能 AC ,不得不说真的有点水) 现在只有 80 分了悲伤

优化二代码:

#include<bits/stdc++.h>
using namespace std;
int t,n,tot,now,check,qrs;
int num[1000001],cnt;
int in[1000001];
char ans[1000001],maybeans[1000001];
stack<int>castle_3;
stack<int>lancet_2;
int main()
{
//	freopen("palin.in","r",stdin);
//	freopen("palin.out","w",stdout);
	cin>>t;
	while(t--)
	{
		memset(ans,0,sizeof(ans));
		memset(in,0,sizeof(in));
		cnt=0;
		qrs=0;
		cin>>n;
		for(int i=1;i<=2*n;i++)
		{
			cin>>num[i];
		}
		int l=1,r=n;
		for(int i=l;i<=r;i++)
		{
			in[num[i]]++;
			if(in[num[i]]==1)
			{
				cnt++;
			}
		}
		while(r<=2*n)
		{
			if(cnt==n)
			{
				memset(maybeans,0,sizeof(maybeans));
				check=0;
				for(int i=1;i<l;i++)
				{
					castle_3.push(num[i]);
				}
				for(int j=2*n;j>r;j--)
				{
					lancet_2.push(num[j]);
				}
				int ll=l,rr=r;
				int a,b;
				int now=0;
				while(ll<=rr)
				{
					a=-1;
					b=-1;
					if(castle_3.size()) a=castle_3.top();
					if(lancet_2.size()) b=lancet_2.top();
					if(b==num[ll])
					{
						maybeans[n-now]='R';
						maybeans[n+1+now]='L';
						lancet_2.pop();
						now++;
						ll++;
					}
					else if(b==num[rr])
					{
						maybeans[n-now]='R';
						maybeans[n+1+now]='R';
						lancet_2.pop();
						now++;
						rr--;
					}
					else if(a==num[ll])
					{
						maybeans[n-now]='L';
						maybeans[n+1+now]='L';
						castle_3.pop();
						now++;
						ll++;
					}
					else if(a==num[rr])
					{
						maybeans[n-now]='L';
						maybeans[n+1+now]='R';
						castle_3.pop();
						now++;
						rr--;
					}
					else
					{
						check=1;
						break;
					}
				}
				if(!check)
				{
					if(!qrs)
					{
						for(int i=1;i<=n*2;i++)
						{
							ans[i]=maybeans[i];
						}
						qrs=1;
					}
					else
					{
						int i=1;
						int checks=0;
						for(;i<=n*2;i++)
						{
							if(ans[i]!=maybeans[i])
							{
								checks=1;
								break;
							}
						}
						if(maybeans[i]=='L'&&checks)
						{
							for(int j=1;j<=n*2;j++)
							{
								ans[j]=maybeans[j];
							}
						}
					}
				}
				while(!lancet_2.empty()) lancet_2.pop();
				while(!castle_3.empty()) castle_3.pop();
			}
			in[num[l]]--;
			if(!in[num[l]]) cnt--;
			l++;
			r++;
			in[num[r]]++;
			if(in[num[r]]==1) cnt++; 
		}
		if(!qrs)
		{
			cout<<-1<<endl;
		}
		else
		{
			for(int i=1;i<=n*2;i++)
			{
				cout<<ans[i];
			}
			cout<<endl;
		}
	}
}

判断可行性解释:用两个栈记录左边前半部分与右边前半部分,然后和后半部分尝试匹配。每匹配成功就把它们加入maybeans方案中。对于这个“匹配”,请好好读读,因为后半段优化是基于这个“匹配”的。

代码解释: in 数组是用来记录区间内所有数的个数, cnt 是用来记录区间内有多少个不同的数;对于 in 和 cnt 的变化规律为什么是这样,手推一遍就很容易明白。然后是让每个区间生成最优解:因为我们需要让操作 R 尽可能在后,那么我们就先让 前半部分的右端先进行匹配,这样对于每个枚举的后半段区间,生成的答案就是局部最优解

然后机房大佬 FJN 因为没有打出暴力 T3 感到非常悲伤,然后听说我打出了一个随机化数据能跑 O(n) ,构造能跑 O(n^2) 的暴力于是来找我看看代码。

看着看着然后他就来了一句:你这个可以再优化成 O(n) 的。

对,这就是最后一个优化。

优化三

概括一下就是:倒序枚举后半段区间,当找到第一个解时就一定是最优解。

为甚么呢?

首先我们想一个基础事实:假如 a 中有 2 个或以上的符合要求的后半段区间,那么它们一定是有重合部分的。

(例外: 1,2,3,1,2,3 中前一个 1,2,3 和后一个 1,2,3 不重合,但是看一眼就知道选后面一个答案更优。)

就像这样:

为什么会重合?因为两段区间都包含 1 至 n 中所有数,而 1 至 n 每个又只有两个,所以一定会重合。

上图的红绿线段就是两个符合要求的后半段区间,那么显而易见的可以知道,没有重叠的部分是相同的。(你问为什么? A-B=A-B )

那么,在进行回文配对时,红色没有重叠部分一定是跟绿色没有重叠部分进行配对。毕竟含有相同的数。

那么,假若我们选择红色作为后半段区间,那么绿色的未重叠部分则是用 R 操作弹出。(假如可行)

假若我们选择绿色作为后半段区间,那么红色的未重叠部分则是用 L 操作弹出。(假如可行)

回看匹配过程,不管我们选择红色还是绿色,我们一定是先将不属于红色或绿色的是跟红绿重合部分匹配(你不把它们匹配完怎么匹配中间的啊),那么它们的在方案中位置及方案是固定的,那么就剩红色和绿色不重叠部分了。

假如我们选择是红色且红色可行,那么在这里我们会全部用 R 进行剩余匹配。

假如是绿色且绿色可行,那么在这里我们会全部用 L 进行剩余匹配。

显而易见的,绿色更优。

所以两段后半部分区间都可行时,那么选择后面那段作为后半区间更优。

于是乎,倒着枚举区间,遇到第一个可行的区间就输出答案,正确性证明完毕,于是就开心滴砍掉了大部分判断这个 n 。结合前面的优化,一个近似于 O(n) 就这么搞出来啦~

当然,如果真的要专门构造数据来卡当然是可以退化到 O(n^2) 的(悲)

完整代码:

#include<bits/stdc++.h>
#define ooffof 1000001
using namespace std;
int t,n,tot,now,check,qrs;
int num[ooffof],cnt;
int in[ooffof];
char ans[ooffof],maybeans[ooffof];
int l[ooffof],r[ooffof],klk=0;;
int main()
{
//	freopen("palin.in","r",stdin);
//	freopen("palin.out","w",stdout);
	cin>>t;
	while(t--)
	{
		memset(ans,0,sizeof(ans));
		memset(in,0,sizeof(in));
		cnt=0;
		qrs=0;
		klk=0;
		cin>>n;
		for(int i=1;i<=2*n;i++)
		{
			cin>>num[i];
			in[num[i]]++;
			if(in[num[i]]==1)
			{
				cnt++;
			}
			if(i>n)
			{
				in[num[i-n]]--;
				if(in[num[i-n]]==0)
				{
					cnt--;
				}
			}
			if(cnt==n)
			{
				l[++klk]=i-n+1;
				r[klk]=i;
			}
		}
		for(int q=klk;q>=1;q--)
		{
			memset(maybeans,0,sizeof(maybeans));
			check=0;
			int ll=l[q],rr=r[q],sl=l[q]-1,rl=r[q]+1;
			int a,b;
			int now=0;
			while(ll<=rr)
			{	
				a=-1;
				b=-1;
				if(sl>=1) a=num[sl];
				if(rl<=2*n) b=num[rl];	
				if(b==num[ll])
				{
					maybeans[n-now]='R';
					maybeans[n+1+now]='L';
					rl++;
					now++;
					ll++;
				}
				else if(b==num[rr])
				{
					maybeans[n-now]='R';
					maybeans[n+1+now]='R';
					rl++;
					now++;
					rr--;
				}
				else if(a==num[ll])
				{
					maybeans[n-now]='L';
					maybeans[n+1+now]='L';
					sl--;
					now++;
					ll++;
				}
				else if(a==num[rr])
				{
					maybeans[n-now]='L';
					maybeans[n+1+now]='R';
					sl--;
					now++;
					rr--;
				}
				else
				{
					check=1;
					break;
				}
			}
			if(!check)
			{
				for(int i=1;i<=n*2;i++)
				{
					cout<<maybeans[i];
					qrs=1;
				}
				cout<<endl;
			}
			if(qrs)
			{
				break;
			}
		}
		if(!qrs)
		{
			cout<<-1<<endl;
		}
	}
}

总结:

比 T2 简单的 T3 这辈子不多了……

是名副其实的签到题……

 不过能从暴力n^3优化到n,这是一个非常难的过程。

题目链接:

[CSP-S 2021] 回文 - 洛谷https://www.luogu.com.cn/problem/P7915 

猜你喜欢

转载自blog.csdn.net/wo_ai_luo_/article/details/131251144