type
status
date
slug
summary
tags
category
icon
password
本人学习ACWING算法基础课&提高课的笔记
目录
目录1 基础算法1.1 排序1.1.1 快排1.1.2 归并排序1.2 二分1.2.1 整数二分1.2.2 浮点数二分1.3 高精度1.3.1 加法1.3.2 减法1.3.3 高精度乘以低精度1.3.4 高精度除以低精度1.4 前缀和&差分1.4.1 一维前缀和1.4.2 二维前缀和1.4.3 一维差分1.4.4 二维差分1.5 位运算1.6 双指针1.7 离散化1.8 区间合并2 数据结构2.1 链表2.1.1 单链表2.1.2 双链表2.2 栈2.2.1 普通栈2.2.2 单调栈2.3 队列2.3.1 普通队列2.3.2 循环队列2.3.3 单调队列2.4 KMP2.5 Trie树2.6 并查集2.7 堆2.8 哈希表2.8.1 哈希表的存储结构2.8.2 字符串的哈希2.9 STL容器2.9.1 顺序性容器2.9.2 关联式容器2.9.3 容器适配器2.9.4 string类2.9.5 bitset压位2.10 树状数组2.11 线段树2.11.1 不带lazy-tag的线段树2.11.2 带lazy-tag的线段树3 图论3.1 树和图的存储3.2 搜索与遍历3.2.1 DFS3.2.2 BFS3.3 最短路径算法3.3.1 dijkstra算法3.3.2 floyd算法3.3.3 Bellman-Ford算法3.3.4 spfa算法3.4 最小生成树3.4.1 Prim算法3.4.2 Kruskal算法3.5 拓扑排序3.6 二分图3.6.1 染色法判别二分图3.6.2 匈牙利算法求最大匹配4 数学知识4.1 质数4.1.1 判定质数5 动态规划5.1 背包问题5.1.1 01背包5.1.2 完全背包5.1.3 多重背包5.1.4 分组背包5.1.5 背包问题常见变形5.2 线性DP 5.2.1 数字三角形模型5.2.2 最长上升子序列模型5.2.3 状态机模型5.3 状态压缩DP5.4 计数类DP5.5 数位统计DP5.6 区间DP5.7 树形DP5.8 记忆化搜索6 贪心6.1 区间问题6.2 哈夫曼树6.3 排序不等式6.4 绝对值不等式6.5 推公式7 时空复杂度分析
1 基础算法
1.1 排序
1.1.1 快排
思想:分治
分界点:q[l] , q[ (l+r) / 2 ] , q[r]随机
注意下标,否则会出现死循环:
下面递归区间为l~j , j+1~r ,那么分界点只能选择q[l]或者q[(l+r)/2]
下面递归区间为l~i-1 , i~r ,那么分界点只能选择q[r]或者q[(l+r)/2]
1.1.2 归并排序
思想也是基于分治,和快排区别在于归并分界点在中间
①确定分界点,
mid = ( l + r ) / 2②递归排序左边和右边
③归并,合二为一
1.2 二分
1.2.1 整数二分
1.2.2 浮点数二分
1.3 高精度
1.3.1 加法
1.3.2 减法
1.3.3 高精度乘以低精度
1.3.4 高精度除以低精度
1.4 前缀和&差分
差分和前缀和互为逆运算
前缀和思想:
计算到是O(n)的
但是O(1)的
1.4.1 一维前缀和
1.4.2 二维前缀和
差分:
对于
构造
使得
思想:
计算a1到an若直接相加是O(n)的,但是用差分可以优化到O(1)
1.4.3 一维差分
1.4.4 二维差分
1.5 位运算
1.6 双指针
情形一:指向两个序列
eg. 归并排序中归并两个数组
情形二:指向一个序列
eg. 快排的时候一个指向开头,一个指向结尾
核心思想:将两个嵌套循环(复杂度)优化为O(n)
最简单的例子:从英语句子拆出单词
1.7 离散化
1.8 区间合并
2 数据结构
2.1 链表
2.1.1 单链表
用数组模拟,比结构体时间开销少很多
2.1.2 双链表
2.2 栈
2.2.1 普通栈
2.2.2 单调栈
最经典的问题:输出序列中每个数左边第一个比它小的数(AcWing 830. 单调栈)
思维过程:
1.思考暴力怎么做
2.哪些部分可以直接优化,哪些情形是一定不会输出的
2.3 队列
2.3.1 普通队列
2.3.2 循环队列
2.3.3 单调队列
经典问题:找出滑动窗口中的最大值/最小值(AcWing 154. 滑动窗口)
2.4 KMP
思考KMP算法之前,先想想朴素暴力做法是什么,再去优化。
KMP字符串问题:
给定一个字符串S,以及一个模式串P,所有字符串中只包含大小写英文字母以及阿拉伯数字。 模式串P在字符串S中多次作为子串出现。 求出模式串P在字符串S中所有出现的位置的起始下标。
暴力做法:
优化:若已经匹配j的长度,下一个字符不匹配了,可以直接滑动到下个匹配j长度的位置,而不是一个一个字符滑。利用模式串的结构性质可以减少匹配步骤,目标就是找到模式串中最大的后缀=前缀。
![ne[i]=j表示以i为终点的后缀,与以1开始的前缀相等,且前后缀的匹配长度j最长](https://www.notion.so/image/attachment%3A95568aca-1670-460d-a7bc-d6df398ebcb0%3Aimage.png?table=block&id=27bec31a-4bca-8027-8d3e-c68f936f2923&t=27bec31a-4bca-8027-8d3e-c68f936f2923)
2.5 Trie树
用数组去建树时需要注意,[N][26]的第一维度不是树的层数,一维是结点总数,而结点和结点之间的关系(谁是谁儿子)是第二个维度。 比如[0][1]=3, [0]表示根节点,[1]表示它有一个儿子‘b’,这个儿子的下标是3;接着如果有一个[3][2]=8 ; 说明根节点的儿子‘b’也有一个儿子‘c’,这个孙子的下标就是8;这样传递下去,就是一个字符串。随便给一个结点][x][y], 并不能看出它在第几层,只能知道,它的儿子是谁。
2.6 并查集
功能
在接近O(1)的时间内完成下列两个操作
1.将两个集合合并
2.询问两个元素是否在一个集合当中
基本原理
每个集合用一棵树来表示。树根的编号就是整个集合的编号。每个节点存储它的父节点,p[x]表示x的父节点。
问题1:如何判断树根:if (p[x] == x)
问题2:如何求x的集合编号:while (p[x] != x) x = p[x];
问题3:如何合并两个集合:px是x的集合编号,py是y的集合编号。p[x] = y
优化:路径压缩
向上搜索的过程中,添加所有节点指向根节点
2.7 堆
堆的结构
完全二叉树,双亲节点值大于儿子(小根堆)
堆的数组存储
维护堆的操作:
上浮操作u p
下沉操作down
手写堆,可进行以下操作:
1.插入一个数
插入元素(最小堆)
建堆
2.求集合当中最小值
3.删除最小值
4.删除任意一个元素
5.修改任意一个元素
2.8 哈希表
2.8.1 哈希表的存储结构
拉链法
开放寻址法
一般N的大小要选择为映射空间大小的2~3倍减小碰撞概率
2.8.2 字符串的哈希
作用:将长度为n的字符串映射成一个整数
字符串前缀哈希法
2.9 STL容器
2.9.1 顺序性容器
顺序性容器就是将一组具有相同类型的元素以严格的线性形式组织起来
一般大多数的题目都可以使用vector容器,除非有特定需求使用其他容器更加合理方便;
如果需要在一串数字的头尾进行操作,偏向deque,对于较中间的元素操作,不推荐 ;
对于中间的元素插入或删除,可采用forward_list(单向链表)或list(双向链表),不需要移动元素,只需改变相关结点的指针域即可;
容器 | 简介说明 |
vector | 可变大小数组。相当于数组,可动态构建,支持随机访问,无头插和尾插,仅支持inset插入,除尾部外的元素删除比较麻烦。但使用最为广泛 |
deque | 双端队列。支持头插、删,尾插、删,随机访问较vector容器来说慢,但对于首尾的数据操作比较方便 |
list | 双向循环链表。使用起来很高效,对于任意位置的插入和删除都很快,在操作过后,以后指针、迭代器、引用都不会失效 |
forward_list | 单向链表。只支持单向访问,在链表的任何位置进行插入/删除操作都非常快 |
array | 固定数组。vector的底层即为array数组,它保存了一个以严格顺序排列的特定数量的元素 |
vector
构造函数
常见的构造方式有四种,一般会前两种就可以
赋值操作
赋值的话可以使用assign()函数,也可以使用其他方式
插入&删除
插入主要是使用push_back(),也可使用insert();删除操作主要是pop_back(),也可使用erase()
容量和大小
对于容量用的是capacity(),对于大小是size(),当然你也可以用resize()来改变其大小,不够在此之前都需用empty()这个函数来判断一下容器是否为空;
遍历
可以按下标遍历,也可用迭代器遍历
比较运算
支持比较运算,按字典序
pair
二元组,前后俩元素类型可以不一样 pair<int,string>p;
取第一个元素pair.first,第二个元素pair.second
构造pair
pair可嵌套
其他用法
at()返回元素,front()返回首元素,back()返回尾元素,clear()清空容器
deque
2.9.2 关联式容器
关联式容器每一个元素都有一个键值(key),对于二元关联容器,还拥有实值(value)容器中的元素顺序不能由程序员来决定,有set(集合)和map(映射)这两大类,它们均是以RB-Tree(red-black tree,红黑树)为底层架构。
如果只负责查找内容,具体到某个单位,使用场景比如对手机游戏的个人的计分的存储,可以使用set或mutliset
如果需要同时放入容器的数据不止一个,并且是不同类型,比如一个为整型int,一个为string字符串型,就可以考虑使用map或mutlimap
容器 | 简介说明 |
set/mutliset | 集合/多重集合。对于set,在使用insert插入元素时,已插入过的元素不可重复插入,这正好符合了集合的互异性,在插入完成显示后,会默认按照升序进行排序,对于multiset,可插入多个重复的元素 |
map/mutlimap | 映射/多重映射。二者均为二元关联容器(在构造时需要写两个参数类型,前者对key值,后者对应value值),因为有两个参数,因此在插入元素的时候需要配合对组pair进行插入,具体见深入详解 |
均支持的操作
set/multiset
map/multimap
unordered_set, unordered_map, unordered_multiset, unordered_multimap
哈希表
和上面类似,增删改查的时间复杂度是 O(1)
不支持 lower_bound()/upper_bound(), 迭代器的++,--
2.9.3 容器适配器
容器适配器是一个封装了序列容器的一个类模板,它在一般的序列容器的基础上提供了一些不同的功能。之所以称为容器适配器,是因为它是适配容器来提供其它不一样的功能。通过对应的容器和成员函数来实现我们需要的功能
容器 | 简介说明 |
stack | 堆栈。其原理是先进后出(FILO),其底层容器可以是任何标准的容器适配器,默认为deque双端队列 |
queue | 队列。其原理是先进先出(FIFO),只有队头和队尾可以被访问,故不可有遍历行为,默认也为deque双端队列 |
priority_queue | 优先队列。它的第一个元素总是它所包含的元素中优先级最高的,就像数据结构里的堆,会默认形成大堆,还可以使用仿函数来控制生成大根堆还是生成小根堆,若没定义,默认使用vector容器 |
stack
queue
priority_queue
默认定义成的是大根堆的形式
定义成小根堆的方式:priority_queue<int, vector<int>, greater<int>> q;
(也可用一些技巧,向大根堆插入x时插入-x,实际维护的是小根堆)
2.9.4 string类
string构造函数
基本赋值操作
string存取字符操作
string拼接操作
string查找和替换
string比较操作
string子串
string插入和删除操作
string和c-style字符串转换
2.9.5 bitset压位
2.10 树状数组
2.11 线段树
应用场景
(1)区间最值问题:求a[i]~a[j]的最值,将a[k]改为x
(2)区间和问题:给出一个长度为n的数列a,修改其中某些数的值
存储
一维数组,区间长度为n,一般开4n
2.11.1 不带lazy-tag的线段树
操作
push_up:从下向上传递区间值
build:初始化方法
query:区间查询
modify:修改值
例题
AcWing 245. 你能回答这些问题吗
查询区间内的最大连续字段和:
①只覆盖左子区间:左子区间的最大连续字段和
②只覆盖右子区间:右子区间的最大连续字段和
③横跨左右区间:左子区间的最大后缀和 + 右子区间的最大前缀和
查询区间内的最大前缀和:
①只覆盖左子区间:左子区间的最大前缀和
②横跨左右区间:左子区间的和 + 右子区间的最大前缀和
AcWing 246. 区间最大公约数
重要性质:gcd(a,b)=gcd(a,b−a)
在lyd的书里提到的这个性质叫更相减损术,可以推广到多个数的情况。 更相减损术其实是欧几里得算法的一个特例。即gcd(a−nb,b)=gcd(a,b)

