2019牛客暑期多校训练营(第三场)----C-Guessing ETT

首先发出题目链接:
链接:https://ac.nowcoder.com/acm/contest/883/C
来源:牛客网
涉及:递归、二分查找

点击这里回到2019牛客暑期多校训练营解题—目录贴


题目如下
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在此之前,先要说明一下欧拉序和栈的关系,首先我们要有一棵任意的树,如下
在这里插入图片描述
如图所示,每个节点都有一个值,我们从树的根节点开始先走左子树,然后走右子树,走到叶子结点就往回走,把每次走过的节点的上编号按顺序写出来就是欧拉序,如下图所示:
在这里插入图片描述
如上图所示,对应的欧拉序为124252131

可以看出,获得欧拉序是一个递归加回溯的过程,那么就可以认为是一个入栈加出栈的过程

1.首先根节点1入栈。

2.走到节点2,2入栈。

3.走到节点4,4入栈。

4.节点4是叶子节点,于是往回走,4出栈。

5.返回到节点2,然后走到节点5,5入栈。

6.节点5是叶子节点,于是往回走,5出栈。

7.返回到节点2,节点2的左右子树都走过,往回走,2出栈。

8.返回到节点1,然后走到节点3,3入栈。

9.节点3是叶子节点,于是往回走,3出栈。

10.返回到节点1,节点1的左右子树都走过,整棵树都走过了,1出栈,结束。

入栈认为是第一次走到了此节点,出栈认为是离开以此节点为根节点的树。


准备工作

了解了欧拉序,再看看题目,题目说的是本来有一个欧拉序,但是中间某些值不知道,叫你补全这个欧拉序。

首先我们可以知道,这个欧拉序的第一个值和最后一个值一定是1。

for(int i = 1; i <= 2*n-1; i++){//输入欧拉序 
	scanf("%d", &num[i]);
}
num[1] = num[2*n-1] = 1;//欧拉序开始和结束一定是1 

其次可以明白的是,整个欧拉序代表着一棵树,原序列中的首字符与尾字符相同的子串,我们可以认为是一颗子树即先进入到这棵子树,然后从这棵子树出来

所以我们需要递归每一棵数(即递归原串中的每一条首字符与尾字符相同的子串),一步步的解决问题即可

再次之前,我们要记录原序列中每一个已知元素的位置,用vector数组来存

vector<int> position[maxn];//position[i]表示数字i在欧拉序中出现的位置 
for(int i = 1; i <= 2*n-1; i++){
	if(num[i] != -1)	position[num[i]].push_back(i);//把所有不是-1的数位置提取出来 
}

同时,原序列中肯定出现了一些没有出现过的值,这些值也提取出来,留到之后用

vector<int> can;//can存1~n中没有出现过的数 
for(int i = 1; i <= n; i++){
	if(!position[i].size())	can.push_back(i);//把所有没有使用出现过的数提取出来 
}

做好了一些准备工作,就可以开始递归解决每一个满足条件子串了(当做子问题来处理),先处理根节点为1的每一棵子树。那就循环原序列中每两个距离最近的1

for(int i = 0; i < position[1].size() - 1; i++){
	solve(position[1][i]+1, position[1][i+1]-1, 1);//递归解决每一个子问题 (子串) 
}

如下所示
在这里插入图片描述
在这里插入图片描述


对于每一个子问题(满足条件的子串),还可以把它分解成更小的子问题(即子问题的子问题),如下图所示,某一个以1位首尾的子问题中,还有其他更小子问题
在这里插入图片描述
但是发现,虽然我们解决了所有的子问题的子问题,但是子问题并没有被完全解决,如图可以看出子问题子问题的子问题-1来组成的。

所以在解决子问题的时候,可以先解决所有的子问题的子问题,然后用每一个子问题的子问题序列的根节点来代替这整个子问题的子问题,然后用新的vector来存当前的子问题。于是新的当前子问题就没有更小的子问题了。

