DP 优化

前缀和优化

转移形式

1D / 1D

\[\text{dp}_i = \sum_{j \in [L(i), R(i)]} \text{dp}_j +C_i \]

\(C_i\) 为仅与 \(i\) 有关的变量

2D / 1D

\[\text{dp}_{i,j} = \sum_{k\in [L(i), R(i)]}\text{dp}_{i-1, k} \]

优化目标

将转移优化成 \(0\text D\)\(\mathcal O(1)\)

例题

力扣1871 跳跃游戏 (Jump Game)

初始状态:

\[\text{dp}_{i} = \left[ \sum_{j \in \left[ i - \max\text{J},i-\min\text{J} \right]} \text{dp}_j \gt 0 \right] \]

优化

\(\text{pSum}_i=\sum_{k \in [1,i]}\text{dp}_{k}\)

则有:

\[\text{dp}_i = \left[\text{pSum}_{i-\min\text J} - \text{pSum}_{i-\max\text J -1} \gt 0 \right] \]

时间复杂度:\(\mathcal O(n)\)

prefixDp[0] = dp[0];
int n = str.size();
for (int i = 1; i < n; i++) {
    if (str[i] == '1') dp[i] = 0;
    else dp[i] = getSum(max(i - maxJump, 0), min(i - minJump, n - 1)) > 0;
    prefixDp[i] = prefixDp[i - 1] + dp[i];
}

AT_dp_m Candies

初始状态

\[\text{dp}_{ij} = \sum_{k =j-a_i}^j\text{dp}_{i-1,k} \ \text{mod} \ \left(10^9+7\right) \]

优化

记:

\[\text{pSum}_{ij} = \sum_{k = 1}^{j}\text{dp}_{ik} \]

有:

\[\text{dp}_{ij} = \text{pSum}_{i-1,j}-\text{pSum}_{i-1,j-a_i-1} \]

for (int i = 0; i <= a[1]; i++) dp[1][i] = 1;
prefixSum[1][0] = dp[1][0];
for (int j = 1; j <= k; j++) prefixSum[1][j] = prefixSum[1][j - 1] + dp[1][j];
for (int i = 2; i <= n; i++) {
    for (int j = 0; j <= k; j++) dp[i][j] = (getSum(i - 1, max(j - a[i], 0), j) mod MOD + MOD) mod MOD;
    prefixSum[i][0] = dp[i][0];
    for (int j = 1; j <= k; j++) prefixSum[i][j] = ((prefixSum[i][j - 1] + dp[i][j]) mod MOD + MOD) mod MOD;
}
cout << (dp[n][k] mod MOD + MOD) mod MOD << "\n";

LOJ6077 [2017 山东一轮集训 Day7]逆序对

正解是 \(\bf OGF\)(推起来有一点痛苦)

\(\bf n \le \bf 20\)

直接枚举?不彳亍,复杂度 \(\mathcal O(n!)\)

用状压是 \(\mathcal(2^nn)\),勉强能过,吗?

\(\bf n \le \bf 2000\)

优化的线性 DP

初始状态

\[\text{dp}_{ij}=\sum_{k=j-i+1}^j \text{dp}_{i-1,j} \ \text{mod} \ \left(10^9+7\right) \]

优化

显然

dp[1][0] = 1;
prefixSum[1][0] = 1;
for (int i = 1; i <= k; i++) prefixSum[1][i] = prefixSum[1][i - 1] + dp[1][i];
for (int i = 2; i <= n; i++) {
    for (int j = 0; j <= k; j++) dp[i][j] = (getSum(i - 1, max(j - i + 1, 0), j) mod MOD + MOD) mod MOD;
    prefixSum[i][0] = dp[i][0];
    for (int j = 1; j <= k; j++) prefixSum[i][j] = ((prefixSum[i][j - 1] + dp[i][j]) mod MOD + MOD) mod MOD;
}
\(\bf n \le \bf 10000\)

显然是 \(\mathcal O(n\log n)\)

