P2178 [NOI2015] 品酒大會 題解(評分:8.0)(2024.2.23)

Fun_Strawberry發表於2024-04-17

前言

"I'm free."

做法與題解區都不同,雖然麻煩,但是畢竟複雜度是對的,而且想法很自然,還是寫一寫吧!

Solution

題意:給定長為 \(n\) 的字串 \(s\) 和長為 \(n\) 的陣列 \(A\),對於每個 \(r\),求 滿足 \(\text{LCP}(\text{Suffix}(x),\text{Suffix}(y))\ge r,x<y\) 的數對 \((x,y)\) 數量。

其中 \(\text{LCP}(x,y)\) 指字串 \(x,y\) 的最長公共字首,\(\text{Suffix}(i)\) 表示串 \(s\) 從第 \(i\) 位開始的字尾。

然後記數對 \((x,y)\) 權值為 \(A_x\times A_y\),對於每個 \(r\),求滿足上述條件的數對的最大權值。

一、串串題慣常套路

看見要求原串兩個字尾的 \(\text{LCP}\),最直接的反應便是啟動字尾陣列,並計算出 \(height\) 陣列。

然後 \(\text{LCP}(\text{Suffix}(x),\text{Suffix}(y))=\min_{i\in [rank_x+1,rank_y]} h_i,rank_x<rank_y\) 便可求出任意兩個字尾的 \(\text{LCP}\)

現在我們就把字串問題轉移為數列問題。

二、具體解決方案

現在相當於詢問對於每個 \(r\),有多少滿足 \(\min_{i\in [x+1,y]}h_i \ge r,x<y\) 的數對 \((x,y)\)

此時出現三個考慮方向:

第一是分別計算每一位 \(h_i\) 對答案貢獻。

第二是從大到小列舉 \(r\) ,使用並查集維護區間的阻斷或者連通性,計算答案是多少。

但是其實正序計算也是可以噠!

我們把 \(h_i\) 從小到大排序,並一個一個按原位置加入陣列中。

容易發現,加入所有 \(h_i<i\) 之後,仍然不包含任何數字的區間,就是對於 \(r=i\) 時的一個合法區間。

舉個例子:

s[i] p o n o i i i p o i
sa[i] \(10\) \(5\) \(6\) \(7\) \(3\) \(9\) \(4\) \(2\) \(8\) \(1\)
h[i] \(0\) \(1\) \(2\) \(1\) \(0\) \(0\) \(2\) \(1\) \(0\) \(2\)
加入 \(0\) \(0\) \(0\) \(0\) \(0\)

\(\min_{i\in [2,4]}=1\),意味著 \(\text{LCP}(\text{Suffix}(sa_{2-1}),\text{Suffix}(sa_4))=1\),事實的確如此。

對於一個連續的空區間,任選其中兩點(左端點可以選到左邊的數字上),皆是一個合法答案,設其長度為 \(l\),對第一問貢獻為 \(l\times (l+1)\div 2\)

考慮在陣列中加入一個數,會對答案產生什麼樣的影響?

會把一個連續空區間分成兩個。

那麼對於第一問是簡單的,減去原來長區間貢獻,分別加入兩個短區間貢獻即可。

三、第二問咋做

仍然考慮一個長區間對於整體的貢獻。

上文已經提到,對於一個連續的空區間,任選其中兩點(左端點可以選到左邊的數字上),皆是一個合法答案。

為了使得權值最大化,顯然要選 \(A_i\) 最大的兩個,或者最小的兩個(負負得正)。

這是一個典型的詢問區間最大和次大,使用資料結構維護即可。

然後對於區間的貢獻,不僅有刪除和新增(把一個大區間分成兩個後,刪除大區間貢獻,加入兩個小區間貢獻),還要實時查詢各個區間貢獻的最大值,還是用資料結構來維護。

四、實現細節

用啥資料結構呢?

詢問區間最大次大和最小次小,由於不帶修,ST 表即可。

(但是我倍增不熟,賽時怕掛,便使用了線段樹來維護)

如何找到左右最靠近的加入過的數,單調棧或者樹狀陣列或者 set 都可以。

(這裡建議最後一個,但是我賽時使用了最麻煩的第二個)

如何實時查詢貢獻最大值並支援加入刪除某個數?堆或者 multiset。

時間複雜度都是 \(\text{O}(n\log n)\),但是一個 \(\log\) 遍地跑,常數賊大。

(實測最慢點 \(970\)\(1500\) 毫秒不等,透過率高於 \(50\%\))。

這個做法最大的區別便是正序列舉 \(r\),並使用堆來解決第二問。

貼一個修改過的賽時程式碼,很唐。

趣事:賽時沒有使用 multiset 而用的 set,但是過了所有測試資料,洛谷上被官方資料搞成 \(70\) 了。

"Save me."

AC Code

