如题目>▽<
自用代码模板
1 |
|
基础
算法基础
二分
特征
- 具有单调性的数组
- 求最大/最小值
- 答案离散(具有有限种可能)
- 不直接找到目标值的写法,跳出循环后的num[l-1]要么是目标值,要么是目标值目标值左面的值;num[l]是目标值右面的值
1 | int l=0,r=n+1; |
三分
特征
类似于二分查找,三分搜索法也是比较常用的基于分治思想的高效查找方法。但是和二分不同,二分只适用于单调函数,三分用于单峰函数(求极值)
代码
1 | long double f(double x){// 算出某个点的函数值 |
位运算
- ’&’符号,x&y,会将两个十进制数在二进制下进行与运算(都1为1,其余为0) 然后返回其十进制下的值。例如3(11)&2(10)=2(10)。
- ’|’符号,x|y,会将两个十进制数在二进制下进行或运算(都0为0,其余为1) 然后返回其十进制下的值。例如3(11)|2(10)=3(11)。
- ’^’符号,x^y,会将两个十进制数在二进制下进行异或运算(不同为1,其余 为0)然后返回其十进制下的值。例如3(11)^2(10)=1(01)。
- ’
’符号,x,按位取反。例如~101=010。 - ’<<’符号,左移操作,x<<2,将x在二进制下的每一位向左移动两位,最右边用0填充,x<<2相当于让x乘以4。 ’>>’符号,是右移操作,x>>1相当于给x/2,去掉x二进制下的最右一位
- 判断一个数字x二进制下第i位是不是等于1。(最低第1位)
方法:if(((1<<(i−1))&x)>0) 将1左移i-1位,相当于制造了一个只有第i位 上是1,其他位上都是0的二进制数。然后与x做与运算,如果结果>0, 说明x第i位上是1,反之则是0。 - 将一个数字x二进制下第i位更改成1。
方法:x=x|(1<<(i−1)) 证明方法与1类似。 - 将一个数字x二进制下第i位更改成0。
方法:x=x&~(1<<(i−1)) - 把一个数字二进制下最靠右的第一个1去掉。
方法:x=x&(x−1) - 获取一个32位数字二进制中1的个数
方法:__builtin_popcount(x)
数据结构
队列
见STL队列的使用
栈
单调栈
特征
定义:内部元素满足单调性的栈。
用途:线性时间内处理出数组中每一个 i 左边/右边 第一个 大于/小于 ai 的位置。
1 | // aj < ai 时,ai的结果记录为aj,没有的话记录为0 |
堆
对顶堆
特征
可用于动态维护第k大的值(例如中位数),每次操作为O(logn)
原理
我们需要分别维护两个堆,让小根堆的堆顶作为分界线,如果大于它就加入小根堆,否则就加入大根堆,如果两个堆的个数相差超过1,就将多的那个堆的堆顶弹出来放到另一个堆的堆顶。奇数个数的中位数显然是堆中数量最多的堆的堆顶。偶数个数的中位数则是两个堆堆顶取平均值。
- 加入元素的操作
1 | priority_queue<int> q_big;// 大顶堆 |
树
BT树(普通平衡树)
您需要写一种数据结构(可参考题目标题),来维护一些数,其中需要提供以下操作:
1.插入数值 x
2.删除数值 x (若有多个相同的数,应只删除一个)。
3.查询数值 x 的排名(若有多个相同的数,应输出最小的排名)。
4.查询排名为 x 的数值。
5.求数值 x 的前驱(前驱定义为小于 x 的最大的数)。
6.求数值 x 的后继(后继定义为大于 x 的最小的数)。
1 | int n,opt,x; |
LCA (最近公共祖先)
特征
在一个树上找到两个点最近的公共祖先,使用倍增法创建ST表需要nlogn时间,查询logn时间,一次建表、多次使用,若只有几次查询还不如暴力()
1 | vector<int> g[MAX]; |
线段树
特征
线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。
使用线段树可以快速的查找某一个节点在若干条线段中出现的次数,时间复杂度为O(logN)。而未优化的空间复杂度为2N,实际应用时一般还要开4N的数组以免越界,因此有时需要离散化让空间压缩。
- 普通带懒标记线段树
1 | // 建树前准备 |
- 好用的一键打包式线段树
1 | // 这两个宏不能忘了 |
上面模板只提供了最基础的加法线段树,其他高端的线段树请自定义
区间乘法:需要将区间内的数*x,两个懒标记可解。更新值时要注意要先乘再加这样才不会丢失精度
树状数组
特征
树状数组(Binary Index Tree, BIT),是一种简洁好用的数据结构,最简单的树状数组支持两种操作
- 单点修改:更改数组中某一个元素的值
- 区间查询:查询一个区间内所有元素的和
可以解决大部分基于区间上的更新与求和问题
1 | int tree[MAXN]; |
- 区间更新,单点查询
我们可以引入一个差分数组b
设数组原数组为a,则有$b[i]=a[i]-a[i-1];(a[0]=0;)$,那么$a[i]=b[1]+….+b[i]$,求单点就是差分数组的前n项和,剩下都一样了 - 多维区间查询
要注意块与块之间的重叠
并查集
特征
并查集主要用于解决一些元素分组的问题。它管理一系列不相交的集合,并支持两种操作:
- 合并(Union):把两个不相交的集合合并为一个集合。
- 查询(Find):查询两个元素是否在同一个集合中。
基础代码(路径压缩)
1 | int fa[MAXN]; |
扩展(按秩合并)
优点:可进一步省时间
原因:我们应该把简单的树往复杂的树上合并,而不是相反。因为这样合并后,到根节点距离变长的节点个数比较少。
1 | inline void init(int n) |
STL引入
引用 Reference
- 是一个别名,相当于给其取个小名(蟑螂和小强都指代一个东东),可以增强代码可读性
1 | int zhanglang[3]={1,2,3}; |
- 函数传参:相当于传入地址
1 | void _swap(int &x,int &y){ |
类 class struct
- 一个构造器
1 | // struct是访问权限为public的class |
1 | // struct是访问权限为public的class |
1 | struct node{ |
- new一个对象
1 | _class a=_class();//先new出对象,再拷贝到新的对象中 |
重载运算符
- 格式:[返回值][operator][运算符][参数列表][const][body]
1 | struct node{ |
匿名函数 lambda expressions
- 可以在函数中定义函数的骚操作(常用于sort中的cmp函数定义),当然也可以将其实名化
1 | auto func=[](int a,int b){ |
pair<T1,T2>
1 | //tip1 |
迭代器 Iterator
Container->Iterator->Algorithm(容器通过迭代器调用算法)
类似于指针,通过迭代器访问容器的值
- 分类
- 正向迭代器
1 | container::iterator it; |
- 反向迭代器
1 | container::reverse_iterator it; |
使用
- 使用auto关键帧循环
1 | map<int, int> dsk; |
- 正常遍历
1 | using Iter=vector<int>::iterator;//迭代器重命名,方便使用 |
STL常用算法
排序 sort&stable_sort
- sort(begin,end,cmp);
- begin:头迭代器
- end:尾迭代器
- cmp:排序方式
1 | int arr[]={1,1,4,5,1,4,114514,-114514,19260817}; |
二分查找 lower_bound&upper_bound
- 在有序容器中,使用二分的方式查找元素
- lower_bound:查找到第一个大于等于目标值的元素,返回迭代器
- upper_bound:查找到第一个大于目标值的元素,返回迭代器
1 | int arr[]={1,1,4,5,1,4,114514,-114514,19260817}; |
两级反转 reverse
- 将能够双向查询的迭代器翻转过来
1 | int arr[]={1,2,3,4,5,6,7,8}; |
填充 fill&fill_n
- 快速填充一段区间内的值(相当于memset)
1 | int arr[]={1,2,3,4,5,6,7,8}; |
最大公约数 gcd
1 | cout<<__gcd(2,4); |
排列
生成全排列(n!复杂度,少用)
1 | int orl[]={2,1,3}; |
STL容器
向量 vector// 查找 find()–>查不到返回_set.end()
1 | cout<<*_set.find(100)<<endl; |
- 传入值
1 | arr.push_back(1); |
- 大小&容积
1 | for (int i = 0; i < 10;i++){ |
- 提取最值&求和
1 | cout<< *max_element(all(arr))<<endl;//求最大值 |
- 重定义大小&清空
1 | arr.resize(100); |
- 推荐宏定义
1 |
- 离散化(得到大数字的相对大小,有些大数要存到的下标太大存不下)
1 | sort(all(arr)); |
- 邻接表(超好用)
1 | struct Edge |
栈 队列 双端队列 stack queue deque
- 栈(建议手写,容易被卡)
1 | stack<int> stk;//建立 |
集合 Set
- 基于红黑树(RB-tree)实现,特点是元素具有互异性
- 优点:有序容器,能实现高效插入、查找、删除(Ologn)
- 使用
multiset可以允许元素重复
1 | set<int> _set;//默认升序 |
映射 Map
- 同样基于红黑树(RB-tree)的有序容器(按照key排序),map可建立[数据结构]到[数据结构]的映射,可以说是very good
- 通常使用场景在于离散化、建立映射关系等等。它同样支持高效插入,查询,修改(可修改value,不能修改key),以及删除
- 每个key(键)只能出现一次,不允许重复。
1 | //前两个是类型,后面的是比较方式 |
无序哈希表 unordered_x
优点:查询速度很快
缺点:哈希表建立比较耗费时间
适用:查找较多时
压位 bitset
1 | bitset, 圧位 |
ST表
特征
- ST表(Sparse Table,稀疏表)是一种简单的数据结构,主要用来解决RMQ(Range Maximum/Minimum Query,区间最大/最小值查询)问题。它主要应用倍增的思想,可以实现 [公式] 预处理、 [公式] 查询。
- RMQ 表示区间最大(最小)值。
- 可重复贡献问题是指对于运算,满足f(a,a)=a,则对应的区间询问就是一个可重复贡献问题。例如,最大值max(a, a) = a ,最大公因数gcd(a, a) = a,所以 RMQ 和区间 GCD 就是一个可重复贡献问题。但像区间和就不具有这个性质。
1 | int Max[P][21]; |
数学
数论
埃氏筛
1 | bool isnp[MAXN]; // is not prime: 不是素数 |
欧拉筛
代码如下(C++11风格):
1 | bool isnp[MAXN]; |
(卡c++11版本)
1 | bool isnp[P]; |
(超短风格)
1 | void get_prime(int n){//n是筛的范围,超1e8就不好使了 |
注意判断越界那里最好使用乘法而不是除法,一般不会溢出,计算机算除法比乘法要慢得多。
欧拉函数
用于求1~n中与n互质数的个数
1 | int primes[N], cnt; // primes[]存储所有素数 |
gcd/lcm
1 | int gcd(int a,int b) |
约数
1 | // 试除法求所有约数 |
调和级数
形如:ans = 1 + 1/2 + 1/3 + … +1/(n-1) = ln(n) + y
其中 y ≈ 0.5772156649(欧拉常数)
快速幂
1 | //非递归快速幂 |
逆元
前置知识
假设未知数分别为m、n、p,需要求C(m,n)%p。(0<=m<=n)
求组合数的数学公式:C(m,n) = n! / (m! *(n - m)!)
数学的世界很奇妙,我们知道,模运算的加法,减法,乘法和四则运算类似,即:模运算与基本四则运算有些相似。
(a + b) % p = (a % p + b % p) % p
(a - b) % p = (a % p - b % p) % p
(a b) % p = (a % p b % p) % p
但对于除法却不成立,即(a / b) % p ≠ (a % p / b % p) % p 。于是我们需要引入逆元的概念
逆元特征
即建立式子 ax%p=1 , x的最小正整数解就是 a 的逆元。
求取 (a / b % p) 等同于 求取 (a∗(b的逆元)%p) 。 因此,求模运算的除法问题就转化为就一个数的逆元问题了。
而求取一个数的逆元,有两种方法:
- 费马小定理
- 扩展欧几里得算法
费马定理/欧拉定理
特征
在比赛中常见的p为质数,如果a,b<=1e5,p是质数时(常见有1e9+7),我们可以使用费马小定理b^(p-1)%p=1,推出b的逆元是b^(p-2),使用快速幂求解即可。
1 | ll qpow(ll a, ll b, ll p) { |
扩展欧几里得
特征
ax + by = m
该算法(exgcd)可以求取ax + by = gcd(a,b)的整数解x,y。进而可以求取a的逆元x。该算法有三个应用:
- 求解不定方程
- 求解模的逆元
- 求解线性同余方程
1 | // 记得int该ll |
用扩展欧几里得求取逆元之后(a / b) % p可以换成a * inv(b, p) % p来让除法对 $p$ 取模。
注意:扩展欧几里得定理的使用前提是 m % gcd(a,b)== 0 ,否则逆元不存在
线性基
性质
线性基具有普通集合所具有的性质,即确定性、互异性、无序性。
线性基中每个数二进制下的 1 的最高位都是不同的。
线性基中没有异或和为 0 的子集。
线性基中任意多元素的异或和的值域等于原集合中任意多元素的异或和的值域。
线性基在满足上一个条件的情况下,所包含元素个数是最少的。
线性基中不同的元素异或出来的值是不同的。
1 | typedef long long LL; |
BSGS算法(大步小步算法)
性质
给定一个质数 p,以及正整数 a,b,求满足同余方程 $a^x \equiv b (mod ; p)$ 的最小非负整数 x,无满足的 x 则输出 -1。 时间复杂度为$O(\sqrt{p})$。
模板
1 | ll bsgs(ll a, ll b, ll mod){ |
组合数学
全错排
1 | // dp[n] 为 n 个东西任意排全排错的组合数 |
组合数
如果n,m很大,能达到1e18,p很小 <= 1e5 ,我们可以利用lucas定理C(n, m) % p = C(n % p, m % p) * lucas(n/p, m/p) % p
1 | //记得int改ll |
博弈论
巴什(巴适)博弈
特征
只有一堆n个物品,两个人轮流从这堆物品中取物,规定每次至少取一个,最多取m个。最后取光者得胜。
结论
1 | // 等于零先手负, 大于0先手胜 |
尼姆博弈
特征
有n堆若干个物品,两个人轮流从某一堆取任意多的物品,规定每次至少取一个,最多不限,最先取完着胜。
结论
1 | // 所有堆的物品的数量异或起来是0,先手必败。 |
SG函数
有向图游戏
给定一个有向无环图,图中有一个唯一的起点,在起点上放有一枚棋子。两名玩家交替地把这枚棋子沿有向边进行移动,每次可以移动一步,无法移动者判负。该游戏被称为有向图游戏。
任何一个公平组合游戏都可以转化为有向图游戏。具体方法是,把每个局面看成图中的一个节点,并且从每个局面向沿着合法行动能够到达的下一个局面连有向边。
定理
(1)有向图游戏的某个局面必胜,当且仅当该局面对应节点的SG函数值大于0。
(2)有向图游戏的某个局面必败,当且仅当该局面对应节点的SG函数值等于0。
有向图游戏的和
有向图游戏的和的SG函数值等于它包含的各个子游戏SG函数值的异或和(因此我们可以把nim博弈和bash博弈杂交一起)
时间复杂度
最多100堆石子,记忆化搜索每个状态计算一次,一个状态最多k个后继状态,sg计算次数最多为1e4次,所以总时间复杂度不超过1e6,O(mk),m为每堆的石子个数
1 | int s[1100], sg[MAX];// s为每堆可以去的石子数, sg[i]为第i堆游戏的结果 |
字符串
字典树(Trie树)
特征
Trie树是一种树形结构,是一种哈希树的变种。典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高。
功能模块
- 存储树
1 | struct Trie { |
- 建树
1 | void build(string s) { |
- 查询
1 | int query(string s) { |
AC自动机
特征
AC自动机用于多模式串匹配问题,例如给几个关键词(模式串),再给一篇文章,判断关键词是否在文章中出现,或出现的次数,有哪些关键词出现等等
1 | namespace ac { |
杂项
对拍器
对拍用咯!!
1 | g++ mkd.cpp -o mkd -g -std=c++20 |
离散化
使用场景
在数据规模较大(大于数组能开的范围),只需要数据的相对关系,而不需要数据的具体值时,我们可以使用离散化
代码
- STL版
1 | for(int i=1;i<=n;i++) |
- 非STL版
1 | int a[MAX];// 源数据数组 |
逆序对
特征
设有一个序列{a_1, a_2, a_3,…a_{n-1}, a_n},对于序列中任意两个元素ai,aj,若i<j,ai>aj,则说明ai和aj是一对逆序对。下面给出求逆序对的数量代码
- 归并排序做法
1 | int n,a[500010],c[500010]; |
- 树状数组做法
1 | int n; |
动态规划
最值型
题意:给定几枚硬币,最少用几枚能拼出目标钱数
动态规划组成部分
1. 确定状态
- 最后一步: (最优策略中使用的最后一枚硬币$a_k$)
- 子问题: (最少的硬币拼出更小的面额$21-a_k$)
2. 转移方程
假设现在有2,5,7三种面额的钱
- $f[x] = min(f[x-2]+1,f[x-5]+1,f[x-7]+1)$
3. 初始条件和边界情况
- 初始条件$f[0] = 0$,如果不能拼出Y,f[Y]=正无穷
- 无边界情况
计数型
题意:给定一个m行n列的网格,一个机器人从左上角(0,0)位置出发,每次只能向右或向下走一步,问走多少种不同的方式从左上角走到右下角
动态规划组成部分
1. 确定状态
- 最后一步:无论机器人如何到达右下角,一定在上一步中挪过来的(向右或向下)
- 右下角坐标设为(m-1,n-1)
- 前一步一定在(m-2,n-1)或者(m-1,n-2)上
- 子问题:如果机器人有X种方式从左上角走到(m-2,n-1),Y种方式从左上角走到(m-1,n-2),则机器人有X+Y种方式走到(m-1,n-1)
- 状态:设f[i][j]为机器人有多少种方式从左上角走到(i,j)
2. 转移方程
- $f[i][j]=f[i-1][j]+f[i][j-1]$
3. 初始条件和边界情况
- 初始条件:f[0][0]=1,机器人只有一种方式走到左上角
- 边界情况:f[i][0]=1,f[0][j]=1,每行每列只能一路走过来,因此也只有一种方式
存在型
题意:有n块石头分别在x轴的0,1,……,n-1位置,一只带黑框眼镜的青蛙在石头0上想跳到n-1上,如果ta在第i块石头上,ta最多可以向右跳距离$a_i$,问ta能否跳到n-1
例子:
- $输入:a=[2,3,1,1,4]$
- $输出:True$
- $输入:a=[3,2,1,0,4]$
- $输出:False$
动态规划组成部分
1. 确定状态
- 最后一步:如果能从i跳过来,那么青蛙一定能跳到i并且最后一步不超过能跳过来的最大距离$n-1-i<=a_i$
- 子问题:能否跳到当前位置
- 状态:设f[j]表示能否跳到石头j
2. 转移方程
- $f[j]=OR_{0<=i<j}(f[i]ANDj-i>=a[i])$
3. 初始条件和边界情况
- 初始条件:f[0]=True 第一块石头一定能跳上去
- 无边界情况
坐标型
1.题目:最长上升连续子序列
题意:给定一个数组,求出最长的连续子序列(单调上升或下降)
动态规划组成部分
1. 确定状态
- 最后一步:对于最优策略,一定有最后一个元素a[j],如果子序列长度大于1,那么最优策略a[j]的前一个元素肯定是a[j-1],使得$a[j-1]<a[j]$
- 子问题: 要求以a[j]结尾的最长连续上升子序列,那么就要求以a[j-1]结尾的最长连续上升子序列
- 状态:设f[j]=以a[j]结尾的最长连续上升子序列的长度
2. 转移方程
- $f[i]=max(1,f[i-1]+1ANDa[j-1]<a[j])$
3. 初始条件和边界情况
- 初始条件:无
- 边界情况:前面的数存在,并且前面的数小于当前数
4. 空间优化
由于转移方程只涉及到i与i-1,所以我们可以用一个长度为2的滚动数组优化
1 | int f[2],now=0,old=0; |
2.题目:最小路径和
题意:
- 给定m*n的网格,每个格子(i,j)里都有一个非负数A[i][j]
- 求一个从左上角到右下角的路径,每一步只能向右或向下走,求所有路径中最小的路径和
动态规划组成部分
1. 确定状态
- 最后一步:一定是从上面或左面的总和最小过来的
- 子问题: 要求(m-1,n-1)的最小路径和,需要知道(m-1,n-2),(m-2,n-1)的最小路径和加上(m-1,n-1)的大小
- 状态:设从(0,0)走到(i,j)的最小数字总和f[i][j]
2. 转移方程
- $f[i][j]=min(f[i-1][j],f[i][j-1])+A[i][j]$
3. 初始条件和边界情况
- 初始条件:f[0][0]=A[0][0]
- 边界情况:第一行和第一列只能从一个方向过来
4. 空间优化
由于转移方程只涉及到两行或两列,所以我们可以用两行的滚动数组优化,详见戳我(~ ̄▽ ̄)→)) ̄▽ ̄)o
3.题目:炸弹袭击
题意:
- 有一个M*N的网格,每个格子可能是空的,可能有一个敌人,可能有一堵墙
- 只能在某个空格子里放一个炸弹,炸弹会炸死所有同行同列的敌人,但是不能穿透墙,问最多能炸死几个敌人
动态规划组成部分
1. 确定状态
- 假设:所有的格子都能放炸弹
- 有敌人的格子:格子里的敌人被炸死并且能够继续向四周爆炸
- 有墙的格子:炸弹炸不死任何人
- 在(i,j)格放一个炸弹,能够向上炸死的敌人数:(接下来以向上为例)
- (i,j)为空地:能向上炸死(i-1,j)人
- (i,j)为敌人:能向上炸死(i-1,j)+1人
- (i,j)为墙:0
- 最后一步:无
- 子问题: 求(i,j)格能向上炸死的人数需要知道(i-1,j)格向上能炸死的敌人数
- 状态:Up[i][j]表示(i,j)格放一个炸弹向上能炸死的敌人数
2. 转移方程
- $f[n]=\begin{cases}
Up[i-1][j],&if(i,j)格是空地\
Up[i-1][j]+1,&if(i,j)格是敌人\
0,&if(i,j)格是墙\
\end{cases}$
3. 初始条件和边界情况
- 初始条件:如果(0,j)格不是敌人,Up[0][j]=0,如果是敌人Up[0][j]=1;
- 边界情况:同上
4. 程序
序列型(从下标1开始计算)
1.题目:房屋染色
题意:
- 有一排N栋房子,每栋房子需要刷:红、蓝、绿其中一种颜色
- 任何两栋相邻房子不能刷成同一颜色
- 第i栋房子染成红、蓝、绿的花费分别是cost[i][0],cost[i][1],cost[i][2]
- 最少要花多少钱油漆这些房子
动态规划组成部分
1. 确定状态
- 最后一步:最优策略中房子N-1一定染成 红、蓝、绿中的一种,而N-2栋房子一定不能和第N-1栋颜色一样(上一步的钱数与颜色两种因素共同影响当前步),所以我们需要再开一维来表示房子的颜色
- 子问题:求出前N栋房子并且第N-1栋房子在红、蓝、绿颜色的最小花费
- 状态:设油漆前i栋房子,并且第i-1栋房子是红、蓝、绿的最小花费为f[i][0],f[i][1],f[i][2]
2. 转移方程
- $f[i][0]=min{f[i-1][1]+cost[i-1][0],f[i-1][2]+cost[i-1][0]}$
- $f[i][1]=min{f[i-1][0]+cost[i-1][1],f[i-1][2]+cost[i-1][1]}$
- $f[i][2]=min{f[i-1][0]+cost[i-1][2],f[i-1][3]+cost[i-1][2]}$
3. 初始条件和边界情况
- 初始条件:f[0][0]=f[0][1]=f[0][2]=0 前0个什么都没有
- 无边界情况
2.题目:房屋染色2
题意:
- 有一排N栋房子,每栋房子需要刷K种颜色的一种
- 任何两栋相邻房子不能刷成同一颜色
- 第i栋房子染成第j种颜色的花费是cost[i][j]
- 最少要花多少钱油漆这些房子
动态规划组成部分
1. 确定状态
- 最后一步:与第一题相似,还是最后一栋与前一栋颜色不能一样所以我们需要再开一维来表示房子的颜色
- 子问题:求出前N栋房子并且第N-1栋房子在K种颜色的最小花费
- 状态:设油漆前i栋房子,并且第i-1栋房子是1,2……K颜色的最小花费为f[i][0],f[i][1],f[i][2]……f[i][k-1]
2. 转移方程
- $f[i][j]=min_{k!=j}(f[i-1][k])+cost[i-1][j]$
-
3. 初始条件和边界情况
- 初始条件:f[0][0]=f[0][1]=f[0][2]=……=f[0][k-1]=0 前0个什么都没有
- 无边界情况
4.时间优化
在转移方程中,每次我们都需要在k不同于j颜色中找到$f[i-1][k]$的最小值,因此这块的时间复杂度为$O(k^2)$,我们可以将其优化到$O(k)$。我们可以不用每次都查找最小值,而是把最小值与次小值都记录下来
如果最小值是第i个元素,次小值是第j个元素
- 除掉的是第i个元素以外的元素,最小值就是第i个元素
- 除掉的是第i个元素,最小值就是第j个元素
最长序列型动态规划
1.题目:最长递增子序列
题意:
- 给定一个数组,找出其中严格递增子序列的长度
动态规划组成部分
1. 确定状态
- 最后一步:最优策略中以a[j]结尾的子序列一定是最长的
- 子问题:本来求a[j]结尾的上升子序列,现在需要知道在它之前小于它的元素a[i]结尾的上升子序列
- 状态:设f[i]是以a[i]结尾的最长上升子序列的长度
2. 转移方程
- $f[j]=max(1,f[i]+1(a[i]<a[j]ANDi<j))$
3. 初始条件和边界情况
- 初始条件:无
- 边界情况:
a[i]<a[j]ANDi<j
总结(坐标型与序列型动态规划的区分)
二者可互通
- 如果第一项不需要空串向后生成,可以直接写出来dp,那么就可以用坐标型
- 如果不是很清楚第0个元素该怎么处理,那么选用序列型比较合适
划分型
1.题目:解码方法
题意:
- 有一段由A-Z组成的字母串被加密成数字串
- 加密方式:A->1,B->2,……Z->26
- 给定加密后的数字串S,问有多少种方式解密成字符串
动态规划组成部分
1. 确定状态
- 最后一步:最后一个字母是最后一位数字还是最后两位数字转化成的
- 子问题: 设数字串长度为N,要 求前N个字符的解密方式数,需要知道前N-1和前N-2个字符的解密方式数
- 状态:设数字串S前i个数字解密成字母串有f[i]种方式
2. 转移方程
- $f[i]=f[i-1]{S[i]对应一个字母}+f[i-2]{S[i-1]S[i]对应一个字母}$
3. 初始条件和边界情况
- 初始条件:f[0]=1 空串有一种解密
- 边界情况:如果i=1,只看最后一位数字(没有两位数)
4. 程序
2.题目:完全平方数
题意:
- 给一个正整数n
- 问最少用几个完全平方数组成n
动态规划组成部分
1. 确定状态
- 最后一步:最后的数一定是由某个数的平方和与剩下的数组成
- 子问题: 要求组成i的最小平方数需要比较出i减去一个平方数后的数需要的最小平方数组合的个数
- 状态:设能够组成i的最少的完全平方数有f[i]个
2. 转移方程
- $f[i] = min_{1<=j*j<=i}(f[i - j * j])+1$
3. 初始条件和边界情况
- 初始条件:f[0] = 0
- 边界情况:无
4. 程序
3.题目:分割回文串 II
题意:
- 给一个字符串s
- 问最少分成几个回文子串需要分割几次
动态规划组成部分
1. 确定状态
- 最后一步:最优策略有最后一段回文串 S[j……N-1],
- 子问题: 求前N个字符S[0..N-1]最少分为几个回文串,需要知道S前j个字符[0..j-1]最少可以划分成几个回文串
- 状态:设S前i个字符[0..i-1]最少可以划分成f[i]个回文串
2. 转移方程
- $f[i] = min_{0<=j<=i-1}(f[j]+1,S[j..i-1]是回文串)$
3. 初始条件和边界情况
- 初始条件:f[0] = 0 ,空串被分成0个回文串(序列型)
- 边界情况:无
4. 程序
4.题目:书籍复印
题意:
- 有N本书待抄写,第i本书有A[i]页
- 有k个抄写员,每个抄写员可以抄写连续的若干本书(每分钟抄一页)
- 最少需要多长的时间抄完所有的书
动态规划组成部分
1. 确定状态
- 最后一步:最后一个抄写员(第k个)抄写第j本到第N-1本书需要时间最短
- 子问题: 求K个人最短多长时间抄完前N本书,需要知道K-1个人最少多长时间抄完前j本书
- 状态:设f[k][i]为k个抄写员需要多长时间抄完前i本书
2. 转移方程
- $f[k][i] = min_{j=0..i}max(f[k-1][j],A[j] +..+ A[i-1])$
3. 初始条件和边界情况
- 初始条件:
- f[0][0] = 0 零个抄写员抄0本书。f[0][1]..f[0][N] = +inf
- f[k][0] = 0 每个抄写员花费0分钟抄0本书
- 边界情况:k > N ,可以赋值 k = N
4. 动态规划程序
小结
- 要求将一个序列或字符串划分成若干满足要求的片段
- 解决方法:最后一步->最后一段
- 枚举最后一段的起点
- 如果题目不指定段数,f[i]表示前i个元素分段后的可行性/最值:解码方法,完全平方数,分割回文串2
- 如果题目指定段数,用f[i][j]表示前i个元素分成j段或前j个元素分成i段后的可行性/最值:书籍复印
博弈型(规模不大)
1.题目:硬币排成线
题意:
- 给N个物品,重量都是正整数
- 一个背包最大承重为M
- 最多能带走多重的物品
动态规划组成部分
1. 确定状态
- 第一步:在当前局面走出一步可以让对手陷入必败的局面则必胜,在当前局面无路可走则必败
- 子问题:面对N个石子是否先手必胜,需要知道面对N - 1, N - 2个石子,是否先手必胜
- 状态:设f[i]为面对i个石子是否先手必胜
2. 转移方程
- $f[i] = f[i-1] == false OR f[i-2] == false$
3. 初始条件和边界情况
- 初始条件:f[0] = false 面对0个石子先手必输,f[1] = f[2] = true面对1,2个石子,先手必胜
4. 程序
背包问题
1.题目:背包问题(可行性)
题意:
- 有n个物品装入背包,重量均为正整数
- 假设背包最多能承重m,最多能装多满
动态规划组成部分
1. 确定状态
- 最后一个物品(重量$A_{N-1}$)是否放入背包
- 情况1:如果前N-1个物品能拼出W,当然前N个物品也能拼出W
- 情况2:如果前N-1个物品能拼出$W-A_{N-1}$,再加上最后的物品$A_{N-1}$,拼出W
- 子问题:要求前N个物品能否拼出重量0,1…,M,需要知道前N-1个物品能否拼出重量0,1,…,M
- 状态:设f[i][w]为能否用前i个物品拼出w
- 注意:背包问题一定要把承重放入状态
2. 转移方程
- $f[i][w] = f[i-1][w] OR f[i-1][w-A[i-1]]$
3. 初始条件和边界情况
- 初始条件:f[0][0]=true 前0个物品可以拼出重量0
- f[0][1..M]=false 0个物品拼不出大于0的重量
- 边界情况:f[i-1][w-A[i-1]]只能在w>=A[i-1]时使用
4. 程序
2.题目:背包问题5(计数型)
题意:
- 有n个正整数,一个正整数Target
- 每个正整数只能用一次,求有多少种组合加起来是Target
动态规划组成部分
1. 确定状态
- 最后一个物品(重量$A_{N-1}$)是否放入背包
- 情况1:如果前N-1个物品能拼出W,当然前N个物品也能拼出W
- 情况2:如果前N-1个物品能拼出$W-A_{N-1}$,再加上最后的物品$A_{N-1}$,拼出W
- 情况1的方式数+情况2的方式数=用前N个物品拼出W的方式数
- 子问题:要求前N个物品能否拼出重量0,1…,,需要知道前N-1个物品能否拼出重量0,1,…,Target
- 状态:设f[i][w]为用前i个物品多少种方式拼出w
- 注意:背包问题一定要把承重放入状态
2. 转移方程
- $f[i][w] = f[i-1][w] + f[i-1][w-A[i-1]]$
3. 初始条件和边界情况
- 初始条件:f[0][0]=1 前0个物品可以有一种方式拼出重量0
- f[0][1..M]=0 0个物品拼不出大于0的重量
- 边界情况:f[i-1][w-A[i-1]]只能在w>=A[i-1]时使用
4. 空间优化

5. 程序
空间优化过的程序
3.题目:背包问题2(最值型)
题意:
- 有n个物品,每个物品都有重量和价值两个属性
- 一个背包最大承重是正整数M
- 最多能带走多大价值的物品
动态规划组成部分
1. 确定状态
- 最后一个物品(重量$A_{N-1}$价值$V_{N-1}$)是否放入背包
- 情况1:如果前N-1个物品能拼出W,最大总价值是V当然前N个物品也能拼出W并且总价值是V
- 情况2:如果前N-1个物品能拼出$W-A_{N-1}$最大总价值是V,再加上最后的物品(重量$A_{N-1}$价值$V_{N-1}$),拼出W,总价值为$V + V_{N-1}$
- 情况1的方式数+情况2的方式数=用前N个物品拼出W的方式数
- 子问题:要求前N个物品能否拼出重量0,1…,M,以及拼出重量W能获得的最大价值,需要知道前N-1个物品能否拼出重量0,1,…M,以及拼出重量W能获得的最大价值
- 状态:设f[i][w]为用前i个物品拼出w时获得的最大价值(-1表示无法拼出)
- 注意:背包问题一定要把承重放入状态
2. 转移方程
- $f[i][w] = max(f[i-1][w], f[i-1][w-A[i-1]] + V[i-1] |w >= A[i-1] AND f[i-1][w-A[i-1]] != -1)$
3. 初始条件和边界情况
- 初始条件:f[0][0]=0 前0个物品可以有一种方式拼出重量0,价值为0
- f[0][1..M]=-1 0个物品拼不出大于0的重量
- 边界情况:f[i-1][w-A[i-1]]只能在w>=A[i-1]时使用
4. 程序
空间优化过的程序
4.题目:背包问题3(最值型)
题意:
- 有n种物品,每种物品都有重量和价值两个属性
- 每种物品都有无穷多个
- 一个背包最大承重是正整数M
- 最多能带走多大价值的物品
动态规划组成部分
1. 确定状态
- 与背包2几乎一致,区别在于每次可以将一个物品放多次
- 状态:设f[i][w]为用前i个物品拼出w时获得的最大价值(-1表示无法拼出)
- 注意:背包问题一定要把承重放入状态
2. 转移方程
- $f[i][w] = max(f[i-1][w], f[i-1][w-kA[i-1]] + kV[i-1] |kw >= A[i-1] AND f[i-1][w-kA[i-1]] != -1)$
- 优化:f[i][w] = max(f[i-1][w], f[i][w-A[i-1]] + V[i-1] |w >= A[i-1] AND f[i-1][w-A[i-1]] != -1)$
3. 初始条件和边界情况
- 初始条件:f[0][0]=0 前0个物品可以有一种方式拼出重量0,价值为0
- f[0][1..M]=-1 0个物品拼不出大于0的重量
- 边界情况:f[i-1][w-A[i-1]]只能在w>=A[i-1]时使用
4. 空间优化

5. 程序
小结
- 可行性背包
- 题面:要求不超过Target时能拼出的最大重量
- 记录f[i][w]=前i个物品能否拼出重量w
- 计数型背包
- 题面:要求有多少种方式拼出重量Target
- 记录f[i][w]=前i个物品有多少种方式拼出重量w
- 最值型背包
- 题面:要求能拼出的最大价值
- 记录f[i][w]=前i个物品能拼出重量w能获得的最大价值
- 关键点
- 最后一步
- 最后一个背包里的物品是哪一个
- 最后一个物品有没有放进背包
- 数组大小和最大承重Target有关
- 最后一步
区间型
1.最长回文子串
题意:
- 一个字符串S,长度为N
- 找到它最长的回文子序列的长度
动态规划组成部分
1. 确定状态
- 子问题:要求S[i..j]的最长回文子串
- 若S[i]=S[j],需要知道S[i+1..j-1]的最长回文子串(两边的字符相等)
- 否则答案是S[i+1..j]或S[i..j-1]的最长回文子串(两边的字符不等)
- 状态:设f[i][j]为S[i..j]的最长回文子串长度
2. 转移方程
- $f[i][j] = max(f[i+1][j],f[i][j-1],f[i+1][j-1]+2 | s[i]=s[j])$
3. 初始条件和边界情况
- 初始条件:f[0][0]=f[1][1]=…=f[n-1][n-1]=1(一个字母为长度为1的回文子串)
- 如果s[i]==s[i+1],f[i][i+1]=2
- 如果s[i]!=s[i+1],f[i][i+1]=1
4. 计算顺序
按照长度j-i,从小到大计算
5. 程序
2.题目:完全平方数
题意:
- 给一个正整数n
- 问最少用几个完全平方数组成n
动态规划组成部分
1. 确定状态
- 最后一步:最后的数一定是由某个数的平方和与剩下的数组成
- 子问题: 要求组成i的最小平方数需要比较出i减去一个平方数后的数需要的最小平方数组合的个数
- 状态:设能够组成i的最少的完全平方数有f[i]个
2. 转移方程
- $f[i] = min_{1<=j*j<=i}(f[i - j * j])+1$
3. 初始条件和边界情况
- 初始条件:f[0] = 0
- 边界情况:无