區區區間間間(單調棧)

2022-07-16 00:54:13 字數 4728 閱讀 4392

時間限制:c/c++ 1秒,其他語言2秒

空間限制:c/c++ 32768k,其他語言65536k

第一行輸入資料組數t

對於每組資料,第一行為乙個整數n,表示序列長度

接下來一行有n個數,表示序列內的元素

對於每組資料,輸出乙個整數表示答案

示例1

334

2351

8439

202815

110519

19356

6282

121638

17

5

572712

對於一組測試資料的解釋:

區間[1, 2]的貢獻為:4 - 2 = 2

區間[1, 3]的貢獻為:4 - 2 = 2

區間[2, 3]的貢獻為:3 - 2 = 1

2 + 1 + 2 = 5.

t⩽20,n⩽105

,0⩽ai

​⩽105

不保證資料隨機生成!

該題就是求所有子區間(子區間長度大於1,至少為2)的最大值減去最小值的和是多少。

可以對原式拆分一下得:

其中max(l, r)表示區間l到r的最大值,min(l, r)表示區間l到r的最小值。

那麼問題就轉化為:求所有區間長度大於1的子區間的最大值之和與最小值之和。

我們以求最大值為例:

我們考慮用單調棧去ai左邊和右邊第乙個比它大的位置,進而求出區間個數。

用l[i]表示以a[i]為最大值,向左最多延伸到l[i],r[i]表示以a[i]為最大值,向右最多延伸到r[i]。

正著跑一次單調棧,倒著跑一次單調棧就能求出來l和r陣列。

那麼對於每乙個a[i],設滿足a[i]為區間最值的區間個數num,那麼a[i]對ans的貢獻就是a[i] * num,那麼我們知道了l[i]和r[i],怎樣求num呢?

有兩種理解方式:

方式一:分兩種情況。num = num1+num2

情況一:a[i]作為乙個區間的端點,那麼可以選擇的區間另乙個端點,可選擇端點個數也就等於區間個數,num1 = r[i]-l[i](當r[i]=l[i]時,也就是區間長度為1時,r[i]-l[i]=0,對答案沒影響)。

情況二:a[i]作為區間中的一點,那麼要選擇區間的左右兩個端點。在l[i]到i之間選乙個作為左端點,r[i]-i之間選乙個作為右端點,乘法原理可得區間的個數num2 = (r[i]-i) * (i-l[i])

num = num1+num2 = r[i]-l[i] + (r[i]-i) * (i-l[i])

方式二:直接num = (r[i]-i+1) * (i-l[i]+1) - 1

舉個例子:1 2 3 4 5,求必須包含3的區間個數,左邊有3種選擇:[1 2];[2];不選;,右邊也有三種選擇:[4 5];[4];不選;但是題目中要求區間長度至少為2,所以兩邊都不選的情況要減去。

有個小技巧:求最小值時,我們可以讓a[i] = -a[i],那麼求最小值,就等於取反後的求最大值,再按照上面的過程求一下就好了(注意因為取了相反數,故最後求出來的和也帶負號,故最後不是-而是+)。

有乙個坑點就是如果陣列中相鄰元素重複的情況會導致最終區間重複計算,為了防止重複,求左右拓展範圍的時候第一遍掃帶等號,第二遍掃不帶等號(或者第一遍不帶等號,第二遍帶)

即單調棧中乙個arr[i]>=arr[sk.top()],另乙個是arr[i]>arr[sk.top()]。

舉個例子:5 6 5。這種情況很明顯只有三個區間[5 6],[5 6 5],[6 5],即最終要減去15。

但是如果都用》=,那麼每個位置對應的區間(li, ri)分別為(1, 3),(2, 2),(1, 3)。最終卻減去了20,可以發現[1,3]區間被減了兩次。

再舉個例子:2 1 2。這種情況的最大值2對應的很明顯只有3個區間[2 1],[2 1 2],[1 2],即最終要加上2*3=6。

但是如果都用》=,那麼每個位置對應的區間(li, ri)分別為(1, 3),(2, 2),(1, 3)。最終卻加上了8。可以發現[1,3]區間被加了兩次,所以需要保證相等的時候一端擴充套件,避免重複計算。