有了这个式子说明可以通过维护序列的差分来达到求gcd同样的效果。
比如求(a,b,c),只需要知道现在a的值,然后知道(b-a,c-b)的gcd,再求一个公约数就行了。
差分就可以把区间加减变成单点加减。可以用没有lazy的线段树来做。
再维护一个差分,做成树状数组或者线段树,用来维护每个数的值。
另:gcd(a,b)=gcd(a,−b)
2.11.2 带lazy-tag的线段树
push_down(用于区间修改):
modify:区间修改
3 图论
3.1 树和图的存储
树是一种特殊的图,与图的存储方式相同。
对于无向图中的边ab,存储两条有向边a->b, b->a。
因此我们可以只考虑有向图的存储。
(1) 邻接矩阵:g[a][b] 存储边a->b
(2) 邻接表:
3.2 搜索与遍历
3.2.1 DFS
例题
利用带标记的DFS解决的问题:AcWing 842. 排列数字
给定一个整数 n,将数字 1∼n 排成一排,将会有很多种排列方法。
现在,请你按照字典序将所有的排列方法输出。
输入格式
共一行,包含一个整数 n。
输出格式
按字典序输出所有排列方案,每个方案占一行。
数据范围
1≤n≤7
输入样例:
输出样例:
参考解答:
利用剪枝的DFS解决的问题:AcWing 843. n-皇后问题
n−皇后问题是指将 n 个皇后放在 n×n 的国际象棋棋盘上,使得皇后不能相互攻击到,即任意两个皇后都不能处于同一行、同一列或同一斜线上。

现在给定整数 n,请你输出所有的满足条件的棋子摆法。
输入格式
共一行,包含整数 n。
输出格式
每个解决方案占 n 行,每行输出一个长度为 n 的字符串,用来表示完整的棋盘状态。
其中
. 表示某一个位置的方格状态为空,Q 表示某一个位置的方格上摆着皇后。每个方案输出完成后,输出一个空行。
注意:行末不能有多余空格。
输出方案的顺序任意,只要不重复且没有遗漏即可。
数据范围
1≤n≤9
输入样例:
Copy
输出样例:
Copy
参考解答:
第一种搜索顺序
3.2.2 BFS
搜到的是最短路
3.3 最短路径算法

3.3.1 dijkstra算法
存在负权边的图不能用dijkstra算法
朴素版本
时间复杂度,n表示点数,m表示边数
图解dijkstra算法
- (令start=1)
- 开始时我们把dis[start]初始化为0,其余点初始化为inf

- 第一轮循环找到dis值最小的点1,将1变成白点,对所有与1相连的蓝点的dis值进行修改,使得dis[2]=2,dis[3]=4,dis[4]=7

- 第二轮循环找到dis值最小的点2,将2变成白点,对所有与2相连的蓝点的dis值进行修改,使得dis[3]=3,dis[5]=4

- 第三轮循环找到dis值最小的点3,将3变成白点,对所有与2相连的蓝点的dis值进行修改,使得dis[4]=4

- 接下来两轮循环分别将4,5设为白点,算法结束,求出所有点的最短路径
- 时间复杂度O(n2)
堆优化版
时间复杂度,n表示点数,m表示边数
找到没被访问过,且距离最近的点所需运算次数为n次 用这个点更新其他点的距离所需运算次数为m次 找距离最近的点,过程可以用堆排序优化 最终时间复杂度O(mlogn)
堆有两种实现。
一种是手写堆,好处是可以随意修改元素。
一种是优先队列,不可以随意修改元素。
3.3.2 floyd算法
时间复杂度,n表示点数
原理基于dp
3.3.3 Bellman-Ford算法
存在负权边的图可能存在负环,若有负环则最短路径不存在,但存在有最大经过边数的最短路径。
时间复杂度,n表示点数,m表示边数
注意在模板题中需要对下面的模板稍作修改,加上备份数组,详情见模板题。
3.3.4 spfa算法
由Bellman-Ford算法优化而来
Bellman-Ford算法每次迭代所有边,若不满足三角不等式则进行下面更新:
但不是每次迭代都会更新。
若dist[b]变小了,则一定是dist[a]变小了。
因此spfa用宽搜进行优化→维护一个队列
平均情况下,最坏情况下,n表示点数,m表示边数
其他应用:
判断图中是否存在负环 模板题
3.4 最小生成树
3.4.1 Prim算法
朴素版
时间复杂度 ,n表示点数,m表示边数
伪码表示
堆优化版
略
3.4.2 Kruskal算法
时间复杂度,n表示点数,m表示边数
伪码
3.5 拓扑排序
3.6 二分图
一些概念
1.二分图:
二分图是图论中的一种特殊图,顶点集合可以分成两个不相交的部分U和V,并且图中的每条边都连接U中的一个顶点和V中的一个顶点(即不存在同一部分顶点之间的边)。
2.匹配:
在一个图 G=(V,E) 中,匹配是这样一组边:任意两条边都没有公共顶点。
- 最大匹配:匹配的边数最多。
- 可增广路径(Augmenting Path):从一个未匹配的顶点开始,交替经过“不在匹配中”的边和“在匹配中”的边,最后到达另一侧的未匹配顶点。例如:未匹配 —— 不匹配边 —— 匹配边 —— 不匹配边 …… —— 未匹配
一些定理
1.一个图是二分图当且仅当图中不含有奇数环
3.6.1 染色法判别二分图
时间复杂度,n表示点数,m表示边数
文字版
3.6.2 匈牙利算法求最大匹配
时间复杂度,n表示点数,m表示边数
概括:待字闺中,据为己有;名花有主,求他放手 (?
步骤
- 初始化: 所有人都单身,当前匹配数为 0。
- 遍历每个男生:
- 拿起第一个男生的名单,尝试为他找一个女生。
- 为当前男生
u寻找配对:
- 遍历
u心仪的女生列表中的每个女生v。
- 情况一:女生
v是单身 (未匹配)
- 太好了!
u和v配对成功。- 将
(u, v)加入匹配。- 这个男生搞定了,轮到下一个男生。
- 情况二:女生
v已经有伴侣了 (比如是u')
- 这时候不能直接抢,要讲“武德”。
u对v说:“你能不能让你的现任u'换个伴侣?如果他能找到别人,你就跟我吧。”- 这个请求就相当于一个递归过程:我们现在尝试为
u'寻找一个新的伴侣(从u'的心仪列表中找,但不能找v,因为v正在被u追求)。- 如果
u'能成功找到另一个伴侣 (比如v'),那么就形成了一条增广路!路径是u -> v -> u' -> v'。
u'就和v'在一起了。v就被“腾”了出来,可以和u在一起了。u配对成功!轮到下一个男生。- 如果
u'尝试了所有可能,都无法找到新伴侣,那么u'只能和v继续在一起。
u对v的追求失败。u只能去尝试他心仪列表中的下一个女生。
- 防止死循环: 在一次为男生
u找对象的过程中,同一个女生不能被重复考虑(比如u拜托u'去找别人,u'又反过来拜托u……)。所以我们需要一个visited数组,记录在当前这次寻找中哪些女生已经被访问过。每次为新的男生开始寻找时,这个visited数组都要清空。
- 算法结束:
- 当所有男生都尝试过后,算法结束。
- 最终得到的匹配就是最大匹配。
4 数学知识
4.1 质数
4.1.1 判定质数
待完善
5 动态规划
5.1 背包问题
每件物品都有自己的重量和价值。目标是:在背包能承受的重量范围内,选择拿走哪些物品,才能使你拿走的物品总价值最高?

通用模板
5.1.1 01背包
每件物品最多只用一次
状态计算部分可以将集合分为第i个物品选还是不选
模板题
有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。
第 i 件物品的体积是 ,价值是 。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。
接下来有 N 行,每行两个整数 ,用空格隔开,分别表示第 i 件物品的体积和价值。
输出格式
输出一个整数,表示最大价值。
数据范围
0<N,V≤1000
0<≤1000
输入样例
输出样例
参考代码
优化为一维写法
- 可行性:二维状态转移方程针对第i个物品做规划的时候,所有数据只依赖于第i-1个物品规划的过程,所以可以使用一维滚动数组;
- 逆序更新:因为 j-v[i] 小于等于j,所以如果j从0开始规划,那么在更新“选取物品i的选择方案”时,会用本轮(第i个物品规划)的比较小的物品容量j时被更新过的数据,这个数据反映的是本轮针对第i个物品的规划,不是第i-1个物品的规划,所以与原始算法不等价,采取倒序的方式更新,这样可以保留上一轮i-1的信息。
常见变形
选体积等于j的物品:数字组合

朴素版本
优化版本
5.1.2 完全背包
每件物品可以有无限个
状态计算部分可以将集合分为第i个物品选多少个

第i个物品选了0个:
f[i-1,j]第i个物品选了k个:
f[i-1,j-k*v[i]]+k*w[i]1.去掉k个物品i
2.求去掉后的max
f[i-1,j-k*v[i]]3.再加回来k个物品
模板题
有 N 件物品和一个容量是 V 的背包,每种物品都有无限件可用。
第 i 件物品的体积是 ,价值是 。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。
接下来有 N 行,每行两个整数 ,用空格隔开,分别表示第 i 件物品的体积和价值。
输出格式
输出一个整数,表示最大价值。
数据范围
0<N,V≤1000
0<≤1000
输入样例
输出样例
参考代码
对 ① 这个式子进行优化
①式可拆为:
又
故
因此只需枚举两个状态,不用枚举k个状态
⚠️这里不用逆序枚举j,因为完全背包用的f[i][j-v[i]]而非f[i-1][j-v[i]]
变1 货币系统ACwing1021
本质是解最大无关向量组个数
题目描述
定义一个 货币系统 :一共有 种货币,每种货币对应面值为
每种货币可以使用 任意多个,进行线性组合:
为该 货币系统 能够 线性表出 的数值
【注】本题的 线性表出 对于 系数的要求 和 线性代数 中的 线性表出 是不一样的本题的系数必须是 非负整数!
定义:
现给定 货币系统 ,求一个 等价 的 货币系统 ,要求 尽可能最小
分析
种货币,每种货币可以使用 无穷多个
通过这些信息,我们可以初步识别该题目是一个 完全背包 的变种题目
接着我们需要挖掘一下题目里的性质:
学过 线性代数 的同学可以无视这一部分,跳到下面的分割线继续看
研究 货币系统 ,如果存在 可以被 中其他的向量 线性表出:
则 在这个货币系统中是 无效的 (所有线性表示中需要用到的项,都可以用 代替)
因此,我们需要求出 货币系统 的 最大无关向量组,即任意 都不能被其他向量 线性表出
如何求向量组 的 最大无关向量组
我们可以利用到 数论 中 埃式筛法 的思想:
对于一个 无效的 元素 ,他只会被 小于 他的元素的 线性组合 表出,满足该要求的 就要被 筛掉
所以我们要先 排序
而我们在做 完全背包 的时候,需要求出所有恰好能被前 个物品选出的体积的方案
即就是在 完全背包 求方案数的过程中,统计那些 初始不能被满足 的物品体积 个数
这就是我们在 AcWing 1021. 货币系统 中所使用的 DP模型
那一题我们求解的是方案数,这题稍微改一下,改成这种方案能否被满足即可,具体如下
闫氏DP分析法

再具体一点的代码实现,我会在代码的 注释 中再具体解释
Code
时间复杂度:
空间复杂度:
其中 是最大物品的体积
注:
- 显式的完全背包转移应该是: 也就是确实要枚举 k(第 i 个物品用几次)。
- 我们实际用的是更简洁的等价式: dp[i][j] = dp[i-1][j] || dp[i][j - v[i]] 右边用的是“同一层 i 的 dp”,这行会把“枚举 k 次”的过程隐含进去。证明方法是把它展开(unroll)几步你就能看出来: dp[i][j] = dp[i-1][j] || dp[i][j - v] = dp[i-1][j] || (dp[i-1][j - v] || dp[i][j - 2v]) = dp[i-1][j] || dp[i-1][j - v] || (dp[i-1][j - 2v] || dp[i][j - 3v])
5.1.3 多重背包
每件物品的数量不一

