前言

本文选题都较为基础,仅用于展示优化方式,如果是要找题单而不是看基础概念,请忽略本文。

本文包含一些常见的dp优化(“√”表示下文会进行展示,没“√”表示暂时还咕着):前缀和优化(√)、单调队列优化(√)、斜率优化(√)、四边形不等式优化、数据结构优化……

由于写本文主要是记录蒟蒻的dp优化学习过程,所以可能很不完善,也会有很多错误 (?) 。推荐看巨佬的:【学习笔记】动态规划—各种 DP 优化 - 辰星凌

1. 前缀和优化dp

进行状态转移时,如果发现需加上前面的一类状态,就可以选择使用数组进行累计操作,以达到降维度的效果。

1.1 P1521 求逆序对

1.1.1 题目大意

给出 \(n\),\(k\),问 \(1..n\) 的排列中正好有 \(k\) 个逆序对的排列数。

1.1.2 数据范围

\(1 \leq n \leq 100\),\(1 \leq k \leq n * (n - 1) / 2\)。

1.1.3 做法

设 \(f_{i, j}\) 表示 \(1..i\) 的全排列中有 \(j\) 个逆序对的排列数。答案即为 \(f_{n, k}\)。

考虑在 \(1..(i-1)\) 的排列中加入一个 \(i\) 所能贡献的逆序对数量。由于 \(i\) 是最大的,故当它被排在第 \(j\) 个时,相应的逆序对数量会增加 \(i - j\) 个。

不难列出转移式:\(f_{i, j}=\sum_{k = 0}^{min(j, i - 1)}f_{i - 1, j - k}\)。

其中的 \(k\) 表示新增的逆序对数。

同时初始化 \(f_{1, 0}=1\)。

由于此题比较水,所以不优化也能过。

const int N = 110, mod = 10000;
int n, k, f[N][N * N >> 1];
int main() {
n = read(), k = read();
f[1][0] = 1;
for (int i = 2; i <= n; i++)
for (int j = 0; j <= (i * (i - 1)) >> 1; j++)
for (int k = 0; k <= min(j, i - 1); k++)
f[i][j] = (f[i][j] + f[i - 1][j - k]) % mod;
printf("%d\n", f[n][k]);
return 0;
}

接下来开始优化

现在把上面转移的式子改一下,方便优化:

\(f_{i, j}=\sum_{k = max(0, j - (i - 1))}^jf_{i - 1, k}\),相应的,代码可以改成这样:

	for (int i = 2; i <= n; i++)
for (int j = 0; j <= (i * (i - 1)) >> 1; j++)
for (int k = max(0, j - (i - 1)); k <= j; k++)
f[i][j] = (f[i][j] + f[i - 1][k]) % mod;

开数组 \(s_{i, j}=\sum_{k = 0}^jf_{i, k}\),那么 \(s_{i, j}=s_{i, j - 1}+f_{i, j}\) 。

相应的,转移式变为 \(f_{i, j}=s_{i - 1,j}-s_{i - 1, j - (i - 1) - 1}\),注意边界问题

for (int i = 1; i <= n; i++) f[i][0] = s[i][0] = 1;
for (int i = 2; i <= n; i++) {
for (int j = 1; j <= (i * (i - 1)) >> 1; j++)
s[i - 1][j] = (s[i - 1][j - 1] + f[i - 1][j]) % mod;
for (int j = 1; j <= (i * (i - 1)) >> 1; j++)
f[i][j] = (s[i - 1][j] + mod - ((j - (i - 1) - 1) < 0 ? 0 : s[i - 1][j - (i - 1) - 1])) % mod;
}

注意到 \(s\) 数组的前一维似乎没有什么用处,考虑使用滚动数组继续优化。

for (int i = 1; i <= n; i++) f[i][0] = 1;
s[0] = 1;
for (int i = 2; i <= n; i++) {
for (int j = 1; j <= (i * (i - 1)) >> 1; j++)
s[j] = (s[j - 1] + f[i - 1][j]) % mod;
for (int j = 1; j <= (i * (i - 1)) >> 1; j++)
f[i][j] = (s[j] + mod - ((j - (i - 1) - 1) < 0 ? 0 : s[j - (i - 1) - 1])) % mod;
}

