DP 优化
前缀和优化
转移形式
1D / 1D
\(C_i\) 为仅与 \(i\) 有关的变量
2D / 1D
优化目标
将转移优化成 \(0\text D\)(\(\mathcal O(1)\)
例题
力扣1871 跳跃游戏 (Jump Game)
初始状态:
优化
记 \(\text{pSum}_i=\sum_{k \in [1,i]}\text{dp}_{k}\)
则有:
时间复杂度:\(\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
初始状态
优化
记:
有:
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
初始状态
优化
显然
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)\)
有递推关系:
化简:
或者一步到位:
每次加入一个数会增加从 \(1\) 至 \(n\) 个逆序对,所以直接出:
形式是多个多项式乘积,考虑先 \(\ln\) 再 \(\exp\)
然后第一项套 Taylor 展开:
或者求导再积分:
带入:
然后再 \(\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
贰 2D
上两种都是直接套线段树,或者是考虑权值
叁 最值 1D
对于这种情况一般就是考虑在固定一些值的情况下的变化
例题
#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;
}

浙公网安备 33010602011771号