模板题
有 N 种物品和一个容量是 V 的背包。
第 i 种物品最多有 件,每件体积是 ,价值是 。
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。
输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。
接下来有 N 行,每行三个整数 ,用空格隔开,分别表示第 i 种物品的体积、价值和数量。
输出格式
输出一个整数,表示最大价值。
数据范围
/1000/1000
/2000/20000
/2000/20000
输入样例
输出样例
暴力版本,可解决数量级
二进制优化,可解决数量级
优化过程
思想:
将「每种物品可以取 个」这个限制,转化为若干个不同的 0/1物品,然后使用普通 0/1 背包 DP 求解。
拆分方法:
用二进制拆分数量:
- 把数量 转换成多个物品的件数分别是1, 2, 4, …, , 剩余部分
- 这样这些物品数量加起来等于
- 每个拆出来的物品有自己对应的重量和价值(乘上数量)
例子
假设某个物品:
- 重量 w=3
- 价值 v=5
- 数量 a=13
拆分:
- 1 个(重量 3,价值 5)
- 2 个(重量 6,价值 10)
- 4 个(重量 12,价值 20)
- 剩余 6 个(重量 18,价值 30)
总数=1+2+4+6=13
这样就把原来的多重背包问题转成了相当于 0/1 背包的物品集合,每个拆分单元只能选一次(取 0 或 1 次)。
单调队列优化,可解决数量

二维朴素版(便于理解代码)
时间复杂度:O()
空间复杂度:O()
滑动窗口的长度为
code(一维优化)
时间复杂度:O()
空间复杂度:O(v)
和 01背包 的优化类似,观察到 状态转移方程,对于 i 阶段,只会用到 i-1 层的状态
因此可以采用 拷贝数组 或 滚动数组 的写法
拷贝数组写法
滚动数组写法
变1 庆功会(多重背包)
题目描述
一共有 $n$ 种奖品,$m$ 元现金
对于第 $i$ 种奖品,其 价格 为 $v_i$,价值 为 $w_i$,数量 为$s_i$
制定一个购买 方案,使得该方案的总价值 最大
分析
物品个数为 $n$,总体积为$m$,初步识别是一个 背包问题
观察到每个物品有 数量限制,断定该题是 多重背包问题
本题是一道 多重背包 的裸题
本篇题解,只展示 多重背包 的朴素优化和分析,关于 单调队列优化 可以看这篇博客
不多废话,我们直接上 闫氏DP分析法
闫氏DP分析法
初始状态:
f[0][0]目标状态:
f[n][m]
Code(朴素写法)
时间复杂度:$O(n \times m \times s)$
空间复杂度:$O(n \times m)$
朴素优化方式
同 01背包 ,对于第 i 阶段的状态更新只会用到第 i-1 阶段的状态
因此可以采用 滚动数组 或者根据状态更新的顺序,直接压缩成 一维 的优化方式
时间复杂度:$O(n \times m \times s)$
空间复杂度:$O(m)$
一维优化方式
常见变形:
5.1.4 分组背包
物品分组,每一组里只能选一个物品

模板
多重背包问题其实是分组背包问题的一个特例
例1 机器分配
题目描述
总公司拥有 M 台 相同 设备,准备分给下属的 N 个分公司。
第 i 家公司分到 j 台机器后,所获的的收益为
求一种分配方案,使得总收益最大,输出该方案
分析
本题乍一看很像是 背包DP,为了转换成 背包DP 问题,我们需要对里面的一些叙述做出 等价变换
每家公司 我们可以看一个 物品组,又因为 每家公司 最终能够被分配的 机器数量 是固定的
因此对于分给第 个 公司 的不同 机器数量 可以分别看作是一个 物品组 内的 物品
该 物品 的含义:分给第 个 公司 台机器该 物品 的体积:该 物品 的价值:
于是,本题就转换成了一个 分组背包问题
直接上 分组背包 的 闫氏DP分析法

初始状态 :
f[0][0]目标状态 :
f[N][M]动态规划求状态转移路径
这里我介绍一个从 图论 角度思考的方法
动态规划 本质是在一个 拓扑图 内找最短路
我们可以把每个 状态
f[i][j]看作一个 点,状态的转移 看作一条 边,把 状态的值 理解为 最短路径长具体如下图所示:

对于 点
f[i][j] 来说,他的 最短路径长 是通过所有到他的 边 更新出来的更新 最短路 的 规则 因题而异 ,本题的 更新规则 是
最终,我们会把从 初始状态(起点)到 目标状态 (终点)的 最短路径长 更新出来
随着这个更新的过程,也就在整个 图 中生成了一颗 最短路径树
该 最短路径树 上 起点 到 终点 的 路径 就是我们要求的 动态规划的状态转移路径
如下图所示:

那么 动态规划求状态转移路径 就变成了在 拓扑图 中找 最短路径 的问题了
可以直接沿用 最短路 输出路径的方法就可以找出 状态的转移
Code
找 最短路径 递归写法
5.1.5 背包问题常见变形
混合背包问题
有依赖的背包问题
例1 金明的预算方案(有依赖的分组背包)
题目描述
一共有 个物品和 元钱
物品之间可能存在 依赖关系,对于第 个物品,价格为 ,价值为 ,依赖的父物品为
每个物品只能被购买一次
依赖关系:只有购买了父物品后,才能购买子物品 (若 表示该物品为 主件)
求一种 购买方案,使得 总花费 不超过 ,且 总价值 最大
分析
本题是一道 背包DP 的 经典变种题目:有依赖的背包 DP
根据题设的 拓扑结构 可以观察出每个 物品 的关系构成了 森林
而以往的 背包DP 每个 物品 关系是 任意的(但我们一般视为 线性的)
所以,这题沿用 背包DP 的话,要从原来的 线性DP 改成 树形DP 即可
在 有依赖的背包问题【有依赖背包DP+子物品体积集合划分】 中我们利用了子物品的体积进行 集合划分
时间复杂度是
但是对于本题 的数据范围是 ,如果用该种集合 划分方案,毫无疑问会超时
注意到题目中有提到,每个 主件 的 附属品 数量不超过 个,且 附物品 不会再有 附属品
因此我们可以采用 分组背包 对本题的状态进行 集合划分
具体思路就是,枚举所有选 子物品 的 组合,每一个 组合 对应一个分组背包中的 物品
这样时间复杂度是
闫氏DP分析法:
- 状态表示—集合:考虑前 个背包组,且总体积不超过 的方案
- 状态表示—属性:方案的总价值 最大MAX
状态计算:
Code
时间复杂度:
例2 有依赖的背包问题(树形DP)
采用树形DP
题目描述
有 件 物品 和一个 体积 为 的 背包
第 件 物品 的 体积 为 , 价值 为
每件 物品 有一个 父节点物品 ,如果想选第 件 物品,就必须选他的 父节点
拓扑结构 如下图所示:

如果选择 物品5,则必须选择 物品1 和 物品2。
这是因为 物品2 是 物品5 的 父节点,物品1 是 物品2 的 父节点。
求一个方案,使得该选择的物品合法,总体积不超过 ,且总价值最大
分析
这是一道 背包DP 的 变种题目
根据题设的 拓扑结构 可以观察出每个 物品 的关系构成了一棵 树
而以往的 背包DP 每个 物品 关系是 任意的(但我们一般视为 线性的)
所以,这题沿用 背包DP 的话,要从原来的 线性DP 改成 树形DP 即可
然后思考 树形DP 的 状态转移
先比较一下以往 线性背包DP 的 状态转移,第 件 物品 只会依赖第 件 物品 的状态
如果本题我们也采用该种 状态依赖关系 的话,对于节点 ,我们需要枚举他所有子节点的组合 种可能
再枚举 体积,最坏时间复杂度 可能会达到 (所有子节点都依赖根节点)
最终毫无疑问会 TLE
因此我们需要换一种思考方式,那就是枚举每个 状态 分给各个子节点 的 体积
这样 时间复杂度 就是
具体分析如下:
闫氏DP分析法

Code
时间复杂度:
二维费用的背包问题
变1-1 潜水员
算法分析
- 状态表示
f[i,j,k]:所有从前i个物品中选,且氧气含量至少是j,氮气含量至少是k的所有选法的气缸重量总和的最小值
- 状态计算:
- 所有不包含物品
i的所有选法:f[i - 1,j,k] - 所有包含物品
i的所有选法:f[i - 1,j - v1,k - v2]
注意:即使所需要的氧气或者氮气所需的是数量是负数,但其所需数量与
0是等价的,因此可以通过所需数量为0来转移扩展
可能很多人会有这样的疑问,二维费用的背包问题的状态转移方程代码如下
而本题的状态转移方程代码如下
为什么上面的代码
j只需要遍历到v,k只能遍历到m。而下面的代码 j还需要遍历到0,k还需要遍历到0 ?同时为什么氧气或者氮气所需的是数量是负数时,可以与数量0的状态等价?解答:对比两题的思路,二维费用的背包问题,求的是不能超过体积
V,重量M的情况下,能拿到价值的最大值。而本题是至少需要体积V,重量M的情况下,能拿到价值的最小值。就拿体积来说,至少需要多少体积,也就是说有体积比需要的体积大的物品还是能用得到,例如f[3][5],至少需要3个体积,5个重量,求能拿到价值的最小值,现在只有一个物品,体积是4,重量是4,价值w,它说至少需要3个体积,那么体积是4还是可以用到,只是多了1个体积没用占着而已,不影响其价值。因此若用了这个物品,则变成了求f[0][1] + w,表示体积已经不再需求了,只需要0个体积即可求最大值最小值初始化总结
二维情况 f(i,j):从前i个物品中选,总体积()()j
1、体积至多
j,f[i,k] = 0,0 <= i <= n, 0 <= k <= m(只会求价值的最大值)2、体积恰好
j,当求价值的最小值:
f[0][0] = 0, 其余f[0][k](k > 0)是INF (f[0][0]表示什么都不选,体积刚好也满足为空,合法,其余不合法)当求价值的最大值:
f[0][0] = 0, 其余f[0][k](k > 0)是-INF3、体积至少
j,f[0][0] = 0,其余f[0][k](k > 0)是INF(只会求价值的最小值)一维情况
1、体积至多
j,f[i] = 0, 0 <= i <= m(只会求价值的最大值)2、体积恰好
j,当求价值的最小值:
f[0] = 0, 其余f[k](k > 0)是INF当求价值的最大值:
f[0] = 0, 其余f[k](k > 0)是-INF3、体积至少
j,f[0] = 0,其余f[k](k > 0)是INF(只会求价值的最小值)时间复杂度 O(nmk)
背包问题方案数
0-1背包求最优方案数
题目描述
有 件 物品 和一个 容量 为 的背包。每件 物品 只能使用 一次
第 件 物品 的 体积 是 ,价值 是
求解将哪些物品装入背包,可使 总体积 不超过 的情况下, 总价值 最大
输出 最优选法的方案数 。答案可能很大,输出答案模 的结果
分析
本题是 背包DP 中的经典题型 —— 【背包DP求最优方案总数】
01背包求最优方案 具体都在这篇博客中 AcWing 423. 采药【01背包DP模型+朴素优化】
本题解主要描述如何求 最优方案总数
在这篇 AcWing 1013. 机器分配【分组背包+背包DP输出方案—拓扑图分析】 有对 DP—拓扑图 的描述
本题我们也可以利用这个 状态转移拓扑图 找出所有 最优解 的 状态转移路径,从而求解出方案数
闫氏DP分析法
01背包模型
状态表示—集合: 考虑前 i 个物品,且当前已使用体积不超过 j 的方案
状态表示—属性: 该方案的价值为最大值
状态转移:
路径跟踪
状态表示—集合: 考虑前 i 个物品,当前已使用体积恰好是 j 的,且 价值 为最大的方案
状态表示—属性: 方案的数量
状态转移:
如果 且 则
如果 且 则
如果 且 则
初始状态:
g[0][0] = 1Code(朴素写法)
时间复杂度:
空间复杂度:
Code(朴素优化)
可以把 g 和 f 写进一个循环里,再判断 f 的 转移路径 的同时用 g 跟踪 转移路径
然后再用 01背包 的 朴素优化,把第一维消掉即可
时间复杂度:
空间复杂度:
背包问题求具体方案
不能用状态压缩,回溯找当前状态是由哪个状态转移过来的
例:01背包 + 背包DP输出方案
题目描述
有N件 物品 和一个 容量 为 V 的 背包
第 件物品的 体积 是, 价值 是,每件 物品 只能使用一次
求解一种 方案,使得选择的 物品 的 总体积 不超过背包的 最大容量,且 总价值 最大
输出 字典序最小的方案
分析
本题是 背包DP 的经典变种模型 背包DP输出方案
输出方案 其实就是输出方案的 转移路径
我们先做一遍正常的 背包DP ,然后从 目标状态 倒推回 初始状态 的整个 转移路径 即可
说的白话一点就是,在考虑第i件物品时,选择了 选 还是 不选 的策略到达了第i+1件物品
伪代码如下:(参考oi wiki)
当然也可以从 拓扑图 的角度来 分析理解,这次不额外展开,具体可以参考下面这篇博客,里面也有 DFS 的写法
AcWing 1013. 机器分配【分组背包+背包DP输出方案—拓扑图分析】
这里主要展示 迭代 的写法(其实就是在跟踪状态转移的路径)
题目里还要求了 输出 字典序最小的方案
而在倒推 状态转移路径 的时候,只能在 分叉转移 的时候,即 当前 物品既可以 选 又可以 不选 时,优先 选
因此,我们本题的 背包DP 需要倒过来(从N递推到1)做,然后再 从1倒推回N 找出路径
这样在抉择时,如果出现 分叉转移,我们就优先 选 当前物品即可
01背包模型
状态表示f(i,j)—集合: 考虑后 i 个物品,且当前已使用体积不超过 j 的方案
状态表示f(i,j)—属性: 该方案的价值为最大值
状态转移f(i,j):
Code
时间复杂度:O()
路径追踪的 递归 写法可以参考这篇博客
DP+贪心
例1 能量石
题目描述
共有 块魔法石
每块魔法石,吃掉的时间是 价值是 ,每单位时间内损失的能量是
规定,每块魔法石刚开吃的时候就会立刻获得他的价值(不会在吃的过程中损失能量)
每块魔法石一旦开吃就不能停下,魔法石能量损失到 以后不会额外损失
求借一种方案,使得最终获得的 总价值 最大
分析
本题抛开 魔法石会损失能量 的限制,就是一道 01背包 的裸题
但是以往的 01背包 每个物品的价值是不变的,因此我们可以指定任意的次序进行 线性dp 求解
但是本题有 魔法石会损失能量 这一限制
因此 如何枚举物品的次序 是首要解决的问题,然后才可以套用 01背包模型 求解
我们这里采用 贪心思路 ,从分析 贪心解 中两个 相邻物品 的次序来入手
假设在 贪心解 中存在 满足 且枚举到 时,能量都不为
如果能量为 ,那么他们的次序可以任意安排
则存在如下不等式(次序 获得的价值 大于等于 获得的价值)
于是对于最优解中所有 的物品次序,我们都可以通过一次 邻项交换 的操作变成我们上述的次序,且保证该次交换完成后,总价值不减少
于是得以证明出 贪心解 最优解
而由于 贪心解 本就是 合法解 ,因此自然而然 贪心解 最优解
得证:贪心解 最优解
于是我们可以直接用 结构体 存储每个物品的三个 属性 ,接着按照上述的 次序 从小到大排序
就会获得能够达到 最优解 的物品次序,然后就是经典 01背包线性DP 求 最大价值 问题
闫氏DP分析法
— 状态表示(集合):考虑前 i 个魔法石,且吃掉最后一个魔法石后,所用总时间恰好为 j 的方案
— 状态表示(属性):方案的总价值 最大 MAX
— 状态计算:
- 吃掉第 i 个魔法石 —
- 不吃第 i 个魔法石 —
Code
时间复杂度:
5.2 线性DP
5.2.1 数字三角形模型
例1 数字三角形
给定一个如下图所示的数字三角形,从顶部出发,在每一结点可以选择移动至其左下方的结点或移动至其右下方的结点,一直走到底层,要求找出一条路径,使路径上的数字的和最大。
输入格式
第一行包含整数 n,表示数字三角形的层数。
接下来 n 行,每行包含若干整数,其中第 i 行表示数字三角形第 i 层包含的整数。
输出格式
输出一个整数,表示最大的路径数字和。
数据范围
1≤n≤500,
−10000≤三角形中的整数≤10000
输入样例
输出样例
参考代码
变1 摘花生

