前言
北大的lyn学长回来给我们集训啦!
集训内容包括了DP,字符串,图论,树上算法,数据结构以及数论
习题难度要比平常习题加大一个难度,但是咬的紧还是能跟上的
接下来我会详细介绍
DP
“DP是整个OI生涯中最重要的算法之一”——lyn
这句话毫不夸张,几乎在每次比赛中都会出现至少一道DP,并且DP非常考验选手的思维能力
背包
讲DP肯定要从较为基础的,模型较容易理解的背包讲起,但若是结合具体问题,背包的思维含量也可以很高,尤其体现在如何将问题转化为背包模型
01背包
背包中最基础的问题:给定n个物品,每个物品可选可不选,体积为w[i],价值为v[i],求总体积不超过m的情况下能拿走物品总价值的最大值(n,m≤1000)
01背包基本原理及实现
设f[i][j]为考虑前i个物品容量为j的最大价值,由于每个物品有选与不选两种状态,易得状态转移方程:
f[i][j]=max(f[i-1][j],f[i-1][j-w[i]]+v[i])
时间复杂度(n2),空间复杂度(n2);
滚动数组优化:可以观察到由于i状态只能由i-1状态转移过来,所以可以采用滚动数组,将空间复杂度优化为(n)
拓展·单调队列优化:可以采用单调队列将背包问题时间复杂度优化到(nlogn)具体实现还需要进一步学习
01背包-改
当01背包必须装满时,问题如何解决呢
我们只需要考虑使没装满的情况均不合法即可
将f[0]初始化为0,其余均为负无穷
这样就保证了一定装满
01背包计数
即求合法情况下能拿走物品的方案数
很简单,不需要考虑价值,当合法时直接转移方案数,求和
转移方程:
f[j]+=f[j-v[i]]
删除物品:在背包问题中选择物品的顺序无所谓,所以支持删除操作,直接从当前方案中减去这个物品的方案数
转移方程:
f[j]-=f[j-v[i]]
血的教训:删除时j一定要从小到大枚举!!!
完全背包
在01背包基础上做改动:物品的数量不限
很简单,j正着枚举就行了,转移方程不变
CF1111D Destroy The Colony
现在你已经会了背包的基本知识,让我们来看一道例题吧/doge
题目解释:给定一字符串,由至多52种字符组成,m次询问,每次询问两个字符x和y,问有多少种重排方案使得所有同种字符在前一半或者后一半,并且x和y必须在同一半(n,m≤105 且为偶数)
题目分析:首先统计每种字符的出现次数,记为 t_i。先不考虑x和y,对于一种合法方案,需要选出一个子集使得其和为n/2,每种选法对应的排列方案数是
答案是2Wd,其中d是选出一个子集使其和为n/2的方案数
于是可以对 d 做一个01背包的计数:52个物品,背包容量是n/2
每次询问x和y,相当于强制让它俩绑定,退掉它俩的方案数再加上新的即可。预处理所有的x,y,时间复杂度O(522n)
血的教训:逆元还是用费马小定理写靠谱
多重背包
n 个物品,每个物品可选至多 n[i] 个,可不选,每个体积为 v[i],价值为 w[i]。求总体积不超过 m 的情况下能拿走物品总价值的最大。(n,m≤1000)
基本原理
在01背包的基础上枚举选择物品的数量
二进制优化
对每个 n[i] 二进制拆分,作 01背包。 O(nmlogn)
具体地说,对于n[i]=2k−1+d,其中 d<2k ,拆成 20,21…2k−1,d 。
一定可以凑出 ≤n[i] 的所有数字
证明:20可以凑出来
若小于2i的数均能被凑出来,则小于2i+1的数(可被表示为2i +一个小于2i的数)也能被凑出来
分组背包
n 个物品,体积为 w[i],价值为 v[i]。
所有物品被分为若干组,同组物品最多选一个。
求总体积不超过 m 的情况下能拿走物品总价值的最大值。
(n,m≤1000)
把每个组当成一个物品,选的时候枚举选哪个物品
温馨提醒:分组背包循环嵌套关系千万别写错了
树上背包
给一棵树,每个节点上有一个物品,每个体积为 w[i],价值为 v[i]。选了一个点必须选它的父亲。问总体积不超过 m 的情况下总价值最大值。(n,m≤100)
基本原理
f[u][j] 表示只考虑以u为根的子树,选j体积时的最大价值,转移时枚举子树v所选的体积k。可以得到转移方程:
f[u][j]=max(f[u][j],f[u][j-k]+f[v][k])
单次转移O(m2),总复杂度O(nm2)
树上背包-改
物品体积恒为1,貌似没什么区别,但是可发现一棵子树最大体积不超过字数大小,那么枚举体积时只需要枚举至字数大小,复杂度降为O(nm)
撞鸭状压
状态压缩,既将一些0/1 状态或更复杂的状态压缩成一个二进制数,用于降低时间或空间复杂度的方法。
状压 dp,顾名思义,就是利用压缩的状态进行转移的动态规划。
状压的应用
状压 dp 的本质还是 dp,实际问题中,状态压缩仅作为一种手段和工具,而如何设计合适的状态和快速的转移方程则是解决状压dp 问题的难点和关键,需要因题而异。
由于一般情况下状压dp 的复杂度为指数级别,因此适用于解决数据范围较小的问题。
状压的基本操作(yjx狂喜)
互不侵犯
经典状压例题:N×N 的棋盘里面放 K 个国王,使他们互不攻击,共有多少种摆放方案。国王能攻击到它上下左右,以及左上左下右上右下八个方向上附近的各一个格子,共8 个格子。
如何对棋盘进行状态压缩:考虑从上到下一行一行放置国王,容易发现,一个摆放方案对后续的影响仅跟它用了几个国王和它最后一行的放置状态有关,因此不难设计出这样的 dp:f[i][j][S]代表放到第 i 行,用了 j个国王,第 i 行的放置状态为 S 的方案数。
其中 S 是一个 n 位二进制数,第 i 位是 0/1 代表第 i 位放/不放国王。
如何对状态进行转移:枚举下一行的状态 T 进行转移。
如何判断一个转移是否合法:首先 T 自身必须合法,即 T 中的国王不能相邻,用位运算表示即T&(T>>1)=0&&T&(T<<1)=0,其 次,S 到 T 的 转 移 必 须 合 法,即S&T=0&&S&(T<<1)=0&&S&(T>>1)=0。
理论时间复杂度O(nk4n)
事实上,合法的转移远比4^n要少
跑的飞快
没有血的教训:要预处理出所有的合法状态和转移,速度会更快一点
区间DP
顾名思义,就是状态维护区间信息的DP
跟普通DP没什么区别
做多了就会了
小贴士:如果区间出现环形,需要将环形展开,展开时不要忘记区间*2哦
血的教训:有时可能出现需要分别对本区间和其他区间进行操作的情况比如String painter,这时不要贪,要分两遍进行DP
图论
最短路
作为图论中的常青树问题,最短路主要分单源最短路和多源最短路,并且扩展出了求环、分层图、差分约束、最短路树、网络流、费用流等最短路相关问题
集训主要从单源最短路与多源最短路两方面讲解
单源最短路
bellman-ford
松弛操作:其实就是最短路逐渐变短的过程
bellman-ford即一条边最多松弛n-1次,时间复杂度O(nm)
主要应用:1、找负环 2、优化后成为SPFA
Dijkstra
本质上是一种贪心算法,使用堆进行优化,是最常用的最短路算法,是不能被SPFA替代的,时间复杂度为稳定的O(mlogn),唯一缺点是必须全部为正边权
关于SPFA
SPFA一直都是争议最大的最短路算法
SPFA是辣鸡
- 本质bellman-Ford,是平方级别
- 任何优化(SLF、LLL)都可以被卡掉(甚至不如bellman-Ford)
- 越来越多的出题人会卡掉SPFA
SPFA也有好处
- 某些题的约束下不存在可以卡掉SPFA的数据(例如某些网络流题)
- 好写,一般情况下比较快,可以做负边权,可以找负环
总之,如果能用Dijkstra就尽量用Dijkstra吧
大陆争霸
一道例题
题意解释:你要摧毁一个国家的首都,给定城市之间连通方式,并且每个城市受别的城市保护,只有摧毁所有保护此城市的城市,才可以摧毁此城市。从起点开始可以向多个方向同时出击,求摧毁首都最短用时(1≤N≤3000,1≤M≤70000)
分析:dijkstra过程中记录一下当前每个城市的被保护情况,如果不被保护并且在此之前已经被搜索到那么入堆
血的教训:写代码之前要想清楚,比如此题,何时入队要清楚,显然是在入驻其他城市和本城时都有可能,所以应该分开记录两者的时间取较大值
多源最短路
唯一指定算法:Floyd
floyd本质DP:f[k][i][j]表示i-j只经过前k个点的最短路,实现时第一维可以直接优化掉。时间复杂度O(n3)
代码
Warning:Floyd循环顺序千万不能错!!!
这里再推一篇自己的博客,介绍了更多有关最短路的问题:最短路习题练习
最小生成树
找一个生成树,使得边权和最小
解决最小生成树问题的算法
Kruskal:每次尝试用最小边缩掉两个点集
prim:和dijkstra类似,用边权代替dist入堆
由 Kruskal 易得:同一个图,最小生成树可能有多个,但最小生成树的边权序列只有一种
最小瓶颈树
最小瓶颈树:最大边最小的生成树
最小瓶颈树的最大边=最小生成树的最大边
类似于最小生成树
求最小生成树上的最大边:O(m)
类似于O(n)求第k小数
随机一个权值key,把小于等于key的边全加入图中
如果图联通,那么答案小于等于key,删除刚刚加入的边
否则,保留图,答案大于key
Kruskal重构树
本质是存储kruskal的所有过程
叶子节点是原图中的点,每次合并集合时建立虚点代表合并后的集合,两儿子是原先的两个集合,点权是这条边的边权。
一共有 2n-1 个点。
任意两叶子节点的lca表示二者被合并的时刻。
NOI2018 归程
题意解释:给一个 n 点 m 边的无向图,每条边有长度和海拔。
给定起点 v 和水位线 p,你可以从起点 v 开车走海拔大于等于 p 的所有边到达任意点;可以步行走任何边到任意点。一旦开始步行就不能开车。
Q次询问,每次给定 v 和 p,问走到 1 号点的最短步行距离。
n≤200000,m,Q≤400000
解法:建出kruskal重构树,每次可以找到免费行走的点集,预处理最短路
Tarjan
将复杂图上问题转化为相对简单的特殊图上问题。
有向图:找强连通分量(scc)
强连通分量:有向图中的任意两个点都互相可达的极大(导出)子图
缩点后是个DAG,DAG每个点内部的点互相可达
边的分类
1.树边
2.返祖边
3.横叉边
维护细节
维护dfs时的栈,表示树根到当前点的节点序列。
时间戳:
维护两个标记
1.dfn[u]:u在dfs序中的序号
2.low[u]:u通过树边和至多一条连向当前强连通分量内部的非树边 能访问到的dfn最小值。
如果一个点的能访问到最早的点为这个点本身(low值等于dfn),就会形成一个新的强连通分量。
实现细节
考虑当前在u,遍历u->v
如果v没有访问过,那么v是树边,low[u]=min(low[u],low[v])
如果v访问过,那么判断它是否已经属于一个scc,如果不属于,那么u和v一定是同一个scc,low[u]=min(low[u],dfn[v])
有趣的小知识:当访问过时的转移中dfn[v]可改为low[v],时间复杂度会常数级降低(想一想为什么)
代码
无向图:找割点、桥(点双、边双)
之前是有向图的tarjan算法,无向图的tarjan也是类似的
但是只有树边和返祖边(想一想为什么)
点连通度:最小的点数使得删去之后图不连通
边连通度:最小的边数使得删去之后图不连通
如果一个图的点连通度大于等于2,那么是点双连通的,边双连通同理
割点:删去这个点,图的联通块个数增加
桥:删去这条边,图的联通块个数增加
割点将图分割成若干个点双,桥将图分成若干个边双
求割点代码
求桥代码
复杂度以及拓展
复杂度均为线性
一般情况下tarjan主要干的是图上问题转化成DAG或树上问题去求解。涉及到“连通性”的题目可以考虑用tarjan简化问题
dfs树的思想十分强大,对于一些图论问题也可以从tarjan的原理下手去分析
tarjan算法的拓展:支配树