上面的图当所有子问题的子问题全部解决之后就变成了
在这里插入图片描述
上图中最后变换得到的序列用一个vector类型的len变量来存

同时就算当前子问题的所有更小的子问题被解决之后,此子问题中还存在未知的数(-1),为了能表示当前-1的位置,我们把这个-1重新赋值为1i    (i1)-1*i\ \ \ \ (i为此-1在原序列中的位置)这样既能表示这个值是未知的,也能表示这个值在原欧拉序中的位置情况

void solve(int l, int r, int tiny){//l,r表示子序列的左右位置,tiny表示递归此部分欧拉游览树的根节点序号 
	if(l > r)	return;
	vector<int> len;//len表示当前子问题中的更小的子问题被解决后的序列
	for(int i = l; i <= r; i++){//遍历原序列
		if(num[i] == -1){
			len.push_back(-i);//如果位置i的数是-1,用-i来进行标记 
		}
		else{
			len.push_back(num[i]);//如果位置i的数不是-1,说明值已确定,直接放入len中 
			for(int j = 0; j < position[num[i]].size()-1; j++){
				solve(position[num[i]][j]+1, position[num[i]][j+1]-1, num[i]);//继续递归解决所有根节点为num[i]子问题的子问题(子序列) 
			}
			i = position[num[i]].back();//由于中间一段的子序列已经被解决,所以把i跳到这一段的最后面 
		}
	}
	return;
}

经过上面 solvesolve 函数的处理,len 序列则相当于是一个最小的子问题序列(没有更小的子问题了),此时需要一个 handhand 函数来处理 len 序列中剩余的未知的数

假设hand所处理的一个len序列为
在这里插入图片描述
遍历此时的 len 序列,开始模拟栈,记得一开始要将 tiny 的值入栈,因为 len 序列中可能会出现与 tiny 相同的值。

如果遇到一个确定的数(正数)len[i]len[i],如果栈顶的下一个元素 Stack[Stack.size()2]Stack[Stack.size()-2] 与此元素相同,则需要把栈顶元素出栈。

if(len[i] > 0){//如果len[i]是已经确定的,则把它入栈 
	if(Stack.size() > 1 && Stack[Stack.size()-2] == len[i]){
	//如果栈顶元素和len[i]相同,则是ETT的出栈过程,所以len[i]不入栈,把栈顶元素出栈即可 
		Stack.pop_back();
	}
	else	Stack.push_back(len[i]);//否则把len[i]入栈 
}

如果遇到一个不确定的数(负数),那么就要考虑一些情况了:

1.这个数可能与序列后面出现的某个数相同,比如下面这种情况的欧拉序
在这里插入图片描述
第二个空(-2那个位置)的数只能填后面出现的4或者2,不能填未曾出现的5,那么通过观察发现,如果当前空 len[i]len[i] 要填后面所出现的某个数 len[j]len[j],我们先将当前的 len 序列写出另一个序列 x。

如果 len[i]len[i]是一个确定的数,那么 x[i]=1x[i]=1
否则 x[i]=0x[i]=0

得到 x 序列之后,我们再得到 x 序列的前缀和 sum 序列,sum 序列才是我们真正用到的,如下
在这里插入图片描述
回过头探讨刚刚的问题:如果当前空 len[i]len[i] 要填后面所出现的某个数 len[j]len[j],那么需要满足条件
(sum[j1]sum[i])2=ji      [ij(mod2)](sum[j-1]-sum[i])*2=j-i\ \ \ \ \ \ [i\equiv j\pmod{2}]
变形得
j2sum[j1]=i2sum[i]      [ij(mod2)]j-2*sum[j-1]=i-2*sum[i]\ \ \ \ \ \ [i\equiv j\pmod{2}]
其中 ii 是已知的,就是当前考虑的 len[i]len[i] 的位置,我们需要找的是序列后面满足条件的 len[j]len[j]jj 是未知的,使 len[i]=len[j]len[i]=len[j]

