POJ3415 Common Substrings

题意: 给出两个字符串S1, S2,并给出数K, 求两个字符串有多少对公共子串的长度大于等于K. S1, S2长度 <= 100000, K <= 100000, S1和S2中都是拉丁文字母. (哪些是拉丁文字母?...)

所谓多少"对"公共子串, 就是一个子串出现在S1的1次出现, 与S2中的1次出现对应, 就是1对. 比如AABABABB和ABABAAAA, 则子串AB在S1出现了3次, 在S2出现了2次, 因此由乘法原理, 子串"AB"带来的公共子串对数就是2*3= 6. 当然, 这是在K <= 2的情况下.

 

         这题比较麻烦, 首先还是考虑用后缀数组, 把两个串拼接到一块, 然后求出后缀数组和height数组. 然后怎么统计有多少对公共子串呢? 先从简单的想起...

         后缀数组sa可以认为是一个后缀的排行榜,名次->位置的映射. 每个子串都是某个后缀的前缀. 重复的前缀都会在排行榜中连续的位置出现, 因此可以从前往后遍历height数组(排行榜上相邻两后缀的公共前缀)来计算出总共有多少对公共前缀. 当然, 我们还要根据位置判定每个前缀属于S1还是S2.

         我们可以定义一个数组cnt表示"有效前缀数量",cnt[j][len] = n表示Sj有n个长度为len的子串还可以与后面的字符串匹配. 如果对于某个长为len的前缀L, 到某时, 它后面还从未出现过height值小于len的情况, 说明此时的当前后缀的前len位仍然是L, 说明L所带来的计数应该保留; 如果某个时刻出现了height值小于len, 那么后面的字符串的前len位再也不会是L了, 此时L的计数就应该去掉. 因此, 我们从前往后遍历height数组的时候, 如果height[i]不小于height[i-1], 则直接计数, 让cnt[j][height[i-1]]++ (height[i-1]表示前一位与后一位的LCP, 这个长度的前缀还可以再往后匹配); 如果height[i]小于height[i-1], 说明有些长度的前缀应该被删掉了, 此时还是先把cnt[j][height[i-1]]++, 然后把所有长度大于height[i]的计数都处理掉(肯定不能直接删掉, 因为人家对公共前缀的数量有贡献, 需要算好贡献了多少之后再删除), 因为后面的字符串和前面的至多匹配height[i]这么长.

         比如height数组局部是: (之前各个长度的有效前缀数量都是0) (K = 1, 相当于没限制)

         0, 1, 2, 2, 3, 2, 0, ...

         这里怎么计数呢? 第一个0不管, 第二个是1, 说明第一个串有1长度的有效前缀, 就把cnt[j][1]++,变成1. (j是1还是2要看这个名次对应位置在S1还是S2, 这个不难) 然后第三个是2, 比前一个大, 说明第二个串有2长度的有效前缀, 就把cnt[j][2]++; 第四, 第五个同理. 到了第6个, 比前一个少了, 此时先把前一个3统计到cnt中(因为height数组里一个3说明有两个串前缀3是相同的), 然后删除长度大于2的, 也就是长度3的. 假设那两个前缀3相同的串分别在S1与S2中, 那么删除3的时候先在最终计数里面加上1*1=1, 然后把3的计数加到2上面, 因为长度3的有效前缀的也包含长度为2的有效前缀, 可以和其他长度2的有效前缀一起匹配, 并记入最终计数当中. (最终计数就是公共前缀的对数). 最后读到0, 那么就先把前面的2统计到cnt当中, 然后全部删除+计数.

        

         唉, 解释清楚比想清楚要麻烦许多呀...

        

         然后我们就有了更为确切的方法了. 从前往后遍历height,到下标i时, 先读出sa[i-1]对应的位置属于S1还是S2, 然后把对应的cnt[][height[i-1]]++, 然后判断, 如果height[i]小于height[i-1], 那么就把cnt数组中大于height[i]的都处理掉, 处理方法就是, 对于每一个长度len, 在最终计数tot中加上cnt[1][len] * cnt[2][len], 然后把这两个值都加给len-1, 然后把这两个值清0. 一直滚雪球地清理下去, 直到达到height[i]为止.

         但还有个小问题, 就是len可能很大, 比如A和B都是abcde...一个重复程度极低的串, 然后排行榜就会长这样...

         abcd...

         abcd...$...

         bcd...

         bcd...$...

         cd...

         cd...$...

         .......

         然后height数组大概就是0, 100000,0, 99999, 0, 99998...然后如果每次删除都把len一个一个的减少, 最后减掉0的话, 那就搞成N^2的了, 那就死了...咋办呢? 注意到len的值可能很大, 但不同的len的个数不会很大, 因此可以设计某个数据结构来维护哪些len是出现过的, 以及能够很方便地进行插入, 以及删除大于某个阈值的所有len. 那就用一个堆就行了呗. 每次新得到一个len的时候, 先去cnt数组查看是否两个cnt值都是0, 不是的话说明目前还没有统计过这个len, 于是把这个len加入到堆里面, 并统计cnt. 如果不都是0, 说明已经有统计过这个len, 那就直接统计cnt. 如果要删除, 那就直接弹堆顶, 然后把cnt相乘之后, 再乘以这个堆顶与弹完之后的堆顶的差, 因为这中间的所有len值的cnt都是0, 所以如果滚雪球下来cnt值都不会变. 然后把这个乘积加到tot上面, 再把旧堆顶的cnt加给新堆顶即可. 这样就可以处理len值过大造成的问题了.


