首页 > 其他分享 >DP笔记

DP笔记

时间:2024-05-31 23:11:26浏览次数:22  
标签:状态 int 笔记 枚举 DP 转移 dp

DP笔记

目录

一、动态规划总结

要使用动态规划需要哪些 条件

  1. 最优子结构
  2. 子问题重叠
  3. 无后效性

12 中只需要满足一个,再加上 3,接下来就可以愉快地使用动态规划了。

动态规划把原问题划分为若干子问题,每个子问题的求解过程构成一个阶段,求解完前一个阶段再求解后一个阶段。根据无后效性,动态规划的求解过程构成一个有向无环图,求解的顺序就是该有向无环图的一个拓扑排序。

以下是处理动态规划问题的 基本要素

  1. 确定状态、保存状态变量

    即设计dp数组,保存当前状态。这个状态就是每个子问题的决策。常常:问什么就设什么。

  2. 划分阶段、设计决策方法

    即设计状态转移方程。常常:根据相邻两个阶段的状态之间的关系来确定决策方法和状态转移方程。

  3. 处理边界

    即设计一个终止条件或边界条件。

以下是处理动态规划问题的 基本步骤

  1. 状态定义: 每个状态的决策,存放每个状态的变量。
  2. 状态转移方程: 当前状态与上一个状态之间的关系。
  3. 初始状态: 初始的状态或者边界条件。

二、线性DP

使用场景相对模糊:通常是有明显的子问题重叠。

状态有多个维度,每个维度都是线性划分的阶段,整体呈现递推的形式。

**状态定义: ** 基本上是问什么设什么。答案即是dp数组的值。

**状态转移方程: ** 分清楚阶段,找到子问题。通过子问题推出当前的状态。

边界条件: 一般比较清楚。(数据的边界)

重点: 先要分维度,再找到可以用于递推的子问题。这两个步骤要一起进行,数据范围可能会提示维度如何分,在此基础上设计出一种可以递推出答案的方法。最后按照维度循环得到答案。

例题:走楼梯,最长上升子序列,最长公共子序列。

三、背包问题

详见背包九讲

四、区间DP

使用场景很明显:处理区间问题,得到某种最优答案。

区间dp的秘籍就是: i 到 j 的这个区间的所期望求的值可能是 i 到 k 的值和 k+1 到 j 的值通过某种方式合并得到。

很明显越大的区间要覆盖更小的区间所得到的值。

状态定义: dp[i][j] 表示:i 到 j 这个区间的答案。有的时候会在加上一维 k (0/1),表示某一段选或者不选。

状态转移方程: dp[i][j] = combine(dp[i][k], dp[k][j]); //k: i ~ j combine () 是一种合并的方式,有的时候是取最大最小,有时是相加,还有可能是其他。

重点: 区间dp的第一层循环大多是遍历区间长度接着循环起点(注意起点要合理,加上长度后不超过终点的范围),由循环得到的起点可以推出终点。最后再进行状态的转移。核心是找到合并两个区间的答案的方法。

例题:[P1880 [NOI1995] 石子合并]

dp[i][j] 表示:取 i ~ j 的最大得分

在 [i, j] 这一区间内,枚举下标k进行拆分。

合并 [i, k] 与 [k+1, j] 的区间,会加上 [i, j] 之间所有石子个数之和。

得到状态转移方程为 f[i][j]=max(f[i][j], f[i][k]+f[k+1][j]+sum[j]-sum[i-1]);

其中sum为前缀和

for (int len = 2; len <= n; ++len) {
	for (int i = 1; i <= n - len + 1; ++i) {
		int j = i + len - 1;
		dp[i][j] = INF;
		for (int k = i; k < j; ++k)
			dp[i][j] = max(dp[i][j], dp[i][k] + dp[k + 1][j] + sum[j] - sum[i - 1]);
	}
}

粗略模板:

for 枚举区间长度 len {
    for 枚举起点 i(i+len 不超过范围){
		计算得到终点 j
		初始化 dp[i][j];
		for 枚举划分点 k in (i, j)
            使用状态转移方程
    }
}				

五、树形DP

使用场景很明显:存在树形结构,求某种最优解。

