HDU 6194 题解报告

HDU[6194] 后缀数组+ST表

题目大意

在指定的字符串中找到仅出现k次的不同子串有几种,不同子串之间可重叠,题目在此。
 做了一天的神题,之前思路一直都很混乱,直到看到同站的一位大佬的博客,emmm,思路豁然开朗好吧,没想到题还能这么做,太NB了。

思路

 首先将输入的字符串按照常规操作(套后缀数组板子)进行处理,获得height数组。因为我们所要求的是出现了k次的字符串,那么符合要求的字符串必定会出现在前缀中。

prefix(最长前缀) height(prefix长度) suffix(原字符串后缀)
NULL NULL cmcmem
cm 2 cmem
NULL 0 m
m 1 mcmcmem
mcm 3 mcmem
m 1 mem
NULL 0 em

如上表格中,只要k大于1,那么结果必定出现在prefix这一列中(k = 1的情况需要单独讨论)。
然后我们逆向思维地考虑,正确字符串的height在height数组中有哪些特征?
假设一个子串出现了n次,那么由于后缀数组的性质(所有后缀由字典序 从小到大 排序)

  1. 这个子串肯定是 连续 出现在后缀数组中的,并且连续出现次数等同于n(不理解的话需要先学习后缀数组这个前置技能);
  2. 这个连续区间内的涉及的所有height值 >= 这个子串的长度;
    (不理解的话可以参照上图,m分别在suffix的第3/4/5/6列出现,height[4/5/6] = 1/3/1,均大于等于"m"的长度)
  3. 最最最重要的性质来了 :如果一个子串作为后缀数组的一个prefix出现时,我们向上或向下遍历height数组,在遍历到小于这个子串长度的height之前,每遍历到1个大于等于该子串长度的height,那么这个子串出现的次数便会增加1,因为其他数组的prefix也包含了这个prefix;同理,只要遍历到小于这个子串长度的height,那么遍历到的这个height对应的prefix就是该子串的前缀;

然后我们考虑k = 1的情况,求的是字符串中仅出现了一次的子串,那么这个子串必定不出现在前缀之中,思路就是先找到每个后缀与其他后缀共有的最长前缀(比如上图第2列的"cmem"与其他后缀共有的最长前缀为"cm"),然后再在这个后缀中一个个添加后面的字符,组成的就是一个个仅出现一次的字符串,不断记个数即可。
(证明:如果这样组成的字符串不止出现一次,那么最长的前缀至少是这个字符串或者向后添加更多的字符)

如果对ST表和后缀数组都有点基础的同学,看到这里应该就可以抄起自己的板子开始A了;
如果不是很能想到用ST表和后缀数组怎么做的同学,可以接着往下看;
如果没学习过ST表/线段树和后缀数组的,可以先去学习一下模板,再回来接着看。

代码思路

  1. 对原数组套后缀数组板子,成功获得height数组;
  2. 对着height数组构建ST表;
  3. 遍历height数组(起始点和终止点需要注意),遍历时要注意运用滑动窗口的思想。我们遍历的不是一个值,而是从这个值开始的 k - 1 个值(为什么是k - 1个值呢?因为一个height代表的是前缀出现两次,k - 1个height值中如果大于某个前缀的height,那么代表这个前缀出现了至少k次,用到了我们第1,2点性质)。
  4. 每遍历到一个窗口,我们就利用ST表找出窗口内的最小值,这个最小值在窗口内必定出现了k次,然后再看窗上外边第一个值和窗下外边第一个值,是否均小于这个最小值,如果是的话则说明这个最小值就只出现了k次(用到了我们的第3点性质),也说明这个最小值代表的一些前缀/子串严格出现了k次。
  5. 可答案所要求的是不同子串的个数,我们获取的最长前缀中的某个前缀可能在窗外也出现了,那么这某个前缀出现的次数就不等于k,所以我们还需要用第4步求得的最小值减去窗外两边中的较大值,才是真正出现了k次的子串个数。

AC代码

#include 

using namespace std;

typedef long long ll;

const ll CHAR_NUM = 1000;//ascii字符
#ifdef ACM_LOCAL
const ll NUM = 1e5 + 10;
#else
const ll NUM = 1e5 + 10;
#endif

struct ST {
    ll STMin[NUM][20], mn[NUM];

    void initST(int n, const ll *a) {
        mn[0] = -1;
        for (int i = 1; i <= n; ++i) {
            mn[i] = ((i & (i - 1)) == 0) ? mn[i - 1] + 1 : mn[i - 1];
            STMin[i][0] = a[i];
        }
        for (int j = 1; j <= mn[n]; ++j) {
            for (int i = 1; i + (1 << j) - 1 <= n; ++i) {
                STMin[i][j] = min(STMin[i][j - 1], STMin[i + (1 << (j - 1))][j - 1]);
            }
        }
    }

    ll rmqMin(int l, int r) {
        int k = mn[r - l + 1];
        return min(STMin[l][k], STMin[r - (1 << k) + 1][k]);
    }
} st;

