Codeforces Round 873 (Div. 2)

HL_ZZP發表於2024-08-30

ABC都很簡單,但是D1寫起來有些麻煩,就沒寫,D2應該是一個分治的思路,後面想不出來了。

D1的思路非常好出,n只有5e3的範圍,意味著\((n^2)\)可過,可以直接列舉所有的子區間,也就是題目所說的子陣列,然後嘗試統計答案。
考慮一個子區間的答案是什麼樣的,發現只有逆序的數字才需要排序,我們直接找到逆序數字中最小和最大的位置,然後二分找到他們對於這個區間的剩餘的位置應該處於哪個位置,這樣就能夠直接得到這個區間所需的最小的時間。用st表統計最大最小值,總體\(O(n^2logn)\)(其實很危險,而且寫起來有點麻煩,就沒寫)

看完題解了。。D1的\(O(n^2)\)做法就是正解,只需要簡簡單單的資料結構暴力最佳化就過了D2。。
難怪D1賽時沒幾個人過。。這麼簡單的三題出來了就是rank1000+。
做法不是分治,資訊明顯無法合併。而是另一個思路,考慮對於每個位置的數字計算答案,也就是這個點會在幾次子陣列排序中被波及到。那麼,我們需要計算的就是對於這個點\(i\)來說,有多少個二元組\((l,r)\)滿足選擇這個區間後會使得\(i\)的位置處於被排序的區間內。

分析思路好混亂。。難受

其實這段話是最關鍵的
With these observations, we can conclude that the answer for a subarray a[l..r] equals the rl minus the number of positions k such that lk<r and max(a[l..k])<min(a[k+1..r]) (∗). Let us analyze how to calculate the sum of this value over all possible l and r.

意識到這種位置的含義其實就是無需排序的位置,其實就是這個題目的關鍵。對於這個區間,我們需要的排序的代價其實就是這個區間的長度減去滿足這個條件的位置的數量。
注意到這個,就可以把要求公式化了。看上去沒有什麼用,但是其實非常關鍵,

啊啊啊啊啊從頭開始,思路全亂了。不應該分兩天寫的。。。

我看到這題的第一個思路是分治,透過合併兩個區間的資訊以一個歸併的形式來得到答案,但是思考過後就會發現這個思路不可行,左右兩邊的資訊合併非常困難,完全沒有規律。
而正解的思路也是很典型,就是對於每一個\(i\in [1,n]\)統計答案,統計對於每個\(i\),有多少個子區間的選擇需要給這個位置的點排序。這個子區間的數量就是這個節點對於答案的貢獻。我們把每個點的貢獻加起來就是最終的答案。並不是很難想的思路。
然後我們現在需要解決的問題就是怎麼統計每個節點的答案。我們考慮什麼情況下一個節點會被排序。發現先固定節點不好考慮,嘗試固定子區間,即先考慮在子區間固定的情況下,擁有哪些特點的節點會被排序並統計答案。
這個是我認為這題最難的地方。。因為這個的思路不常規吧。。大概吧
需要一些觀察( ?)
觀察發現,對於\(k\)個位置,\(l< i_1 <i_2< i_3 ... <i_k <r\),我們可以分別對區間\(a[l...i_1],a[i_1+1...i_2]...a[i_k+1...r]\)進行排序的條件是,\(max(a[l...i_x])<min(i_x+1...r)\)(\(*\))全部成立。也就是前面一段的最大值一定要小於後面一段的最小值。很容易可以發現這樣的話每一段的排序是沒有相互影響的。
很明顯,這樣的分段對於我們答案來說是非常重要的。因為每一個這樣的分段就意味著一個位置不產生貢獻。這個子陣列\(a[l..r]\)所產生的貢獻就是就是\(r-l-k\)
然後把這個結論給轉化為以點作為主體,對於一個位置\(i\),我們計算有多少個三元組\((l,k,r)\),滿足\(min(a[k+1..r])==a[i]\)以及\(max(a[l..k])<min(a[k+1...r])\),這個三元組是對於每一個\(i\)進行計算的,其實就能夠很容易發現,這裡面的\(k\)就是距離\(i\)左側最近的且\(a[k]<a[i]\)的點。
我們再對於這個三元組統計一個\(x\),表示\(k\)左側的第一個滿足\(a[x]>a[i]\)的位置,\(y\),表示\(i\)右側第一個\(a[y]<a[i]\)的位置。
要是上面的部分全部都理解了的話,這個時候再看一眼我們所需的東西,其實就能夠恍然大悟了。我們所需的就是\(x<l\leq k\)\(i\leq r <y\)的三元組。
所以對於\(i\)這個點的貢獻就是為全部的答案減去上\((k-x)*(y-i)\) ,直接列舉\(O(n^2)\),st表+二分或者倍增最佳化\(O(n\ log\ n)\)
這題就沒了。