树形DP指在树型结构上实现的动态规划。树形结构有明显的层次性,动态规划是多阶段决策问题,正好对应。

状态定义: dp[i] 其中 i 一般是树上的节点编号,代表以该节点为根的子树的答案。有的时候会在加上一维 j (0/1),表示这一个节点的状态 (选或者不选) 。

状态转移方程: 树形DP一般自底向上回溯,将子树从小到大作为dp的阶段。遍历的时候一般采用深度优先遍历,递归求解每颗子树,回溯时从子结点向上进行状态转移。在当前结点的所有子树都求解完毕后,才可以求解当前结点。

重点: 树形dp的精髓就是回溯时的状态转移,通过深度优先搜索的方式,向下的过程中进行初始化操作,向上回溯的时候再转移状态。核心是找到节点的答案与它的子树的答案之间的关系。

例题:P8625 [蓝桥杯 2015 省 B] 生命之树P1352 没有上司的舞会P2016 战略游戏

P8625 [蓝桥杯 2015 省 B] 生命之树

用 \(a_i\) 表示第 \(i\) 个点的权值,\(dp_u\) 表示以u为根节点的子树的答案。

状态转移方程:

\[dp_u = a_u + \sum_{u \rightarrow v} max\{dp_v, 0\} \]

void dfs(int u,int fa){
	dp[u] = a[u];
	for(int v : tree[u]){
		if(v == fa) continue;
		dfs(v, u);
		dp[u] += max(dp[v], 0ll);
	}
}

P2016 战略游戏

状态 dp[u][0/1] 表示:在 \(u\) 节点放哨兵(0) / 不放哨兵(1)的情形下,以 \(u\) 为根节点的子树的答案

(加了一维 (0/1) 表示当前节点放不放哨兵。)

状态转移方程为:

dp[u][0] += dp[to][1]

dp[u][1] += min(dp[to][0], dp[to][1])

void dfs(int u,int fa){
	int dp1 = 1;
	int dp0 = 0;
	for(int v : tree[u]){
		if(v == fa) continue;
		dfs(v, u);
		dp1 += min(dp[v][0], dp[v][1]);
		dp0 += dp[v][1];
	}
	dp[u][1] = dp1;
	dp[u][0] = dp0;
}

粗略模板:

void dfs(int u, int fa) {
    预处理 dp[u];
    for(int v : tree[u]) {
        if(v == fa) continue;
        dfs(v, u);
        使用状态转移方程;
    }
}

换根DP(也算是树形的DP)

对于之前的树形DP,都是以一个固定的结点也就是1来作为根求解最优解d额动态规划思想。而换根DP,顾名思义就是根不一定只有1,这涉及到换根的操作

最笨的方法是以每个点作为根分别进行之前的树形DP操作,但是实际上这样的操作基本上都是会超时的。

正确的方法是只求解一个根,并且你要看出来当选择其他点作为根(当然这个点肯定是先从原先被求解的那个根的孩子)时,和父亲的关系。通过这种方式来设计递归的操作,不断地递归找孩子,并且通过某种关系得到对应解。

六、状压DP

使用场景:最显著的看数据范围,一般表示状态的变量不超过20。当然得有明显的状态区分。

状压DP是利用计算机二进制的性质来描述状态的一种DP方式。

很多棋盘问题都运用到了状压,同时,状压也很经常和BFS及DP连用。

状态定义: dp[i][j] 表示:包含变量 i 的某种情形下,j (状压) 状态下的答案。i 有的时候会扩展到二维,即用两个变量来表示情形。但是统一的特征就是有一个 状压 j 表示当前状态。

状态转移方程: 先枚举前面的变量,相当于枚举情形。然后枚举状态。判断状态是否合法后,枚举上一个情形的状态,再判断状态是否合法并进行状态的转移。

重点: 状压dp的显著特征就是会有一个关键变量的数据范围很小 ( 1 ≤ K ≤ 9 )。通过变量表示情形不需要复杂,主要是方便遍历和状态的判断。核心就是某种情形下如何判断状态的合法,注意全面性不要漏判断。

例题:P1879 [USACO06NOV] Corn Fields GP1896 [SCOI2005] 互不侵犯

P1879 [USACO06NOV] Corn Fields G