注意 \(n\)\(k\) 并不同阶(\(k \le {n \choose 2}\)

考虑原始方程 \(\text{dp}_{ij}\) ,设其对应的生成函数是:\(F_i(x)\)

有递推关系:

\[F_i = \left(\sum_{j=0}^{i-1}x^j\right) F_{i-1}(x) \]

化简:

\[F_n = \frac{\prod\limits_{i=1}^{n}(1-x^i)}{(1-x)^n} \]

或者一步到位:

每次加入一个数会增加从 \(1\)\(n\) 个逆序对,所以直接出:

\[F_n(x) = \prod_{i=1}^n \sum_{j=0}^{i}x^i \]

形式是多个多项式乘积,考虑先 \(\ln\)\(\exp\)

\[\ln F_n = \sum_{i=1}^{n}\ln\ (1-x^i) - n \ln(1-x) \]

然后第一项套 Taylor 展开:

\[\ln (1 - x^a) = \sum_{i\ge1}-\frac{x^{ai}}{i} \]

或者求导再积分:

\[\begin{split} \left[\ln (1-x^a) \right]' &= -\frac{ax^{a-1}}{1-x^a} = \sum_{i \ge 1} -ax^{ai+a-1} \\ \to \ln (1 - x^a) &= \int \left[\ln[1-x^a]'\right]\text{d}x \\ &= \sum_{i\ge 1}-\frac{a}{ai+a}x^{ai+a}\\ &=\sum_{i\ge 1} -\frac{x^{ai}}{i} \end{split} \]

带入:

\[\ln F_n = \sum_{i=1}^{n}\sum_{j\ge 1}-\frac{x^{ij}}{j}\ - n\ln (1-x) \]

然后再 \(\exp\)

复杂度:

\(\ln\)\(\mathcal O(n \log n)\)\(\exp\)\(\mathcal O(n\log n)\),合并:\(\mathcal O(n)\)

总复杂度:\(\mathcal O(n\log n)\)

但是!!!,模数是 \(10^9+7\) ,不满足 NTT 的要求,只能打任意模数的 NTT,个人认为三模数比较好(快,精度高,就是不好写(也就64行))