可以先遍历一次原序列,把所有已知的元素 len[i]len[i]ilen[i1]i-len[i-1] 按照 ii 的奇偶性分开放在两个set中,以后每次考虑当前 len[i]len[i] 是否可以与后面某个 len[j]len[j] 相同时,就对于set二分就可以了,用一个变量 pospos 来记录满足条件那个值的位置

typedef pair<int, int> P;
static set<P> line[2];//line[1]存len序列下标(i)为奇数的i-2*sum[i-1]并排序 , line[0]存len序列下标为偶数的i-2*sum[i-1]并排序 
//p.first表示i-2*sum[i-1]的值,p.second表示i的值
for(int i = 1; i < len.size(); i++){//遍历len序列 ,把sum序列和两个set序列确定下来 
	sum[i] = sum[i-1] + (len[i] > 0);//更新sum[i]的值 
	if(len[i] > 0){//如果len[i]的值是确定的(大于0) ,则把i-2*sum[i-1]插入set中 
		line[i&1].insert(P(i-2*sum[i-1], i));//i&1判断下标是奇数还是偶数 ,i表示位置 
	}
}
int pos = len.size();//先确定len[i]是否能与len序列后面某一个元素相同,pos存那个元素的位置,先初始化为len.size() 
while(1){//开始寻找 
	auto p = line[i&1].lower_bound(P(i-2*sum[i], 0));
	//需要寻找的那个元素满足两个条件,下标奇偶性相同,(sum[pos-1]-sum[i])*2=pos-i ,pos>i
	if(p == line[i&1].end() || (*p).first != i-2*sum[i])	break;//如果找到end()的位置还找不到,说明不存在,break 
	else if((*p).second < i){
	//如果找到的这个元素的位置比len[i]的位置(i)还前面,则删除这个元素,因为以后它都用不着 
		line[i&1].erase(p);
	}
	else{
	//满足条件,则把此元素的位置赋值给pos 
		pos = (*p).second;
		break;
	}
}

当前 len[i]len[i] 成功赋予一个值 len[pos]len[pos] 之后,还要判断 len 序列在 ii 位置之前有没有出现过与 len[pos]len[pos] 同样的值

此时需要用一个 visitvisit 数组来判断,用一个 timestimes变量来判断这是第几次执行 handhand 函数

遍历 lenlen 序列时,如果 visit[len[i]]!=timesvisit[len[i]] != times,那么就把 visit[len[i]]visit[len[i]] 的值赋为 timestimes,后面如果又遍历到相同的值 len[i]len[i] ,则此时 visit[len[i]]=timesvisit[len[i]] = times,表示 len 序列之前出现过 len[i]len[i]

if(pos < len.size()){//如果找到一个满足条件的元素 
	num[-len[i]] = len[pos];//那么原序列的这个未知的值赋值为len[pos] 
	if(visit[len[pos]] == times){
	//如果visit[len[pos]]等于times,表示在当前的len序列中之前已经出现len[pos],那就已知出栈知道栈顶为len[pos]即可 
		while(Stack.back() != len[pos])	Stack.pop_back();
	}
	else{
	//否则把visit[len[pos]]赋值为times,把len[pos]入栈 
		visit[len[pos]] = times;
		Stack.push_back(len[pos]);
	}
}

2.这个数可能与序列前面出现的某个数相同,最简单情况就是下面的欧拉序
在这里插入图片描述
最后那个-5的位置肯定填的是前面出现过的1,但是很容易发现,如果当前 len[i]len[i] 与 len 序列之前出现过的某个值相同,那么肯定与栈顶的下一个元素值相同,前提是栈内至少有两个元素。
(因为该出栈的都出栈了,上图中遍历到-5时栈内情况为1,3)

else{//如果找不到一个满足条件的元素 
	if(Stack.size() > 1){//如果栈内有剩余元素 ,就把原序列的这个未知的值赋值为栈顶的下一个元素(不能赋值为栈顶元素) 
		num[-len[i]] = Stack[Stack.size()-2];
		Stack.pop_back();//将栈顶元素出栈,模拟ETT遍历 
	}
}