集合划分时,很重要的划分依据是考虑最后一步
变2 最低通行费
和摘花生等价
变4 传纸条(还没做)
变5 K取方格数(还没做)
5.2.2 最长上升子序列模型

例1 最长上升子序列(LIS)
给定一个长度为 的数列,求数值严格单调递增的子序列的长度最长是多少。
输入格式
第一行包含整数 。
第二行包含 个整数,表示完整序列。
输出格式
输出一个整数,表示最大长度。
数据范围
输入样例:
输出样例:

变1-1 怪盗基德的滑翔翼
在整个序列正向反向应用LCS算法
变1-2 登山
一定是先严格上升,再严格下降的。
对于某个点,以其为最高海拔的最多浏览景点数 = 左边上升的景点数 + 右边下降的景点数 + 1
如果每个点都去求一次LIS,会变成O(n^3),可以对整个序列正向和反向求一遍LIS,将结果保存在f(i)、g(i)中,要用的时候直接查,
变1-3 友好城市
题目描述
本题的背景是一个造桥项目,初始给定我们 座打算建造的桥
每座桥有两个参数 和 ,表示该桥一头连接在上岸坐标为的地方,一头连在下岸坐标为的地方
题目要求我们找出一种建桥方案,使得在所有建造的桥不相交的前提下,建造尽可能多的桥
求出该方案的造桥数量
具体的情况用文字可能不太好解释,我这里画了一张图方便大家理解

红色虚线表示初始提供的打算建造的桥
我们要找出的方案是在不相交的前提下的最大建桥数量
也就是上面的绿线连接而成的方案
这就是本题的大致意思
题解
我们先想一下暴力怎么做
很容易想到,我们可以枚举所有的方案,然后检查方案是否合法,如果合法就考虑是否能够更新最大值答案
这么做的时间复杂度是 ,而 的数据范围是 ,毫无疑问会超时。
所以,我们就需要找出一些性质进行优化
既然是要暴力枚举,我们可以考虑一个枚举方案,按照上岸的坐标从小到大来枚举
然后我们只需关心下岸的坐标之间有何关系即可
于是,可以轻易发现,上坐标从小到大枚举选择到的桥,其对应下坐标也必然是从小到大的
具体见下图:
蓝色表示该方案按照上坐标从小到大先选出来的桥
红色表示该方案的下一座桥的下坐标不是从小到大的,绿色表示是从小到大的

因此,该方案中,在上坐标排序的情况下,下坐标次序不是从小到大的,则必然不合法(会有相交)
于是,这题就变成了:桥以上坐标从小到大排序后,找出下坐标的最长上升子序列长度
关于如何求最长上升子序列模型的DP,大家可以看一下之前的博客,里面有详细的闫氏DP分析法
Code
我们可以用
pair或者struct先把所有的桥存下来,然后按照上坐标排序再然后,下坐标就构成了一个序列,我们只需找出该序列的最长上升子序列的长度,就求出本题的答案了
变1-4 最大上升子序列和
题目描述
本题定义了一个上升子序列和:对于元素满足从左往右数值递增的次序的子序列的和
我们要求出该序列中最大上升子序列和
题解
该题目定义的最大上升子序列模型与我们熟悉的最长上升子序列模型非常相似
因此我们可以采用最长上升子序列DP模型来分析本题
时间复杂度:
闫氏DP分析法
状态表示 : 考虑前i个元素,以第i个元素结尾的最大上升子序列的方案
状态属性: 最大上升子序列和最大
状态转移: (满足才能转移)
集合划分:

Code:
变1-5 拦截导弹
题目描述
本题给定一个数组 ,让我们求解两个量
- 该数组的最长不上升子序列
- 该数组最少能被几个最长下降子序列全部覆盖
样例的图例:

题解
对于第一问不再做过多赘述,关于如何求最长上升子序列模型DP,可以看之前的博客
这里主要说一下第二问的贪心思路及证明
第二题要求我们用最少的最长下降子序列对原数组进行全覆盖
考虑一种贪心方案:
对于第i个数来说,把它加入前 i - 1 个数构成的下降子序列组中,所有结尾元素大于第i个数的数中最小的那个数
证明:(最优解 = 贪心解)
假设存在一个最优解,他在考虑第 i 个数放入的下降子序列组中,选择了贪心解方案的后面的一个位置
具体如图所示:(绿色部分,更新了后为保证递增顺序,交换了和,这一步省略了)

可以观察到,该最优策略使得当前局面差于贪心策略,即能接在范围的子序列少了一个
即贪心解 最优解
同理可证,最优策略在考虑第 i 个数放入的下降子序列组中,选择了贪心解方案的后面的第 个位置
也有结论贪心解 最优解
此外,由于贪心解是合法解,所以必然 贪心解 最优解
于是有 贪心解 最优解
证明:(调整法)
假设存在一种最优策略,不是按照贪心方案进行阶段决策的
则我么可以通过有限次的调整,把最优解调整成贪心解的方案,具体如下图所示

于是,由该决策包容性,得出最优解可以是贪心解。
Code
从前往后做一遍最长下降子序列,同时维护一个数组长度为的单调不减数组
数组 q[N] 中每个元素维护的是当前以 q[i] 结尾的下降子序列
于是,对于第 i 个元素来说,他能插入的到 q[N] 中的哪个下降子序列中,是存在一个二分性质的
由于要求的是下降子序列且 q[N] 是单调不减的
因此对于所有的 w[i] ,必然存在一个边界 j ,满足 且
于是我们就可以用二分来优化找满足性质: 的区间左端点即可
具体代码如下:(此题考的主要是这个贪心,对于前面的DP较为简单,因此不画闫氏DP分析法了)
变1-7 最长上升子序列 II
和例1区别在于N的范围更大
AcWing. 896 最长上升子序列 II
题目描述
给定一个长度为N的数列,求数值严格单调递增的子序列的长度最长是多少。
样例
限制
算法1(TLE)
(贪心 + 暴力枚举)
- 贪心思路:较小的数开头的数作为的子序列 比 较大的数作为开头的子序列 更好
- 贪心步骤:
- 开一个数组
q[i],存的是以长度为i的上升子序列中末尾元素最小的数, q[]一开始是空集,长度为0- 遍历每个数
x,对于当前数x, 先找到一个最大的小于x的数c感谢@sort: - 情况
1:若找不到c, 扩大q[]的长度并记录当前数x - 情况
2:若找到c, 就存在一个不等式c < x ≤ a < b, 则将x覆盖a的位置
时间复杂度
- 遍历每个数是
- 遍历
q数组每个数, 找到一个最大的小于当前数x的数c,最坏情况需要枚举所有数
- 因此时间复杂度是
- 由于本题的
N = 100000, (10^5)^2 = 10^10会TLE
C++ 代码
算法2
(贪心 + 二分)

- 基于
算法1的单调性:找到一个最大的小于等于当前数的数, 我们可以用 二分 来优化
- 二分的思路:
- 先定义边界,
l = 0, r = len, 其中len是q数组的长度 - 然后确定
check函数, 可以先找到不等式中c < x ≤ a ≤ b的c - 通过
q[r + 1] = a[i]来将x覆盖a的值 - 同时也要考虑
算法1的情况1, 需要扩大q数组的长度 - 即
r + 1 > len时, 表示超出了二分边界,这时就要len ++更新q的长度
时间复杂度
- 遍历每个数是
- 遍历
q数组每个数, 找到一个最大的小于当前数x的数c
- 因此时间复杂度是
C++ 代码
求最长上升子序列的长度 和 最小的用不上升子序列覆盖 是对偶问题 可以相互转化
- 把一个数列看成一个偏序集,最长上升子序列是该偏序的最长链,非上升子序列是该偏序的反链。
- Dilworth 定理与其对偶(Mirsky 定理)把“最大链/反链的大小”和“用反链/链分解的最少条数”对应起来。
- 因此:
- 最长上升子序列长度 = 最少用不上升子序列将全序列分解的条数(Mirsky 定理)。
- 最长不上升子序列长度 = 最少用上升子序列将全序列分解的条数(Dilworth 定理)。
变1-8 导弹拦截系统
题目描述
题目给定一个长度为 n 的数组 w[n] ,要求我们用最少的上升子序列和下降子序列完全覆盖该数组
求该方案的上升子序列和下降子序列的总个数
解析
本题是对上题AcWing 1010. 拦截导弹的拓展
对于当前元素 w[i] 应该被加入到 上升子序列 还是 下降子序列 我们可以采用 暴力枚举 的方式
如果当前元素 w[i] 我们选择加入到 下降子序列 中,那么具体要加入到哪个 下降子序列 中
可以参考该篇博客 拦截导弹【附贪心证明】
如果加入到 上升子序列 中,那么具体要加入到哪个 上升子序列 中,方法类似加入 下降子序列 的
时间复杂度: 因此需要用到 迭代加深/维护全局最小值,剪枝 的优化
Code(维护全局最小值)
Code(迭代加深)
变1-9 最长公共子序列(LCS)(待完善)
给出两个长度为 的整数序列,求它们的最长公共子序列(LCS)的长度,保证第一个序列中所有元素都不重复。
注意:
- 第一个序列中的所有元素均不重复。
- 第二个序列中可能有重复元素。
- 一个序列中的某些元素可能不在另一个序列中出现。
输入格式
第一行包含一个整数 。
接下来两行,每行包含 个整数,表示一个整数序列。
输出格式
输出一个整数,表示最长公共子序列的长度。
数据范围
序列内元素取值范围 。
输入样例1:
输出样例1:
输入样例2:
输出样例2:
变1-10 最长公共上升子序列(LCIS)
闫氏DP分析法:(结合了LCS与LIS的状态表示的方法,可以很直接的发现二者的影子)
状态表示 f[i][j]—集合:考虑 a 中前 i 个数字,b 中前 j 个数字 ,且当前以 b[j] 结尾的子序列的方案
状态表示 f[i][j]—属性:该方案的子序列长度最大
状态转移:
- 从考虑 a数组 中前 i-1 个数字, b数组 中前 j 个数字 ,且当前以 b[j] 结尾的子序列的方案转移过来:
- 从考虑 a数组 中前 i 个数字, b数组 中前 k 个数字 ,且当前以 b[k] 结尾的子序列的方案转移过来:
初始状态:
f[0][0]目标状态:
f[n][i]集合划分