#include<bits/stdc++.h>
#define int long long
#define For(l,r,b) for(b=l;b<=r;b++)
#define N 300005
#define inf 1000000007ll
using namespace std;
string s;
int i,j,sa[N],rk[N],rk1[N],sec[N],tot[N],n,h[N];
int tr[N][2];
int a[N],A[N];
vector<int> hp[N];
struct tree{
	int l,r,s1,s2,b1,b2;
}t[4*N];
struct an{
	int s1,s2,b1,b2;
};
an mi(an x,an y)
{
	return {min(x.s1,y.s1),min(max(x.s1,y.s1),min(x.s2,y.s2)),max(x.b1,y.b1),max(min(x.b1,y.b1),max(x.b2,y.b2))};
}
void pushup(int o)
{
	an x={t[o*2].s1,t[o*2].s2,t[o*2].b1,t[o*2].b2},y={t[o*2+1].s1,t[o*2+1].s2,t[o*2+1].b1,t[o*2+1].b2};
	an xy=mi(x,y);
	t[o]={t[o].l,t[o].r,xy.s1,xy.s2,xy.b1,xy.b2};
}
multiset<int> se;
multiset<int>::iterator it;
void build(int l,int r,int o)
{
	t[o].l=l;t[o].r=r;
	if(l==r)
	{
		t[o].s1=t[o].b1=A[l];
		t[o].s2=inf;
		t[o].b2=-inf;
		return;
	}
	int mid=(l+r)>>1;
	build(l,mid,o*2);
	build(mid+1,r,o*2+1);
	pushup(o);
}
an query(int l,int r,int o)
{
	if(t[o].l>=l&&t[o].r<=r) return {t[o].s1,t[o].s2,t[o].b1,t[o].b2};
	int mid=(t[o].l+t[o].r)>>1;
	an ans={inf,inf,-inf,-inf};
	if(l<=mid) ans=mi(ans,query(l,r,o*2));
	if(r>mid) ans=mi(ans,query(l,r,o*2+1));
	return ans;
}
void change(int x,int y,int id)
{
	x++;
	for(;x<=n;x+=x&-x) tr[x][id]=max(tr[x][id],y);
}
int ask(int x,int id)
{
	x++;
	int ans=-1;
	for(;x>0;x-=x&-x) ans=max(ans,tr[x][id]);
	return ans;
}
signed main()
{
	cin.tie(0)->sync_with_stdio(0);
	cout.tie(0); 
	cin>>n>>s;
	For(1,n,i) cin>>a[i];
	s='#'+s;
	For(1,n,i) tot[s[i]]++,rk[i]=s[i];
	For(1,128,i) tot[i]+=tot[i-1];
	for(i=n;i;i--) sa[tot[rk[i]]--]=i;
	int sz=128;
	for(int k=1;k<=n;k<<=1)
	{
		int cnt=0;
		For(n-k+1,n,i) sec[++cnt]=i;
		For(1,n,i) if(sa[i]>k) sec[++cnt]=sa[i]-k;
		memset(tot+1,0,sz<<3);
		For(1,n,i) ++tot[rk[i]];
		For(1,sz,i) tot[i]+=tot[i-1];
		for(i=n;i>=1;i--) sa[tot[rk[sec[i]]]--]=sec[i];
		memcpy(rk1+1,rk+1,n<<3);
		sz=1;
		rk[sa[1]]=1;
		For(2,n,i) rk[sa[i]]=(rk1[sa[i]]==rk1[sa[i-1]]&&rk1[sa[i]+k]==rk1[sa[i-1]+k])?sz:++sz;
		if(sz==n) break;
	}
	For(1,n,i)
	{
		int pt=max(0ll,h[rk[i-1]]-1);
		while(s[i+pt]==s[sa[rk[i]-1]+pt]) pt++;
		h[rk[i]]=pt;
	}
	h[1]=0;
	For(1,n,i) A[i]=a[sa[i]];
	For(1,n,i) hp[h[i]].push_back(i);
	build(1,n,1);
	an as=query(1,n,1);
	change(1,1,0);change(0,0,1);
	long long ans=(n-1)*n/2,an2=max(as.b1*as.b2,as.s1*as.s2);
	cout<<ans<<" "<<an2<<endl;
	For(0,n-2,i)
	{
		for(auto j:hp[i])
		{
			int lf=ask(j-1,0),rf=n+1-ask(n-j,1);
			if(j==1) lf++;
			lf++,rf--;
			an gx;int gp;
			if(rf>=lf)
			{
				gx=query(lf-1,rf,1),gp=max(gx.b1*gx.b2,gx.s1*gx.s2);
				it=se.lower_bound(-gp);
				if(gp!=inf*inf&&j!=1)se.erase(se.lower_bound(-gp));
			}
			ans-=(rf-lf+1)*(rf-lf+2)/2;
			ans+=(j+1-lf)*(j-lf)/2;
			ans+=(rf-j+1)*(rf-j)/2;
			if(j-1>=lf)
			{
				gx=query(lf-1,j-1,1),gp=max(gx.b1*gx.b2,gx.s1*gx.s2);
				if(gp!=inf*inf) se.insert(-gp);
			}
			if(j+1<=rf)
			{
				gx=query(j,rf,1),gp=max(gx.b1*gx.b2,gx.s1*gx.s2);
				if(gp!=inf*inf) se.insert(-gp);
			}
			change(j,j,0);change(n+1-j,n+1-j,1);
		}
		cout<<ans<<" "<<(ans==0?0:(-(*se.lower_bound(-inf*inf))))<<endl;
	}
	return 0;
}

後記

寫得比較認真的一篇題解。

謹以此題解,為我長達四天的字串簡單學習作結,也為基本 OI 知識的第一輪學習作結。

"I won't leave you."

相關文章