1. 程式人生 > 實用技巧 >DP學習記錄Ⅱ

DP學習記錄Ⅱ

DP學習記錄Ⅰ

以下為 DP 的優化。

人腦優化DP

P5664 Emiya 家今天的飯

正難則反。考慮計算不合法方案。一個方案不合法一定存在一個主食,使得該主食在多於一半的方法中出現。

列舉這個“超標”的主食 \(i\)。設 \(f[j][k][l]\) 表示前 \(j\) 種方法中一共選擇了 \(k\) 個主食 \(i\),一共選擇了 \(l\) 個主食 的方案數。最終答案為 \(f[n][u][v]\),其中 \(u > v / 2\)。這樣,我們得到了一種 \(O(m^2n^3)\) 的方法。

優化1:

轉移的時候把其它菜的方案和預處理一下,可以砍掉轉移的一個 \(m\)。複雜度: \(O(mn^3)\)

優化2:

我們發現最終我們關心的不是 \(u,v\) 具體是多少,而是 \(u, v\) 的相對大小。因此我們可以再合併一些狀態,把狀態定義為:\(f[j][k]\) 表示前 \(j\) 種方法中第 \(i\) 種主食比其餘主食多 \(k\) 個 的方案數。這樣可以再狀態數上砍掉一個 \(n\)。複雜度: \(O(mn^2)\)

優化3:

各種卡常。

資料結構優化DP:單調佇列優化DP

單調佇列優化多重揹包詳解

題目

仔細觀察樸素多重揹包的轉移方程:

\[f[j] <- g[j - k * w] + k * v \]

其中 \(w\) 表示當前物品的代價, \(v\) 表示當前物品的價值,\(k\)

為列舉的數量,需要保證 \(k <= K\)

仔細觀察發現由於 \(j - k * w\),影響 \(f[j]\) 的狀態只有與 \(j\)\(w\) 同餘的那些狀態,我們把這些狀態單獨拿出來。這時,問題就有點像“在距離 \(j\) 不超過 \(K\) 的狀態中取最大值”,即“滑動視窗”。故可以使用單調佇列優化。這裡維護的是一個單調下降的佇列。

還差 \(k * v\) 沒有處理。我們發現 \(k * v\) 在提溜出來的那一堆狀態裡面永遠是個公差為 \(-v\) 的等差數列,那麼我們乾脆在加入佇列的時候讓狀態減去 \(id * v\) (加入的是提溜出來的陣列中的第 \(id\)

個),就能保證佇列中的相對大小。經過手玩嘗試後,知道佇列的真實值應該再加上 \(ct * v\)(當前是第 \(ct\) 個)。

對於每一個剩餘系都這樣做一下,就能 \(O(m)\) 地刷完一遍 \(f\)

//q 記錄佇列中的值,id 記錄佇列中狀態的位置
for (register int i = 1; i <= n; ++i) {
	int w, v, s; read(w), read(v), read(s);
	memcpy(g, f, sizeof(g));
	for (register int j = 0; j < w; ++j) {
		int front = 0, rear = 0;
		for (register int k = j, ct = 1; k <= V; k += w, ++ct) {
			if (ct - id[front + 1] > s)	++front;
			if (front < rear)
				MAX(f[k], q[front + 1] + ct * v);
			while (front < rear && q[rear] <= g[k] - ct * v)	--rear;
			q[++rear] = g[k] - ct * v;
			id[rear] = ct;
		}
	}
}

資料結構優化DP:線段樹優化DP

資料結構優化DP:平衡樹優化DP

T132728 最大價值(value)

按照 \(a\) 排序,這樣我們加入這個數的時候一定會把它放在最後一位,這樣我們就可以用揹包來解決了。思路來源:拯救小矮人。

轉移方程: \(f[i][j] <- f[i - 1][j - 1] + (j - 1) * a + b\)

通過對拍可知,這個轉移一定會在 \(j\) 挨著 \(i\) 的一段狀態發生。假設中間點為 \(k\)

由於轉移是否盡心與兩個相鄰的 \(f\) 都有關係,我們考慮差分

\(g[j] = f[j] - f[j - 1]\)

轉移成功: \(f[i][j] < f[i - 1][j - 1] + (j - 1) * a + b\)

即:\(g[j] < (j - 1) * a + b\)

可以據此二分 \(k\)

轉移後對 \(f\) 陣列的影響:

\(k\) 前: 不變。

\(k\) 及以後: \(f[i][j] = f[i - 1][j - 1] + (j - 1) * a + b\)

轉移後對 \(g\) 陣列的影響:

\(k\) 前:不變。

\(k\) : \(g[k] = (k - 1) * a + b\)

\(k\) 後: \(g[j] = g[j - 1] + a\)

對應的 \(g\) 陣列的變化:

\(k\) 前插入了一個 \((k - 1) * a + b\);後面的所有數都加了 \(a\)

\(Splay\) 可以輕鬆維護這些操作。(二分k,插入,區間加)

決策單調性優化DP

前置技能:四邊形不等式 打表

如果發現決策點單調遞增,那麼可以套用一下板子(注意:一定注意各種細節,比如對佇列非空的特判)

一定要背熟板子啊!!!