const ll MAXN = 2e5 + 10;

ll SA[MAXN], myRank[MAXN], height[MAXN], sum[MAXN], tp[MAXN];
//rank[i] 第i个后缀的排名, SA[i] 排名为i的后缀的位置, Height[i] 排名为i的后缀与排名为(i-1)的后缀的LCP
//sum[i] 基数排序辅助数组, 存储小于i的元素有多少个, tp[i] rank的辅助数组(按第二关键字排序的结果),与SA意义一样
char str[MAXN];

bool cmp(const ll *f, ll x, ll y, ll w) {
    return f[x] == f[y] && f[x + w] == f[y + w];
}

void get_SA(const char *s, ll n, ll m) {
    //先预处理长度为1的情况
    for (ll i = 0; i < m; i++) sum[i] = 0;//清0
    for (ll i = 0; i < n; i++) sum[myRank[i] = s[i]]++;//统计每个字符出现的次数
    for (ll i = 1; i < m; i++) sum[i] += sum[i - 1];//sum[i]为小于等于i的元素的数目
    for (ll i = n - 1; i >= 0; i--) SA[--sum[myRank[i]]] = i;//下标从0开始,所以先自减
    //SA[i]存储排名第i的后缀下标,SA[--sum[rank[i]]] = i 即下标为i的后缀排名为--sum[rank[i]],这很显然
    for (ll len = 1; len <= n; len *= 2) {
        ll p = 0;
        //直接用SA数组对第二关键字排序
        for (ll i = n - len; i < n; i++) tp[p++] = i;//后面i个数没有第二关键字,即第二关键字为空,所以最小
        for (ll i = 0; i < n; i++) {
            if (SA[i] >= len) tp[p++] = SA[i] - len;
        }
        //tp[i]存储按第二关键字排序第i的下标
        //对第二关键字排序的结果再按第一关键字排序,和长度为1的情况类似
        for (ll i = 0; i < m; i++) sum[i] = 0;
        for (ll i = 0; i < n; i++) sum[myRank[tp[i]]]++;
        for (ll i = 1; i < m; i++) sum[i] += sum[i - 1];
        for (ll i = n - 1; i >= 0; i--) SA[--sum[myRank[tp[i]]]] = tp[i];
        //根据SA和rank数组重新计算rank数组
        swap(myRank, tp);//交换后tp指向旧的rank数组
        p = 1;
        myRank[SA[0]] = 0;
        for (ll i = 1; i < n; i++) {
            myRank[SA[i]] = cmp(tp, SA[i - 1], SA[i], len) ? p - 1 : p++;//注意判定rank[i]和rank[i-1]是否相等
        }
        if (p >= n) break;
        m = p;//下次基数排序的最大值
    }
    //求height
    ll k = 0;
    n--;
    for (ll i = 0; i <= n; i++)
        myRank[SA[i]] = i;
    for (ll i = 0; i < n; i++) {
        if (k)
            k--;
        ll j = SA[myRank[i] - 1];
        while (s[i + k] == s[j + k])
            k++;
        height[myRank[i]] = k;
    }
}

ll cnt[MAXN], pos[MAXN], rmv[NUM];

void reset() {
    memset(SA, 0, sizeof(SA));
    memset(myRank, 0, sizeof(myRank));
    memset(height, 0, sizeof(height));
    memset(sum, 0, sizeof(sum));
    memset(tp, 0, sizeof(tp));
    memset(cnt, 0, sizeof(cnt));
    memset(pos, 0, sizeof(pos));
    memset(rmv, 0, sizeof(rmv));
}

ll k;

int main() {
#ifdef ACM_LOCAL
    freopen("in.txt", "r", stdin);
    freopen("out.txt", "w", stdout);
#endif
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);

    ll t;
    cin >> t;
    while (t--) {
        cin >> k >> str;
        int len = strlen(str);
        str[len] = 0;
        str[len + 1] = 0;
        len++;
        get_SA(str, len, CHAR_NUM);

        len--;
        ll ans = 0;
        if (k > 1) {
            k--;
            st.initST(len + 1, height);
            for (int i = 1; i <= len - k + 1; i++) {
                int tmp = st.rmqMin(i, i + k - 1);
                if(height[i - 1] < tmp && height[i + k] < tmp)
                    ans += tmp - max(height[i - 1], height[i + k]);
            }
        } else {
            for (ll i = 1; i <= len; i++) {
                ll tmp = min(len - SA[i] - height[i], len - SA[i] - height[i + 1]);
                ans += max(tmp, 0LL);
            }
        }

        cout << ans << endl;
        reset();
    }

    return 0;
}
var foo = 'bar';

ST表和后缀数组都套了网上大佬的板子,自己写的板子太LOW了,大家作个参考就行。
第一次写博客,如果有写的不对或是不清晰的地方请在下方评论指出~

你可能感兴趣的:(HDU,后缀数组,ST表,题解,字符串)