一些淺顯的 dp 最佳化策略

Zhang_Wenjie發表於2024-09-03

字首和最佳化

這個最佳化的方法還是顯然的,就是當遇到形如 當前狀態是由先前的連續狀態轉移, 則考慮將這種 連續 用字首和維護,一般便會將這一部分的複雜度由 \(O(n)\) 降為 \(O(1)\)

對於我最近遇到的題來說是這樣思考的,當然,更重要的還是最佳化前的轉化,比如透過推式子形成可以字首和處理的求和式


[ABC179D] Leaping Tak

link

考慮定義 \(f[i]\) 表示走到第 \(i\) 個點的方案數

樸素地想,如果設給定的這些不重合區間的數的集合為 \(D\),則有很顯然的遞推式:

\[f[i] = \sum\limits_{j\in D}f[\max\{i - j, 0\}] \]

但是這樣的複雜度是 \(O(n^2)\) 的,瓶頸在於每次遍歷集合 \(D\)

進一步,發現這樣算沒有用到 \(k\) 個區間 這個約束,同時 \(k\) 很小,考慮變形為:

\[f[i] = \sum\limits_{j = 1}^{k}\sum\limits_{p = L[j]}^{R[j]}f[\max\{i - p, 0\}] \]

這就好了, 發現 \(\sum\limits_{p = L[j]}^{R[j]}f[\max\{i - p, 0\}]\) 是一段連續的 \(f\) 值區間,

啟發我們可以搞一個字首和陣列 \(s[i] = \sum\limits_{j=1}^{i}f[j]\),就像用字首和陣列求區間和一樣,顯然對於區間 \([L[j], R[j]]\) 的和就是 \(s[\max\{i - L[j], 0\}]-s[\max\{i - R[j] - 1\}]\)

注意 \(L[j] \leq R[j]\),所以 \(i - L[j]\) 會更大些在右邊,我一開始就搞反了

這樣就把裡邊那層求和式消掉了,複雜度降到 \(O(nk)\)

code
#include <bits/stdc++.h>
#define re register int 
#define max(x, y) (x > y ? x : y)

using namespace std;
typedef long long LL;
const int N = 2e5 + 10, mod = 998244353;

int n, k, L[20], R[20];
LL f[N], s[N];

int main()
{
	ios::sync_with_stdio(false);
	cin.tie(0); cout.tie(0);
	
	cin >> n >> k;	
	for (re i = 1; i <= k; i ++) cin >> L[i] >> R[i];
	
	s[1] = f[1] = 1;
	for (re i = 2; i <= n; i ++)
		for (re j = 1; j <= k; j ++)
		{
			f[i] = (f[i] + s[max(i - L[j], 0)] - s[max(i - R[j] - 1, 0)] + mod) % mod;
			s[i] = (s[i - 1] + f[i]) % mod;
		}
	cout << f[n] % mod << '\n';
	
	return 0;
} 

相關文章