Code(朴素版)
时间复杂度:O(n^3)
对于本题的数据规模,毫无疑问会超时
这里贴出代码是方便大家理解这个DP模型,因为接下来的优化,就和DP无关,是一个代码的等价变形优化
Code(优化版)
我们可以观察到,对于第二种状态转移:
每次用到的 状态 都是第 i - 1 个阶段的
因此我们可以用一个变量,存储上一个阶段的能够接在 a[i] 前面的最大的状态值
时间复杂度:
例3 最短编辑距离(待完善)
变3-1 编辑距离(待完善)
5.2.3 状态机模型
例1 大盗阿福
题目描述
街道上有 家店铺,按照顺序从 到 排好
第 家店铺的财产是
大盗阿福如果偷了第 家店铺,则他不能偷 相邻的店铺,否则会被抓起来
帮助大盗阿福找出一种偷盗 方案,使得获得的 总财产最大
分析
这是一道经典的 线性DP 问题
为何本题可以线性DP求解?
先考虑的问题是,我们能否按照从 到 的 线性顺序 找出 最优解
答案当然是可以的,因为每家店铺在第 i 次被偷和在第 j 次被偷,获得的 价值 是一样的
对于一个 非线形 顺序的最优解,可以通过有限次交换,在保证答案不会减少的情况下,变成 线性顺序
于是我们从前往后 线性DP 即可
考虑如何表示当前的状态?
需要记录的有,当前线性递推到第 i 家店铺,以及对于第 i 家店铺,我们选择 偷 或者 不偷
于是就有了如下的 闫氏DP分析法
闫氏DP分析法
状态表示—属性: 考虑前 i 家店铺,当前第 i 家店铺选择 偷(j=1) 或者 不偷(j=0) 的方案
状态表示—集合: 方案的总价值 最大Max
状态计算:
接下来解释一下这里的状态计算
状态机模型分析

如果要偷第 i 家店铺,则第 i-1 店铺不能被偷:
如果不偷第 i 家店铺,则第 i-1 店铺任意安排:
Code
时间复杂度:
空间复杂度:
初始状态:
f[0][0]目标状态:
f[n][0] 和 f[n][1]滚动数组优化
很多 线性DP 都可以用此类优化,详见 【01背包DP模型+朴素优化】
时间复杂度:
空间复杂度:
例2 股票买卖II
DP + 贪心 双解 (附贪心证明)
一、状态机模型DP解法
我这里直接贴出闫氏DP分析法的思维导图

具体的状态机模型分析如下图:

一共只有种状态:
1. 当前处于
未持股状态0:2. 当前处于
持股状态1:Code:
二、贪心解法
纵然,我们可以用
DP搜索出所有的方案数,但是通过观察,我们可以发现本题最优解方案存在一定的性质。我先贴出测试样例的折线图形式(
绿色标出下降,红色标出上升):
考虑一种方案,在每次
上升的前一天购入股票,并在上升后的当天卖出的方案接下来证明该
贪心思路得出的方案即是最优解。(1)证明贪心解 最优解:
由于贪心解都是取区间长度为 的解,因此假设存在于最优解中的某个区间 的长度
那么会出现一下三种情况:

对应三种情形:最优解选取的区间最终点位于上方、下方、相等。
对于
情形一:显然 最优解 < 贪心解对于
情形二:显然 最优解 < 贪心解对于
情形三:毫无疑问,这就是存在于贪心解中的情形,因此 贪心解 = 最优解得证
(2)证明贪心解 最优解:
这部分无需证明,因为
贪心解即是合法解,所以他的方案必定大于等于最优解Code
变2-1 股票买卖IV
题目描述
给定一个长度为 的数组 ,数组的第 个元素 表示第 天的股票 价格
一次买入一次卖出为一笔 合法交易,且不能同时产生多笔交易(必须在再次购买前出售掉之前的股票)
设计一个方案,使得总 合法交易数 不超过 次,总利润最大
接 股票买卖II 的拓展思考
如果你做过前几版的 股票买卖 可以只看下面这几行的分析
AcWing 1055. 股票买卖 II DP + 贪心 双解 中分析过 第二版 的解法
而本题是 股票买卖 系列的 第四版
在 第二版 上, 额外 限制了 总交易的次数,因此 第二版 中的 贪心思路 不再适用
我们可以在 第二版 的 DP模型 上,额外加上记录 当前交易笔数 的参数即可
原题分析
对于第 天 购入 的股票,当且经当第 天才能 卖掉,获得的 利润 为
由此可知求解 股票交易 问题是一个 线性 的过程,于是我们思考一下能否用 线性DP 进行 状态记录 和 转移
线性DP如何记录当前的状态?
我们以当前递推到的天数,作为 线性DP 的阶段
对于递推到的第 天来说,我们需要记录的信息有:
- 在完成第 天的决策后,状态是持仓()还是空仓()
- 在第 天交易完成时,总共完成的 完整 的交易数 (题目规定一次买入一次卖出是一笔 完整的交易)
那么如何利用该状态表示出题设中的状态转移行为呢?
- 买入行为:k=0 k=1
- 卖出行为:k=1 k=0
- 持仓行为:k=1 k=1
- 空仓行为:k=0 k=0
于是,有了如下的 闫氏DP分析法
闫氏DP分析法
状态表示—集合: 考虑前 i 天的股票,第 i 天的 决策 是 k,且完成的 完整交易数 为 j 的方案
状态表示—属性: 方案的总利润 最大MAX
状态计算:
接下来用 状态机模型 解释一下状态计算

状态机模型DP
- 第 i 天状态是持仓状态(j=1),则第 i 天可能产生的行为是 买入行为 或 持仓行为
- 第 i 天状态是空仓状态(j=0),则第 i 天可能产生的行为是 卖出行为 或 空仓行为
【注】:卖出行为 会构成一次完整的交易,所以进行该类转移时, j 的参数也要变动
Code
时间复杂度:
空间复杂度:
初始状态:
目标状态:
滚动数组优化
很多 线性DP 都可以用此类优化,详见 【01背包DP模型+朴素优化】
时间复杂度:
空间复杂度:
变2-2 股票买卖V
题目描述
给定一个长度为 的数组 ,数组的第 个元素 表示第 天的股票 价格
- 一次买入一次卖出为一笔 合法交易,且不能同时产生多笔交易(必须在再次购买前出售掉之前的股票)
- 卖出股票后,无法在第二天买入股票(冷冻期为1天)
设计一个方案,使得总利润最大
分析
本题是 股票买卖系列 的 第五版,我们可以沿用 第四版 继续做一些改变
与 第四版 相关的 dp 分析 和 状态机模型分析 ,在这里我不会具体展开
想了解 第四版 的,具体可以参考 股票买卖 IV【线性DP+状态机模型DP+滚动数组优化】在这篇博客里,我有详细的分析
以 线性 的方式 动态规划,考虑第 i 阶段/天 的状态,需要记录的参数有哪些:
- 第 i 天的 决策状态:
- (j=0) 当前没有股票,且不处于冷冻期 (空仓)
- (j=1) 当前有股票 (持仓)
- (j=2) 当前没有股票,且处于冷冻期 (冷冻期)
注意:这里的 冷冻期 状态,实际意义是指当天卖出了股票,所以 后一天是没法交易
状态机模型分析

- 如果第 i 天是 空仓 (j=0) 状态,则 i-1 天可能是 空仓 (j=0) 或 冷冻期 (j=2) 的状态
- 如果第 i 天是 冷冻期 (j=2) 状态,则 i-1 天只可能是 持仓 (j=1) 状态,在第 i 天选择了 卖出
- 如果第 i 天是 持仓 (j=1) 状态,则 i-1 天可能是 持仓 (j=1) 状态 或 空仓 (j=0) 的状态 (买入)
闫氏DP分析法
状态表示—属性: 考虑前 i 天股市,当前第 i 天的状态是 j 的方案
状态表示—集合: 方案的总利润 最大Max
状态计算:
初始状态:
目标状态:
Code
时间复杂度:
空间复杂度:
滚动数组优化
时间复杂度:
空间复杂度:
思路同 01背包
例3 设计密码(KMP自动机) (有问题)
题目描述
给定一个字符串 ,和一个整数
现需设计一个密码 , 需满足:
- 的长度是
- 只包含小写英文字母
- 不包含子串
求该密码的 方案数,答案对 取模
分析
首先读者需要了解一部分自动机的概念,我推荐一下OI-Wiki的这篇博客 自动机 - OI Wiki了解自动机是学字符串的基础,之后学习 AC自动机、 KMP 、 后缀自动机SAM 都有很大的帮助
求方案数 的题目难免不会让人想到用 动态规划 来求解
设计一个 合法的密码,从前往后 线性构造 是一个不错的枚举方案
因为如果 当前设计的密码 是合法的话,在他后面添上一个 新字符,可能会导致 新密码不合法 的情况,只有可能是 新密码 的 后缀子串 中出现了
通过 线性构造 的方法,我们只需判断 后缀子串 中是否出现 即可
那么如果我们想用线性DP来求解,应该如何记录当前的状态呢?
首先,毫无疑问是用当前构造的密码长度 作为 线性DP 的阶段
可是如何记录当前的长度为 的字符串的 后缀 与 字符串 的匹配情况呢?
关于 单母串 和 单模式串 的 在线匹配,会自然而然的想到一个 字符串算法 —— 前缀函数与KMP
借用 KMP 的思想,我们可以用参数 来表示当前 母串后缀 与 模式串 匹配的 最大长度
KMP 求前缀函数的 在线性 ,使得我们可以任意枚举第 个字符后,快速获得其 前缀函数值
该 前缀函数值 也就是 状态对应的新的 的值
于是我们就可以利用 来更新 的状态,具体看下面的分析:
自动机模型分析
本题对于不同的 模式串,自动机模型 不同,因此我以 模式串
ababc 举例
先看上面的转移

该转移表示 而发生的转移,也就是枚举的字符 刚好等于 模式串 的第 个字符
于是就会使 最大长度 增加 ,即
然后我再具体分析一下这个状态机里的 状态(其他状态可以类比)
我们枚举下一个添加的字符(即枚举 的字符 )时:

根据如下转移函数:

