【题解】P2178 [NOI2015]品酒大会 后缀数组,可以维护最大积的并查集

给定一个长为n(3e5)的字符串,如果两个后缀 l , r l,r l,r l c p lcp lcp至少为 i i i,那么称它们是 i 相 似 i相似 i的。每个位置还有一个权值a(±1e9)。
现在对于每个 i = 0 到 n − 1 i=0到n-1 i=0n1,问有多少个 i 相 似 i相似 i的无序对 ( l , r ) (l,r) (l,r),并且输出最大的 a l ∗ a r a_l*a_r alar


和lcp有关,自然想到后缀数组。跑一遍后缀数组求出height之后,这题就和字符串没什么关系了。。

考虑从大到小找答案,按height从大到小遍历id,每一个id的意义是id和id-1这两种酒是height[id]相似的。
也就是说,每个id的意义相当于把id-1和id这两种酒连起来了。

和并查集非常像,但每次合并之后需要知道新增了多少个无序对以及合并后新集合无序对的最大积。
易知新增无序对数等于两个集合的size之积。
为了求最大积,我们可以维护每个集合的最大值、最小值、最大积,然后对【原来的最大积】、【最大值之积】、【最小值之积】取max,就是新的最大积。


总结:

  1. 好多题的字符串只是一个壳。。。内部全是其它东西。
  2. 并查集的最大积可以通过维护最大值,最小值,历史最大积维护。
  3. 洛谷黑题第二道,独立做出的哟
/* LittleFall : Hello! */
#include <bits/stdc++.h>
using namespace std; using ll = long long; inline int read();
const int M = 300016, MOD = 1000000007;

char str[M];
namespace SA
{    
    /* 后缀数组 */
    int sa[M], ra[M], height[M]; //后缀三数组,sa和ra下标从0开始,height下标从1开始
    int t1[M], t2[M], c[M]; // 用于基数排序的三个辅助数组
    void build(char *str, int n, int m) // 构造后缀三数组,字符串下标从0开始,n表示长度,m表示字符集大小
    {  
        str[n] = 0;
        n++;  
        int i, j, p, *x = t1, *y = t2;  
        for(i = 0; i < m; i++) c[i] = 0;  
        for(i = 0; i < n; i++) c[x[i]=str[i]]++;  
        for(i = 1; i < m; i++) c[i] += c[i-1];  
        for(i = n-1; i >= 0; i--) sa[--c[x[i]]] = i;  
        for(j = 1; j <= n; j<<=1)  
        {  
            p = 0;  
            for(i = n-j; i < n; i++) y[p++] = i;  
            for(i = 0; i < n; i++) if(sa[i] >= j) y[p++] = sa[i]-j;  
            for(i = 0; i < m; i++) c[i] = 0;  
            for(i = 0; i < n; i++) c[x[y[i]]]++;  
            for(i = 1; i < m; i++) c[i] += c[i-1];  
            for(i = n-1; i >= 0; i--) sa[--c[x[y[i]]]] = y[i];  
            swap(x, y);  
            p = 1; x[sa[0]] = 0;  
            for(i = 1; i < n; i++)  
                x[sa[i]] = (y[sa[i-1]]==y[sa[i]]&&y[sa[i-1]+j]==y[sa[i]+j]) ? p-1 : p++;  
            if(p >= n) break;  
            m = p;  
        }  
        n--;  
        for(int i = 0; i <= n; i++) ra[sa[i]] = i;  
        for(int i=0, j=0, k=0; i < n; i++)  
        {   
            if(k) k--;  
            j = sa[ra[i]-1];  
            while(str[i+k]==str[j+k]) k++;  
            height[ra[i]] = k;  
        }  
    }
};

struct UFS
{
	int fa[M], cnt[M]; //父亲(当父亲不是自己时失效),元素个数
	ll mx[M], mi[M], mul_mx[M];
	//集合中元素个数,最大值,最小值,最大对乘积

	void init(int n, ll *pri)
	{
		for(int i=1; i<=n; ++i)
		{
			cnt[i] = 1; fa[i] = i;
			mx[i] = mi[i] = pri[i];
			mul_mx[i] = LLONG_MIN;
		}
	}
	int find(int i) 
	{
		return fa[i]==i ? i : fa[i] = find(fa[i]);
	}

	//合并集合,返回新增对数,最大对乘积
	pair<ll,ll> join(int a, int b) 
	{
		a = find(a), b = find(b);

		ll res_cnt = 1ll*cnt[a]*cnt[b];
		ll res_max = max(mul_mx[a], mul_mx[b]);
		res_max = max(res_max, mx[a]*mx[b]);
		res_max = max(res_max, mi[a]*mi[b]);

		fa[b] = a;
		mx[a] = max(mx[a], mx[b]);
		mi[a] = min(mi[a], mi[b]);
		cnt[a] += cnt[b];
		mul_mx[a] = res_max;

		return {res_cnt, res_max};
	}
}ufs;
/*
	每个id的意义在于, id和id-1这两种酒是v相似的。
	也就是说,每个id实际上是把id和id-1这两种酒连起来了
	我要把它们放到一个并查集里面去

	cnt的增量等于,待合并集合的元素个数之积
	max的更新,max可以取得哪些值呢

	这个问题实际上是: 一个可以求最大乘积的并查集
	初始时有n个数,每个数属于自己的集合,若干次操作:
	选定x和y,把第x个数和第y个数所在的集合合并,
	返回合并后集合中任意两个数最大的乘积(不能是一个数)
*/

ll pri[M]; using SA::height; //权值,height
ll ans_cnt[M], ans_max[M]; //答案计数,答案最大值

vector<int> ids[M]; //ids[i]表示height=i的所有位置
//带并查集
int main(void)
{
	#ifdef _LITTLEFALL_
	freopen("in.txt","r",stdin);
    #endif

	int n = read();
	scanf("%s",str);
	SA::build(str, n, 128);
	for(int i=0; i<n; ++i)
		pri[SA::ra[i]] = read();
	//以下与字符串无关

	for(int i=2; i<=n; ++i)
		ids[height[i]].push_back(i);	
	ans_max[n] = LLONG_MIN;
	ufs.init(n, pri);

	for(int v=n-1; v>=0; --v)
	{
		ans_cnt[v] = ans_cnt[v+1];
		ans_max[v] = ans_max[v+1];
		for(auto id:ids[v])
		{
			auto p = ufs.join(id-1, id);
			ans_cnt[v] += p.first;
			ans_max[v] = max(ans_max[v], p.second);
		}
	}

	for(int v=0; v<n; ++v)
	{
		if(ans_cnt[v]==0) ans_max[v] = 0;
		cout << ans_cnt[v] << " " << ans_max[v] << "\n";
	}

    return 0;
}


inline int read(){
    int x=0,f=1;char ch=getchar();
    while(ch<'0'||ch>'9') {if(ch=='-')f=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
    return x*f;
}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值