3.这个数应该赋值为原欧拉序中从未出现过的数

所以之前要用一个vector类型的 cancan 变量来存这些数。这种情况发生在第二种情况栈内元素小于2的情况。

所以最后第一种情况没有发生的代码为

else{//如果找不到一个满足条件的元素 
	if(Stack.size() > 1){//如果栈内有剩余元素 ,就把原序列的这个未知的值赋值为栈顶的下一个元素(不能赋值为栈顶元素) 
		num[-len[i]] = Stack[Stack.size()-2];
		Stack.pop_back();//将栈顶元素出栈,模拟ETT遍历 
	}
	else{
		num[-len[i]] = can.back();//否则就把原序列的这个未知的值赋值为一个原序列未曾出现过的新元素 
		Stack.push_back(can.back());//把这个元素入栈,模拟ETT遍历 
		can.pop_back();//把这个新元素从can中移出,表示这个元素被使用过了 
	}
}

整个 handhand 函数为

void hand(vector<int> &len, int tiny){
	times ++;//次数加1 
	static vector<int> sum;//sum[i]表示从len[0]到len[1]有多少个不是-1的数 ,类似于前缀和 
	static vector<int> Stack;//Stack模拟欧拉游览树类似出栈入栈的过程 
	static set<P> line[2];//line[1]存len序列下标(i)为奇数的i-2*sum[i-1]并排序 , line[0]存len序列下标为偶数的i-2*sum[i-1]并排序 
	Stack.clear();Stack.push_back(tiny);//清空Stack,并把tiny先入栈 
	line[0].clear();line[1].clear();//清空 
	sum.clear();//清空 
	sum.resize(len.size());//sum的大小根据当前考虑的序列len来决定 
	sum[0] = (len[0] > 0);//先求出sum[0]的值,根据len[0]来决定 
	for(int i = 1; i < len.size(); i++){//遍历len序列 ,把sum序列和两个set序列确定下来 
		sum[i] = sum[i-1] + (len[i] > 0);//更新sum[i]的值 
		if(len[i] > 0){//如果len[i]的值是确定的(大于0) ,则把i-2*sum[i-1]插入set中 
			line[i&1].insert(P(i-2*sum[i-1], i));//i&1判断下标是奇数还是偶数 ,i表示位置 
		}
	}
	for(int i = 0; i < len.size(); i++){//遍历 len序列,把里面没有确定的值确定下来 
		if(len[i] > 0){//如果len[i]是已经确定的,则把它入栈 
			if(Stack.size() > 1 && Stack[Stack.size()-2] == len[i]){
			//如果栈顶元素和len[i]相同,则是ETT的出栈过程,所以len[i]不入栈,把栈顶元素出栈即可 
				Stack.pop_back();
			}
			else	Stack.push_back(len[i]);//否则把len[i]入栈 
		}
		else{//如果len[i]没有被确定 
			int pos = len.size();//先确定len[i]是否能与len序列后面某一个元素相同,pos存那个元素的位置,先初始化为len.size() 
			while(1){//开始寻找 
				auto p = line[i&1].lower_bound(P(i-2*sum[i], 0));
				//需要寻找的那个元素满足两个条件,下标奇偶性相同,(sum[pos-1]-sum[i])*2=pos-i ,pos>i
				if(p == line[i&1].end() || (*p).first != i-2*sum[i])	break;//如果找到end()的位置还找不到,说明不存在,break 
				else if((*p).second < i){
				//如果找到的这个元素的位置比len[i]的位置(i)还前面,则删除这个元素,因为以后它都用不着 
					line[i&1].erase(p);
				}
				else{
				//满足条件,则把此元素的位置赋值给pos 
					pos = (*p).second;
					break;
				}
			}
			if(pos < len.size()){//如果找到一个满足条件的元素 
				num[-len[i]] = len[pos];//那么原序列的这个未知的值赋值为len[pos] 
				if(visit[len[pos]] == times){
				//如果visit[len[pos]]等于times,表示在当前的len序列中之前已经出现len[pos],栈内肯定存在len[pos]这个值,那就一直出栈知道栈顶为len[pos]即可 
					while(Stack.back() != len[pos])	Stack.pop_back();
				}
				else{
				//否则把visit[len[pos]]赋值为times,把len[pos]入栈 
					visit[len[pos]] = times;
					Stack.push_back(len[pos]);
				}
			}
			else{//如果找不到一个满足条件的元素 
				if(Stack.size() > 1){//如果栈内有剩余元素 ,就把原序列的这个未知的值赋值为栈顶的下一个元素(不能赋值为栈顶元素) 
					num[-len[i]] = Stack[Stack.size()-2];
					Stack.pop_back();//将栈顶元素出栈,模拟ETT遍历 
				}
				else{
					num[-len[i]] = can.back();//否则就把原序列的这个未知的值赋值为一个原序列未曾出现过的新元素 
					Stack.push_back(can.back());//把这个元素入栈,模拟ETT遍历 
					can.pop_back();//把这个新元素从can中移出,表示这个元素被使用过了 
				}
			}
		}
	}
	return;
}