对应于本样例就是:
- 是 ,则最大长度增加一,对应到
- 不是 ,则根据自动机跳到位置 ,如果 则最大长度增加一,对应到
- 不是 ,则根据自动机跳到位置 ,如果 则最大长度为0,对应到
闫氏DP分析法
状态表示—集合: 考虑构造一个 长度 为 的密码,且后缀与模式串匹配的 最大长度 为 的方案
状态表示—属性: 方案的总数
状态计算:
Code
时间复杂度:
初始状态:
目标状态:
和状态压缩结合的状态机
5.3 状态压缩DP
例题2 最短Hamilton路径
给定一张 个点的带权无向图,点从 标号,求起点 到终点 的最短 Hamilton 路径。
Hamilton 路径的定义是从 到 不重不漏地经过每个点恰好一次。
输入格式
第一行输入整数 。
接下来 行每行 个整数,其中第 行第 个整数表示点 到 的距离(记为 )。
对于任意的 ,数据保证 并且 。
输出格式
输出一个整数,表示最短 Hamilton 路径的长度。
数据范围
输入样例:
输出样例:
分析
1.本题思路
假设:一共有七个点,用0,1,2,3,4,5,6来表示,那么先假设终点就是5,在这里我们再假设还没有走到5这个点,且走到的终点是4,那么有以下六种情况:
first: 0–>1–>2–>3–>4 距离:21
second: 0–>1–>3–>2–>4 距离:23
third: 0–>2–>1–>3–>4 距离:17
fourth: 0–>2–>3–>1–>4 距离:20
fifth: 0–>3–>1–>2–>4 距离:15
sixth: 0–>3–>2–>1–>4 距离:18
如果此时你是一个商人你会走怎样的路径?显而易见,会走第五种情况对吧?因为每段路程的终点都是4,且每种方案的可供选择的点是0~4,而商人寻求的是走到5这个点的最短距离,而4到5的走法只有一种,所以我们选择第五种方案,可寻找到走到5这个点儿之前,且终点是4的方案的最短距离,此时0~5的最短距离为(15+4走到5的距离).(假设4–>5=8)
同理:假设还没有走到5这个点儿,且走到的终点是3,那么有一下六种情况:
first: 0–>1–>2–>4–>3 距离:27
second: 0–>1–>4–>2–>3 距离:22
third: 0–>2–>1–>4–>3 距离:19
fourth: 0–>2–>4–>1–>3 距离:24
fifth: 0–>4–>1–>2–>3 距离:26
sixth: 0–>4–>2–>1–>3 距离:17
此时我们可以果断的做出决定:走第六种方案!!!,而此时0~5的最短距离为(17+3走到5的距离)(假设3–>5=5)
在以上两大类情况之后我们可以得出当走到5时:
1.以4为终点的情况的最短距离是:15+8=23;
2.以3为终点的情况的最短距离是:17+5=22;
经过深思熟虑之后,商人决定走以3为终点的最短距离,此时更新最短距离为:22。
当然以此类推还会有以1为终点和以2为终点的情况,此时我们可以进行以上操作不断更新到5这个点的最短距离,最终可以得到走到5这个点儿的最短距离,然后再返回最初的假设,再依次假设1,2,3,4是终点,最后再不断更新,最终可以得出我们想要的答案
2.DP分析:
用二进制来表示要走的所以情况的路径,这里用i来代替
例如走0,1,2,4这三个点,则表示为:10111;
走0,2,3这三个点:1101;
状态表示:f[i][j];
集合:所有从0走到j,走过的所有点的情况是i的所有路径
属性:MIN
状态计算:如1中分析一致,0–>·····–>k–>j中k的所有情况

代码
例题3 小国王(线性状压DP+滚动数组优化+目标状态优化)
题目描述
在 的棋盘上放 个国王
国王可攻击相邻的 个格子,求使它们 无法互相攻击 的 方案总数
八相邻:通常意义上的八相邻指的是当前元素的上、下、左、右、左上、右上、左下、右下八个方向
分析
这种 棋盘放置类 问题,在没有事先知道一些特定 性质 的情况下来做,都会想到 爆搜
本题的数据规模,也是向着 爆搜 去设置的
如果我们直接 爆搜,则 时间复杂度 为 是会超时的,因此会想到用 记忆化搜索 来进行优化
考虑一下如何进行 动态规划
由于在第 层放置国王的行为,受到 层和 层以及 层的状态影响
那么我们就可以规定从上往下枚举的顺序,这样考虑第 层状态时,只需考虑 层的状态即可
于是乎我们可以考虑把层数 作为动态规划的 阶段 进行 线性DP
而第 阶段需要记录的就是前 层放置了的国王数量 ,以及在第 层的 棋盘状态
这里先分析一下,哪些 棋盘状态 是合法的,哪些 棋盘状态的转移 是合法的
合法的棋盘状态

如上图所示,绿色方块为摆放国王的位置,红色方块为王的 攻击范围
只要任意王之间只要 不相邻,就是 合法的状态
合法的棋盘转移状态

如上图所示,绿色方块为摆放国王的位置,红色方块为王的 攻击范围
只要任意王的 纵坐标不相邻,就是 合法的转移状态
我们可以用 二进制 来表示当前的状态
怎么用二进制表示状态?
若
(state >> i) == 1 则表示在当前状态中,第 个位置放置了国王(下标从 开始)再举一个简单的例子,见下图:

用二进制表示该状态,就是
于是,就有如下 闫氏DP分析
闫氏DP分析法
状态表示—集合 考虑前 层的棋盘,前 层放置了 个国王,且第 层状态是 的方案
状态表示—属性 方案的总数 $Sum$
状态计算—
其中是枚举的能够与$k$合法存在于相邻行中的所有状态,表示状态 中的国王数量
初始状态:
目标状态:
这样直接做,时间复杂度是 是会超时的
但是我们可以通过预处理所有的 合法状态,以及合法的 相邻转移状态,以及 合法状态 摆放的国王数量
因为虽然状态很多,但是 合法状态 并不多, 合法的转移状态 更不多
Code
滚动数组优化思路
由于第 阶段状态只会用到第 阶段的状态,因此我们可以采用滚动数组来优化空间
只贴上优化的部分
y总的目标状态优化思路
我们可以很轻易的观察到,如果当前层的一个棋子都不摆,即
那么所有 合法的状态 都是该状态的 合法转移状态
翻译成白话就是:只要这层不摆东西,则上一层 只要合法,那一定可以转移到这一层的这个状态
因此我们可以增加一个虚拟的第 n+1 行,状态为 0(不放任何棋子),即把目标状态设为
该状态表示 考虑前 层的棋盘,前 层放置了 个国王,且第 层什么也没放的方案
根据我们之前提到的 状态转移 可知,该状态会把所有到第 层的 合法状态 都转移到自己身上
这样最后我们就 不需要额外枚举所有的目标状态 了
完整代码如下:
变3-1 玉米田(线性状压DP+滚动数组优化+目标状态优化)
题目描述
给定一个 的 矩阵, 的位置不能放置棋子, 的位置能放置棋子
若在坐标 放置了棋子,则 四相邻 的位置上 不能 放置棋子
求摆放的 方案数,答案对 取模
分析
本题基本类似于上一题 AcWing 1064. 小国王【线性DP+状压DP+滚动数组优化+细节优化】在本篇中没提及的内容,基本都在上一篇中详细分析了 【很详细!】
本题需要额外处理的,是给定的 矩阵里存在着的不能放置棋子的位置
我们用二进制存储的状态 中,使用 表示在该位置放置棋子, 表示在该位置没有棋子
因此我们可以把矩阵每一层的状态也用二进制来压缩存储,且用 表示该位置能放棋子, 表示不能放
这样,只需把枚举的状态 与这一层的状态 做
& 运算- 结果为0,表示 放置棋子 的位置没有与 不能放置棋子 的位置重叠,则该状态 合法
- 结果为1,表示 放置棋子 的位置与 不能放置棋子 的位置发生重叠,则该状态 不合法
具体如下图所示:

闫氏DP分析法
状态表示—集合 考虑前 层,且第 层状态是 的方案
状态表示—属性 方案的总数
状态计算—
其中是枚举的能够与合法存在于相邻行中的所有状态
Code
这里用了 目标状态优化
有不理解的可以看 AcWing 1064. 小国王【线性DP+状压DP+滚动数组优化+细节优化】 里的内容
滚动数组优化
同上一题思路一样,也可以用滚动数组优化
变3-2 炮兵阵地(线性状压DP+常规优化+转移优化)
题目描述
给定一个 矩阵,矩阵中标有 字样的位置 不能 放置棋子,标有 字样的位置 可以 放置棋子
棋子的 攻击方向 为 上、下、左、右 四个方向, 攻击距离 为 2个格子
现在在该棋盘上放置棋子,使得棋子之前 不能被互相攻击 到,能够放置棋子的 最大个数
分析
关于如何对状态进行 二进制压缩,在 玉米田【线性状压DP】 有详细提及关于如何不同层不同合法状态判断,在 小国王【线性状压DP】 有详细提及以上两点在本篇博客中不会再重复提及
观察到数据范围
故我们可以把 行 作为动态规划的阶段,然后对于第 i 行的摆放状态进行 状态压缩
这就变成了一道标准的 线性状压DP
但是本题不同于 小国王 和 玉米田,这两题中棋子的 攻击距离 只有1
因此在这两题里,我们只需压缩存储 当前层的状态 ,然后枚举 合法的上个阶段 的状态进行 转移 即可
但是本题的棋子攻击范围是 ,我们只压缩当前层一层状态后进行转移,是不能保证该次转移是 合法的
即不能排除第 层摆放的棋子可以攻击到第 层棋子的 不合法 情况
而解决该问题的手段就是:压缩存储两层的信息,然后枚举合法的第 层状态进行转移即可
闫氏DP分析法
状态表示—集合 考虑前 层,且第 层状态是 ,第 层状态是 的方案
状态表示—属性 该方案能够放置棋子的最大个数
状态计算—
其中是枚举的能够与 和 合法存在于三行中的所有状态
初始状态:
目标状态:
对于本题来说,直接开所有的状态空间,空间复杂度是 是会爆内存的
因此我们必须使用 滚动数组 进行优化
然后也可以预处理出来 相邻行之间的合法转移,也能避免掉一些不必要的不合法状态的枚举
Code
目标状态优化
思路同前两题一样,这里不做额外阐述
例4 愤怒的小鸟(状压DP+重复覆盖)
题目描述
给定 个 pig 的坐标,其中第 个 pig 的坐标为
弹弓的坐标是 ,弹弓一次可以发射一条轨迹是抛物线的子弹,并且可以消灭该条抛物线上所有的 pig
求解一个方案,使得弹弓发射的所有子弹可以消灭所有的 pig 且弹弓 发射次数最少
本题的参数 是要求大数据搜索优化的,也就是用 Dancing Links 做的 重复覆盖问题但这不在本篇题解的讨论范围之内(以后有机会可以更)
分析
首先分析一下我们用弹弓发射的子弹的轨迹有哪些特点:
- 一条经过原点的抛物线
- 抛物线的开口朝下
这样抛物线方程就可以设为:
方程中有两个参数 和 ,因此我们可以用具体两个点的坐标来唯一的确定一条 抛物线
参数的计算公式如下:

