1. 程式人生 > 其它 >Spring中沒有Resource、PostConstruct、PreDestroy註解

Spring中沒有Resource、PostConstruct、PreDestroy註解

技術標籤:演算法字串資料結構java

傳送門

括號樹

題目背景

本題中合法括號串的定義如下:

  1. () 是合法括號串。
  2. 如果 A 是合法括號串,則 (A) 是合法括號串。
  3. 如果 AB 是合法括號串,則 AB 是合法括號串。

本題中子串不同的子串的定義如下:

  1. 字串 S 的子串是 S連續的任意個字元組成的字串。S 的子串可用起始位置 \(l\) 與終止位置 \(r\) 來表示,記為 \(S (l, r)\)(\(1 \leq l \leq r \leq |S |\)\(|S |\) 表示 S 的長度)。
  2. S 的兩個子串視作不同當且僅當它們在 S 中的位置不同,即 \(l\) 不同或 \(r\)
    不同。

題目描述

一個大小為 \(n\) 的樹包含 \(n\) 個結點和 \(n − 1\) 條邊,每條邊連線兩個結點,且任意兩個結點間有且僅有一條簡單路徑互相可達。

小 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後)

R39230100

真不戳,得到了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\))。

如何理解這個規律呢?

來看看合法括號串的第三條性質(就在題面的最上面):

如果 AB 是合法括號串,則 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