#include <iostream>
#include <algorithm>
#include <cstring>
#include <cstdio>
#include <queue>
#define N 200007
using namespace std;

int sa[N + 1], ranks[N + 1], height[N + 1];
int sz; // 后缀数组长度
int cnt[N + 1]; //基数排序的桶
char input[N + 1]; //原始数组
int K;

// int efflen[N]={};
priority_queue<int> pq;
long long cnt1[N]={};
long long cnt2[N]={};
int delim;
bool vis[N]={};

struct node {
    int v[2]; // v[0],v[1]表示二元对。v[0]是高位,v[1]是低位。
    int p; // p表示这个后缀的起始位置。
    bool operator == (const node & n) const {
        return v[0] == n.v[0] && v[1] == n.v[1];
    }
    bool operator < (const node &n) const {
        if (v[0] != n.v[0]) return v[0] < n.v[0];
        return v[1] < n.v[1];
    }
}arr[N + 1], temp[N + 1];

void print() {
    cout << "sa" << endl;
    for(int i = 1; i <= sz; i++) cout << sa[i] << " ";
    cout << endl << "rank" << endl;
    for(int i = 1; i <= sz; i++) cout << ranks[i] << " ";
    cout << endl << "height" << endl;
    for(int i = 1; i <= sz; i++) cout << height[i] << " ";
    cout << endl;
}

// 填入ranks数组。注意相同的后缀的排名应该相同
void ra() {
    for(int i = 1, j = 1, k = 1; i <= sz; i = j, k++) {
        while(j <= sz && arr[i] == arr[j]) {
            ranks[arr[j++].p] = k;
        }
    }
}

// 基数排序 桶的范围是[0, base]
void sort(int base) {
    for(int i = 1; i >= 0; i--) {
        memset(cnt, 0, sizeof(int) * (base + 1));
        for(int j = 1; j <= sz; j++) cnt[arr[j].v[i]]++;
        for(int j = 1; j <= base; j++) cnt[j] += cnt[j - 1];
        for(int j = sz; j > 0; j--) temp[cnt[arr[j].v[i]]--] = arr[j]; // 从后往前遍历,这样是稳定的
        memcpy(arr, temp, sizeof(node) * (sz + 1));
    }
    ra();
}