1.2 P2513 [HAOI2009]逆序对数列

1.2.1 题目大意

给出 \(n\),\(k\),问 \(1..n\) 的排列中正好有 \(k\) 个逆序对的排列数。

1.2.2 数据范围

\(1 \leq n, k \leq 1000\)。

1.2.3 做法

乍一眼看是不是和上题一模一样。

如果直接提交上题的代码(改了数据范围),就会得到30分的好成绩。(最后几个点全部MLE)

稍稍计算一下,就会发现 \(499500000\) 的 int 数组是不是有那么亿点点大?

那么如何优化代码呢?

注意到上题的代码中,逆序对数枚举的上限为 \(\frac {n \times (n-1)} {2}\),再瞅一眼本题数据范围,最大逆序对数只有 \(1000\)?!

不难想到改成以下代码:

const int N = 1010, mod = 10000;
int n, k, f[N][N], s[N ];
int main() {
n = read(), k = read();
for (int i = 1; i <= n; i++) f[i][0] = 1;
s[0] = 1;
for (int i = 2; i <= n; i++) {
for (int j = 1; j <= min((i * (i - 1)) >> 1, k); j++)
s[j] = (s[j - 1] + f[i - 1][j]) % mod;
for (int j = 1; j <= min((i * (i - 1)) >> 1, k); j++)
f[i][j] = (s[j] + mod - ((j - (i - 1) - 1) < 0 ? 0 : s[j - (i - 1) - 1])) % mod;
}
printf("%d\n", f[n][k]);
return 0;
}

真好,既优化了空间又优化了时间。

2. 单调队列优化dp

OI-Wiki 传送门

借助单调队列的单调性,及时排除不可能的决策,保持候选集合的高度有效性和秩序性。

单调队列尤其适合优化决策取值范围的上、下界均单调变化,每个决策在候选集合中插入或删除至多一侧的问题。

2.1 P1440 求m区间内的最小值

2.1.1 题目大意

给定一个长度为 \(n\) 的数列 \(a\),对于每个 \(i\) 输出 \(min\{a_{i-m},a_{i-m+1},..,a_{i-1}\}\)。

2.1.2 数据范围

\(1\leq m\leq n\leq 2\times10^6\),\(1\leq a_i\leq3\times10^7\)。

2.1.3 做法

好像和单调队列优化dp没什么关系?

此题用于体验单调队列,就不多写了,直接用单调队列模拟操作即可。

const int N = 2000010;
int n, m, s[N], l = 1, r, a[N];
int main() {
n = read(), m = read();
printf("0\n");
for (int i = 1; i <= n - 1; i++) {
a[i] = read();
while (r >= l && a[s[r]] > a[i]) r--;
s[++r] = i;
while (s[r] - s[l] + 1 > m && l <= r) l++;
printf("%d\n", a[s[l]]);
}
return 0;
}

2.2 P5858 「SWTR-03」Golden Sword

2.2.1 题目大意

有 \(n\) 个物品,编号 \(1..n\),每个物品有坚固值 \(a_i\)。

进行 \(n\) 次操作,对于每次操作,执行以下步骤:

  1. 取出不超过 \(s\) 个物品。
  2. 放入物品 \(i\)。

其中容器最多容纳 \(w\) 个物品。

每次操作会产生 \(a_i\times 物品数(包括放入的物品)\) 的贡献。

求 \(n\) 次操作后总贡献的最大值。

2.2.2 数据范围

\(1\leq s\leq w\leq n\leq5\times10^3\),\(|a_i|\leq10^9\)。

2.2.3 做法

设 \(f_{i,j}\) 表示正在执行第 \(i\) 次操作,容器内共有 \(j\) 个物品所能得到的最大贡献值。

那么 \(f_{i,j}=\max\{f_{i-1,k}+a_i\times j\}\)。

其中 \(j-1\leq k\leq \min\{w,j-1+s\}\)。

于是就得到了一个45分做法(long long没开全只有35)