(燃成舍利子了,\(\exp\) 写挂了,要改的话牵扯到太多东西了(求逆要改,\(\ln\) 要改)

(为了饺子包了一盘醋,确信,因为一连 A 了 4 道模版紫题

贴个板子 click

[update 2026 Feb 25th]

终于肝出来了:(根本没有多难,但是千万不要直接模拟生成函数(直接算 \(1-x^i\)\(\ln\) 再加起来,别问我怎么知道)

int main() {
	int n = in(), k = in();
	Polynomial lnF(0);
	lnF.resize(k + 1, 0);
	for (int i = 1; i <= n; i++) {
		for (int j = 1; i * j <= k; j++) lnF[i * j] = (lnF[i * j] - fpow(j, MOD - 2, MOD) + MOD) % MOD;
		if (i > k) break;
	}
	for (int j = 1; j <= k; j++) lnF[j] = (lnF[j] + n * fpow(j, MOD - 2, MOD)) % MOD;
	Polynomial F = lnF.exp(k + 1);
	out(F[k]);
	putchar('\n');
	return 0;
}

逆天,主函数只有 \(20\) 行,而模版有足足 \(400\) 多行

P10741 [SEERC 2020] Fence Job

依旧费脑子(写完上一道题变成舍利子了,直接无脑写了

转移简单,正确性逆天

转移:
dp[0] = 1;
for (int i = 1; i <= n; i++) {
    int left = i, right = i;
    while (height[left - 1] > height[i]) left--;
    while (height[right + 1] > height[i]) right++;
    for (int j = left; j <= right; j++) dp[j] = ((dp[j] + dp[j - 1]) mod MOD + MOD) mod MOD;
}

居然只用 \(264\text{ms}\) ,直接抢到了最优解

正确性

只用证明一点,选取栏杆的顺序固定,且一定按照输入顺序(选取栏杆的编号单调不降)

再化归一下,即证明不会在两个不相邻的段内存在相同的数

抓住题目中的关键一句:包含 \(n\)互不相同的整数

直接就说明了以上:

当一种高度已经在前面出现过,则之后一定不会有,为什么呢?

考虑第二类数学归纳法证明

假设 \(\left\{a_i\right\}\) 是满足题目条件的序列,目前考虑到了 \(i\),且前 \(j,j\in[1,i-1]\) 个都满足条件 ,那么就考虑 \(i + 1\)

此时有两种与之不同的情况:\(a_i \lt a_{i+1}\) 或者 \(a_i \gt a_{i+1}\)

第一种情况下,虽可以直接覆盖,但是这样就成了连续一段

第二种情况,显然无法覆盖之后的数了

所以在 \(i+1\) 下依旧成立

\(\square\)

⊰线段树优化⊱

转移形式

壹 1D

\[\text{dp}_{i} = \sum_{j<i,a_i\mathcal R a_j} \text{dp}_{j} + C \]

贰 2D

\[\text{dp}_{ij} = \sum_{k<j,a_j\mathcal R a_k} \text{dp}_{i-1,k}+C \]

上两种都是直接套线段树,或者是考虑权值

叁 最值 1D

\[\text{dp}_i = \max_{j\in N(i)} \left( \text{dp}_j+f(i,j) \right) \]

对于这种情况一般就是考虑在固定一些值的情况下的变化

例题

#6077. 「2017 山东一轮集训 Day7」逆序对

又是它……

当我们使用刷表法更新 DP 的话,会发现需要用到线段树优化

然后就没了

dp[1][0] = 1;
st.build(1, 1, n);
for (int i = 1; i <= n; i++) {
    for (int j = 0; j <= k; j++) st.update(j, min(j + i, k), dp[i][j]);
    for (int j = 1; j <= k; j++) dp[i + 1][j] = st.query(j, j);
    st.build(1, 1, k);
}

这样复杂度是 \(\mathcal O(nk\log n)\)

还可以差分优化,复杂度 \(\mathcal O(nk)\)

dp[1][0] = 1;
for (int i = 1; i <= n; i++) {
    for (int j = 0; j <= k; j++) {
        dp[i + 1][j] += dp[i][j];
        dp[i + 1][min(j + i, k) + 1] -= dp[i][j];
    }
    for (int j = 1; j <= k; j++) dp[i + 1][j] = ((dp[i + 1][j] + dp[i + 1][j - 1]) % MOD + MOD) % MOD;
}

P3970 [TJOI2014] 上升子序列

难点在于去重

for (int i = 1; i <= n; i++) {
    int pos = lower_bound(v.begin(), v.end(), a[i]) - v.begin() + 1;
    ll sum = st.query(1, pos - 1);
    ll newDp = (sum + 1) % MOD;
    ll delta = (newDp - last[pos] + MOD) % MOD;
    if (delta) st.update(pos, (st.query(pos, pos) + delta) % MOD);
    last[pos] = newDp;
}

ll ans = (st.query(1, cnt) - cnt + MOD) % MOD;
cout << ans << "\n";

还是比较简单

The Bakery

某一次周考题

遇到这种题(对应情形3),首先应该思考 \(f(i,j)\) 如何随着 \(i,j\)(大多数时候是一个元)变化

像这道题,可以暂时将 \(j\) 看做定值

不放举个例子来理解 \(f(i,j)\) 的变化

a = {0, 1, 2, 4, 1, 2, 1}

1 (\(i\)) 2 3 4 5 6
\(\textcolor{red}1\) (\(f(x+1,i)\)) \(\textcolor{red}2\) \(\textcolor{red}3\) \(3\) \(3\) \(3\)
\ \(\textcolor{red}1\) \(\textcolor{red}2\) \(\textcolor{red}3\) \(3\) \(3\)
\ \ \(\textcolor{red}1\) \(\textcolor{red}2\) \(\textcolor{red}3\) \(3\)
\ \ \ \(\textcolor{red}1\) \(\textcolor{red}2\) \(2\)
\ \ \ \ \(\textcolor{red}1\) \(\textcolor{red}2\)
\ \ \ \ \ \(\textcolor{red}1\)
\ \ \ \ \ \

注意标红部分

pre[a[i]] 表示 a[i] 上一次出现的位置

则修改范围是 \([\text{pre}[a[i]], i-1]\)

现在就十分明了

for (int i = 1; i <= n; i++) {
    for (int j = 1; j <= k; j++) {
        st[j - 1].update(i - 1, i - 1, dp[i - 1][j - 1]);
        st[j - 1].update(pre[a[i]], i - 1, 1);
        if (j - 1 <= i - 1) dp[i][j] = st[j - 1].query(0, i - 1);
        //			st[j].update(i - 1, i - 1, dp[i][j]);
    }
    pre[a[i]] = i;
}
posted @ 2026-02-26 08:03  Yangyihao  阅读(24)  评论(0)    收藏  举报