腾飞营_DP
DP
前言
来自课堂笔记
代码做题的话再补充
经典问题
编辑距离
给定 \(A\) 串 \(B\) 串 长度分别为 \(n, m\) 求使 \(A\) 串通过删除或插入成为 \(B\) 串的最小代价 如果最小修改次数小于 \(K\) 输出修改次数 否则输出 \(-1\)
\(1 \leq n, m \leq 10^5, 1 \leq K \leq 100\)
状态 \(f_{i, j}\) 表示 \(A\) 串中第 \(i\) 个字符与 \(B\) 串中第 \(j\) 个字符匹配时的最小代价
复杂度爆炸
考虑优化
优化的思路
我应该放 \(PPT\) 上 但是...没放 —— zzy
-
数据结构优化 当转移复杂度过高时
-
利用题目中的性质 精简状态
-
当作为状态的某一维非常大的时候 考虑作为最优化的量 将原来的 \(dp\) 量作为状态中的某一维记录
比如在背包中 若是体积和过大 而物品价值和比较小 可以将价值作为 \(dp\) 数组的第二维 将体积作为 \(dp\) 值
有问题吗? 都没有问题. 说明
我讲课大家理解能力很强 ——zzy
- 决策单调性优化
- 斜率优化
- 四边形不等式优化
其实我学过, 尝试学过, 就是没用吧, 然后就那个了 ——zzy
题目里的性质, 打个表嘛,对个拍,打出几组, 发现满足决策单调性嘛, 再打几组, 还行, 就用嘛——zzy
回到上面的题目
答案太大不输出了 优化可行的状态
昨天考第二的人是谁, 你有什么想法
没, 没有
哦, 没有想法, 那考第三的是谁
...
\(f_{i,j}\) 比 \(k\) 大 直接丢掉
两串的长度差大于 \(k\) 的时候 答案一定是大于 \(k\) 的 丢掉就好了 即 \(|i - j| \leq k\)
枚举 \(j\) 的时候 只枚举 \(j\) 在 \(i - k\) 到 \(i + k\) 之间的 空间的话滚一下
\(LIS\) 问题
\(LIS\) 的优化
听不懂的英文...
-
树状数组
前缀最大值
\(f_i = \max\{f_j \} + 1, v_j \leq v_i\) 把 \(v_j\) 直接丢到树状数组中
-
二分 \(O(n\log n)\)
维护一个 \(b\) 数组 满足
同等长度 位置越靠前越优
考虑用 \(b\) 求 \(f\)
二分找 \(b_k \leq v_i\) 的 \(k\) 更新 \(f_i\) 即
LCS 的优化
如果 \(A\) 中没有重复元素
将 \(B\) 中的元素映射到 \(A\) 中的位置 此时 \(B\) 中的 \(LIS\) 即为两串的 \(LCS\)
我总感觉我今天讲课口齿...应该是话筒原因吧. ——zzy
区间 \(DP\)
区间 \(DP\)
状态的设计描述了
一种写法
for(int len = 1; len <= n; ++len)
for(int l = 1, r = len; r <= n; ++l, ++r)
环上 \(DP\)
破环成链 装换成序列上的 \(dp\)
题目
区间 \(dp\) , 闭着眼都能写出来的玩意.——zzy
P1430
昨天考第一的是不是你, 上去给大家讲讲吧. ——zzy
完全不能理解平面几何为什么能被做出来. ——zzy
\(n \leq 100\) 时比较显然?
从左边 或 从右边 两边是对称的 考虑一边 枚举取到的位置进行转移
维护一个前缀和 时间复杂度 \(O(n^3)\)
数组怎么不是数据结构了, 你瞧不起数组.——zzy
考虑优化
发现前面哪一项是固定的 也就是使后面那一项最小
新建一个数组 \(p\)
转移改为:
从后面取同理
代码
#include<cstdio>
#include<cstring>
#define pn putchar('\n')
#define emm(x) memset(x, 0, sizeof x)
#define Min(x, y) ((x) < (y) ? (x) : (y))
/*-----------------------------------------------------------------------*/
const int A = 1e3 + 7;
/*-----------------------------------------------------------------------*/
void File() {
freopen(".in", "r", stdin);
freopen(".out", "w", stdout);
}
/*-----------------------------------------------------------------------*/
int T, n, f[A][A], a[A], pl[A][A], pr[A][A];
/*-----------------------------------------------------------------------*/
inline int read() {
int x = 0, f = 0; char ch = getchar();
while(ch < '0' || ch > '9') {if(ch == '-') f = 1; ch = getchar();}
while(ch >= '0' && ch <= '9') {x = (x << 3) + (x << 1) + (ch ^ 48); ch = getchar();}
return f ? -x : x;
}
void Print(int x) {if(x < 0) putchar('-'), x = -x; if(x > 9) Print(x / 10); putchar(x % 10 ^ 48);}
/*-----------------------------------------------------------------------*/
void work() {
n = read(); emm(f); emm(pl); emm(pr);
for(int i = 1; i ^ n + 1; i++) a[i] = a[i - 1] + read(), pl[i][i] = pr[i][i] = f[i][i] = a[i] - a[i - 1];
for(int i = 2; i ^ n + 1; i++)
for(int l = 1, r = i; r ^ n + 1; l++, r++)
{
f[l][r] = a[r] - a[l - 1] - Min(0, Min(pl[l][r - 1], pr[l + 1][r]));
pl[l][r] = Min(pl[l][r - 1], f[l][r]); pr[l][r] = Min(pr[l + 1][r], f[l][r]);
}
Print(f[1][n]); pn;
}
/*-----------------------------------------------------------------------*/
int main() {
T = read(); while(T--) work();
return 0;
}
P4170
区间 \(dp\) , 设计状态的时候考虑先干什么, 后干什么, 会带来怎么样的转移. ——zzy
状态 \(f_{l, r}\) 将区间 \([l, r]\) 涂成制定状态的最小代价
转移 考虑能否从已有区间扩展
P4342
做法写在题面上. ——zzy
删边 成链
这个题我觉着非常简单吧,有没有同学讲一讲. ——zzy
我是拿钱的, 你们是交钱的, 你们交钱讲课, 赚的是我, 所以有没有同学想让我赚一下.——zzy
那个, 昨天考第一的那个同学, 你今天还没讲过题吧, 来, 来, 讲讲.——zzy
首先的想法是 \(f_{i, j}\) 表示合并区间 \([l, r]\) 得到的最大值 枚举断点直接转移
但是点权有负的 所以顺便维护一个最小值 多考虑几种情况 枚举断点转移即可
时间复杂度 \(O(n^3)\)
HDU4283
\(n\) 个人排成一排 每个人权值 \(v_i\) 可以通过栈调整顺序 假如第 \(i\) 个人为第 \(k\) 个上场 代价为 \(\sum_{i = 1}^nv_ik_i\) 确定 \(n\) 个人的上场顺序使总代价最小
\(1 \leq n \leq 100\)
首先要想到这是一个区间 \(dp\) (虽然从题面上这个题和区间 \(dp\) 并没有什么关系
设 \(f_{l, r}\) 表示仅考虑区间 \([l, r]\) 中的人的最小的代价
枚举第 \(l\) 个人是第几个出场的 假设它是第 \(k\) 个出场的 有
为什么枚举 \(l\)
\(l\) 是第一个入栈的 在出栈前不会有其他元素取代其位置 同时 \([l + 1, l + k - 1]\) 一定先于 \(l\) 出栈 \([l + k, r]\) 一定在 \(l\) 出栈后入栈 也就是说当 \(l\) 出栈后栈中为空
那么代价可以明显的分为两部分 分别是 \(f_{l + 1, l + k - 1}\) 和 \(f_{l + k, r}\)
根据状态转移方程的含义 算贡献的时候需要算上
(相当于加上一个增量
转移:
(下标从 \(1\) 开始
昨天 \(rank1\) 那位小哥是有什么高问?——zzy
P1864
略
Loj2292
略
P3736
状压 + 区间
如果只考虑一个区间 不断合并字符 这个区间留下的字符数量是固定的
\(f_{l, r, s}\) 表示只考虑区间 \([l, r]\) 留下的字符串为 \(s\) 的最大分数
转移枚举把区间分为两部分 分别处理
对于留下刚好 \(k\) 个字符的区间 将这 \(k\) 个字符合并到一起
其他的区间 不妨设右边的区间合成最后一个字符 左边的区间提供其他的字符 转移时使得右侧区间长度为 \(k - 1\) 的整数倍
为什么是对的
\(dp\) 出来一定是可行状态
可行状态一定会 \(dp\) 出来
代码
/*
Source: P3736 [HAOI2016]字符合并
*/
#include<cstdio>
#include<cstring>
#define int long long
#define Max(x, y) ((x) > (y) ? (x) : (y))
/*-----------------------------------------------------------------------*/
const int INF = 0x3f3f3f3f3f3f3f3f;
/*-----------------------------------------------------------------------*/
int n, k, f[310][310][260], w[260], ans = -INF;
bool a[310], c[260];
/*-----------------------------------------------------------------------*/
inline int read() {
int x = 0, f = 0; char ch = getchar();
while(ch < '0' || ch > '9') {if(ch == '-') f = 1; ch = getchar();}
while(ch >= '0' && ch <= '9') {x = (x << 3) + (x << 1) + (ch ^ 48); ch = getchar();}
return f ? -x : x;
}
void Print(int x) {if(x < 0) putchar('-'), x = -x; if(x > 9) Print(x / 10); putchar(x % 10 ^ 48);}
/*-----------------------------------------------------------------------*/
/*-----------------------------------------------------------------------*/
signed main() {
n = read(); k = read();
for(int i = 1; i ^ n + 1; i++) for(int j = 1; j ^ n + 1; j++)
for(int s = 0; s ^ (1 << k); s++) f[i][j][s] = -INF;
for(int i = 1; i ^ n + 1; i++) f[i][i][read()] = 0;
for(int i = 0; i ^ (1 << k); i++) c[i] = read(), w[i] = read();
for(int i = 2; i ^ n + 1; i++) for(int l = 1, r = i; r ^ n + 1; l++, r++)
{
int x = (i - 1) % (k - 1); if(!x) x = k - 1;
for(int j = r - 1; j >= l; j -= k - 1) for(int s = 0; s ^ (1 << x); s++)
f[l][r][s << 1] = Max(f[l][r][s << 1], f[l][j][s] + f[j + 1][r][0]),
f[l][r][s << 1 | 1] = Max(f[l][r][s << 1 | 1], f[l][j][s] + f[j + 1][r][1]);
if(x == k - 1)
{
int g[2]; g[0] = g[1] = -INF;
for(int s = 0; s ^ (1 << k); s++)
g[c[s]] = Max(g[c[s]], f[l][r][s] + w[s]);
f[l][r][0] = g[0]; f[l][r][1] = g[1];
}
}
for(int s = 0; s ^ (1 << k); s++) ans = Max(ans, f[1][n][s]);
Print(ans);
return 0;
}
状压 \(DP\)
前置
枚举子集
for(int S = 0; S < 1 << n; S++)
for(int s0 = S; s0; s0 = s0 - 1 & S)
时间复杂度 \(O(3^n)\)
P2150
考虑 \(n = 20\)
第一维滚掉
状态 \(f_{s_1, s_2}\) 表示一个人的状态为 \(s_1\) 第二个人的状态为 \(s_2\) 时的方案数
以 \(v_x\) 表示 \(x\) 的质因子构成的集合
考虑 \(n = 500\)
对于一个小于等于 \(n\) 的数 其大于 \(\sqrt n\) 的质因子最多只有一个
对于小于等于 \(\sqrt n\) 的质因子 直接状压 大于 \(\sqrt n\) 的质因子 考虑让哪个人取 最后再合并
数位 \(DP\)
针对某一进制表示下的 \(dp\)
CF908G
对于一个数字 \(x\) 其对答案的贡献可以表示为 \(c_x = \sum 10^i\) 所以 有
考虑枚举 \(i\) 如何计算 \(\sum_{j = i}^9c_i\) 实际上取决于每个数中不小于 \(i\) 的数位有多少个
直接做数位 \(dp\) 记录不小于 \(i\) 的数位有多少即可
\(dp\) 状态中 第一维记录当前数位 第二维与第三维记录大于等于 \(i\) 的数位有多少个 后面是常数个?
\(700^3\) 起飞了
所以上面那个式子是用来优化的
第二三维放到一起 记录
找性质 做转化
有的题, 找个性质或者做个转化就做完了, \(dp\) 只是个外面的壳
题目
CF GYM 102331B Bitwise Xor
给定一个长度为 \(n\) 的整数序列 \(a\) 和一个整数 \(k\) 求 \(a\) 有多少子序列 两两异或值至少为 \(k\)
\(1 \leq n \leq 3 \times 10^5, 1 \leq a_i, k \leq 2^{60}\)
若干个数两两异或的最小值必在排序后相邻两个取到
排序后直接用 \(trie\) 优化 \(dp\) 即可
在 \(trie\) 上先序遍历相邻的两个数也是排序后相邻的两个数
排序后取一个子集
树形 \(DP\)
P3177
考虑每条边的贡献
直接树上背包 \(dp\)
状态 \(f_{u, i}\) 当以 \(u\) 为根的子树中选择 \(i\) 个黑点时 以 \(u\) 为根的子树中的贡献和
转移考虑每条边的贡献
Bombing plan
给定一棵 \(n\) 个点的无根树 节点 \(i\) 权值为 \(w_i\) 攻击节点 \(i\) 的同时 会把所有与 \(i\) 节点距离在 \(w_i\) 内的节点全部摧毁 边权为 \(1\) 问至少需要攻击几次才能把整个树的全部节点摧毁
\(1 \leq n \leq 10^5, 1 \leq w_i \leq 100\)
状态
\(f_{u, i}\) 表示以 \(u\) 为根的子树内全部被摧毁 并还能向上破坏至多 \(j\) 个距离的最小代价
\(g_{u, j}\) 表示 以 \(u\) 为根的子树内的点还没被全部摧毁 且子树中未被破坏的点离 \(u\) 点的距离最多为 \(j\) 的最小代价
转移
如果攻击 \(u\) 点 有
转移前缀最小值优化 转移 \(O(1)\)
如果不攻击 \(u\) 点 考虑枚举 \(u\) 点的一个孩子 \(v\) \(y\) 是除 \(v\) 以外的其他孩子 有
你有没有问题啊.
没问题.
那你有没有问题啊.
没问题.
那行, 我高超的讲课能力再次得到了验证.——zzy
对于每个 \(u\) 在 \(dp\) 完成之后扫描所有 \(f_{u, i}\) 以及 \(g_{u, i}\) 维护单调性
通过维护前缀最小值可以将转移优化到 \(O(nw)\)
大家考前打的模板都用不上
代码能力都要写, 不要嫌恶心, 大模拟, 大数据结构题, 都要写
能写代码的不写讨论
尽量避免手算, 能写代码的写代码
——zzy
purfer 序列
(oi-wiki
purfei 序列
prufer 序列 一种将带标号的树用一个为一的整数序列表示的方法(不考虑含有一个节点的树
可以将一个带标号 \(n\) 个节点的树用 \([1, n]\) 中的 \(n - 2\) 个整数表示
完全图的生成树与数列之间的双射 常用于组合计数问题
建立序列
每次选择一个编号最小的叶节点并删掉它 然后再序列中记录它连接到的那个节点 重复 \(n - 2\) 次后只剩下两个节点 算法结束
使用堆可以做到 \(O(n\log n)\)
线性构造
维护一个指针指向我们要删除的节点
发现叶节点是非严格单调递减的 要么删一个 要么删一个得一个
于是考虑这样一个过程
维护一个指针 \(p\) 初始时 \(p\) 指向编号最小的叶节点 同时维护每个节点的度数 方便知道在删除节点的时候是否产生新的叶节点 操作:
-
删除 \(p\) 指向的节点 并检查是否产生新的叶节点
-
如果删除新的叶节点 假设编号为 \(x\) 比较 \(p\) 与 \(x\) 的大小关系
-
\(x > p\)
不做其他操作
-
否则
立刻删除 \(x\) 然后检查删除 \(x\) 后是否产生新的叶节点
-
重复 \(2\) 步骤 知道未产生新节点或新节点的编号大于 \(p\)
-
-
让指针 \(p\) 自增 知道也到一个未被删除的叶节点为止
循环 \(n - 2\) 次 就完成了序列的构造
正确性
\(p\) 是当前编号最小的叶节点 若删除 \(p\) 后未产生叶节点 寻找下一个叶节点 若产生了叶节点 \(x\) 则
- 若 \(x > p\) 则 \(p\) 在之后的扫描中会扫到他 就不用管了
- 若 \(x < p\) 因为 \(p\) 原本就是编号最小的 而 \(x < p\) 所以 \(x\) 即为当前编号最小的叶节点 有限删除 继续这样考虑知道没有更小的叶节点
复杂度
每条边最多被访问一次 指针最多便利每个节点一次 复杂度 \(O(n)\)
性质
- 构造完 prufer 序列后原树中会剩下两个节点 其中一个一定是编号最大的点 \(n\)
- 每个节点在序列中出现的次数是其度数减 \(1\)
序列重建树
根据序列性质 可以得到原树上每个点的度数 也可以得到度数最小的叶节点编号 这个节点一定与 prufer 序列的第一个数连接 然后同时删掉两个节点的度数
每次选择一个度数为 \(1\) 的最小的节点编号 与当前枚举到的 prufer 序列的点连接 然后同时减掉两个点的度 到最后剩下两个度数为 \(1\) 的点 其中一个是节点 \(n\) 就把它们建立连接 使用堆维护这个过程 在减度数的过程中如果发现度数减到 \(1\) 就把这个节点添加到堆中这样做的复杂度是 \(O(n\log n)\)
线性重建树
同线性构造 prufer 序列的方法 在删除度数的时候产生新的叶节点 于是判断这个叶节点与指针 \(p\) 的大小关系 如果更小就优先考虑它
杂
一棵无根树 其中第 \(i\) 个点度数为 \(d_i\)
一个正整数序列 \(i\) 出现了 \(d_i\) 次
双射
题目
Dominating Set
给定一个 \(n\) 个点 \(m\) 条边的二分图 选择其中的一些点 使对于任意一个点 要么它被选择 要么它相邻的点至少有一个点被选择 求所有可以选择的集合的方案数
\(1 \leq n \leq 30, 0 \leq m \leq 225\)
\(dist = 1\) 的支配集问题 \(NPC\) 问题
分析性质
二分图 每个连通块互不干扰 算出方案数后可以直接乘
分类, 发现较小的一个东西很小, 或较大的东西很大, 以此来设计算法——zzy
黑白染色 点数较小的一侧点数设为 \(t\) 不会超过 \(15\)
暴力枚举较小的一侧的点假设为黑色的选择情况 需要额外的取一些白点使黑点中未选择的点都被覆盖到
设黑点选择了 \(x\) 个 问题等价与给定 \(k\) 个 \(t - x\) 位的二进制数 求并集为满集的方案数 \(O(k2^{t - x})\) 的 \(dp\) 解决
时间复杂度相当于枚举一个集合 再枚举其子集的复杂度 即 \(O(n3^{\frac n2})\)
Unicyclic Graph Counting
计数有多少个标号环套树 第 \(i\) 个点的度数为 \(d_i\)
取模
\(n \leq 3000\)
弱化版
计数有多少个标号无根树 第 \(i\) 个点的度数为 \(d_i\)
取模
\(n \leq 3 \times 10 ^ 6\)
数树太麻烦 干脆数 prufer 序列
一个数可以出现 \(d_i\) 次 可重排列 预处理逆元阶乘什么的 套公式
回到原题
环套树 不考虑森林的情况
先特判掉一个大环的情况 所有点的度数都为 \(2\)
假设已经得到了一个环 大小为 \(k\) 考虑一种新的 prufer 编码方式 不删除环上的点 只删除树上的点 树上的点 \(u\) 在编码中出现的次数一定为 \(d_u - 1\) 环上的点 \(v\) 在这个编码中出现次数一定为 \(d_v - 2\) 而且序列的最后一个点一定是环上的点 当环的形状确定后 编码方式就和环套树一一对应 编码总长度为 \(n - k\)
\(dp\) 解决计数问题
做 \(dp\) 的时候, 想写非常非常蠢的暴力怎么写, 然后想怎么用 \(dp\) 去优化. ——zzy
对于 \(n\) 个点 爆搜出 \(k\) 个点 让它在环上 再逐一枚举 使其为序列的最后一个点 \(r\) 设 \(c' = c - \{r\}\)
其他点
下面那一坨东西可以 \(dp\)
\(f_{i, k, 0/1}\) 考虑 前 \(i\) 个点 选出 \(k\) 个点作为环上的点 环上最后一个点是否被决定
答案 \(ans = \sum_{k \geq 3}f_{n, k, 1}\)
大家感觉今天我将的东西怎么样, 我希望大家说难, 如果大家说难, 那么大家听明白了, 说明我讲课水平还是不错的. ——zzy
AGC035D
给定一个长度为 \(n\) 的数列 \(a\) 每次操作可以选择其中连续的三项 权值依次为 \(a, b, c\) 将其替换为 \(a + b, c + b\) 这两项
求经过操作后剩下的两个数的和的最小值
\(2 \leq n \leq 18, 0 \leq a_i \leq 10^9\)
操作可以看做选择不是两边的一个数 将它删去并加到左右两项上
倒序的考虑整个过程 最后的结果一定是每个数乘上一个系数的和 那么最开始 \(a_1, a_n\) 的系数为 \(1\)
设 \(f_{l, r, x, y}\) 表示 当前未被删除的数两端是 \(l\) 和 \(r\) 中间的数已经被删除过了 \(a_l\) 对答案贡献的系数为 \(x\) \(a_r\) 对答案贡献的系数为 \(y\)
转移考虑上一个被删掉的数 \(a_k\) 其必然会贡献到两边 代价为 \(f_{l, k, x, x + y} + f_{k, r, x + y, y} - (x + y) \times a_k\) 重复计算 需要减掉
答案为 \(f_{1, n, 1, 1}\)
复杂度
初始状态 \(x = 1, y = 1\) 开始 向下不会超过 \(n\) 层 每一步只有两个状态 \((x, y)\) 的状态的个数为 \(O(2^n)\)
粗略分析 时间复杂度 \(O(2^n \times n^3)\) 精细分析可以得到更好的结果
bzoj4380 洛谷上 P3592
笛卡尔树 dp
笛卡尔树是一棵 \(treap\) 其中权值是下标 优先级是权值 排列与笛卡尔树是一一对应的 每个排列都有唯一的笛卡尔树
笛卡尔树的性质
以一个点 \(u\) 为根的子树就是就是以其为最小值的极长区间 \([l, r]\) 要么 \(rs(l - 1) = u\) 要么 \(ls(r + 1) = u\)
一些 \(dp\) 会以笛卡尔树为背景
回到上面那个题目
首先将权值离散化 考虑区间 \(dp\) 设 \(f_{l, r, v}\) 为区间 \([l, r]\) 最小值为 \(v\) 的最大收益 (只考虑 \(l \leq a \leq b \leq r\) 的人)
转移时枚举最小值所在位置 \(x\) 那么可以用 \(f_{l, k - 1, p \geq v} + f_{k + 1, r, p \geq v} + cost(l, r, k)\)
时间复杂度 \(O(n^3m)\)
可以理解为将序列构成一棵笛卡尔树 并且考虑在笛卡尔树上计算贡献
子序列自动机
计数
错排技术
拆分数
容斥原理
找限制 对限制做容斥
带标号联通欧拉图计数
散
bitset
开一个长度为 \(m\) 的 \(ull\) 的数组 对应 \(64\) 个位
考虑 \(x\) 个位
x >> 6 & (1ull << (x & 63ull)