• [luogu p5658] 括号树


    传送门

    括号树

    题目背景

    本题中合法括号串的定义如下:

    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)(1leq ileq n))求出,(s_i) 中有多少个互不相同的子串合法括号串

    这个问题难倒了小 Q,他只好向你求助。设 (s_i) 共有 (k_i) 个不同子串是合法括号串, 你只需要告诉小 Q 所有 (i imes k_i) 的异或和,即:

    [(1 imes k_1) ext{xor} (2 imes k_2) ext{xor} (3 imes k_3) ext{xor} cdots ext{xor} (n imes 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 ightarrow 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 imes 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 ightarrow 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
    ", 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
    ", 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
    ", ans);
        return 0;
    }   
    

    评测记录

    评测记录

    后记 & 有感

    遇到这种一眼没正解的题目,不要慌,

    一定要从部分分入手,逐渐进军

    比如本题就是从 链暴力->链优化->正解 逐一击破的

    最后,在CSP的考场上,一定别轻言放弃

    CSP给的时间是足够的,

    所以一开始直接想满分的,是非常愚蠢的做法,除非一眼正解。

    另外,注意数据范围是不是要开longlong,

    最后CSP临近,东北小蟹蟹祝大家(CSP_{2020}++)

    本篇题解花了我两个小时的时间,是我写过的最详细的一篇题解了吧。233

  • 相关阅读:
    【转载】如何学JavaScript?前辈的经验之谈
    javascript基础学习心得01
    CSS之浮动
    HTML5基础学习心得
    如何使用HTML5的canvas属性
    HTML标记语言
    HTML+CSS学习
    线性表
    (2)协程之 greenlet模块
    (1)协程之理论
  • 原文地址:https://www.cnblogs.com/crab-in-the-northeast/p/luogu-p5658.html
Copyright © 2020-2023  润新知