dp[i][j] 表示:在前 \(i\) 行中,在状态 \(j\) 下的最大方案数 (即答案)

状态转移方程:

dp[i][j] = (dp[i][j] + dp[i - 1][k]) % mod;

for (int i = 1; i <= n; i++) {
	for (int mask = 0; mask < (1 << m); mask++) {
		if (mask & (mask << 1)) continue;
		if (mask & (mask >> 1)) continue;
		if ((mask | a[i]) != a[i]) continue;
		for (int mask1 = 0; mask1 < (1 << m); mask1++) {
			if ((mask & mask1) != 0) continue;
			dp[i][mask] = (dp[i][mask] + dp[i - 1][mask1]) % mod;
		}
	}
}

粗略模板:

for i for j { // 枚举情形
	for mask in (0, maxn) { // 枚举状态
		判断状态 mask 是否合法
		for mask1 in (0, maxn) { // 枚举上一层状态
			判断 mask1 是否合法 以及 两次状态之间是否合法
			使用状态转移方程
		}
	}
}

七、数位DP

使用场景很明显:和数位有关的最优解问题。

数位DP一般用于统计 [l, r] 区间满足特定条件的元素个数。经常要和记忆化搜索放在一起使用。

状态定义: dp[i][j] 表示:[i , j] 这个区间中符合要求的数的个数 (即答案)。有的时候会在加上一维 k (0/1),表示某一种要求。

状态转移方程: 很特殊,常和记忆化搜索一起使用。从高位向低位枚举 '0',减少分叉出的情况个数。常常:用limit 表示上界的限制(上界不超过某个数字),lead 表示是否存在前导 ‘0’ 问题。在这些限制之下,用深度优先搜索枚举下一位。

重点: 记忆化搜索要注意枚举无限制时才可记忆化;有限制时,不可以记忆化,需要继续根据限制进行枚举。常用这些变量作为限制int dfs(int pos, int limit, int lead, int cnt)。核心很模板,记住枚举的套路,设计统计答案的方式即可

例题:P6218 Round Numbers S

P6218 Round Numbers S

\(pos\): 表示当前位置

\(limit\):表示是否有上界限制

\(lead\): 表示是否有前导 0 限制

\(cnt\): 记录答案

\(f\) 数组 在没有特殊限制的时候可以记忆化答案

int dfs(int pos, int limit, int lead, int cnt) {
	if (pos == 0) return (cnt >= 30);
	if (!limit && !lead && f[pos][cnt]) return f[pos][cnt];
	int res = 0;
	int up = limit ? a[pos] : 1;
	for (int i = 0; i <= up; i++) {
		res += dfs(pos - 1, limit && (i == a[pos]),
                   lead && (i == 0),
				   cnt + (i == 0 ? (lead ? 0 : 1) : -1));
	}
	if (!limit && !lead)
		f[pos][cnt] = res;
	return res;
}

粗略模板:

int dfs(int pos, int limit, int lead, int cnt) {
	if (pos == 0) 返回统计答案;
	if 没有特殊限制并且已经有记忆值
        直接返回记忆值;
    
	计算上界 up
	for i in (0, up) { // i 是这一位要填的数字
		使用状态转移方程;
        继续深度优先搜索;
	}
	if 没有限制
        记忆答案值;
    
	返回统计答案;
}

八、概率DP、期望DP

使用场景很常见:有关求解期望、概率等题目

一般求概率是正推,求期望是逆推

总体来说比较灵活

状态定义: 一般就设概率或者期望,加上几个维度来表示状态,进行转移。

状态转移方程:

概率 DP 总逃不开 设计dp转移,多半逃不出高斯消元(手动 和 写代码 两种)

期望 DP 一般来说有它固定的模式。

一种模式是直接 DP,定义状态为到终点期望,采用逆序计算得到答案。

一种模式是利用期望的线性性质,对贡献分别计算,这种模式一般要求我们求出每种代价的期望使用次数,而每种代价往往体现在 DP 的转移之中。

重点: 多手动计算,常常用到许多有关概率或期望的常见公式。还经常结合逆元

费马小定理:若a与m互质,且m为质数,则

\[a^{m-1} \equiv 1 \mod m \,. \]

另一种形式为

\[a^m \equiv a \mod m \,. \]

