NERC 12 H. Hyperdrome

14 篇文章 0 订阅
3 篇文章 0 订阅

Hypergnome planet is famous for its Great Universal Games between gnomes — the Games between gnomes from each part of the galaxy in various disciplines.
The most popular discipline in the Games is the Hyperdrome discipline. The rules are the follows: one string of length n is given to all gnomes. The gnomes shall find, as fast as they can, the total number of Hyperdrome substrings — such strings that characters inside the string can be rearranged to get a palindrome.
Substring is defined as a sequence of characters from position i to position j inclusive, where 1 ≤ i ≤ j ≤ n.
Substrings with different pairs of positions (i, j) are considered different regardless of their contents.
Palindrome is defined as a string x1x2…xl, where xi = xl−i+1 for all 1 ≤ i ≤ l.
Judges choose a string and your task is to help them find the answer.
The gnome alphabet consists of lowercase and uppercase English letters — ‘a’–‘z’ and ‘A’–‘Z’ where letters in different case are considered to be different letters.

Input
The first line of the input file contains a single integer n (1 ≤ n ≤ 3 · 105).
The second line of the input file contains the string for Hyperdrome discipline — n lowercase or uppercase English letters.

Output
Output the answer for the Hyperdrome discipline — the number of Hyperdrome substrings in the input string.

题目大意:给你一个长度为n的字符串,要求输出该字符串的所有子串有多少个可以经过重新排序构成回文串。

输入是字符串的题目,如果不允许你重新排序,那就是字符串的问题,如果允许你重新排序,则一般可以通过对字母计数变成区间问题。

由于可以自由排序,这题还是应该从字母数的角度切入。

当一个区间内字母每种都是偶数时,一定可以成为回文串,无论有多少种字母,都可以通过“每个字母一边一个”来组成回文串。

比如2个a和2个b,可以组成“abba”。

当区间内奇数个字母只有一种的时候,可以把它放在中间,也可以组成回文串,比如“abcba”。

但是当奇数个字母大于一种的时候,无论怎样都无法组成回文串。

所以这个问题就成功转变成了“判断一个区间内的奇数个字母种类有多少”。

由于这题大小写敏感,所以一共有26 + 26 = 52种字母,可以压缩在一个2^56的数上,以二进制形式表示,每一位的0表示偶数个或没有,1则表示奇数个。

之后,可以通过一些神奇的操作来计算究竟有多少个子串是回文串,先上代码:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;

const int maxn = 3 * 1e5 + 5;

int main() {
    // freopen("hyperdrome.in", "r", stdin);
    // freopen("hyperdrome.out", "w", stdout);
    int n;
    char str[maxn];
    unordered_map<ll, int> um;
    scanf("%d%s", &n, str + 1);

    ll s = 0, ans = 0, a = 0;
    um[0] = 1;

    for(int i = 1; i <= n; i++) {
        if(islower(str[i])) {
            a = str[i] - 'a';
        } else {
            a = str[i] - 'A' + 26;
        }

        s ^=   1LL << a;
        for(int j = 0; j < 52; j++) {
            if(um.count(s ^ (1LL << j))) {
                ans += um[s ^ (1LL << j)];
            }
        }

        if(um.count(s)) {
            ans += um[s];
        }

        um[s]++;

    }
    printf("%lld\n", ans);
    return 0;
}

解释一下这段代码,这段代码很短,而且干的事情非常简单:

首先,在unordered_map(哈希表)上事先把状态0(这里和下文提到的状态都是二进制)的计数加一。
然后遍历字符串,把‘a’-‘z’的字符修改成0 - 25,‘A’-‘Z’的字符修改成26 - 51,一个一个压缩(加进)数s上,s即为遍历过程中的当前状态。
接着枚举52种状态,和当前状态进行异或操作,如果异或后的状态曾经出现过,则对答案计数加上该状态出现过的次数。
枚举结束后,再判断当前状态是否出现过,如果出现过,则对答案计数加上当前状态出现过的次数。
最后,在哈希表中对当前状态计数加一(上面提到的“曾经出现过的状态”,除开状态0,全都是在这一步进行记录的)。

代码非常玄学,仅仅看代码完全不知所以,实际上,这段代码的几乎所有操作都基于几个关键结论:

    如果一个状态重复出现,那么至少有一种(可以是所有)字母从上次状态出现时到这一次
出现增加了偶数个,并且增加的个数是出现次数 * 2,其他字母个数不变。

以abc(状态111)为例,什么时候状态111会重复出现呢?

那就是abc变成abcabcabc、abccc或者abcdd等等等的时候。

顺带一提,新状态的生成是不断加入字母的结果,所以字母只能增加不能减少,这才有了结论1。

那么状态重复出现意味着什么呢?为什么数一数重复出现的状态就行了?那是因为结论2:

    一个状态重复出现,意味着0到当前状态编号i区间内,至少有它出现次数的子区间,可以
通过重新排序,构成回文串。

为什么?因为结论1中提到,重复出现就意味着偶数,而偶数,就意味着一定可以组成回文。出
现次数实质是偶数对的个数,所以能排序成回文串的区间数,就是他出现的次数。

接着就到最后部分了,由结论1、2,可以得出引申结论:

    如果一个状态重复出现,则去掉该状态的最小表示的剩余部分,一定可以重新排序组成该
状态出现次数个数的回文串。

什么是最小表示?就是用最少的字母表示的状态,比如111状态的最小表示就是abc而不是abcabcabc。

有了这几个结论,相信代码后半部分“判断当前状态是否出现过”的操作就很明朗了,而“枚举52种状态,判断跟当前状态异或后生成的状态是否出现过”的操作,则是在枚举当前状态的子状态,如果子状态曾经出现过,证明剩余状态肯定可以重新排序构成回文串。

而事先记录的状态0,则是一种特殊状态,代表着“所有字母都是偶数个”的状态,当然要予以考虑。

一个循环下来,有点像一个dp的过程,不断从前一状态的结果中生成新结果,最后递推出最终结果。


后记
代码并不是我写的,这是中大集训比赛上大佬的ac代码,对于博主这种菜鸡来说,这种玄学的代码真的很难弄懂,几乎花了我一天的时间去思考。弄懂之后,实在是觉得这段代码又短又美,网上这题的题解又极少,就忍不住写了一篇比较长的题解,希望路过的大佬看出bug一定要在评论里指出。
感谢玄学提点的聪哥,提出了一点思路的迪鸿(虽然后来发现思路是错的)和跟我一起推演了一中午、写了好几块黑板的洲哥。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值