void buildSA() {
    for(int i = 1; i <= sz; i++) {
        arr[i].p = i;
        arr[i].v[0] = input[i];
        arr[i].v[1] = 0;
    }
    sort(arr + 1, arr + sz + 1); //第一次有负数直接排序,不影响最终复杂度
    ra();
    for(int i = 1; i < sz; i <<= 1) {
        for(int j = 1; j <= sz; j++) {
            arr[j].v[0] = ranks[j];
            arr[j].v[1] = j + i <= sz ? ranks[j + i] : 0;
            arr[j].p = j;
        }
        sort(sz);
    }
    for(int i = 1; i <= sz; i++) {
        sa[ranks[i]] = i;
    }
}

void lcp() {
    int len = 0;
    for(int i = 1; i <= sz; i++) {
        if (len > 0) len--;
        if (ranks[i] > 1) {
            while(input[i + len] == input[sa[ranks[i] - 1] + len]) len++;
        }
        height[ranks[i]] = len;
    }
}

void insHeap(int clen, int pos)	//插入长度为clen, 位置为pos的记录 
{
	if(clen < K)		return;
	if(pos > 0 & vis[pos])	return;
	vis[pos] = true;
	if(cnt1[clen] == 0 && cnt2[clen] == 0)	pq.push(clen);	//如果原来没有这个长度则插入, 避免插入两个一样的长度值 

	if(pos < delim)		cnt1[clen]++;	//用pos判断在S1还是在S2, 统计出现次数 
    if(pos >= delim)
	{
		cnt2[clen]++;
	}
}

long long delHeap(int clen)		//删除所有长度len以上的记录, 并返回计数增加的数量 
{
	long long tot = 0;
	if(clen >= K)			//删除一个堆里没有的长度clen可能造成有些长度较大的记录直接落到clen以下, 统计错误 
	{
		if(cnt1[clen] == 0 && cnt2[clen] == 0)
			pq.push(clen);
	}
	else
	{
		clen = K-1;
	}

	while(clen < pq.top())		//反复删除大于clen的 
    {
    	int oTop = pq.top();
    	pq.pop();
        int nTop = pq.top();
        tot += cnt1[oTop]*cnt2[oTop]*(oTop - nTop);		//统计该次删除长度对应的公共子串计数 
      	cnt1[nTop] += cnt1[oTop];				//把删掉的长度的计数落到下一个最大长度上 
        cnt2[nTop] += cnt2[oTop];
        cnt1[oTop] = cnt2[oTop] = 0;				//清除该次删掉长度的计数 
    }
    return tot;
}

int main(int argc, const char * argv[])
{
    while(true)
	{
        scanf("%d", &K);
        if (K == 0) break;
        
        scanf("%s", input+1);		//构造工作串: S1$S2 
        sz = strlen(input+1);
        delim = sz+1;
        input[delim] = '$';
        scanf("%s", input+sz+2);
        sz = strlen(input+1);
        
        buildSA();
        lcp();
        //print();
        
        while(!pq.empty())	pq.pop();
        pq.push(K-1);		//监视哨, 避免弹空栈RE 
        memset(cnt1, 0, sizeof(cnt1));
        memset(cnt2, 0, sizeof(cnt2));
        memset(vis, 0, sizeof(vis));	//(vis数组最后并没有用...) 
        
        long long tot = 0;
        for(int i = 1; i <= sz; i++)
        {
        	if(height[i-1] <= height[i])	//height值上升, 则当前height值就是i-1位置的有效前缀长度 
        	{
        		insHeap(height[i], sa[i-1]);
        	}
        	else			//height值下降, 则之前height值超出的部分就没用了,
        	{			//先把i-1还没有统计上的有效前缀统计上, 然后把超出当前height的长度值删掉  
        		insHeap(height[i-1], sa[i-1]);
        		int low = max(height[i], K-1);
        		tot += delHeap(low);
        	}
        }
        insHeap(height[sz], sa[sz]);	//统计上最后一位的有效前缀长度 
        tot += delHeap(K-1);		//最后清空栈, 得到最终公共子串计数 
        
        printf("%lld\n", tot);
    }
    return 0;
}


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值