const int N = 5010;
const ll INF = 1e18;
int n, w, s;
ll f[N][N], ans = -INF, a[N];
int main() {
n = read(), w = read(), s = read();
for (int i = 1; i <= n; i++) a[i] = read();
for (int i = 0; i <= n; i++)
for (int j = 0; j <= w; j++)
f[i][j] = -INF;
f[0][0] = 0;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= w; j++)
for (int k = j - 1; k <= min(w, j - 1 + s); k++)
f[i][j] = max(f[i][j], f[i - 1][k] + a[i] * j);
for (int i = 0; i <= w; i++) ans = max(ans, f[n][i]);
printf("%lld\n", ans);
return 0;
}

(不如先动手写个部分分做法?)

考虑优化。先把式子变一下:\(f_{i,j}=\max\{f_{i-1,k}\}+a_i\times j\) \((j-1\leq k\leq \min\{w,j-1+s\})\)。很显然对吧,就是把原来max中重叠的部分提出来而已。虽然说这么一提好像不能优化什么,你会发现,\(\max\{f_{i-1,k}\}\) 好像可以用单调队列优化?!

const int N = 5010;
const ll INF = 1e18;
int n, w, s;
ll f[N][N], ans = -INF, a[N];
int ss[N];
int main() {
n = read(), w = read(), s = read();
for (int i = 1; i <= n; i++) a[i] = read();
for (int i = 0; i <= n; i++)
for (int j = 0; j <= w; j++)
f[i][j] = -INF;
f[0][0] = 0;
for (int i = 1; i <= n; i++) {
int l = 1, r = 0;
ss[++r] = w;
for (int j = w; j; j--) {
while (f[i - 1][ss[r]] < f[i - 1][j - 1] && r >= l) r--;
ss[++r] = j - 1;
while ((ss[l] - ss[r] + 1) - 1 > s && l <= r) l++;
f[i][j] = f[i - 1][ss[l]] + j * a[i];
}
}
for (int i = 0; i <= w; i++) ans = max(ans, f[n][i]);
printf("%lld\n", ans);
return 0;
}

3. 斜率优化dp

OI-Wiki 传送门

3.1 P3195 [HNOI2008]玩具装箱

3.1.1 题目大意

有 \(n\) 件物品,第 \(i\) 件物品压缩后占用 \(C_i\) 的长度。

现需把这些物品压缩进一些容器里,制作一个容器的花费为 \((x-L)^2\),其中 \(x\) 表示容器长度。

每个容器中的物品编号需要是连续的,而将编号 \(i\) 到 \(j\) 的所有物品放在一个容器中,占用的空间 \(x=j-i+\sum_{k=i}^j C_k\)。

求压缩完所有物品所需的总花费的最小值。

3.1.2 数据范围

\(1\leq n\leq 5\times10^4\),\(1\leq L\leq10^7\),\(1\leq C_i\leq10^7\)。

3.1.3 做法

设 \(f_i\) 表示压缩到第 \(i\) 件物品所需的最小花费,不难列出转移方程:

\(f_i=\min\{f_j+(i-j-1+\sum_{k=j+1}^i c_k-L)^2\}\)


令 \(sum_i=\sum_{k=1}^i c_k\),原式可转化为:

\(f_i=\min\{f_j+(i-j-1+sum_i-sum_j-L)^2\}\)。

移项得:

\(f_i=\min\{f_j+((i+sum_i)-(j+sum_j)-(L+1))^2\}\)

令 \(pre_i=sum_i+i\),原式可转化为:

\(f_i=\min\{f_j+(pre_i-pre_j-(L+1))^2\}\)


把式子展开再合并:

\(f_i=\min\{f_j+pre_i^2-pre_i\times pre_j-(L+1)\times pre_i-pre_i\times pre_j+pre_j^2+(L+1)\times pre_j-(L+1)\times pre_i+(L+1)\times pre_j+(L+1)^2\}\)

\(f_i=\min\{f_j+pre_i^2+pre_j^2-2\times pre_i\times pre_j-2\times(L+1)\times(pre_i-pre_j)+(L+1)^2\}\)

\(f_i=\min\{f_j+(pre_i-pre_j)^2-2\times(pre_i-pre_j)\times(L+1)+(L+1)^2\}\)

\(f_i=\min\{f_j+(pre_i-pre_j-(L+1))^2\}\)


