ACM第三周总结

目录

背包问题:

01背包:

完全背包问题:

一维背包优化:

多重背包:

分组背包问题:

背包问题总结:

 经典例题(本人挑了几个自己感觉还行的):

推荐文章阅读:

线性DP:

数字三角形:

最长上升子序列:

最长公共子序列:

编辑距离:

推荐内容:

推荐例题:

线性DP总结:

区间dp:

记忆化搜索:

阅读文章:

记忆化搜索总结:

动态规划问题的总结:

图论:

DFS/BFS:

拓扑排序:

简单概述:

算法概念:

应用场景:

推荐文章:

Dijkstra:

推荐文章(例题):

Floyed:

核心代码:

推荐文章(例题):

bellman-ford:

算法描述:

核心代码:

注意:

推荐文章:

SPFA:

详细介绍:

关于本人阅读的文章:

spfa判断负环:

例题:

题目分析:

Prim/Kruskal

最后总结:


  •  首先这周我阅读的文章大部分是关于动态规划问题,思维+算法详解+例题大约也有个30多篇了,因为动态规划的问题必须要转化成自己的一个思想,还有在图论方面,最小生成树的更新,包括复习之前的搜索+最短路径方法+负环+搜索的基础题目,也能够勉勉强强的完成50篇博客任务,下面的知识大部分都是自己的概述,个人认为比较值得阅读的文章和例题也有推荐。

这周学习了动态规划,复习了图论知识,下面就详细介绍一下吧!

  • 动态规划:(看得主要内容)
  • 背包问题
  • 01背包
  • 完全背包问题
  • 01背包的一维优化
  • 完全背包的一维优化
  • 多重背包
  • 多重背包的二进制优化为什么是正确的
  • 分组背包问题
  • 线性DP
  • 数字三角形
  • 最长上升子序列
  • 最长公共子序列
  • 最长公共子序列输出
  • 编辑距离
  • 动态规划 —— 线性 DP_Alex_McAvoy的博客-CSDN博客_线性dp这个题库真的是相当好,但是目前还没有学完。
  • 区间DP
  • 记忆化搜索
  • (我真的崩溃了,我刚刚写的博客呢?为什么我要从头再写)

背包问题:

01背包:

  • 就是这个物品到底装不装,每个物品只有一件,你的选择就是这一件到底装还是不装。
  • 状态转移方程:dp[i][j]=max(dp[i-1][j],dp[i-1][j-v[i]]+w[i])(从大到小进行枚举)

完全背包问题:

  • 每一件物品都有无数件,你可以自由的选择去放几件物品。
  • 状态转移方程:dp[i][j]=max(dp[i-1][j],dp[i-1][j-k*v[i]]+k*w[i])
  • 但此时复杂度非常高,我们就要思考优化的方式,即从小到大枚举j,之前的都已经装进去尝试过了,所以状态转移方程:

dp[i][j]=max(dp[i-1][j],dp[i-1][j-v[i]]+w[i])从小到大进行枚举

一维背包优化:

  • 理解的话就是因为每次更新都和i-1次还有体积的大小有关系,即j>v[i]即可,再就是考虑如何枚举,01背包的枚举因为不能让之前的物品出现在背包里所以从大到小进行枚举:
  • 状态转移方程:dp[j]=max(dp[j],dp[j-v[i]]+w[i]),
  • 而完全背包问题恰恰相反,应该保证前面的每个种类都尝试过,所以从小到大枚举,状态转移方程也是:dp[j]=max(dp[j],dp[j-v[i]]+w[i])

多重背包:

分组背包问题:

背包问题总结:

  • 其实背包问题的处理无非就是那几种类型,仔细的话就会发现所有的问题都可以向01背包问题思路转换(特别是在优化这一方面),找准枚举的范围,以及保证j>v[i]即可 .

 经典例题(本人挑了几个自己感觉还行的):

推荐文章阅读:

线性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);
}

编辑距离:

P2758 编辑距离 - 洛谷​​​​​t

题目分析:(二维数组dp)

有四种操作,分别为:

  • 1.删除dp[i-1,j]+1把字符串A的第i个字符删除,操作次数+1.
  • 2.添加dp[i,j-1]+1在字符串A末添加字符串B的第j个字符,操作次数+1
  • 3.替换dp[i-1,j-1]+1将字符串A的第i个字符替换成字符串B的第j个字符,操作次数+1
  • 4.不变dp[i-1,j-1]如果字符串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;
 
}

推荐内容:

推荐例题:

线性DP总结:

  • 相对别的DP问题来说,思维比较顺,比较符合正常的思维方式,分情况讨论,适当情况打表来观察规律,写出状态转移方程即可。

区间dp:

记忆化搜索:

记忆化搜索总结:

  • 这里面包含很多的经典问题如何用记忆化搜索写出来,先记录该点,然后返回,查阅记录,如果记录中有也进行返回操作

动态规划问题的总结:

  • 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网络)

应用场景:

  • 工程的如何进展效率最高,(工程项目类应用比较广泛)

推荐文章:

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];
            }
        }
    }
}

推荐文章(例题):

Floyed:

核心代码:

 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];
                }
            }
        }
    }

推荐文章(例题):

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相同数量级的数即可。这个算法理解起来还是有一些难度的,所以可以多看一些博客来巩固一下知识(特别是对于负环的理解,松弛的理解)

推荐文章:

SPFA:

详细介绍:

关于本人阅读的文章:

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

最后总结:

  •  到现在已经是很困了,如果有继续可以补充,会单独制成专题来进行补充,这些就是我这周所学习的内容以及对于我阅读博客的部分感受,这周会对主要对例题进行分析总结,感谢观看!
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

钟一淼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值