代码如下:

#include <iostream>
#include <vector>
#include <set>
using namespace std;
typedef pair<int, int> P;
const int maxn = 5e5+5;
int T, num[maxn], n, times = 0;//T表示T组输入,num存欧拉序的数,n表示1~n的欧拉序,times表示第几次进行hand函数处理 
vector<int> position[maxn];//position[i]表示数字i在欧拉序中出现的位置 
vector<int> can;//can存1~n中没有出现过的数 
int visit[maxn];
void hand(vector<int> &len, int tiny){
	times ++;//次数加1 
	static vector<int> sum;//sum[i]表示从len[0]到len[1]有多少个不是-1的数 ,类似于前缀和 
	static vector<int> Stack;//Stack模拟欧拉游览树类似出栈入栈的过程 
	static set<P> line[2];//line[1]存len序列下标(i)为奇数的i-2*sum[i-1]并排序 , line[0]存len序列下标为偶数的i-2*sum[i-1]并排序 
	Stack.clear();Stack.push_back(tiny);//清空Stack,并把tiny先入栈 
	line[0].clear();line[1].clear();//清空 
	sum.clear();//清空 
	sum.resize(len.size());//sum的大小根据当前考虑的序列len来决定 
	sum[0] = (len[0] > 0);//先求出sum[0]的值,根据len[0]来决定 
	for(int i = 1; i < len.size(); i++){//遍历len序列 ,把sum序列和两个set序列确定下来 
		sum[i] = sum[i-1] + (len[i] > 0);//更新sum[i]的值 
		if(len[i] > 0){//如果len[i]的值是确定的(大于0) ,则把i-2*sum[i-1]插入set中 
			line[i&1].insert(P(i-2*sum[i-1], i));//i&1判断下标是奇数还是偶数 ,i表示位置 
		}
	}
	for(int i = 0; i < len.size(); i++){//遍历 len序列,把里面没有确定的值确定下来 
		if(len[i] > 0){//如果len[i]是已经确定的,则把它入栈 
			if(Stack.size() > 1 && Stack[Stack.size()-2] == len[i]){
			//如果栈顶元素和len[i]相同,则是ETT的出栈过程,所以len[i]不入栈,把栈顶元素出栈即可 
				Stack.pop_back();
			}
			else	Stack.push_back(len[i]);//否则把len[i]入栈 
		}
		else{//如果len[i]没有被确定 
			int pos = len.size();//先确定len[i]是否能与len序列后面某一个元素相同,pos存那个元素的位置,先初始化为len.size() 
			while(1){//开始寻找 
				auto p = line[i&1].lower_bound(P(i-2*sum[i], 0));
				//需要寻找的那个元素满足两个条件,下标奇偶性相同,(sum[pos-1]-sum[i])*2=pos-i ,pos>i
				if(p == line[i&1].end() || (*p).first != i-2*sum[i])	break;//如果找到end()的位置还找不到,说明不存在,break 
				else if((*p).second < i){
				//如果找到的这个元素的位置比len[i]的位置(i)还前面,则删除这个元素,因为以后它都用不着 
					line[i&1].erase(p);
				}
				else{
				//满足条件,则把此元素的位置赋值给pos 
					pos = (*p).second;
					break;
				}
			}
			if(pos < len.size()){//如果找到一个满足条件的元素 
				num[-len[i]] = len[pos];//那么原序列的这个未知的值赋值为len[pos] 
				if(visit[len[pos]] == times){
				//如果visit[len[pos]]等于times,表示在当前的len序列中之前已经出现len[pos],栈内肯定存在len[pos]这个值,那就一直出栈知道栈顶为len[pos]即可 
					while(Stack.back() != len[pos])	Stack.pop_back();
				}
				else{
				//否则把visit[len[pos]]赋值为times,把len[pos]入栈 
					visit[len[pos]] = times;
					Stack.push_back(len[pos]);
				}
			}
			else{//如果找不到一个满足条件的元素 
				if(Stack.size() > 1){//如果栈内有剩余元素 ,就把原序列的这个未知的值赋值为栈顶的下一个元素(不能赋值为栈顶元素) 
					num[-len[i]] = Stack[Stack.size()-2];
					Stack.pop_back();//将栈顶元素出栈,模拟ETT遍历 
				}
				else{
					num[-len[i]] = can.back();//否则就把原序列的这个未知的值赋值为一个原序列未曾出现过的新元素 
					Stack.push_back(can.back());//把这个元素入栈,模拟ETT遍历 
					can.pop_back();//把这个新元素从can中移出,表示这个元素被使用过了 
				}
			}
		}
	}
	return;
}
void solve(int l, int r, int tiny){//l,r表示子序列的左右位置,tiny表示递归此部分欧拉游览树的根节点序号 
	if(l > r)	return;
	vector<int> len;//len来存这一子序列中-1被替代后的结果子序列 
	for(int i = l; i <= r; i++){
		if(num[i] == -1){
			len.push_back(-i);//如果位置i的数是-1,用-i来进行标记 
		}
		else{
			len.push_back(num[i]);//如果位置i的数不是-1,说明值已确定,直接放入len中 
			for(int j = 0; j < position[num[i]].size()-1; j++){
				solve(position[num[i]][j]+1, position[num[i]][j+1]-1, num[i]);//继续递归解决子问题的子问题(子序列) 
			}
			i = position[num[i]].back();//由于中间一段的子序列已经被解决,所以把i跳到这一段的最后面 
		}
	}
	hand(len, tiny);//hand函数处理len中的负数 
	return;
}
int main(){
	cin >> T;//一共T组输入 
	while(T--){
		scanf("%d", &n);
		for(int i = 1; i <= 2*n-1; i++){//输入欧拉序 
			scanf("%d", &num[i]);
		}
		num[1] = num[2*n-1] = 1;//欧拉序开始和结束一定是1 
		for(int i = 1; i <= 2*n-1; i++){
			if(num[i] != -1)	position[num[i]].push_back(i);//把所有不是-1的数位置提取出来 
		}
		for(int i = 1; i <= n; i++){
			if(!position[i].size())	can.push_back(i);//把所有没有使用出现过的数提取出来 
		}
		for(int i = 0; i < position[1].size() - 1; i++){
			solve(position[1][i]+1, position[1][i+1]-1, 1);//递归解决每一个子问题 (子串) 
		}
		for(int i = 1; i <= 2*n-1; i++){
			printf("%d ", num[i]);//输出已经处理后的原序列 
		}
		for(int i = 1; i <= n; i++){
			position[i].clear();//清空 
		} 
		can.clear();//清空 
		printf("\n");
	}
	return 0;
} 
发布了49 篇原创文章 · 获赞 91 · 访问量 3985
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章