\(f_i=\min\{f_j+((pre_i-(L+1))-pre_j)^2\}\)

\(f_i=\min\{f_j+(pre_i-(L+1))^2-2\times(pre_i-(L+1))\times pre_j+pre_j^2\}\)

\(f_i-(pre_i-(L+1))^2=\min\{f_j+pre_j^2-2\times(pre_i-(L+1))\times pre_j\}\)


令:

\(\begin{eqnarray}\begin{cases}b_i=f_i-(pre_i-(L+1)^2)\\x_j=pre_j\\y_j=f_j+pre_j^2\\k_i=2\times(pre_i-(L+1))\end{cases}\end{eqnarray}\)

发现原式转化为 \(b_i=\min\{y_j-k_i\times x_j\}\)。

看上去有那么亿点点的像 \(y=kx+b\) 呢……

考虑这个求 \(b_i\) 的最小值的过程,就是在最小化直线的截距。把 \((x_j,y_j)\) 看作平面上的一个点,现在有一条斜率为 \(k_i\) 的直线,从下往上找(最小化),找到的第一个点就是转移决策点。

实际上,只需维护下凸壳的那些点。

对于本题,\(k_i\) 随 \(i\) 的增大而增大,所以可以用单调队列进行维护。

const int N = 50010;
int n, c[N], l = 1, r = 0;;
ll sum[N], s[N], f[N], L;
ll Get(int x) {
return f[x] + (sum[x] + L) * (sum[x] + L);
}
long double slope(int x, int y) {
return (Get(y) - Get(x)) * 1.0 / (sum[y] - sum[x]);
}
int main() {
n = read(), L = read() + 1;
for (int i = 1; i <= n; i++) c[i] = read();
for (int i = 1; i <= n; i++) sum[i] = sum[i - 1] + c[i] + 1;
s[++r] = 0;
for (int i = 1; i <= n; i++) {
while (l < r && slope(s[l], s[l + 1]) <= (sum[i] << 1)) l++;
f[i] = f[s[l]] + (sum[i] - sum[s[l]] - L) * (sum[i] - sum[s[l]] - L);
while (l <= r && slope(s[r - 1], s[r]) >= slope(s[r - 1], i)) r--;
s[++r] = i;
}
printf("%lld\n", f[n]);
return 0;
}

N. 参考内容

DP优化 - zuytong

单调队列优化DP - superPG

