1. 程式人生 > 其它 >【演算法筆記】單調佇列和優化DP

【演算法筆記】單調佇列和優化DP

「冥界の土地も有限よ、餘計な霊魂は全て斬る!」

單調佇列

單調佇列是一種神奇的資料結構。

一般我們用來優化這一類決策有單調性的問題:

比如:一個元素很明顯不可能進入決策的時候,我們可以用單調佇列來做到 \(\text{O}(1)\) 的決策。

這裡來一道題(來自WC2016 by 宋新波)

這裡相當於是在保質期之內,求每一天最大能吃到的值。

這裡考慮維護一個佇列,隊列當中單調不上升。

那麼隊頭就是我們所想要的結果(最優)。

  • Day1:
--------------
 80
--------------

這裡隊頭是80.

  • Day2:
-------------
 80 75
-------------

這裡隊頭仍然是80.

  • Day3

這裡開始就要注意了,我們放入78,但是放進去之後就不滿足條件了。

為什麼?

不是因為後面小了就是前面大了(這裡是單調不上升所以是這樣,其他同理)。

不過我們一般考慮後面小了(從後面進隊出隊)。

那麼在插入78 之前,我們就要彈出75 (從後方)

-------------
 80 75(pop) --->
-------------

-------------
 80   <------ 78(push)
-------------

-------------
 80 78
-------------

此時隊頭仍然是 80。

這裡我們不難看出,如果存在 \(a_i <a_j\)

那麼 \(a_i\) 一定會被 \(a_j\) 替換掉。

因為 \(a_i\) 不僅時間靠前(更快過期),價值也不大。

所以 \(a_i\) 就是 冗雜的多餘決策。

那麼單調佇列在維護的時候的意義就是去除這些冗雜的元素。

因為每個元素只會入隊一次(所以要小心某些題的條件),所以 複雜度是 \(\text{O}(n)\) 的。

  • Day 4
-------------
 80 78 73
-------------
  • Day 5

這裡根據 Day3,我們需要把78,73彈出來讓更好的 79 入隊。

所以佇列就變成了這樣:

-------------
 80 79
-------------
poped:78 73

但是注意!這裡80過期了,所以要把80也彈出(從隊頭彈出)

那麼就是這樣的:

       -------------
<---80  79
       -------------

所以第五天就是 79 為最值。

那麼我們就可以知道,在這道題使用單調佇列時,要注意一下幾點:

  1. 佇列中始終單調

  2. 隊首為最優情況

  3. 及時去除冗雜情況

那麼我們每一次迴圈的時候,都檢查一下隊首是否過期還有新入隊元素會不會造成更新就好了。

總結一下:也就是這樣的

這裡給出維護一個長度為 \(k\) 的滑動視窗中的最值的程式碼。


#include<bits/stdc++.h>
using namespace std;

const int si=1e6+10;
int n,k;
int a[si];

int q1[si],head1=1,tail1=0; //這樣子賦值是為了讓第一個元素能入隊。
int q2[si],head2=1,tail2=0;
int ans[si][2]; //0->min 1->max

int main(){
	scanf("%d%d",&n,&k);
	for(register int i=1;i<=n;++i){
		scanf("%d",&a[i]);
	}
	for(register int i=1;i<=n;++i){
		while(head1<=tail1 && q1[head1]+k<=i) head1++;//排除隊頭過期
		while(head1<=tail1 && a[i]<=a[q1[tail1]]) tail1--;//排除隊尾冗雜。
		q1[++tail1]=i;
		while(head2<=tail2 && q2[head2]+k<=i) head2++;
		while(head2<=tail2 && a[i]>=a[q2[tail2]]) tail2--;
		q2[++tail2]=i;
		ans[i][0]=a[q1[head1]],ans[i][1]=a[q2[head2]];//注意這裡q存的是下標,所以要將其作為下標。
	}
	for(register int i=k;i<=n;++i){//k是因為它的區間長度已經固定,所以前面的那些長度不滿足,要去掉。
		printf("%d ",ans[i][0]);
	}puts("");
	for(register int i=k;i<=n;++i){
		printf("%d ",ans[i][1]);
	}
	return 0;
}

在每道題使用單調佇列之前需要看清題目的端點取捨和條件。

比如 求m區間內的最小值 那道題就是求 \([i-k-1,i)\) 的最小值.

所以說最好在用之前手玩一下資料。