于是我们就可以预处理出最多 条抛物线,然后用这些抛物线对 点集 进行覆盖即可
用已知两点预处理出来的抛物线一定要满足合法(即)
然后对于两点构成的抛物线,我们还要处理出他穿过的其他的点
这样预处理的时间复杂度就是
到此处位置,我们就把问题转化为, 重复覆盖问题 (大家可以用搜索优化 Dancing Links 处理该问题 )
本题解采用 状态压缩DP 来完成
闫氏DP分析法
状态表示—集合:当前点集覆盖状态为 的方案(是采用二进制压缩存储的点集覆盖状态)
状态表示—属性:方案用的抛物线数量最少
状态表示—计算:
其中 是状态 枚举到新抛物线,并进行覆盖以后生成的新状态
初始状态 :
目标状态 :
Code
时间复杂度:
1. 预处理
2. 状压DP
5.4 计数类DP
例题 整数划分
一个正整数 可以表示成若干个正整数之和,形如:,其中 。
我们将这样的一种表示称为正整数 的一种划分。
现在给定一个正整数 ,请你求出 共有多少种不同的划分方法。
输入格式
共一行,包含一个整数 。
输出格式
共一行,包含一个整数,表示总划分数量。
由于答案可能很大,输出结果请对 取模。
数据范围
输入样例:
输出样例:
思路:
把 1, 2, 3, ... n 分别看做 n 个物体的体积,这 n 个物体均无限/次数限制,问恰好能装满总体积为 n 的背包的 总方案数(完全背包问题变形)
初值问题:
求最大值时,当都不选时,价值显然是 0
而求方案数时,当都不选时,方案数是 1(即前 i 个物品都不选的情况也是一种方案),所以需要初始化为 1
即:
for (int i = 0; i <= n; i++) f[i][0] = 1;等价变形:
f[0] = 1状态计算:
f[i][j] 表示前 i 个整数(1, 2, ..., i)恰好拼成 j 的方案数求方案数:把集合选0个i, 1个1, 2个1, ...全部加起来
f[i][j] = f[i - 1][j] + f[i - 1][j - i] + f[i - 1][j - 2 * i] + ...去掉,变形为:
f[i][j] = f[i - 1][j] + f[i][j - i] # (这一步类似完全背包的推导)朴素做法:
等价变形:
5.5 数位统计DP
例题 计数问题
给定两个整数 和 ,求 和 之间的所有数字中 的出现次数。
例如,,则 和 之间共有 个数如下:
1024 1025 1026 1027 1028 1029 1030 1031 1032其中
0 出现 次,1 出现 次,2 出现 次,3 出现 次等等…输入格式
输入包含多组测试数据。
每组测试数据占一行,包含两个整数 和 。
当读入一行为
0 0 时,表示输入终止,且该行不作处理。输出格式
每组数据输出一个结果,每个结果占一行。
每个结果包含十个用空格隔开的数字,第一个数字表示
0 出现的次数,第二个数字表示 1 出现的次数,以此类推。数据范围
输入样例:
输出样例:
分析
分类讨论做法
比如说我要找[1,abcdefg]中的数中1出现的个数
就得先求1在每一个位置上出现的次数
比如我要找第4位上出现的1的数有几个
就是要找满足 1 <= xxx1yyy <= abcdefg
1.xxx∈[000,abc-1] , yyy∈[000,999] , ans += abc*1000
//如果前三位没填满,则后三位就可以随便填
2.xxx==abc , yyy∈?
if(d<1) yyy不存在 , ans += 0
if(d==1) yyy∈[000,efg] , ans += efg+1
if(d>1) yyy∈[000,999] , ans += 1000
//如果前三位填满了,后三位怎么填取决于当前这一位
然后每一位上都是这么讨论的,最后累加起来就是总共出现的次数
这样就是求出来了[1,n]的
然后如果我想求[l,r]的用一下前缀和就搞定了
但是有一些特殊情况
- x在第1位上出现的次数(不用考虑前半段):
bcdefg∈[00000,bcdefg] , ans += bcdefg+1
- x在最后一位上出现的次数(不用考虑后半段):
//不知道为什么y老大没有说这种情况,但是我觉得应该讨论一下的吧 如果g<x,那么不存在这样的数,ans += 0 如果g==x,那么有一个这样的数,ans += 1 如果g>x,yyyyyy∈[000000,abcdef] , ans += abcdef+1
- 如果我们枚举的数是0的话 : //恶魔讨论 0不能在第一位 而且枚举到的这一位前面不能全是0,即 xxx∈[001,abc-1] 那当时直播的时候秦淮岸大佬说应该是从100到abc-1 因为我们说“不能有前导零”
正确理解“前导零”:
这个题里面,如果我要找一个数x在[1,n]中出现了几次
这里我们假设n是个五位数
要想找x在[1,n]中的一位数、两位数、三位数、四位数中出现的次数,
都是通过前面有那么一个没有实际作用的前导零来完成的
那么如果我们执着于“不能有前导零”,
则我们找出来的数只可能是个五位数,就不能正确完成了
说到这里,大家应该明白为什么从100到abc-1不对了吧
从100开始的话我们凑出来的数全是五位数啊
所以该有0的时候前面是可以有0的
我们在这里说的没有前导零,
指的是我们枚举x=0在哪一位上时,不考虑0做当前这个数的第一位
举个例子:
如果我们要找[1,1234]中0出现次数
那么首先0不可以做第一位
然后当0做第二位的时候,第一位不能是0
(因为第一位如果是0的话这个数就是00xx,还是不含有0的)
然后0做第三位的时候前两位不能同时为0
(因为前两位都是0那这个数就是000x,还是不含有0)
最后0做第4位的时候前三位不能是000
(因为这样最后这个数就是0,而我们要从[1,1234]中找)
这跟我们故意弄上去几个0当空气是有区别的,在询问0的个数的时候才有这种特殊情况
代码
简化版
DP做法(待完善)
5.6 区间DP
例1 石子合并
题意:合并 N 堆石子,每次只能合并相邻的两堆石子,求最小代价
解题思路:
关键点:最后一次合并一定是左边连续的一部分和右边连续的一部分进行合并
状态表示: 表示将 i 到 j 这一段石子合并成一堆的方案的集合,属性 Min
状态计算(s是前缀和数组):
(1) 时,
(2) 时, (合并一堆石子代价为 0)
问题答案:
区间 DP 常用模版
所有的区间dp问题枚举时,第一维通常是枚举区间长度,并且一般 len = 1 时用来初始化,枚举从 len = 2 开始;第二维枚举起点 i (右端点 j 自动获得,j = i + len - 1)
模板代码如下:
本题C++代码
补充1:从下往上倒着枚举状态
除了按长度枚举,也可以倒着枚举,因为只要保证每种状态都被提前计算即可
从下往上倒着枚举,可以保证你算dp[i][j]时,dp[i][k]和dp[k + 1][j]一定是被提前计算好的,而从上往下枚举则无法保证这一点。所以我们采用倒着枚举
图解:

补充2:记忆化搜索
如果学过后面的记忆化搜索,那也可以用下面的代码。虽然时间会比递推稍微慢一丢丢,但是呢他的思路比较好写
变1-1 环形石子合并
题目描述
给定 个石子的 分数 ,且石子是 环形放置 的(在给定的顺序上,同时满足 和 也是相邻的)
规定每次只能合并 相邻 的两堆石子,形成一个新的石子堆,合并的 费用 是两个石子堆的 分数之和
求解两个方案:
- 方案一:把这 堆石子合并为一堆,且满足费用最大的方案
- 方案二:把这 堆石子合并为一堆,且满足费用最小的方案
分析
拓展:
如果每轮合并的石子 可以是任意 的 两堆 石子,那么用到的就是经典的 Huffman Tree 的二叉堆模型如果每轮合并的石子 可以是任意 的 堆 石子,那么用到的就是经典的 Huffman Tree 的 叉堆模型以上两种题型可以参考:
- 二叉堆:AcWing 148. 合并果子
回归本题,本题要求每轮合并的石子 必须是相邻的 两堆石子,因此不能采用 Huffman Tree 的模型
这类限制只能合并相邻两堆石子的模型,用到的是经典的 区间DP 模型
考虑如何设定 动态规划 的阶段,既可以表示出初始每个石子的费用,也可以表示出合并后整个堆的费用
不妨把当前合并的石子堆的大小作为DP的阶段
这样 表示初值,即每个堆只有一个石子; 表示终值,即一个堆中有所有的石子
这种阶段设置方法保证了我们每次合并两个区间时,他们的所有子区间的合并费用都已经被计算出来了
阶段设定好后,考虑如何记录当前的状态,无外乎就两个参数:
- 石子堆的左端点
- 石子堆的右端点
闫氏DP分析法
状态表示—集合 当前合并的石子堆的大小为 ,且石子堆的左端点是 ,右端点是 的方案
状态表示—属性 方案的费用最大/最小(本题两者都要求)
状态计算—
初始状态:
目标状态:
在区间DP中,我们也常常省去 这一维的空间因为 ,也就保证了在已知 和 的情况下,不会出现状态定义重复的情况根据线性代数中方程的解的基本概念,我们就可以删掉 这一维不存在的约束但为了方便读者理解,以及介绍区间DP的阶段是如何划分的,我还是写了出来
以上就是所有有关石子合并的区间DP分析
在考虑一下本题的 环形相邻 情况如何解决,方案有如下两种:
- 我们可以枚举环中分开的位置,将环还原成链,这样就需要枚举 次,时间复杂度为
- 我们可以把链延长两倍,变成 个堆,其中 和 是相同的两个堆,然后直接套 区间DP 模板,但对于 阶段 只枚举到 ,根据 状态的定义,最终可以得到所求的方案,时间复杂度为
一般常用的都是第二种方法,我也只会演示第二种方法的写法,对第一种有兴趣的读者可以自行尝试
Code
时间复杂度:
变1-2 能量项链
题目描述
给定 个能量石,每个能量石有一个二元属性
其中 表示第 个能量石和第 个能量石融合产生的 能量 的其中一个参数
当然 表示第 个能量石和第 个能量石融合产生的 能量 的其中一个参数
魔法石是顺序且环形摆放的,每次可以融合相邻两个魔法石
融合两个能量石 所产生的能量为(题目保证,相邻能量石的参数一致,首尾一致)
融合后左侧魔法石的第一个参数和右侧魔法石的第二个参数合并成为一颗新的 魔法石
具体如下图所示:

求最终把所有石头融合成一个石头时,产生的最大能量值
分析
本题可以把区间长度作为搜索的阶段来进行记忆话搜索,因此我们也可以采用 区间DP 的方式来处理
这题和 环形石子合并 十分相似,但又不尽相同
在 环形石子 中,每个石头只有单一的参数,而本题有两个参数,也就意味着我们需要在细节上做出改变
经过观察我们发现,合并两个石头 的操作就像是矩阵乘法一样,合并完后就变成了
因此我们可以离散的来存储每个参数,具体如下所示:

这样 状态表示 就更新为:当前合并的石子堆的左端石头的左参数是 ,右端石头的右参数是 的方案
这样对应的 初始状态 本来应该是一个有着 二元属性 的石头,现在就变成了长度为 的区间
这样合并区间后,需要记录的新石头的参数也刚好是 区间的两端 对应的参数,如下图所示:

而且这里我们的转移方程也要修改为
以往的 区间DP 我们是把区间 拆分为
因为 同一个石子 只会被合并到 一个石子堆 里
但本题合并魔法石时,分割点 要被分到 左侧石子堆的右端点 和 右侧石子堆的左端点 中
因此,参数 要作为两个区间的共同端点来使用,即 和
此外我们原来只需要合并 个石头,这样转换后就要合并 个石头了
具体分析如下所示:
闫氏DP分析法
状态表示—集合 当前合并的石子堆的左端石头的左参数是 ,右端石头的右参数是 的方案
状态表示—属性 方案的费用最大
状态计算—
初始状态:
目标状态:
本题关于如何解决 环的问题 不做额外阐述,有需要的可以参考之前写的这篇 环形石子合并
Code
时间复杂度:
例2 加分二叉树(记忆化搜索)
题目描述
给定一个含有 个结点的二叉树的 中序遍历 序列中每个节点的 权值
定义一棵 子树 的 分数 为
额外规定 空树 的 分数 为
求一种满足该 中序遍历 的建树方案,使得整棵树的 分数 最大
分析
因为本题是一道与树相关的区间DP,因此本题解采用 记忆化搜索 的思想来分析,抛开常规 动态规划 思路
首先读者需要知道一个二叉树的小常识:
二叉树节点 向下投影,映射成的数组序列就是 中序遍历序列,入下图所示

这也是诱使我们本题采用 区间DP 的一大原因之一(但这篇题解采用 记忆化搜索 思想分析)
借助上图直观的表象,我们发现可以任意的选择 中序遍历 的某一段区间便可生成多棵子树(枚举根节点)
于是我们就会想到 分治 的思想,枚举好根节点后,递归的处理左右区间生成的 最大分数子树
回溯后,利用计算好的子树的分数 相乘,再加上根结点的 权值,就可以得出该方案的 最大分数
而直接递归 搜索 的时间复杂度是 (每次枚举当前区间的根结点,然后递归处理左右区间)
(每次向下层递归时,会确定一个根节点,因此每次少 )
因为在这个递归中,有相当大的计算是去处理的 相同的区间
因此我们不妨采用 记忆化搜索 的形式优化掉这些 重复的搜索
考虑用数组 记录以 为左端点, 为右端点的区间,生成的树的最大分数
这样就会 剪枝 掉相当大的冗余 搜索分支
关于输出方案这里就不做过多阐述了,想了解的可以看一下下面两篇博客
Code(记忆化搜索)
时间复杂度:
秉持着详细全面的态度,我这里也贴出 闫氏DP分析法 ,但是关于 DP 就不做分析啦,都是裸的 区间DP
闫氏DP分析法
状态表示—集合 当前以 为左端点, 为右端点的区间作为 中序遍历,生成树的方案
状态表示—属性 方案的分数最大
状态计算—
初始状态: 题设规定只有一个节点的子树分数就是其权值
目标状态:
Code(区间DP)
时间复杂度:
变2-1 棋盘分割(记忆化搜索+二维区间DP)
题目描述
给定一个 的棋盘,棋盘的每个小方格都有一个权值
每次我们可以对棋盘进行一次横或竖切,将棋盘分成两块矩形的子棋盘
分割完一次后,我们可以选择两个子棋盘中的一个再继续递归操作

可以发现题目中给的这个图片,右边这个并不是递归下去做的:他对第一次分割的两个矩形又分别进行了分割(题目要求我们只能保留一个继续分割)
现需要把棋盘按照上述分割方案,分成 块( 次划分操作)
求一个划分方案,使得各子棋盘的 总分的均方差最小
分析
本题如果用 动态规划 来分析,状态表示要至少开5维,记录分割操作的阶段,以及当前的棋盘状态
我这里直接贴出 闫氏DP分析法
闫氏DP分析法
状态表示 —集合:
当前已经对棋盘进行了 次划分,且 次划分后选择的棋盘是 左上角为 ,右下角为
状态表示 —属性:
划分出来的 个子矩阵的 最小(下面给出了这样定义的原因)
状态计算 :

