[luogu p5658] 括號樹
括號樹
題目背景
本題中合法括號串的定義如下:
()
是合法括號串。- 如果
A
是合法括號串,則(A)
是合法括號串。 - 如果
A
,B
是合法括號串,則AB
是合法括號串。
本題中子串與不同的子串的定義如下:
- 字串
S
的子串是S
中連續的任意個字元組成的字串。S
的子串可用起始位置 \(l\) 與終止位置 \(r\) 來表示,記為 \(S (l, r)\)(\(1 \leq l \leq r \leq |S |\),\(|S |\) 表示 S 的長度)。 S
的兩個子串視作不同當且僅當它們在S
中的位置不同,即 \(l\) 不同或 \(r\) 不同。
題目描述
一個大小為 \(n\)
小 Q 是一個充滿好奇心的小朋友,有一天他在上學的路上碰見了一個大小為 \(n\) 的樹,樹上結點從 \(1\) ∼ \(n\) 編號,\(1\) 號結點為樹的根。除 \(1\) 號結點外,每個結點有一個父親結點,\(u\)(\(2 \leq u \leq n\))號結點的父親為 \(f_u\)(\(1 ≤ f_u < u\))號結點。
小 Q 發現這個樹的每個結點上恰有一個括號,可能是(
或)
。小 Q 定義 \(s_i\) 為:將根結點到 \(i\) 號結點的簡單路徑上的括號,按結點經過順序依次排列組成的字串。
顯然 \(s_i\) 是個括號串,但不一定是合法括號串,因此現在小 Q 想對所有的 \(i\)(\(1\leq i\leq n\))求出,\(s_i\) 中有多少個互不相同的子串是合法括號串。
這個問題難倒了小 Q,他只好向你求助。設 \(s_i\) 共有 \(k_i\) 個不同子串是合法括號串, 你只需要告訴小 Q 所有 \(i \times k_i\) 的異或和,即:
\[(1 \times k_1)\ \text{xor}\ (2 \times k_2)\ \text{xor}\ (3 \times k_3)\ \text{xor}\ \cdots\ \text{xor}\ (n \times k_n) \]其中 \(xor\) 是位異或運算。
輸入輸出格式
輸入格式
第一行一個整數 \(n\),表示樹的大小。
第二行一個長為 \(n\) 的由(
與)
組成的括號串,第 \(i\) 個括號表示 \(i\) 號結點上的括號。
第三行包含 \(n − 1\) 個整數,第 \(i\)(\(1 \leq i \lt n\))個整數表示 \(i + 1\) 號結點的父親編號 \(f_{i+1}\)。
輸出格式
僅一行一個整數表示答案。
輸入輸出樣例
輸入樣例 #1
5
(()()
1 1 2 2
輸出樣例 #1
6
說明
【樣例解釋1】
樹的形態如下圖:
將根到 1 號結點的簡單路徑上的括號,按經過順序排列所組成的字串為 (
,子串是合法括號串的個數為 \(0\)。
將根到 2 號結點的字串為 ((
,子串是合法括號串的個數為 \(0\)。
將根到 3 號結點的字串為 ()
,子串是合法括號串的個數為 \(1\)。
將根到 4 號結點的字串為 (((
,子串是合法括號串的個數為 \(0\)。
將根到 5 號結點的字串為 (()
,子串是合法括號串的個數為 \(1\)。
【資料範圍】
CSP 2019 D1T2
分析
刷基礎數學題單刷累了,來水水去年提高D1T2。
去年我還是一個菜雞,連暴力都不會,但現在的我,哼哼,啊啊啊啊啊啊啊啊啊早已不是當年那個菜雞了,是比當年還菜的菜雞,再看到這道題我就有思路了。
做這種題,如果不是一眼正解的,一定要從部分分出發,慢慢向滿分進軍。
純鏈暴力
觀察到資料範圍中有一個 \(f_i = i - 1\)。這是什麼意思呢?也就是說這棵樹是一條鏈。
問題就可以轉化為,給你一個括號串 \(S\),對於每一個 \(S(1, i)\),求該子串的合法括號子串數量,並進行一些奇怪的運算(就是那個什麼xor的)。
奇怪的運算不是重點,重點是我們怎麼求重點加粗的內容。
顯然我們可以暴力:列舉 \(i\) 從 \(1 \rightarrow n\),用來鎖定 \(S(1, i)\),然後再在該子串中列舉左邊界和右邊界 \(l\) 和 \(r\),鎖定 \(S(l, r)\) 這個子串,接著再判斷這個字串是否是合法括號串。核心就是以上,至於計算什麼的就略了。
\(i, l, r\) 三重迴圈,裡面再套一個判斷合法括號串,時間複雜度為 \(\operatorname{O}(n ^ 4)\)。來看看這個時間複雜度能過多少資料:
\(n \le 8\) 的肯定有戲。能通過1,2資料。
\(n \le 200\) 的話……如果是嚴格 \(n ^ 4\) 應該會T的,但是實際上演算法跑不滿 \(n ^ 4\),所以有望能通過,但不知可否。
\(n \le 2000\) 及更高的就洗洗睡吧。這個 \(n ^2\) 都懸。
在CSP考場上,你就可以這樣估分了。該演算法最低10pts,最高20pts。當然前提是不寫掛。
但是既然我們不在CSP考場,那咱們不妨試一試能得多少分:
(dbxxx花了5分鐘打暴力,20分鐘調bug後)
真不戳,得到了20pts,是我們的期望最高值。
但是咱們肯定不能滿足在20pts停滯不前呀!繼續!
純鏈演算法優化
如果你是一個細心的孩子,你會發現純鏈的資料點有 \(11\) 個,而我們通過了其中的 \(4\) 個,如果我們能再拿下剩下那 \(7\) 個,能得到 \(55 pts\)。這是非常友好的一個分了。
怎麼優化呢?
觀察資料範圍是 \(5 \times 10 ^ 5\),要不然 \(n \log n\),要不然 \(n\)。
\(\log\) 演算法有點懸,不妨看看 \(n\)。
每次暴力算 \(k_i\) 太麻煩了,我們能不能用遞推求 \(k_i\) 呢?
來舉幾個例子:
例子1
()()()
定義 \(con_i\) 為 \(i\) 對 \(k_i\) 的貢獻值。
顯然,當 \(i \le j\) 時,\(k_i \le k_j\),說明我們應該從前往後推。
- 當 \(i = 1\) 時,只有一個左括號,沒有辦法形成合法括號序列。因此 \(con_1 = 0\)。
- 當 \(i = 2\) 時,發現了一個新合法括號序列 \(S(1, 2)\),\(con_2 = 1\)。
- 當 \(i = 3\) 時,沒有發現新的合法括號序列。\(con_3 = 0\)。
- 當 \(i = 4\) 時,發現了兩個新合法括號序列 \(S(1, 4)\) 和 \(S(3, 4)\)。因此 \(con_4 = 2\)。
- 當 \(i = 5\) 時,沒有發現新的合法括號序列。\(con_5 = 0\)。
- 當 \(i = 6\) 時,發現了三個新合法括號序列 \(S(1, 6)\)、\(S(3, 6)\) 和 \(S(5, 6)\)。因此 \(con_6 = 3\)。
\(con\) 陣列的值為:
\(0, 1, 0, 2, 0, 3\)。
根據 \(k_i = \sum _{i = 1} ^ kcon_i\),可得 \(k\) 陣列的值為:\(0, 1, 1, 3, 3, 6\)。
另外我們還發現,\(S_i =\)( 的時候,\(con_i = 0\)。原因很簡單,在後面插一個左括號,不可能讓合法括號序列的數量增加。
因此此後的舉例中,將略過這些左括號的 \(con\) 值計算。
例子2
這次我們來在中間插個障礙吧:
())()
- 當 \(i = 2\) 時,發現了一個新合法括號序列 \(S(1, 2)\),\(con_2 = 1\)。
- 當 \(i = 3\) 時,沒有發現新的合法括號序列。\(con_3 = 0\)。
- 當 \(i = 5\) 時,發現了一個新合法括號序列 \(S(4, 5)\),\(con_5 = 1\)。
你會發現,因為中間那個突兀的右括號存在,\(S(1, 5)\) 不再是一個合法括號序列了。
記住這個例子哦。
\(con\) 陣列的值為:\(0, 1, 0, 0, 1\);
\(k\) 陣列的值為:\(0, 1, 1, 1, 2\)。
例子3
這次我們在中間插個反的:
()(()
- 當 \(i = 2\) 時,發現了一個新合法括號序列 \(S(1, 2)\),\(con_2 = 1\)。
- 當 \(i = 5\) 時,發現了一個新合法括號序列 \(S(4, 5)\),\(con_5 = 1\)。
\(con\) 陣列的值為:\(0, 1, 0, 0, 1\);
\(k\) 陣列的值為:\(0, 1, 1, 1, 2\)。
和上面那個差不多,只是中間那個括號反了而已,省了一個計算 \(con\)。
例子4
合法括號串還可以巢狀,我們來看看巢狀的情況如何。
()(())
- 當 \(i = 2\) 時,發現了一個新合法括號序列 \(S(1, 2)\),\(con_2 = 1\)。
- 當 \(i = 5\) 時,發現了一個新合法括號序列 \(S(4, 5)\),\(con_5 = 1\)。
在 \(i = 5\) 前,你會發現這就是例子3。但是到 \(i = 6\) 呢?
- 當 \(i = 6\) 時,發現了兩個新合法括號序列 \(S(1, 6)\) 和 \(S(3, 6)\),\(con_6 = 2\)。
\(con\) 陣列的值為:\(0, 1, 0, 0, 1, 2\);
\(k\) 陣列的值為:\(0, 1, 1, 1, 2, 4\)。
好了,例子都舉完了,你發現什麼規律了嗎?
對於一個匹配了的右括號 \(R_1\),如果這個匹配的括號的左括號 \(L_1\) 的左邊,還有一個匹配了的 右括號 \(R_2\)(這句話有點繞,稍微理解下),那麼 \(R_1\) 的 \(con\) 值,等於 \(R_2\) 的 \(con\) 值 \(+1\)(即 \(con_{R_1} = con_{R_2} + 1\))。
如何理解這個規律呢?
來看看合法括號串的第三條性質(就在題面的最上面):
如果
A
,B
是合法括號串,則AB
是合法括號串。
\(L_1 \sim R_1\) 代表的這個括號串就相當於性質中的 \(B\) 串;\(L_2 \sim R_2\) 代表的括號串就相當於性質中的 \(A\) 串。如果 \(R_2\) 在 \(L_1\) 的左邊且位置是挨著的,那麼 \(AB\) 是合法括號串,因此 \(R_1\) 的貢獻值還得多加一個 \(AB\) 這個串。
此處請一定要理解透徹,再慢理解也一定要理解透徹,因為這個規律對遞推出結果非常重要,可以說是該演算法的心臟。
好了,現在所有的問題都被解決了。就差一個問題:
我找到 \(R_1\) 了,我怎麼找 \(L_1\)?壓入棧的是括號,也沒法找位置啊?
少年,思想別那麼固執。既然要找位置,那麼就把壓括號改成壓位置不就完了嗎?
核心程式碼如下:
for (int i = 1; i <= n; ++i) {
if (str[i] == '(')
bra.push(i);
else if (!bra.empty()){
num = bra.top();
bra.pop();
con[i] = con[num - 1] + 1;
}
k[i] = k[i - 1] + con[i];
}
時間複雜度 \(\operatorname{O}(n)\)。
和預期一樣,能獲得 \(55pts\) 的友好分。評測記錄
正解
接下來我們就來進軍正解吧。
從鏈變到樹,con[i] = con[num - 1] + 1;
這個遞推式就有問題了。因為在鏈中,編號是連續的,因此我們可以直接num - 1
。但是樹的編號可就不連續了。
那麼怎麼改遞推式呢?其實非常簡單,把num - 1
改成fa[num]
就可以了。
很好理解,因為括號樹是從根不斷往下的,因此如果想找一個節點的上一個,顯然就是其父親節點。鏈中的num - 1
其實就相當於fa[num]
嘛!
當然了,\(k_i\) 的遞推式也得改了,k[i] = k[i - 1] + con[i]
應該變為 k[i] = k[fa[i]] + con[i]
。
但是這樣就結束了嗎?
鏈的遍歷直接從 \(1 \rightarrow n\) 就可以了,但是樹的遍歷有回溯。那麼在進行dfs的過程中,棧也得復原。
這樣,我們就能拿到 \(100pts\) 的滿分了!
核心程式碼如下:
void dfs(int u) {
int num = 0;
if (str[u] == '(')
bra.push(u);
else if (!bra.empty()){
num = bra.top();
bra.pop();
con[u] = con[fa[num]] + 1;
}
k[u] = k[fa[u]] + con[u];
for (int i = 0; i < G[u].size(); ++i)
dfs(G[u][i]);
if (num != 0)
bra.push(num);
else if (!bra.empty())
bra.pop();
//復原
//就是上邊棧的操作反過來就可以了
return ;
}
最後的最後,別忘了,不開longlong見祖宗!!!!!!!
好了,接下來我們來上一下三種方法的整體程式碼,供大家參考。
程式碼
\(\operatorname{O}(n ^ 4)\),僅支援鏈演算法程式碼
/*
* @Author: crab-in-the-northeast
* @Date: 2020-10-04 12:10:42
* @Last Modified by: crab-in-the-northeast
* @Last Modified time: 2020-10-04 13:35:50
*/
#include <iostream>
#include <cstdio>
#include <string>
typedef long long ll;
const int maxn = 500005;//儘管我們知道該演算法通過不了500005數量級的,但maxn建議還是開到最大值。萬一能通過比200大的資料,但是因為開的不夠大掛了,那就很可惜了。
int fa[maxn];
ll k[maxn];
bool check(std :: string str) {
//std :: cout << str << std :: endl;
int status = 0;
for (int i = 0; i < str.length(); ++i) {
if (str[i] == '(') ++status;
else --status;
if (status < 0) return false;
}
if (status == 0) return true;
return false;
}//判斷合法括號串可以看P1739。
int main() {
int n;
std :: string str;
std :: scanf("%d", &n);
std :: cin >> str;
str = ' ' + str;
for (int i = 2; i <= n; ++i)
std :: scanf("%d", &fa[i]);
for (int i = 1; i <= n; ++i)
for (int l = 1; l <= i; ++l)
for (int r = l; r <= i; ++r)
if (check(str.substr(l, r - l + 1)))
++k[i];
//for (int i = 1; i <= n; ++i)
//std :: printf("%lld ", k[i]);
ll ans = 0;
for (int i = 1; i <= n; ++i)
ans ^= k[i] * ll(i);
std :: printf("%lld\n", ans);
return 0;
}
\(\operatorname{O}(n)\),僅支援鏈演算法程式碼
/*
* @Author: crab-in-the-northeast
* @Date: 2020-10-04 12:10:42
* @Last Modified by: crab-in-the-northeast
* @Last Modified time: 2020-10-04 12:32:26
*/
#include <iostream>
#include <cstdio>
#include <string>
#include <stack>
typedef long long ll;
const int maxn = 500005;
int fa[maxn];
ll k[maxn], con[maxn];
std :: stack <int> bra;
int main() {
int n;
std :: string str;
std :: scanf("%d", &n);
std :: cin >> str;
str = ' ' + str;
for (int i = 2; i <= n; ++i)
std :: scanf("%d", &fa[i]);
for (int i = 1; i <= n; ++i) {
if (str[i] == '(')
bra.push(i);
else if (!bra.empty()){
int num = bra.top();
bra.pop();
con[i] = con[num - 1] + 1;
}
k[i] = k[i - 1] + con[i];
}
//for (int i = 1; i <= n; ++i)
//std :: printf("%lld ", k[i]);
ll ans = 0;
for (int i = 1; i <= n; ++i)
ans ^= k[i] * ll(i);
std :: printf("%lld\n", ans);
return 0;
}
\(\operatorname{O}(n)\),滿分演算法
/*
* @Author: crab-in-the-northeast
* @Date: 2020-10-04 10:27:40
* @Last Modified by: crab-in-the-northeast
* @Last Modified time: 2020-10-04 11:53:50
*/
#include <iostream>
#include <cstdio>
#include <vector>
#include <stack>
typedef long long ll;
const int maxn = 500005;
std :: vector <int> G[maxn];
std :: stack <int> bra;
char str[maxn];
int fa[maxn];
ll con[maxn], k[maxn];
void dfs(int u) {
int num = 0;
if (str[u] == '(')
bra.push(u);
else if (!bra.empty()){
num = bra.top();
bra.pop();
con[u] = con[fa[num]] + 1;
}
k[u] = k[fa[u]] + con[u];
for (int i = 0; i < G[u].size(); ++i)
dfs(G[u][i]);
if (num != 0)
bra.push(num);
else if (!bra.empty())
bra.pop();
return ;
}
int main() {
int n;
std :: scanf("%d", &n);
std :: scanf("%s", str + 1);
for (int i = 2; i <= n; ++i) {
std :: scanf("%d", &fa[i]);
G[fa[i]].push_back(i);
}
ll ans = 0;
dfs(1);
for (int i = 1; i <= n; ++i)
ans ^= k[i] * (ll)i;
std :: printf("%lld\n", ans);
return 0;
}
評測記錄
後記 & 有感
遇到這種一眼沒正解的題目,不要慌,
一定要從部分分入手,逐漸進軍,
比如本題就是從 鏈暴力->鏈優化->正解 逐一擊破的
最後,在CSP的考場上,一定別輕言放棄
CSP給的時間是足夠的,
所以一開始直接想滿分的,是非常愚蠢的做法,除非一眼正解。
另外,注意資料範圍是不是要開longlong,
最後CSP臨近,東北小蟹蟹祝大家\(CSP_{2020}++\)!
本篇題解花了我兩個小時的時間,是我寫過的最詳細的一篇題解了吧。233