dp优化 | 各种dp优化方式例题精选的更多相关文章

  1. 长链剖分优化树形DP总结

    长链剖分 规定若\(x\)为叶结点,则\(len[x]=1\). 否则定义\(preferredchild[x]\)(以下简称\(pc[x]\),称\(pc[x]\)为\(x\)的长儿子)为\(x\) ...

  2. 「DP 浅析」斜率优化

    #0.0 屑在前面 将结合经典例题 「HNOI2008」玩具装箱 以及 「NOI2007」货币兑换 进行讲解. #1.0 简述 #1.1 适用情况 斜率优化一般适用于状态转移方程如下的 DP \[f_ ...

  3. [noip科普]关于LIS和一类可以用树状数组优化的DP

    预备知识 DP(Dynamic Programming):一种以无后效性的状态转移为基础的算法,我们可以将其不严谨地先理解为递推.例如斐波那契数列的递推求法可以不严谨地认为是DP.当然DP的状态也可以 ...

  4. hdu1059 dp(多重背包二进制优化)

    hdu1059 题意,现在有价值为1.2.3.4.5.6的石头若干块,块数已知,问能否将这些石头分成两堆,且两堆价值相等. 很显然,愚蠢的我一开始并想不到什么多重背包二进制优化```因为我连听都没有听 ...

  5. 5501环路运输【(环结构)线性DP】【队列优化】

    5501 环路运输 0x50「动态规划」例题 描述 在一条环形公路旁均匀地分布着N座仓库,编号为1~N,编号为 i 的仓库与编号为 j 的仓库之间的距离定义为 dist(i,j)=min⁡(|i-j| ...

  6. HDOJ(HDU).2191. 悼念512汶川大地震遇难同胞――珍惜现在,感恩生活 (DP 多重背包+二进制优化)

    HDOJ(HDU).2191. 悼念512汶川大地震遇难同胞――珍惜现在,感恩生活 (DP 多重背包+二进制优化) 题意分析 首先C表示测试数据的组数,然后给出经费的金额和大米的种类.接着是每袋大米的 ...

  7. HDU 2829 Lawrence (斜率优化DP或四边形不等式优化DP)

    题意:给定 n 个数,要你将其分成m + 1组,要求每组数必须是连续的而且要求得到的价值最小.一组数的价值定义为该组内任意两个数乘积之和,如果某组中仅有一个数,那么该组数的价值为0. 析:DP状态方程 ...

  8. 【转】关于LIS和一类可以用树状数组优化的DP 预备知识

    原文链接 http://www.cnblogs.com/liu-runda/p/6193690.html 预备知识 DP(Dynamic Programming):一种以无后效性的状态转移为基础的算法 ...

  9. POJ 3744 【矩阵快速幂优化 概率DP】

    搞懂了什么是矩阵快速幂优化.... 这道题的重点不是DP. /* 题意: 小明要走某条路,按照个人兴致,向前走一步的概率是p,向前跳两步的概率是1-p,但是地上有地雷,给了地雷的x坐标,(一维),求小 ...

随机推荐

  1. 手把手教你 Apache DolphinScheduler 本地开发环境搭建 | 中英文视频教程

    点击上方 蓝字关注我们 最近,一些小伙伴反馈对小海豚的本地开发环境搭建过程不太了解,这不就有活跃的贡献者送来新鲜的视频教程!在此感谢@Tianqi-Dotes 的细致讲解 贡献者还贴心地录制了中英文两 ...

  2. 走进Redis:哨兵集群

    为什么需要哨兵 在 Redis 的主从库模式中,如果从库发生了故障,用户的操作是可以继续进行的,因为写操作是只在主库中进行的.那么,如果主库发生了故障,用户的操作将会收到影响.这时候可能会需要选择一个 ...

  3. NC20471 [ZJOI2007]棋盘制作

    题目链接 题目 题目描述 国际象棋是世界上最古老的博弈游戏之一,和中国的围棋.象棋以及日本的将棋同享盛名. 据说国际象棋起源于易经的思想,棋盘是一个8*8大小的黑白相间的方阵,对应八八六十四卦,黑白对 ...

  4. Luogu2783 有机化学之神偶尔会做作弊 (树链剖分,缩点)

    当联通块size<=2时不管 #include <iostream> #include <cstdio> #include <cstring> #includ ...

  5. Codeforces 1715E - Long Way Home

    又是废掉的一个div2啊 第一次在学校熬夜打cf,开心还看到了自己最喜欢的斜率优化ohhh 链接 :E - Long Way Home 看到那个平方就可以靠感觉认为是斜率优化了.... 感觉似不似有点 ...

  6. Manacher算法讲解——字符串最长回文子串

    引 入 引入 引入 Manachar算法主要是处理字符串中关于回文串的问题的,这没什么好说的. M a n a c h e r 算 法 Manacher算法 Manacher算法 朴素 求一个字符串中 ...

  7. CSP-S 2020 T4 贪吃蛇 (双队列模拟)

    题面 题解 先看数据,T<=10,用平衡树或优先队列是可以拿70分的,大体思路和正解思路是一样的,每次直接修改,然后模拟. 我们模拟的时候,主要是在过程中算出最终被吃的有选择权的蛇的最后选择时刻 ...

  8. Java中字节流的总结及代码练习

    Java中的字节流 在描述字节流时,先知道什么是流 流可以分为:输入流和输出流 输入流和输出流 示意图: 字节流读取内容:二进制,音频,视频 优缺点:可以保证视频音频无损,效率低,没有缓冲区 字节流可 ...

  9. 【manim】学习路径2-构建一些基础的图形,场景

    头文件引入 导入manim命名空间 from manim import * manim基本结构 这是一个最基本的manim结构,格式: from manim import * class 类的名字(S ...

  10. qt C2144 语法错误,需要在类型前添加;(分号)

    可能原因:有部分头文件未以";"结尾.