这里关于 集合的属性 需要一点变化,直接使用 标准差 作为属性,在进行状态转移的时候不好计算
我们把标准差做出如下变化:
由于 与 的单调性相同,要求 最小,就相当于 最小
而转化为 后,对于子状态,我们就可以直接进行 加法运算 来转移了(原来套着根号很难处理)
记忆化搜索
本题有这明显的 分治 引导,即给定一个初始的棋盘,然后我们选择进行分割
分割完后,选择保留一个棋盘,然后对另一个棋盘继续进行分割
直到分割次数达到上限
考虑一下直接递归操作的时间复杂度:
这是一个排列数,计算方法很简单,每轮会使用一条分割线,且每条分割线在一个方案里仅能使用一次
不难发现,递归操作会有很多冗余的重复计算,于是我们可以采用 记忆化搜索 进行优化
表示对棋盘进行了 次划分,且 次划分后选择的棋盘是 左上角为 ,右下角为
这一步分析就有点雷同于上面的 DP 了
关于如何枚举矩阵的分割(会的可以直接跳过)
由于我们这里记录矩阵的状态是通过他的 对角顶点 记录的,因此分割是我们也可以通过枚举对角顶点完成分割
如下图所示:
竖着切:

横着切:

Code (记忆化搜索)
由于会频繁地计算某个矩阵的方差,因此我们需要先预处理出前缀和,保证计算方差的时间复杂度为
时间复杂度 :
我这里 阶段定义 的含义和 y总 是反过来的,读者注意区分两者的不同
例3 凸多边形的划分(区间DP+高精度)
题目描述
给定 个点的 凸多边形 中每个 顶点 的 权值
我们可以把该 凸多边形 划分成 n - 2 个 三角形(三角形不能相交,这样就能保证是 了)
每次划分三角形的 费用 为 三个顶点权值 的 乘积
求一个划分 方案, 方案的 费用总和 最小
本题是一个给定的 凸多边形 求 三角剖分 的最小费用方案
很显然一个 凸多边形的剖分方案 并不唯一:
在 选定 多边形中 两个点 后,找出 三角形 的 第三个点 的方案有 个

然后还要分别 划分 他的 左右两块区域
因此我们就会想到用 记忆化搜索 或者 区间DP 来进行处理
记忆化搜索介绍 可以参考这篇 加分二叉树【记忆化搜索思想】区间DP介绍 可以参考这篇 环形石子合并【区间DP+环形区间问题】
本题解采用 区间DP 的方式进行求解
闫氏DP分析法
状态表示—集合 当前划分到的多边形的左端点是 ,右端点是 的方案
状态表示—属性 方案的费用最小
状态计算—
区间DP 在状态计算的时候一定要 认真 划分好 边界 和 转移,对于不同题目是不一样的
然后本题非常的嚣张,直接用样例的 的点告诉我们答案会爆 和
并且没有 取模 要求,那就只能上 高精度 了
Code
时间复杂度: 区间DP
5.7 树形DP
例1 没有上司的舞会
Ural 大学有 名职员,编号为 。
他们的关系就像一棵以校长为根的树,父节点就是子节点的直接上司。
每个职员有一个快乐指数,用整数 给出,其中 。
现在要召开一场周年庆宴会,不过,没有职员愿意和直接上司一起参会。
在满足这个条件的前提下,主办方希望邀请一部分职员参会,使得所有参会职员的快乐指数总和最大,求这个最大值。
输入格式
第一行一个整数 。
接下来 行,第 行表示 号职员的快乐指数 。
接下来 行,每行输入一对整数 ,表示 是 的直接上司。(注意一下,后一个数是前一个数的父节点,不要搞反)。
输出格式
输出最大的快乐指数。
数据范围
输入样例:
输出样例:
分析

参考代码
例2 树的直径
题目描述
给定一个含有 个节点的 树,以及树中每条边的 权值
现需要在树中找出一条 路径,使得该 路径 上所有 边 的 权值之和 最大
分析
这是一道比较经典的 树形DP 题目,我们来一步步来剖析这个问题
我们知道:树上 任意两点 的路径是 唯一 确定的,因此我们可以暴力枚举 起点 和 终点 找出最长路径
如果这样做的话,我们来思考一下时间复杂度:
- 枚举 起点 和 终点 —
- 找出两点之间的路径长度 —
这里找出两点之间的路径有很多种做法,预处理 st 表然后倍增求LCA、树链剖分、Link-Cut Tree
但是光是枚举 起点 和 终点,时间复杂度 就直接拉满了,显然这种做法不可取
既然这 条路径不能 一一枚举,那么有什么方式可以把他们 分类枚举 呢?
我们考虑换一种 枚举方式:枚举路径的 起点和终点 枚举路径的 中间节点
我们先讨论一下,对于给定拓扑结构的树里的任意节点,经过他的路径有哪些:

观察我标出的 红色节点,那么经过他的路径有:
- 以其 子树中的某个节点 作为 起点,以他作为 终点 的 粉色路径
- 以其 子树中的某个节点 作为 起点,以 子树中的某个节点 作为 终点 的 蓝色路径
- 以其 子树中的某个节点 作为 起点,以 非其子树的节点 作为 终点 的 橙色路径
对于第 1 种情况,我们可以直接递归处理其子树,找出到当前子树根节点最长的路径长度即可
对于第 2 种情况,我们在处理第 1 种情况时,顺便找出 1 类路径的 次长路径
把 最长 和 次长 拼在一起,就是我们要的第 2 种情况
而对于第 3 种情况,我们可以把它归类为其 祖先节点 的第 种情况,让其 祖先节点 去处理即可
把上述两类路径的分析,用 闫氏DP分析法 写出,就是如下形式:
闫氏DP分析法
状态表示—集合: 以节点 为根的子树中,从子树某个节点到 的最长路为 ,次长路为
状态表示—属性: 路径长度的最大值
状态计算—
Code
时间复杂度:
变2-1 树的中心(换根DP)
题目描述
给定一棵树,树中包含 个 节点 和 条 无向边,每条边都有一个权值
请在树中找到一个点,使得该点到树中其他结点的 最远距离 最近
分析
这个问题是 树形DP 中的一类 经典模型,常被称作 换根DP
同样,先来想一下如何暴力求解该问题:先 枚举 目标节点,然后求解该节点到其他节点的 最远距离
时间复杂度为 ,对于本题的 数据规模,十分极限,经测试只能过
考虑如何优化求解该问题的方法
思考一下:在确定树的 拓扑结构 后单独求一个节点的 最远距离 时,会在该树上去比较哪些路径呢?
- 从当前节点往下,直到子树中某个节点的最长路径
- 从当前节点往上走到其父节点,再从其父节点出发且不回到该节点的最长路径
此处就要引入 换根DP 的思想了
换根DP 一般分为三个步骤:
- 指定任意一个根节点
- 一次dfs遍历,统计出当前子树内的节点对当前节点的贡献
- 一次dfs遍历,统计出当前节点的父节点对当前节点的贡献,然后合并统计答案
那么我们就要先 dfs 一遍,预处理出当前子树对于根的最大贡献(距离)和 次大贡献(距离)
处理 次大贡献(距离) 的原因是:
如果 当前节点 是其 父节点子树 的 最大路径 上的点,则 父节点子树 的 最大贡献 不能算作对该节点的贡献
因为我们的路径是 简单路径,不能 走回头路
然后我们再 dfs 一遍,求解出每个节点的父节点对他的贡献(即每个节点往上能到的最远路径
两者比较,取一个 max 即可
时间复杂度 为
闫氏DP分析法
状态表示—集合: 对于树中 号节点,其子节点的贡献为,父节点的贡献为
状态表示—属性: 贡献值最大
状态计算—
Code
时间复杂度:
Code(直接暴力换根)
时间复杂度: 通过数据
例3 数字转换
例4 二叉苹果树
5.8 记忆化搜索
例题 滑雪
给定一个 行 列的矩阵,表示一个矩形网格滑雪场。
矩阵中第 行第 列的点表示滑雪场的第 行第 列区域的高度。
一个人从滑雪场中的某个区域内出发,每次可以向上下左右任意一个方向滑动一个单位距离。
当然,一个人能够滑动到某相邻区域的前提是该区域的高度低于自己目前所在区域的高度。
下面给出一个矩阵作为例子:
在给定矩阵中,一条可行的滑行轨迹为 。
在给定矩阵中,最长的滑行轨迹为 ,沿途共经过 个区域。
现在给定你一个二维矩阵表示滑雪场各区域的高度,请你找出在该滑雪场中能够完成的最长滑雪轨迹,并输出其长度(可经过最大区域数)。
输入格式
第一行包含两个整数 和 。
接下来 行,每行包含 个整数,表示完整的二维矩阵。
输出格式
输出一个整数,表示可完成的最长滑雪长度。
数据范围
输入样例:
输出样例:
分析
那个人可以从矩形网格滑雪场的任意一点出发
可以上下左右移动
每次只能滑一个单位距离
这个人能够滑动到某相邻区域的前提是该区域的高度低于自己目前所在区域的高度
样例解释



参考代码
6 贪心
6.1 区间问题
例题1 区间选点
给定 个闭区间 ,请你在数轴上选择尽量少的点,使得每个区间内至少包含一个选出的点。
输出选择的点的最小数量。
位于区间端点上的点也算作区间内。
输入格式
第一行包含整数 ,表示区间数。
接下来 行,每行包含两个整数 ,表示一个区间的两个端点。
输出格式
输出一个整数,表示所需的点的最小数量。
数据范围
输入样例
输出样例
参考代码
例题2 区间分组
给定 个闭区间 ,请你将这些区间分成若干组,使得每组内部的区间两两之间(包括端点)没有交集,并使得组数尽可能小。
输出最小组数。
输入格式
第一行包含整数 ,表示区间数。
接下来 行,每行包含两个整数 ,表示一个区间的两个端点。
输出格式
输出一个整数,表示最小组数。
数据范围
输入样例
输出样例
参考代码
例题3 区间覆盖
给定 个区间 以及一个区间 ,请你选择尽量少的区间,将指定区间完全覆盖。
输出最少区间数,如果无法完全覆盖则输出 。
输入格式
第一行包含两个整数 和 ,表示给定区间的两个端点。
第二行包含整数 ,表示给定区间数。
接下来 行,每行包含两个整数 ,表示一个区间的两个端点。
输出格式
输出一个整数,表示所需最少区间数。
如果无解,则输出 。
数据范围
输入样例:
输出样例:
参考代码
6.2 哈夫曼树
例题 合并果子(待完善)
6.3 排序不等式
例题 排队打水
有 个人排队到 个水龙头处打水,第 个人装满水桶所需的时间是 ,请问如何安排他们的打水顺序才能使所有人的等待时间之和最小?
输入格式
第一行包含整数 。
第二行包含 个整数,其中第 个整数表示第 个人装满水桶所花费的时间 。
输出格式
输出一个整数,表示最小的等待时间之和。
数据范围
输入样例:
输出样例:
参考代码
6.4 绝对值不等式
例题 货仓选址
在一条数轴上有 家商店,它们的坐标分别为 。
现在需要在数轴上建立一家货仓,每天清晨,从货仓到每家商店都要运送一车商品。
为了提高效率,求把货仓建在何处,可以使得货仓到每家商店的距离之和最小。
输入格式
第一行输入整数 。
第二行 个整数 。
输出格式
输出一个整数,表示距离之和的最小值。
数据范围
输入样例:
输出样例:

6.5 推公式
例题 耍杂技的牛
农民约翰的 头奶牛(编号为 )计划逃跑并加入马戏团,为此它们决定练习表演杂技。
奶牛们不是非常有创意,只提出了一个杂技表演:
叠罗汉,表演时,奶牛们站在彼此的身上,形成一个高高的垂直堆叠。
奶牛们正在试图找到自己在这个堆叠中应该所处的位置顺序。
这 头奶牛中的每一头都有着自己的重量 以及自己的强壮程度 。
一头牛支撑不住的可能性取决于它头上所有牛的总重量(不包括它自己)减去它的身体强壮程度的值,现在称该数值为风险值,风险值越大,这只牛撑不住的可能性越高。
您的任务是确定奶牛的排序,使得所有奶牛的风险值中的最大值尽可能的小。
输入格式
第一行输入整数 ,表示奶牛数量。
接下来 行,每行输入两个整数,表示牛的重量和强壮程度,第 行表示第 头牛的重量 以及它的强壮程度 。
输出格式
输出一个整数,表示最大风险值的最小可能值。
数据范围
输入样例:
输出样例:
参考代码
证明:
贪心得到的答案≥最优解 → 易知
贪心得到的答案≤最优解:

7 时空复杂度分析
- Author:Cormac
- URL:https://blog.cormac.top/article/algorithm-1
- Copyright:All articles in this blog, except for special statements, adopt BY-NC-SA agreement. Please indicate the source!