兩種寫法:

寫法一:

1 #include 2 typedef long

long

ll;3

#define pb push_back

4#define mst(a) memset(a,0,sizeof(a))

5const

int inf = 0x3f3f3f3f;6

const

double eps = 1e-8;7

const

int mod = 1e9+7;8

const

int maxn = 1e5+10;9

using

namespace

std;

1011

inta[maxn], b[maxn];

12int

l[maxn], r[maxn];

13 stacksk;

1415 ll solve(int arr, int

n)16

25while(!sk.empty()) sk.pop(); //

別忘了26

for(int i=n;i>=1;i--)

2733 ll res = 0;34

for(int i=1;i<=n;i++)

35 res += arr[i] * ((ll)(i-l[i]+1)*(r[i]-i+1)-1); //

記得加ll

36return

res;37}

3839

40int

main()

4157 ll maxsum =solve(a, n);

58 ll minsum = solve(b, n); //

已經取負過了

59 printf("

%lld\n

",maxsum+minsum);60}

6162

return0;

63 }

寫法二:

1 #include 2 typedef long

long

ll;3

#define pb push_back

4#define mst(a) memset(a,0,sizeof(a))

5const

int inf = 0x3f3f3f3f;6

const

double eps = 1e-8;7

const

int mod = 1e9+7;8

const

int maxn = 1e5+10;9

using

namespace

std;

1011

inta[maxn], b[maxn];

12int

l[maxn], r[maxn];

1314 ll solve(int arr, int

n)15

23for(int i=n;i>=1;i--)

2430 ll res = 0;31

for(int i=1;i<=n;i++)

3236

return

res;37}

3839

intmain()

4056 ll maxsum =solve(a, n);

57 ll minsum = solve(b, n); //

已經取負過了

58 printf("

%lld\n

",maxsum+minsum);59}

6061

return0;

62 }

看到有別的寫法,粘一下,可以研究研究:

1 #include2

using

namespace

std;

3 typedef long

long ll;//

要用long long

4int

read()

9return

sum;

10 }//

資料量大,用快讀

11const

int n=100005;12

int t,n,a[n],s[n];//

s為單調遞減棧,儲存的是下標,單調遞減指的是下標對應a陣列元素的單調遞減

1314

//返回最大(小)值的和

15ll solve()

26 s[++top] = i;//

不管怎麼樣,i都要入棧的

27 sum += (ll)(s[top] - s[top-1]) * a[ s[top] ];//

s[top]其實就是i,這裡這樣寫是為了保持和上面while迴圈式子的一致性(好看)

28//

要轉long long,不然只有70分

29 ans +=sum;30}

31return

ans;32}

3334

intmain()

44return0;

45 }

牛客 區區區間間間 單調棧

定義 v max a i a j l i,j r 求 sum sum v 轉換 sum sum max a i sum sum min a i 因此這裡就轉換成了求乙個區間內的最大和最小值。我們考慮最大值的區間覆蓋,這裡就形成了乙個單調棧了,在當前取值的左右兩側都是遞增的,因此我們得到當前值得區間覆...

每日一題 區區區間間間(單調棧的應用)

原題的題意可以理解為求所有子區間的最大值減去最小值的和。即所有子區間的最大值減去最小值。我們考慮用單調棧求解。維護兩個陣列 l i r i 表示當前元素作為最大值所能到達的左邊和右邊的下標是多少 當前元素作為最值 用單調棧維護。先正著維護左區間,再倒著維護右區間。維護時的操作 當前元素大於前乙個元素...

HDU5696 區間的價值(分治 單調佇列)

題目 定義區間的價值為這個區間內最大值和最小值的乘積。給定乙個序列,求這個序列的每個長度的區間價值的最大值。所有測試資料嚴格隨機。思路 初學分治,感覺挺不好想出來,參考了一下別人的思路。因為要求每個區間最大值和最小值的乘積,先固定最小值,然後列舉最大值,由於確定了最小的一邊,隨著長度增加,區間的價值...