在理解這題前,我是真的感覺這題的答案統計非常的難以想到,但是在我剛剛寫的時候我突然發現其實啟發點就是對於每個點,答案的統計標準會變的這麼簡單,而且理解起來非常的合理和順利,這個應該在開始的時候就能夠找到一些端倪的才對。
其實最終要的還是嘗試以點為主題的統計答案,一旦想到了這個,其實這個特殊性發現起來就很方便了。當前前提是觀察得到了(\(*\))式,而沒有後面的思路,我要怎麼樣才能知道我需要觀察得到(\(*\))式呢?正向嘗試一下,然後反向嘗試一下?而事實上正向的嘗試也不一定會得到這個式子,完全可能是其他的式子。
可能是可以直接感覺到對於單點的答案統計不會複雜?然後再向這個方向思考。那麼這個又是從哪裡感覺到的呢...不過是不是這類題目就這些思路了?不太有其他方向的可能,或者說這個思路是我最後的辦法,他只能有用吧。。感覺是這個了,先確定對於單點計算貢獻的思路,然後再去嘗試分析每個點會做出怎麼樣的貢獻。這樣才能夠比較順理成章的得到上面的思路吧。。

#include<bits/stdc++.h>
#define ll long long
using namespace std;
inline int read(){
	int a=0,b=1;char c=getchar();
	for(;c<'0'||c>'9';c=getchar())if(c=='-')b=-1;
	for(;c>='0'&&c<='9';c=getchar())a=a*10+c-'0';
	return a*b;
}
int a[300002],n,b[300002];
int l[300002],r[300002];
int sta[300002][2],top;
bool mycmp(int x,int y)
{
	return a[x]>a[y];
}
int main()
{
	int T=read();
	while(T--)
	{
		n=read();
		ll ans=0;
		for(ll i=1;i<=n;i++)
			b[i]=i,a[i]=read(),ans+=i*(i-1)/2;
		sort(b+1,b+1+n,mycmp);
		top=0;
		for(int i=1;i<=n;i++)
			r[i]=n+1,l[i]=0;
		for(int i=1;i<=n;i++)
		{
			while(top!=0&&sta[top][0]>a[i])r[sta[top][1]]=i,top--;
			if(top!=0)l[i]=sta[top][1];
			sta[++top][0]=a[i],sta[top][1]=i;
		}
		set<int> s;
		s.insert(0);
		for(int i=1;i<=n;i++)
		{
			int p=b[i];
			int k=l[p],y=r[p];
			int x;
			if(k==0)x=0;
			else 
			x=*--s.lower_bound(k);
			s.insert(p);
			ans-=1LL*(k-x)*(y-p);
		}
		cout<<ans<<endl;
	}
	return 0;
}

這份程式碼和上面的程式碼的思路區別其實有些大,沒用用st表求,其實這個求數字左右兩邊的第一個小/大的數字的位置是經典的單調棧模型,所以直接用單調棧就搞定了,然後另一個需求是另一個數字的左側的第一個比k大的位置,其實是一個二位偏序,但是因為答案只能是最靠近的一個而不是所有,所以樹狀陣列不太能做,而是用set結合插入的順序完成。

相關文章