SP9070 LIGHTIN - Lightning Conductor

P3515 [POI2011]Lightning Conductor

P5503 [JSOI2016]燈塔

P1912 [NOI2009]詩人小G

T134955 shoes

棧(單調佇列)寫法

/*
維護三元組單調佇列<l, r, pos>,pos為[l,r]內的決策點。
由於有決策單調性,佇列裡的區間和決策點都是單調遞增的。
此題型的大致過程如下:
*/

for (int i = 1; i <= n; ++i) {
	if (佇列非空 && 隊頭右端點比i小)	彈掉隊頭
    else	修改隊頭的l
    用隊頭計算f[i]
    
    if (i -> n 不如 隊尾決策點 -> n 優)	continue;
    while (佇列非空 && i -> 隊尾左端點 優於 隊尾決策點 -> 隊尾左端點)	彈隊尾
    if (隊空)	新增節點(i, n, i)於隊尾
    else {
    	查詢x = 隊尾區間中 i佔優勢的第一個點(可能為r+1)
        修改隊尾右端點
        新增節點(x, n, i)
    }
}

Code:

inline ll calc(int j, int i)//計算用j來轉移i
...
//main()中
q[++rear] = (node){1, n, 1};
f[1] = ...
for (register int i = 2; i <= n; ++i) {
	if (q[front + 1].l == q[front + 1].r)	++front;
	else	++q[front + 1].l;
    //調整最靠前的三元組的 l。注意判空三元組
	register int pos = q[front + 1].pos;
	f[i] = calc(pos, i);
	pre[i] = pos;
    //計算
    //-------
    //插入i
	if (calc(i, n) >= calc(q[rear].pos, n))	continue;
    //i更新無望,提前推出,防止出現空三元組
	while (front < rear && calc(q[rear].pos, q[rear].l) >= calc(i, q[rear].l))
		rear--;
    //嘗試用i彈掉後面的整塊,注意判非空
	if (front >= rear) {
		q[++rear] = (node){i, n, i};
		continue;
	}
    //i足夠優秀,把三元組全部彈完了,就特判退出
	int x = ...
	/*
	二分查詢i佔優勢的最靠左的點,為x
	*/
    
	q[rear].r = x - 1;
	q[++rear] = (node){x, n, i};
}

遞迴寫法

對於一些決策點的貢獻都已知前提下的問題,即狀態有層次的情況,還有一種細節更少,更方便的方法。

對於狀態 \((il, ir, jl, jr)\),其中 \(il, ir\) 表示當前需要算的狀態的區間;\(jl, jr\) 表示當前區間對應的決策點區間。我們可以暴力算出mid(il, ir)的決策點的位置,然後把決策點區間分裂成兩段,然後遞迴子問題。

ll f[N], g[N];//f: 當前要算的狀態;g:上一層的狀態
inline void sol(int ql, int qr, int l, int r) {
	if (ql > qr)	return ;
	int mid = (ql + qr) >> 1;
	int pos = 0;
	int limi = min(mid - 1, r);
	f[mid] = inf;
	for (register int i = l; i <= limi; ++i) {
		ll tmp = g[i] + Query(i + 1, mid);
		if (tmp < f[mid])	f[mid] = tmp, pos = i;
	}
	sol(ql, mid - 1, l, pos); sol(mid + 1, qr, pos, r);
}

inline void work() {
	memset(f, 0x3f, sizeof(f));
	f[0] = 0;
	for (register int i= 1; i <= K; ++i) {
		memcpy(g, f, sizeof(g));
		memset(f, 0, sizeof(f));
		sol(1, n, 0, n);
	}
}

斜率優化

P3628 [APIO2010]特別行動隊

P3195 [HNOI2008]玩具裝箱

P2120 [ZJOI2007]倉庫建設

DP凸優化(wqs二分)

暫時沒時間寫了。

主要思想是:不管它的“恰好K個”限制,但是每選一個就罰它一點代價,並記錄它實際選了幾個,根據實際選了幾個來二分。

一般的理解是上/下凸函式,二分斜率切凸包。

另一種理解(見DP凸優化/WQS二分/帶權二分
):上/下凸函式,二分斜率,平移導數,移動導數“零點”。

P2619 [國家集訓隊2]Tree I

CF125E MST Company

P5633 最小度限制生成樹

P1792 [國家集訓隊]種樹

P4383 [八省聯考2018]林克卡特樹

P5308 [COCI2019] Quiz

注意幾點:

  • 假設是上凸函式求最大,那麼我們二分一個斜率 \(k\),應該滿足 \(f(x) = k * x + b(x)\),而我們可以比較容易求出最大的 \(b(x)\),(不是求最大的 \(f(x)\)),然後根據 \(b(x)\)\(x\) 可以反推出 \(f(x)\).因為這裡是反推,所以雖然斜率是 \(k\),我們卻讓每個數都減去 \(k\),最終算 \(f(x)\) 的時候再加 \(k\)

  • 最好在結構體比大小的時候搞一個第二關鍵字:\(ct(x)\),在 \(val\) 相同的時候比較 \(ct\).

  • 最終反推的時候要減去 \(need * mid\),而不是 \(res.ct * mid\)