由此可知: \(a^{m - 2}\) 就是 \(a\) 在模 \(m\) 意义下的逆元。

标签:状态,int,笔记,枚举,DP,转移,dp
From: https://www.cnblogs.com/pengcheng-official/p/18225413

相关文章

  • Django 笔记 - 特殊操作符 2
    前一篇博文介绍了Django中单独符号构成的常用特殊操作符,这篇博文接着介绍Django中组合符号构成的特殊操作符,即{{}} 和{%%}。这两个组合符号构成的特殊操作符都用于Djangotemplate,常用于HTML模板文件。下面分别介绍这两种特殊操作符:{{value}}  {{value}}可......
  • c++参数 使用笔记
    前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站:前言–人工智能教程目录函数两个返回值:1.按值传递(PassbyValue)2.按引用传递(PassbyReference)3.按常量引用传递(PassbyConstReference)4.按指针传递(PassbyPoint......
  • 阅读笔记5
    在短短一周的时间里,我快速浏览了一本关于软件工程的书籍,这让我对这个领域有了更深入的理解。软件工程,顾名思义,是将系统化、有序、可度量的方法应用于软件开发、运营和维护的过程。成功并非一蹴而就,而是通过不断积累逐步实现的。优秀的程序员也是通过日积月累的学习和经验积累而......
  • 阅读笔记4
     通过结对合作,令我意识到了编写程序不仅仅要自己能明白,也要便与他人查看和理解自己的程序。4.1大节提到的代码规范,我们编写代码时要注重代码风格规范和代码设计规范,无论是类名,对象名,缩进还是行宽什么的,在结对子编程时都要有所规定,不然到后面出现的类或是对象多了,就很容易混乱,分......
  • 07Linux学习笔记
    Day7Linux网络管理目录文章目录Day7Linux网络管理1.查看Windows网络配置(ipconfig)2.查看Linux网络配置3.指定LinuxIP方法一:3.1查看所有网络连接3.2修改指定网络连接的IP地址3.3重新启动网络连接方法二:3.4找到要编辑的文件3.5编辑完配置文件后,应用更改:4.主机名和......
  • Nginx 实战-02-nginx proxy_pass 服务代理访问 使用笔记 ubuntu nodejs
    前言大家好,我是老马。很高兴遇到你。我们为java开发者实现了java版本的nginxhttps://github.com/houbb/nginx4j如果你想知道servlet如何处理的,可以参考我的另一个项目:手写从零实现简易版tomcatminicat手写nginx系列如果你对nginx原理感兴趣,可以阅读:从零......
  • 华为交换机配置实验项目笔记
    一、项目需求1、内网互通2、只有销售部vlan20可以访问外网3、技术部vlan10不能访问server1服务器涉及知识点:1.vlan虚拟局域网作用:隔离广播域2.链路聚合作用:增加链路带宽、冗余3.DHCP动态主机配置协议作用:动态分配IP地址4.静态路由5.ACL访问控制列表6.NAT网......
  • 【备战蓝桥杯】蓝桥杯省一笔记:算法模板笔记(Java)
    蓝桥杯0、快读快写模板1、回文判定2、前缀和3、差分4、二分查找5、快速幂6、判断素数7、gcd&lcm8、进制转换9、位运算10、字符串常用API11、n的所有质因子12、n的质因子个数13、n的约数个数14、n阶乘的约数个数15、n的约数和16、阶乘&双阶乘17、自定义升序降序18、动态......
  • PTE笔记:SQL注入-报错注入
    适用于界面不回显的场景,通过注入语句在报错信息中回显我们想要的信息常用函数1:floor+rand配合count+groupby函数rand() 生成0-1之间的随机数,默认完全随机,加参数后固定随机(多次执行随机生成的数是固定的)floor()取整groupby()分组count()计数concat(字符串)拼接字符串group_......
  • 高斯消元学习笔记
    引入高斯-约当消元法(Gauss–Jordanelimination)是求解线性方程组的经典算法,它在当代数学中有着重要的地位和价值,是线性代数课程教学的重要组成部分。高斯消元法除了用于线性方程组求解外,还可以用于行列式计算、求矩阵的逆,以及其他计算机和工程方面。过程一个经典的问题,给定一......