單調佇列優化DP

Example

題目描述
在幻想鄉,琪露諾是以笨蛋聞名的冰之妖精。

某一天,琪露諾又在玩速凍青蛙,就是用冰把青蛙瞬間凍起來。但是這隻青蛙比以往的要聰明許多,在琪露諾來之前就已經跑到了河的對岸。於是琪露諾決定到河岸去追青蛙。

小河可以看作一列格子依次編號為0到N,琪露諾只能從編號小的格子移動到編號大的格子。而且琪露諾按照一種特殊的方式進行移動,當她在格子i時,她只移動到區間[i+l,i+r]中的任意一格。你問為什麼她這麼移動,這還不簡單,因為她是笨蛋啊。

每一個格子都有一個冰凍指數A[i],編號為0的格子冰凍指數為0。當琪露諾停留在那一格時就可以得到那一格的冰凍指數A[i]。琪露諾希望能夠在到達對岸時,獲取最大的冰凍指數,這樣她才能狠狠地教訓那隻青蛙。

但是由於她實在是太笨了,所以她決定拜託你幫它決定怎樣前進。

開始時,琪露諾在編號0的格子上,只要她下一步的位置編號大於N就算到達對岸。

輸入格式
第1行:3個正整數N, L, R

第2行:N+1個整數,第i個數表示編號為i-1的格子的冰凍指數A[i-1]

輸出格式
一個整數,表示最大冰凍指數。保證不超過2^31-1

輸入輸出樣例
輸入 
5 2 3
0 12 3 11 7 -2
輸出 
11

說明/提示
對於60%的資料:N <= 10,000

對於100%的資料:N <= 200,000

對於所有資料 -1,000 <= A[i] <= 1,000且1 <= L <= R <= N

這題明顯的是一個線性DP。

首先我們考慮 60pts 的資料怎麼做。

\(f[i]\) 表示Cirno走到第 \(i\) 個格子的時候可以取得的最大值。

那麼很明顯方程式這樣子的:

\[f[i]=\max \{ f[j]+ice[i] \} ,(i \in [l,n],j \in [i-r,i-l]) \]

轉移應該是 \(\text{O}(n^2)\) 的。 60% 資料勉強過。

現在考慮對它進行優化。

仔細想想,我們每次更新都是這樣子的。

你會發現 \([i-r,i-l]\) 這個區間的上下界是隨 \(i\) 不斷後移的(區間平移,具有單調性)

而且對於每一個決策它只會進入這個區間一次並且只出去一次(區間不會平移回來)。

所以我們想到了“滑動視窗” 這個東西。

那麼就可以使用單調佇列進行決策的優化。

我們首先維護佇列裡的單調性,保證隊頭一定是當前可以轉移過來的狀態的最優狀態。

另外,當隊頭無法到達當前決策點 \(i\) 的時候,我們把隊頭出隊。

這樣子我們就做到了 \(\text{O}(1)\) 讓每一次轉移時都取最優值(並且這個狀態是合法的)。

說白了就是使用單調佇列代替 for 來決策。

注意,按照我的這種寫法,單調佇列維護的是 \([head,tail]\) 這個閉區間的元素。

Code:


#include<bits/stdc++.h>
using namespace std;

const int si=2e5+10;
int n,l,r;
int a[si];
int f[si];
int q[si],head=1,tail=0;

int main(){
	scanf("%d%d%d",&n,&l,&r);
	if(l>r) swap(l,r);
	for(register int i=0;i<=n;++i){
		scanf("%d",&a[i]);
	}
	memset(f,-0x3f,sizeof f);
	int ans=-INT_MAX;//attention here
	f[0]=0;//init
	for(register int i=l;i<=n;++i){ start from 0,so the first point we can arrive is l.
		while(head<=tail && q[head]<=(i-r-1)) head++;
		while(head<=tail && f[q[tail]]<f[i-l]) tail--;
		q[++tail]=i-l;
		f[i]=f[q[head]]+a[i];
		if(i+r>n) ans=max(f[i],ans); // reach the destination
	}
	printf("%d",ans);
	return 0;
}

這時候就有人要問了,二維的怎麼做?

也是同理。

這樣子的轉移(\([l,r]\)\(j\) 平移) 都是可以的。

只是你需要根據題目要求判斷一下什麼時候出隊,什麼時候入隊,什麼時候決策。