[归纳整理]动态规划从入门到入门
考前,感觉动态规划很烂,这里来总结一下动态规划的一些规律。
背包问题
背包问题应该是我学的第一种动态规划。我们常常要把一维价值或者重量放在状态上来表示,所以谁小就放谁。对于一般的背包,当 \(w_i\) 和 \(v_i\) 都很大的时候是没有合法的解的。所以如果这两个都很大,你就要想一些特殊性质上的优化了。
比如说 梦幻岛宝珠 这道题目就比较典型。利用了二进制优化,我每次只需要保存后几位就行了。这是探究了重量上如果有过多无用剩余的省空间。
然后对于多维背包的处理 有 \(n\) 维,就把 \(n-1\) 维压在状态上。
背包插入和删除操作是互逆的,这之前是写过的。所以如果遇到了之前模拟赛做的若干询问的区间背包,可以尝试一下莫队。
背包如果变化,只可能是,题面搞人,或是存的状态要精简,或者是存的答案改变意义,实时应变就好了。
状压dp
这种 dp 一般要找到一个范围很小的东西把它压在状态里面。
有一种略带 trick 的状压 dp。动物园 学校食堂 这两道题的 trick 是一致的,都是把动态的取法记录下来。
状压 dp 枚举子集的操作其实是 \(\mathcal{O}(3^n)\) 的,之前也做过一个题目 CF1209E2。这个题目也告诉我们一点,对于状压 dp,其实如果常数够小,时间复杂度有一定容忍性。这个题目有一个更优的做法,好像省掉了一个 \(n\) 但是他太小了,卡卡常数就可能能过(这个题还没卡常)。
还有两种有点怪的状压 dp,一种是子集和。求 \(g_i=\sum\limits _{i\&j=j} f_j\),其中 \(f\) 数组是给出的。这种东西有一个办法叫高维前缀和可以做到 \(\mathcal{O}(n2^n)\), \(n\) 是位数, 这是非常优秀的复杂度。因为要求子集和,我们可以把一位想成一维,然后高维前缀和也就好想了。
另一种是 and 卷积,不知道会不会考,但还是说一下,其实就是维护一个后缀和,然后反着操作一遍。
数位dp
数位 dp 的一般套路。
这只是我的,做法不唯一
数位 dp 本身做的就不多,有一种题型是求出区间 \([l,r]\) 中有多少个满足要求的数字。
这种问题我们一般可以把位数计入状态,这也是数位 dp 的精髓,因为对于后面的位,前面的位对其影响不大或是可以把影响记录下来。
具体怎么做,首先先把每个位置上拆分开来。然后从后往前做,把后缀都算一下,最后在枚举位数乱搞,或者是写一个一堆参数的 dfs,都是可以的,注意参数里一般有一个东西表示这个东西是不是贴着放的,贴着放的意思就是前面每一位都与上阶一模一样。
树形dp
树形 dp 我掌握的还是不错吧(yiw,放在这里鸽子,以后在总结。
区间dp
区间 dp,一般记录状态为 \(dp_{l,r}\) 然后根据题目记录剩下的状态。复杂度应该最小是 \(\mathcal{O}(n^2)\) 的。
题目有两种转移方式,一种是我们需要枚举中间点,然后左边和右边是一个独立的子问题。这种的话时间复杂度最小是 \(\mathcal{O}(n^3)\) 的。所以我们可以通过数据范围稍微判断一下题目的做法。
而其的一个变种,环形 dp,其实有关环的很多东西,我们都可以尝试一下断环成链,两倍链长。
还有一个办法判断是不是能用 \(\mathcal{O}(n^2)\) 的办法做这道题,如果一个问题你发现可以从一个 \([l+1,r]\) 或 [l,r-1] 的区间推到 \([l,r]\) 的区间那么这个题就不需要枚举中间点了。例题1 例题2。
这也有很多跟括号序列有关的题,比如说 CSP-S2021T2
首先是普通的注意括号,然后按照题意的意思搞就行了。对于优化,可能是我们算了一些奇怪的重复情况,所以注意一下有没有什么特殊的情况。
dp 的一些简单优化
数据结构优化
如果把普通的 dp 想到,直接观察状态专题应该是不难想到的。
单调队列优化
这应该也算数据结构优化中的一种,但是这里还是单独说一下。
这主要处理这个 dp 值有之前的某一段的 dp 值和某种不会变的值转移出的,这个时候发现如果在前面的小的比后面的大的更不优,那这样的话我们显然可以舍弃这些元素。
单调队列优化多重背包
我们考虑最基本的单调队列优化多重背包的柿子 (第一位滚掉了)
\(dp_{j} = \max{dp_{j - k\times v[i]} + k\times w[i]}\)
显然,一个模 \(v_i\) 相同的东西之间才会互相影响。
所以考虑枚举模 \(v_i\) 的余数,假如说为 \(mo\)。那么令 \(j=q\times v_i+mo\)
\(dp_{q\times v_i + mo}= \max\{dp_{q\times v_i + mo - k\times v_i} + k\times w_i\}\)
\(dp_{q\times v_i + mo}= \max\{dp_{(q-k)\times v_i + mo} + k\times w_i\}\)
这似乎做不了单调队列?那么还原试试,令 \(k=q-k\)
\(dp_{q\times v_i + mo}= \max\{dp_{k\times v_i + mo} + (q-k)\times w_i\}=\max\{dp_{k\times v_i + mo} - k\times v_i\} + q\times v_i\)
至此,我们发现我们可以运用单调队列优化了。
总时间复杂度 \(\mathcal{O}(nV)\)
#include <bits/stdc++.h>
using namespace std;
template <typename T>inline void read(T& t){t=0; register char ch=getchar(); register int fflag=1;while(!('0'<=ch&&ch<='9')) {if(ch=='-') fflag=-1;ch=getchar();}while(('0'<=ch&&ch<='9')){t=t*10+ch-'0'; ch=getchar();} t*=fflag;}
template <typename T,typename... Args> inline void read(T& t, Args&... args) {read(t);read(args...);}
const int N = 2e4 + 10, inf = 0x3f3f3f3f;
int n, m, w[N], v[N], s[N];
int f[N], front, tail;
pair<int, int>Q[N * 5];
int main() {
read(n, m);
for(int i = 1; i <= n; ++i) {
read(v[i], w[i], s[i]);
if(m / v[i] < s[i]) s[i] = m / v[i];
for(int mo = 0; mo < v[i]; ++mo) {
front = 1; tail = 0;
for(int k = 0; k <= (m - mo) / v[i]; ++k) {
while(front <= tail && Q[front].second < k - s[i]) front++;
while(front <= tail && Q[tail].first <= f[k * v[i] + mo] - k * w[i]) tail--;
Q[++tail] = {f[k * v[i] + mo] - k * w[i], k};
f[k * v[i] + mo] = Q[front].first + k * w[i];
}
}
}
cout << f[m] << endl;
return 0;
}
CF372C Watching Fireworks is Fun
这题朴素的 dp 非常一眼。直接定义 \(dp_{i, x}\) 表示现在是第 \(i\) 个烟花放了,并且位置在 \(x\) 的最大的开心值。
转移方程式是 \(dp_{i,x} = \max{dp_{i-1,y}}+b_i-|a_i-x|\) 其中, \(y\) 是范围 \([x-(t_i-t_{i-1})*d,x+(t_i-t_{i-1})*d]\) 范围内的。
我们能发现这个东西每次往后移 \(d\) 长度不会变化,我们直接单调队列。
不是很难。
#include <bits/stdc++.h>
#define int long long
using namespace std;
template <typename T>inline void read(T& t){t=0; register char ch=getchar(); register int fflag=1;while(!('0'<=ch&&ch<='9')) {if(ch=='-') fflag=-1;ch=getchar();}while(('0'<=ch&&ch<='9')){t=t*10+ch-'0'; ch=getchar();} t*=fflag;}
template <typename T,typename... Args> inline void read(T& t, Args&... args) {read(t);read(args...);}
const int N = 150005, inf = 0x3f3f3f3f;
typedef long long ll;
int n, m, d;
ll a[N], b[N], t[N];
ll dp[2][N];
signed main() {
read(n, m, d);
int p = 0;
// memset(dp, 0xcf, sizeof(dp));
// for(int i = 1; i <= n; ++i) dp[1][i] = 0;
for(int i = 1; i <= m; ++i) {
// [j - (t[i] - t[i - 1]) * d, j + (t[i] - t[i - 1]) * d]
read(a[i], b[i], t[i]);
int now = 0;
deque<pair<int, int> >Q;
for(int j = 1; j <= n; ++j) {
while(now < j + (t[i] - t[i - 1]) * d && now < n) {
++now;
while(!Q.empty() && Q.back().first <= dp[p ^ 1][now]) Q.pop_back();
Q.push_back(make_pair(dp[p ^ 1][now], now));
}
while(!Q.empty() && Q.front().second < (j - (t[i] - t[i - 1]) * d)) Q.pop_front();
dp[p][j] = Q.front().first + b[i] - abs(j - a[i]);
// cout << j << ' ' << Q.front().second << ' ' << b[i] << ' ' << abs(j - a[i]) << ' ' << dp[p][j] << endl;
}
p ^= 1;
}
long long maxx = 0xcfcfcfcfcfcfcfcf;
for(int i = 1; i <= n; ++i) maxx = max(maxx, dp[p ^ 1][i]);
cout << maxx << endl;
return 0;
}
斜率优化
斜率优化顾名思义,主要是通过斜率来确定当前 dp 需要取的最大值或者是最小值这类的。
比如说我们要处理 \(a,b\) \(ax+by\) 的最大值。
这里假设 \(a,b>0\)。
那么我们先画出 \(ax+by=0\) 的图像,显然这是一条过原点的直线。
这里取得是 \(5x+3y=0\)
我们可以将其平移,平移的结果就反映了 \(ax+by\) 的变化结果比如
这是 \(5x+3y=1\) 所以我们知道了,我们 dp 结果为 \(1\) 的都在这条直线上。
那么我们就是要找这条直线斜率不变左右平移的最远点。
主要是处理 dp 柿子长这样 : \(dp_i\) 等于 \(j\) 相关 \(\pm\) \(f(j) \times g(i)\) + \(i\) 相关。
这里要把 \(g(i)\) 看成斜率通常。
P3195 玩具装箱
这道题目实际上就是要求:我们把这些东西分成若干个小块后每段长度 -L 后平方的最小值。
我们可以做一个等价转换。中间的我们不计算,而是把所有的 \(c\) 全部加 1,然后发现每段多算了 \(1\),那么就把 \(L\) 也 +1。
我们能很轻松的写出 dp,\(dp_i\) 表示前 \(i\) 个已经匹配好了的最小的值。
\(dp_i=\min\{dp_j+(sum_i-sum_j-L)^2\}, j\in[1,i)\)
因为对于这个柿子计算, \(sum_i\) 和 \(L\) 都是常量,我们选择合并,叫做 \(sum_i'\),那么柿子再展开一下变成了 \(dp_i=\min\{dp_j+sum_j'^2-2sum_jsum_i'\}+sum_i'^2, j\in[1,i)\)
这样是不是就是我们上面所说的柿子了?
我们可以把 \(sum_j\) 看成 \(x\) 把 \(2*sum_i'\) 看成斜率,把 \(dp_j+sum'j^2\) 看成 \(y\)。那么这个柿子不就变成了 \(dp_i=\min\{y-kx\}+sum_i'^2, j\in[1,i)\)。我们这样就成功把这个柿子化成了一个上面所说的模样。
柿子已经是上面那个东西了,那么接下来我们应该怎么做呢?
按照之前讲的,我们应该找这个斜率的直线平移到一个极限位置。而这个极限位置算出来的答案就是最值。
我们会发现,只有外面一圈的点是有用的,红色阴影区域的点是无用的。那么我们想到维护外面一圈凸壳就好了。
是不是真的要维护一个凸壳呢?
实际上并不用,注意到 \(sum\) 数组是递增的。那么说明斜率也是递增的。
这说明什么?这说明我们只需要维护一个斜率以及 \(x\) 不会减的下凸壳就好了,这怎么操作?
首先,如果我们现在的斜率已经大于了凸壳第一个点和第二个点的斜率,那么第一个点是不是没用了。(如下图)
红色的线只会逆时针旋转,所以不可能再把最左边的点搞到凸壳上了。
那么左边可以退出了,右边如何加点呢?
最右边两个点的斜率大于了右边第二个点和右边第三个点的斜率,说明下凸包不满足要求了,所以不断弹出,把右边第二个和右边第三个全弹出了。
至此,我们已经可以 AC 此题了。
一些奇淫技巧
这些奇淫技巧可能是需要自己动脑子思考的。
这里还是整理一些把。
Emiya家今天的饭
我们把普通的 dp 写出来之后,我们会发现我们要维护一个 \(dp_{i,j,k}\),而最后我们只需要查询 \(j>k\) 的和就行了,\(j>k\) 显然等价于 \(j-k>0\) 这样的话我们维护 \(j-k\) 的差值就好了