目录:
- 首先这周我阅读的文章大部分是关于动态规划问题,思维+算法详解+例题大约也有个30多篇了,因为动态规划的问题必须要转化成自己的一个思想,还有在图论方面,最小生成树的更新,包括复习之前的搜索+最短路径方法+负环+搜索的基础题目,也能够勉勉强强的完成50篇博客任务,下面的知识大部分都是自己的概述,个人认为比较值得阅读的文章和例题也有推荐。
这周学习了动态规划,复习了图论知识,下面就详细介绍一下吧!
- 动态规划:(看得主要内容)
- 背包问题
- 01背包
- 完全背包问题
- 01背包的一维优化
- 完全背包的一维优化
- 多重背包
- 多重背包的二进制优化为什么是正确的
- 分组背包问题
- 线性DP
- 数字三角形
- 最长上升子序列
- 最长公共子序列
- 最长公共子序列输出
- 编辑距离
- 动态规划 —— 线性 DP_Alex_McAvoy的博客-CSDN博客_线性dp这个题库真的是相当好,但是目前还没有学完。
- 区间DP
- 记忆化搜索
(我真的崩溃了,我刚刚写的博客呢?为什么我要从头再写)
背包问题:
01背包:
- 就是这个物品到底装不装,每个物品只有一件,你的选择就是这一件到底装还是不装。
- 状态转移方程:(从大到小进行枚举)
完全背包问题:
- 每一件物品都有无数件,你可以自由的选择去放几件物品。
- 状态转移方程:
- 但此时复杂度非常高,我们就要思考优化的方式,即从小到大枚举j,之前的都已经装进去尝试过了,所以状态转移方程:
(从小到大进行枚举)
一维背包优化:
- 理解的话就是因为每次更新都和i-1次还有体积的大小有关系,即j>v[i]即可,再就是考虑如何枚举,01背包的枚举因为不能让之前的物品出现在背包里所以从大到小进行枚举:
- 状态转移方程:,
- 而完全背包问题恰恰相反,应该保证前面的每个种类都尝试过,所以从小到大枚举,状态转移方程也是:
多重背包:
- 详细内容请看多重背包问题_钟一淼的博客-CSDN博客
- 关于二进制的优化,其实个人感觉这个优化方式特别的神奇,因为划分集合,2是以指数速度递增,所以很大的集合也可以压缩成很小(所以之后做题要更加关注二进制的优化问题)。
分组背包问题:
- 与完全背包问题非常类似,所以详细请看
- 分组背包问题_钟一淼的博客-CSDN博客
背包问题总结:
- 其实背包问题的处理无非就是那几种类型,仔细的话就会发现所有的问题都可以向01背包问题思路转换(特别是在优化这一方面),找准枚举的范围,以及保证j>v[i]即可 .
经典例题(本人挑了几个自己感觉还行的):
推荐文章阅读:
- 01背包问题相关优化大全(二维到一维)_曼切斯特的流氓的博客-CSDN博客_01背包优化
- 背包问题(01背包、完全背包、多重背包)_XY迷迭香的博客-CSDN博客
- 完全背包问题(详细解答)_曼切斯特的流氓的博客-CSDN博客_完全背包问题
- 背包问题-笔记整理_迷亭1213的博客-CSDN博客_背包问题
线性DP:
数字三角形:
- (应该不用我再多介绍什么了)从下向上进行,(这里不得不多注意在DP问题中,不仅仅要思考从前往后写方程,也应该思考从后向前写方程是否会更加方便计算)
- 线性dp入门题目,详情参考我的->ACM周总结2_钟一淼的博客-CSDN博客
最长上升子序列:
- 其实思想特别好理解,就是数一下前面的数(必须得是比当前数小的数),然后比较这个前面的数的f数组谁大,然后再+1就是我们的目前的最长上升子序列了,然后就是这个思想,一直找到最后一个数,然后再比较谁的f数组值更大即可。(这里就是简单的描述一下思路)
- 详细请看我的->线性DP(最长上升子序列LIS)_钟一淼的博客-CSDN博客
最长公共子序列:
- 分两种情况进行讨论的一个问题,个人觉得在dp状况下基本都是要分情况讨论的,第一种就是最后一个字符相等的时候,第二种情况就是最后一个字符不同的情况,字符相同,继承上一个的最大值+1,字符不同,继承两个当中字符最大的那个(思路还是比较清晰的),详细请看->
- 最长公共子序列(LCS)_钟一淼的博客-CSDN博客
- 最长公共子序列的输出:
- 记录值,进行一个递归搜索即可(补充前面没有写到的)代码如下:
#include <iostream>
#include <algorithm>
#define maxn 1010
using namespace std;
string s1;
string s2;
int f[maxn][maxn];
void LCS(int i, int j) {
if (i == 0 || j == 0)return;//第一种情况
if (s1[i - 1] == s2[j - 1]) {
LCS(i - 1, j - 1);//递归,调用函数
cout << s1[i - 1];
}
else if(f[i][j-1]>f[i-1][j]) {
LCS(i, j - 1);
}
else {
LCS(i - 1, j);
}
}
int main()
{
int n, m;
cin >> s1;
cin >> s2;
n = s1.size();
m = s2.size();
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (s1[i - 1] == s2[j - 1]) {
f[i][j] = 1 + f[i - 1][j - 1];//当末尾字符相同时加1
}
else {
f[i][j] = max(f[i - 1][j], f[i][j - 1]);
}
}
}
cout << f[n][m] << endl;
LCS(n, m);
}
编辑距离:
题目分析:(二维数组dp)
有四种操作,分别为:
- 1.删除:把字符串A的第i个字符删除,操作次数+1.
- 2.添加:在字符串A末添加字符串B的第j个字符,操作次数+1
- 3.替换:将字符串A的第i个字符替换成字符串B的第j个字符,操作次数+1
- 4.不变:如果字符串A的第i个字符等于字符串B的第j个字符,操作次数不变
注意:
- dp[i][0]=dp[0][i]=i
代码如下:
#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>
using namespace std;
string s1, s2;//初始字符串
int len1, len2;
int f[5010][5010];
int main()
{
cin >> s1 >> s2;
len1 = s1.length();
len2 = s2.length();
memset(f,127, sizeof(f));
for (int i = 0; i <= len2; ++i)
f[0][i] = i;
for (int i = 0; i <= len1; ++i)
f[i][0] = i;
for (int i = 1; i <= len1; ++i)
{
for (int j = 1; j <= len2; ++j)
{
if (s1[i - 1] == s2[j - 1])
f[i][j] = min(f[i][j], f[i - 1][j - 1]);//两位相同
else
f[i][j] = min(f[i][j], f[i - 1][j - 1] + 1);//替换
f[i][j] = min(f[i][j], f[i][j - 1] + 1);//添加
f[i][j] = min(f[i][j], f[i - 1][j] + 1);//删除
}
}
cout << f[len1][len2] << endl;
}
推荐内容:
- 【题解】- AcWing - 779.最长公共字符串后缀_Kapo1的博客-CSDN博客
- 动态规划 —— 线性 DP_Alex_McAvoy的博客-CSDN博客_线性dp
- 最长上升子序列 (LIS) 详解+例题模板 (全)_lxt_Lucia的博客-CSDN博客_最长上升子序列
推荐例题:
- [USACO1.5][IOI1994]数字三角形 Number Triangles - 洛谷
- P1020 [NOIP1999 普及组] 导弹拦截 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
- 最大食物链计数 - 洛谷
- 【模板】最长公共子序列 - 洛谷
线性DP总结:
- 相对别的DP问题来说,思维比较顺,比较符合正常的思维方式,分情况讨论,适当情况打表来观察规律,写出状态转移方程即可。
区间dp:
- 详细请看我->区间DP(石子合并)_钟一淼的博客-CSDN博客
记忆化搜索:
- 这一块仍然在学习当中,实话实说,个人的思维确实在动态规划上具有一定的局限性。模式的话就是搜索+动态规划,个人理解就是将每一条的规划写出来,再用搜索的方式去找出最优解。(会单独更新一个专题,目前仍在学习)
-
阅读文章:
- 记忆化搜索专题_剑锋OI的博客-CSDN博客_记忆化搜索
- 动态规划+深度优先搜索+记忆化搜索(干货满满)_励志成为技术大佬的博客-CSDN博客_记忆化搜索和深度优先搜索
- 浅谈记忆化搜索_熙言丶的博客-CSDN博客
- 记忆化搜索(搜索+dp思想)_瞭望天空的博客-CSDN博客_记忆化搜索
记忆化搜索总结:
- 这里面包含很多的经典问题如何用记忆化搜索写出来,先记录该点,然后返回,查阅记录,如果记录中有也进行返回操作。
动态规划问题的总结:
- 1.拆分成子问题,对子问题进行求解,这一方面就比较类似于递推的一种思想。
- 2.写出状态转移方程。
- 3.确定好边界的一些条件。
- 4.动态规划问题更类似于数学的思维解决问题,难就难在状态转移方程我该如何去写,所以多思考才是王道。
图论:
- DFS和BFS
- 拓扑排序
- 二分图
图的最短路径
- Dijstra
- Floyed
- bellman-ford
- spfa
- spfa判断负环
最小生成树
- Prim
- Kruskal
注意:顺序可能会不同,因为要进行一些比较
DFS/BFS:
- 这周复习了不少例题,主要还是剪枝一类的算法,还有一道我十分推荐的例题->
- UVA524 素数环 Prime Ring Problem_钟一淼的博客-CSDN博客z
- 最后说一下我对剪枝的理解吧,无非就是过滤掉那些根本不可能满足条件的路径,因为dfs是每一条路都会走,每一条路都会走到黑那种,所以我们把那些不能满足条件的通通删去,来提高算法效率。
拓扑排序:
简单概述:
- 利用有向无环图(DAG)如果Vi和Vj之间存在路径,那么vi一定在vj的前面。(符合AOV网络特点)
算法概念:
- 1.在图中找到一个入度为0,即前驱为0的顶点输出。
- 2.找到这个结点连接的子结点(即子工程),输出。
- 3.注意,因为入度为0的点可能不止一种,所以说拓扑排序的不唯一。
- 4.如果出现了没有入度为0的点,那就不能进行拓扑排序(即不是AOV网络)
应用场景:
- 工程的如何进展效率最高,(工程项目类应用比较广泛)
推荐文章:
- 拓扑排序入门(真的很简单)_独-的博客-CSDN博客_拓扑排序
- 拓扑排序之AOV网及其拓扑排序思想(C语言)_bfhonor的博客-CSDN博客_aov网的拓扑排序
- 数据结构(20)图的拓扑排序_发量充足的小姚的博客-CSDN博客_图的拓扑排序
- 拓扑排序__CorLéone_的博客-CSDN博客_拓扑排序
- 加深对AOV网络的理解和一些基本拓扑排序实现的方法,比如利用栈,邻接表等。
Dijkstra:
- 单源最短路径,特别好描述的一个做法,我首先初始化每一条路径,让它都是INF(一个很大很大的数),就想想自己在大街上走,打开地图软件,我就找哪条路最短能够到达下一个路口(更新该路径,标记该结点我已经走过了),如果已经没有路可以走了,即我已经经历过很多路口最终到达了重点了,结束循环。
- 核心代码:(不要看这个,错的)
int dijkstra(int n, int m) {//n为顶点数,m为起点开始的位置
while (true) {
memset(dis, dis + maxn, INF);
dis[m] = 0;//初始化起点为0
int index = -1;
int minx = 0;//定义
for (int i = 1; i <= n; i++) {
if (!vis[i] && minx > dis[i]) {//寻找到该点
index = i;
minx = dis[i];
}
}
if (index == -1) {//说明没有点可以继续搜索了
break;//退出循环条件
}
vis[index] = 1;//已经确定该点为最短路径点了,标记上踢出
for (int j = 1; j <= n; j++) {
if (dis[j] > dis[index] + mp[index][j]&&vis[j]==0&&mp[index][j]!=INF) {//该点有路可以走,之前已经经过的结点不算,没有路可以走的结点不算
dis[j] = dis[index] + mp[index][j];
}
}
}
}
- 原来这个写错了(居然写错了都没发现),原来我读不懂不之前写的就是这个原因->改正代码:
int dijkstra(int n, int m)
{
for (int i = 1; i <= n; i++) {
int index = -1;
dis[1] = 0;
int minx = INF;
for (int j = 1; j <= n; j++) {
if (!visited[j] && (index == -1 || minx > dis[j])) {
index = j;
minx = dis[j];
}
}
vis[index] = 1;
for (int j = 1; j <= n; j++) {
if (dis[index] + edges[index][j] < dis[j]) {
dis[j] = dis[index] + edges[index][j];
}
}
}
}
推荐文章(例题):
- 【模板】最小生成树 - 洛谷
- 迪杰斯特拉算法详解+模版+例题_俗人Layman的博客-CSDN博客_迪杰斯特拉算法例题
- 849. Dijkstra求最短路 I - AcWing题库
- 图论-Dijksyta算法(最短路径算法)_钟一淼的博客-CSDN博客
- 邮递员送信 - 洛谷
Floyed:
- 详细内容:
- 直接看我写的博客->
- 图论:图的四种最短路径算法_钟一淼的博客-CSDN博客
- 这里我只说一下需要注意的地方:首先该算法时间复杂度虽然很高,但是作为多源路径求解还是比较方便的,适用于没有负边权和回路的情况.
核心代码:
for (int k = 1; k <= n; k++) {//从1到n依次各点进行中转
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (e[i][j] > e[i][k] + e[k][j]) {//如果该路径更短,更新成该路径
e[i][j] = e[i][k] + e[k][j];
}
}
}
}
推荐文章(例题):
- P1119 灾后重建 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
- 854. Floyd求最短路 - AcWing题库
- 最短路径模板+解析——(FLoyd算法)_coderyzh的博客-CSDN博客_floyd算法
bellman-ford:
算法描述:
- 求含负权图的单源最短路径的一种算法,效率较低,原理为连续进行松弛,在每次松弛时把每条边都更新一下,若在 n-1 次松弛后还能更新,则说明图中有负环,因此无法得出结果,否则就完成。
核心代码:
int bellman_ford() {
memset(dis, 0x3f, sizeof dis);
dist[1] = 0;
for (int i = 0; i < k; i++) {//k次循环
memcpy(back, dis, sizeof dis);//将dis数组拷贝到back(防止串联,进行二次迭代)
for (int j = 0; j < m; j++) {//遍历所有边
int a = e[j].a, b = e[j].b, w = e[j].w;
dis[b] = min(dis[b], back[a] + w);//更新
}
}
if (dist[n] > 0x3f3f3f3f / 2)
{
return -1;
}//有负环,退出
else
{
return dis[n];
}
}
注意:
- 是否能到达n号点的判断中需要进行if(dis[n] > INF/2)判断,而并非是if(dis[n] == INF)判断,原因是INF是一个确定的值,并非真正的无穷大,会随着其他数值而受到影响,dist[n]大于某个与INF相同数量级的数即可。这个算法理解起来还是有一些难度的,所以可以多看一些博客来巩固一下知识(特别是对于负环的理解,松弛的理解)
推荐文章:
- 负环详解_weixin_30340617的博客-CSDN博客
- Bellman-ford算法_可可亚的博客-CSDN博客_bellmanford时间复杂度
- 因为 bellman-ford其实时间复杂度较高,所以一般都会采用SPFA进行求解,所以例题在这就没有具体练习过。
SPFA:
详细介绍:
- SPFA算法(最短路径算法)_钟一淼的博客-CSDN博客
- 无非就是利用邻接表建图,然后利用队列进行松弛操作,这个算法之前写的还是比较详细的。
关于本人阅读的文章:
- 直接用vector建邻接表不香嘛
- vector 邻接表_珍惜每分每秒的博客-CSDN博客
- 用vector表示邻接表_curry___的博客-CSDN博客_vector表示邻接表
- #1093 : 最短路径·三:SPFA算法(邻接表)_tb_youth的博客-CSDN博客
- 邻接表存图 SPFA__Zer0的博客-CSDN博客
- 最短路计数 - 洛谷
spfa判断负环:
例题:
题目分析:
- 1、dis[x] 到x的最短距离。
- 2、cnt[x] 记录当前最短路的边数,初始每个点到终点的距离为0,如果cnt[x] >= n,表示该图中一定存在负环。
- 3、不断更新边权值,增加边权个数。
#include <cstring>
#include <iostream>
#include <algorithm>
#include <queue>
using namespace std;
const int N = 100050;
int n, m;
int h[N], e[N], ne[N], idx;
int w[N];
int dist[N], cnt[N];
bool st[N];
//建立邻接表
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
int spfa()
{
queue<int> q;
for (int i = 1; i <= n; i++)
{
q.push(i);
st[i] = true;
}
while (q.size())
{
auto t = q.front();
q.pop();
st[t] = false;
//更新与t邻接的边
for (int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
if (dist[j] > dist[t] + w[i])
{
dist[j] = dist[t] + w[i];
cnt[j] = cnt[t] + 1;
if (cnt[j] >= n) return true;
if (!st[j])
{
q.push(j);
st[j] = true;
}
}
}
}
return false;//函数没结束,意味着边数一直小于结点数,不存在负环
}
int main()
{
cin >> n >>m;
memset(h, -1, sizeof h);
for (int i = 0; i < m; i++)
{
int a, b, c;
cin >> a >> b >> c;
add(a, b, c);
}
if (spfa()) {
cout << "Yes" << endl;
}
else {
cout << "No" << endl;
}
}
二分图和差分约束我还没有学会。
Prim/Kruskal
最后总结:
- 到现在已经是很困了,如果有继续可以补充,会单独制成专题来进行补充,这些就是我这周所学习的内容以及对于我阅读博客的部分感受,这周会对主要对例题进行分析总结,感谢观看!