首先發出題目鏈接:
鏈接: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重新賦值爲這樣既能表示這個值是未知的,也能表示這個值在原歐拉序中的位置情況。
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;
}
經過上面 函數的處理,len 序列則相當於是一個最小的子問題序列(沒有更小的子問題了),此時需要一個 函數來處理 len 序列中剩餘的未知的數。
假設hand所處理的一個len序列爲
遍歷此時的 len 序列,開始模擬棧,記得一開始要將 tiny 的值入棧,因爲 len 序列中可能會出現與 tiny 相同的值。
如果遇到一個確定的數(正數),如果棧頂的下一個元素 與此元素相同,則需要把棧頂元素出棧。
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 序列寫出另一個序列 x。
如果 是一個確定的數,那麼
否則
得到 x 序列之後,我們再得到 x 序列的前綴和 sum 序列,sum 序列纔是我們真正用到的,如下
回過頭探討剛剛的問題:如果當前空 要填後面所出現的某個數 ,那麼需要滿足條件
變形得
其中 是已知的,就是當前考慮的 的位置,我們需要找的是序列後面滿足條件的 , 是未知的,使
可以先遍歷一次原序列,把所有已知的元素 的 按照 的奇偶性分開放在兩個set中,以後每次考慮當前 是否可以與後面某個 相同時,就對於set二分就可以了,用一個變量 來記錄滿足條件那個值的位置
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 序列在 位置之前有沒有出現過與 同樣的值
此時需要用一個 數組來判斷,用一個 變量來判斷這是第幾次執行 函數
遍歷 序列時,如果 ,那麼就把 的值賦爲 ,後面如果又遍歷到相同的值 ,則此時 ,表示 len 序列之前出現過
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 序列之前出現過的某個值相同,那麼肯定與棧頂的下一個元素值相同,前提是棧內至少有兩個元素。
(因爲該出棧的都出棧了,上圖中遍歷到-5時棧內情況爲1,3)
else{//如果找不到一個滿足條件的元素
if(Stack.size() > 1){//如果棧內有剩餘元素 ,就把原序列的這個未知的值賦值爲棧頂的下一個元素(不能賦值爲棧頂元素)
num[-len[i]] = Stack[Stack.size()-2];
Stack.pop_back();//將棧頂元素出棧,模擬ETT遍歷
}
}
3.這個數應該賦值爲原歐拉序中從未出現過的數
所以之前要用一個vector類型的 變量來存這些數。這種情況發生在第二種情況棧內元素小於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中移出,表示這個元素被使用過了
}
}
整個 函數爲
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;
}