该系列博客旨在记录我的刷题心得和一些解题技巧,题目全部来源于力扣,一些技巧和方法参考过力扣上的题解和labuladong大佬的文章。虽然说这些内容主要是写给我自己看的,但也欢迎大家发表自己新颖的解法和不一样的观点。


目录

一、图的存储

二、图的遍历

1.DFS

2.BFS

三、拓补排序

四、二分图

1.二分图的判断

五、并查集

六、最短路算法

1.单源无权图

2.单源有权图——Dijkstra算法

3.多源最短路——Floyd算法

七、最小生成树

1.定义

2.Prim算法

3.Kruskal算法


一、图的存储

图的常见存储方式有两种:邻接矩阵和邻接表。邻接矩阵适用于存储稠密图,而且能以O(1)的复杂度判断两个顶点是否相邻。邻接表适用于存储稀疏图,占用空间少,存各种图都很适合,除非有特殊需求。

二、图的遍历

1.DFS

DFS跟多叉树的前序/后序遍历如出一辙,都是先访问当前节点,再递归的遍历当前节点的相邻节点。下面给出两种形式的代码(假设图用邻接表存储):

void dfs(vector<vector<int>>& graph,int u){if(visited[u])return;//在这里访问数据visited[u]=true;for(auto x:graph[u]){dfs(graph,x);}
}
void dfs(vector<vector<int>>& graph,int u){//在这里访问数据visited[u]=true;for(auto x:graph[u]){if(!visited[x])dfs(graph,x);}
}

第一个问题,visited数组是用来干什么的?它可以防止在遍历无向图和存在环的图时不会陷入死循环。只有当我们遍历有向无环图(即多叉树)时可以不用visited数组。

第二个问题,这两种形式有什么区别?最好用哪种?第一种形式与多叉树的遍历统一,也比较简洁美观,但最好用第二种形式,因为有时候我们需要在for循环内对那些未访问过的节点进行操作,如果用第一种,我们是不知道相邻的节点是否已访问的。这一点在下文二分图问题中会有具体体现。而且第二种形式 if 在 for 循环内与BFS是统一的。

2.BFS

同样的,BFS与多叉树的层序遍历类似,需要用队列辅助

void bfs(vector<vector<int>>& graph,int u){queue<int> q;q.push(u);//在这里访问数据            visited[u]=true;       //每次进队表示一次访问 while(!q.empty()){int sz=q.size();for(int i=0;i<sz;i++){int cur=q.front();q.pop();for(auto x:graph[cur]){if(!visited[x]){   q.push(x);//在这里访问数据visited[x]=true;}}}}
}

三、拓补排序

拓补排序的经典应用就是排课问题,如力扣 201.课程表 II

第一种从入度考虑,用一个indegree数组记录每个顶点的入度,在每次循环内寻找入度为0的顶点。那么如何寻找呢?如果每次遍历一遍indegree,那未必也太不“聪明”了。我们可以用栈或队列优化(拓补排序顺序不唯一,因此两者都可),当我们降低入度为0的顶点的相邻顶点的入度时,检查它的入度,降为0时放入栈或队列中。

class Solution {int n;vector<vector<int>> graph;vector<int> indegree;bool hasCycle=false;vector<int> ans;
public:vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites) {n=numCourses;indegree.resize(n);buildGraph(prerequisites);topSort(graph);if(hasCycle)return {};elsereturn ans;}void buildGraph(vector<vector<int>>& prerequisites){graph.resize(n);for(auto& x:prerequisites){graph[x[1]].emplace_back(x[0]);++indegree[x[0]];}}void topSort(vector<vector<int>>& graph){queue<int> q;int cnt=0;for(int i=0;i<n;i++){if(indegree[i]==0){q.push(i);++cnt;ans.emplace_back(i);}}while(!q.empty()){int sz=q.size();for(int i=0;i<sz;i++){int cur=q.front();q.pop();for(auto next:graph[cur]){if(--indegree[next]==0){q.push(next);++cnt;ans.emplace_back(next);}}}}hasCycle=(cnt!=n);}
};

以上代码中并没有用到visited数组,程序也不会进入死循环,这是因为成环的顶点的入度始终不可能等于0,这时若要判断是否有环,只有用一个cnt遍历记录输出的顶点个数,若cnt与总个数n不相等,说明含有环。

第二种从出度考虑,寻找出度为0的顶点,然后逆向输出。如何寻找呢?是不是要用一个outdegree数组呢?其实还有一种更巧妙地方式,出度为0的顶点一定在图最深那个位置,因此使用dfs遍历一遍图,把后序遍历的结果逆序输出即可,因为要用dfs,所以必须有visited数组防止死循环,onPath数组来判断是否含有环。

class Solution {int n;vector<vector<int>> graph;vector<bool> visited;vector<bool> onPath;bool hasCycle=false;vector<int> postOrder;
public:vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites) {n=numCourses;visited.resize(n,false);onPath.resize(n,false);buildGraph(prerequisites);if(!canFinish()){return {};}else{reverse(postOrder.begin(), postOrder.end());return postOrder;}}bool canFinish() {for(int i=0;i<n;i++){dfs(graph,i);}return !hasCycle;}void buildGraph(vector<vector<int>>& prerequisites){graph.resize(n);for(auto& x:prerequisites){graph[x[1]].emplace_back(x[0]);}}void dfs(vector<vector<int>>& graph,int u){if(onPath[u]){hasCycle=true;return;}if(hasCycle || visited[u])return;visited[u]=true;onPath[u]=true;for(auto x:graph[u]){dfs(graph,x);}postOrder.emplace_back(u);onPath[u]=false;}
};

四、二分图

二分图是什么?节点由两个集合组成,且两个集合内部没有边的图。换言之,若将每条边的两个端点分别染成黑色和白色,可以发现二分图中所以相邻顶点的颜色均不同。

1.二分图的判断

我们用color数组记录每个顶点的颜色,用visited数组代表当前节点是否已染过色,遍历一边图,若相邻顶点未染色,则把它染成不同的颜色;若已染色,则判断是否异色,若相同,则不是二分图。下面给出dfs和bfs的两种实现:

class Solution {int n;vector<bool> visited;vector<bool> color;bool isbipartite=true;
public:bool isBipartite(vector<vector<int>>& graph) {n=graph.size();visited.resize(n);color.resize(n);for(int i=0;i<n;i++){if(!visited[i])dfs(graph,i);}return isbipartite;}void dfs(vector<vector<int>>& graph,int u){if(!isbipartite)return;visited[u]=true;for(auto next:graph[u]){if(!visited[next]){color[next]=!color[u];dfs(graph,next);}else{if(color[next]==color[u]){isbipartite=false;return;}}}}
};
class Solution {int n;vector<bool> visited;vector<bool> color;bool isbipartite=true;
public:bool isBipartite(vector<vector<int>>& graph) {n=graph.size();visited.resize(n);color.resize(n);for(int i=0;i<n;i++){if(!visited[i])bfs(graph,i);}return isbipartite;}void bfs(vector<vector<int>>& graph,int u){if(!isbipartite)return;queue<int> q;q.push(u);visited[u]=true;while(!q.empty()){int sz=q.size();for(int i=0;i<sz;i++){int cur=q.front();q.pop();for(auto next:graph[cur]){if(!visited[next]){q.push(next);color[next]=!color[cur];                        visited[next]=true;}else{if(color[next]==color[cur]){isbipartite=false;return;}}}}}}
};

五、并查集

并查集支持两种操作:

  • 合并(Union):合并两个元素所属集合(合并对应的树)
  • 查询(Find):查询某个元素所属集合(查询对应的树的根节点),这可以用于判断两个元素是否属于同一集合,通常还会顺便压缩一下路径
void initialize(vector<int>& parent, int n){parent.resize(n);for(int i=0;i<26;i++){parent[i]=i;}
}void Union(int p,int q){int rootP=find(p),rootQ=find(q);if(rootP==rootQ)return;parent[rootP]=rootQ;
}bool isConnected(int p,int q){return find(p)==find(q);
}int find(int x){if(parent[x]!=x){parent[x]=find(parent[x]);}return parent[x];
}

再来说一种特殊的操作:删除。详情见这篇文章

这里附我的代码实现:

int index;void initialize(vector<int>& parent, int n){index=n;for(int i=0;i<n;i++){parent.emplace_back(index++);}for(int i=n;i<2*n;i++){parent.emplace_back(i);}
}void del(int x){parent[x]=index;parent.emplace_back(index++);
}//其他方法不变

六、最短路算法

1.单源无权图

单源无权图的最短路算法是 bfs 的改造,我们用 dist 数组记录给定的 s 顶点到各顶点的最短路径长度。dist[s] 初始化为0,其他初始化为正无穷INF(其他标志性的数如-1也可以)。对于当前访问顶点v的所有未访问的邻接点u,dist[u]=dist[v]+1

void Unweighted(vector<vector<int>> graph, int s){queue<int> q;q.push(s);while(!q.empty()){int v=q.front();q.pop();for(auto& u:graph[v]){if(dist[u]==INF){dist[u]=dist[v]+1;q.push(u);}}}}

2.单源有权图——Dijkstra算法

Dijkstra算法同无权最短路径算法,用 dist 数组记录给定的 s 顶点到各顶点的最短路径长度。dist[s] 初始化为0,其他必须初始化为正无穷INF,每次选取一个顶点v ,它在所有未访问顶点中具有最小的dist[v](贪心思想),同时dist[v]是已知的(Dijkstra算法的前提是每次选取的dist[v]是递增的,若dist[v]未知,说明还存在一个更小的dist[x],这和dist[v]最小矛盾),然后更新v的所有邻接点u。在无权情况下dist[u]=dist[v]+1,在赋权情景下,若dist[v]+weight(v,u)是一个更小的值就更新dist[u]=dist[v]+weight(v,u)

class Vertix{
public:int v,dist;Vertix(){}Vertix(int _v, int _dist){v=_v;dist=_dist;}bool operator < (const Vertix uv)const{return dist > uv.dist;}
};int INF=0x3f3f3f3f;
vector<int> dist;
vector<bool> visited;void Dijkstra(vector<vector<pair<int,int>>>& graph, int n, int s) { //把编号和权重作为pairpriority_queue<Vertix> pq;dist.resize(n,INF);dist[s]=0;visited.resize(n);pq.push(Vertix(s,dist[s]));while(!pq.empty()){Vertix cur=pq.top();pq.pop();if(visited[cur.v])       //跳过被重复入堆的顶点continue;visited[cur.v]=true;for(auto& e:graph[cur.v]){int u=e.first;int w=e.second;if(dist[cur.v]+w < dist[u]){dist[u]=dist[cur.v]+w;pq.push(Vertix(u,dist[u]));}}}
}

接下来看一道有意思的题目:力扣 1631.最小体力消耗路径。这道题跟求最短路径有点像,不过它求的是路径权重的最大值的最小值。

其实我们可以把Dijkstra算法中的 dist 抽象为“成本”这一概念,只要在寻找最小成本路径的过程中,这个成本是递增的,那么Dijkstra算法就是正确的。在求最短路径中,"成本"是权重和,因为无负值边,所以成本是递增的;那么本题中,“成本”是路径上权重的最大值,显然成本也是递增的。所以可以用Dijstra算法解决

class Vertix{
public:int x,y;int effort;Vertix(){}Vertix(int _x, int _y, int _effort){x=_x;y=_y;effort=_effort;}bool operator < (const Vertix v)const{return effort > v.effort;}
};class Solution {int row,col;int INF=0x3f3f3f3f;priority_queue<Vertix> pq;
public:int minimumEffortPath(vector<vector<int>>& heights) {row=heights.size();col=heights[0].size();int dist[row][col];for(int i=0;i<row;i++){for(int j=0;j<col;j++){dist[i][j]=INF;}}dist[0][0]=0;pq.push(Vertix(0,0,0));while(!pq.empty()){Vertix curV=pq.top();pq.pop();if(curV.x==row-1 && curV.y==col-1)return dist[row-1][col-1];const vector<vector<int>>& neighbor=neighbors(curV.x, curV.y);for(auto v:neighbor){int maxEffort=max(curV.effort,abs(heights[v[0]][v[1]]-heights[curV.x][curV.y]));if(dist[v[0]][v[1]]>maxEffort){dist[v[0]][v[1]]=maxEffort;pq.push(Vertix(v[0],v[1],dist[v[0]][v[1]]));}}}return dist[row-1][col-1];}int d[4][2]={{0,1},{0,-1},{-1,0},{1,0}};vector<vector<int>> neighbors(int x, int y){vector<vector<int>> res;for(int i=0;i<4;i++){int newX=x+d[i][0];int newY=y+d[i][1];if(newX<0 || newX>=row || newY<0 || newY>=col)continue;res.push_back({newX,newY});}return res;}
};

再来看一道题:力扣 1514.概率最大的路径

前面说过,Dijkstra算法的正确性的前提是,在寻找最小成本路径的过程中,这个成本是递增的。其实反过来也是对的。本题在寻找最大概率路径的过程中,这个概率是递减的。因此只有稍微修改一下代码,把小堆顶换成大堆顶,把判断条件反过来就可以了。

class Vertix{
public:int v;double succprob;Vertix(){}Vertix(int _v, double _succprob){v=_v;succprob=_succprob;}bool operator < (const Vertix v)const{return succprob < v.succprob;}
};class Solution {vector<vector<pair<int,double>>> graph;vector<double> dist;vector<bool> visited;priority_queue<Vertix> pq;
public:double maxProbability(int n, vector<vector<int>>& edges, vector<double>& succProb, int start, int end) {graph.resize(n);dist.resize(n,0);dist[start]=1;visited.resize(n);for(int i=0;i<edges.size();i++){graph[edges[i][0]].emplace_back(pair<int,double>(edges[i][1],succProb[i]));graph[edges[i][1]].emplace_back(pair<int,double>(edges[i][0],succProb[i]));}pq.push(Vertix(start,dist[start]));while(!pq.empty()){Vertix cur=pq.top();pq.pop();if(cur.v==end)return dist[end];if(visited[cur.v])       //重复入队情况continue;visited[cur.v]=true;for(auto& x:graph[cur.v]){if(dist[cur.v] * x.second > dist[x.first]){dist[x.first]=dist[curV.v]*x.second;pq.push(Vertix(x.first,dist[x.first]));}}}return dist[end];}
};

3.多源最短路——Floyd算法

Floyd算法适用于任何图,不管有向无向,边权正负,但是最短路必须存在。(不能有个负环)代码也十分简洁,三个 for 循环即可搞定

for (k = 1; k <= n; k++) {for (x = 1; x <= n; x++) {for (y = 1; y <= n; y++) {f[x][y] = min(f[x][y], f[x][k] + f[k][y]);}}
}

七、最小生成树

1.定义

一个无向图G的最小生成树是由该图的那些连接G的所有顶点的边构成的树,而且其总权重最低。

2.Prim算法

Prim算法是对点的贪心,该算法的基本思想是从一个结点开始,每次要选择距离最小的一个结点,不断加点。同Dijkstra算法的思想相同,用一个优先队列来存顶点,用 inMST 数组记录结点是否已在生成树中。同时,为了判断图是否连通,用 cnt 变量代表未在生成树的结点数量,若最后 cnt 不为0,说明图不连通。

例题:1584.连接所有点的最小费用

class Vertix{
public:int v;int cost;Vertix(){};Vertix(int _v, int _cost){v=_v;cost=_cost;}bool operator < (const Vertix V)const{return cost>V.cost;}
};class Solution {vector<vector<pair<int,int>>> graph;priority_queue<Vertix> pq;vector<bool> inMST;int cnt;int ans=0;
public:int minCostConnectPoints(vector<vector<int>>& points) {int n=points.size();graph.resize(n);inMST.resize(n);cnt=n;for(int i=0;i<n;i++){for(int j=i+1;j<n;j++){graph[i].emplace_back(pair<int,int>(j,abs(points[i][0]-points[j][0])+abs(points[i][1]-points[j][1])));graph[j].emplace_back(pair<int,int>(i,abs(points[i][0]-points[j][0])+abs(points[i][1]-points[j][1])));}}pq.push(Vertix(0,0));while(!pq.empty()){Vertix cur=pq.top();pq.pop();if(inMST[cur.v])continue;inMST[cur.v]=true;ans+=cur.cost;cnt--;if(cnt==0)break;for(auto& x:graph[cur.v]){if(!inMST[x.first])        //不能有环pq.push(Vertix(x.first,x.second));}}return ans;}
};

3.Kruskal算法

Kruskal算法是对边的贪心,该算法的基本思想是从小到大加入边。为了判断加入该边后是否会形成环,需要用并查集判断该边的两个端点是否已在最小生成树中。所以Kruskal算法是把森林合并成一颗树的过程。

同样以 1584.连接所有点的最小费用 为例

class Edge{
public:int v,u;int cost;Edge(){}Edge(int _v, int _u, int _cost){v=_v;u=_u;cost=_cost;}bool operator < (const Edge e)const{return cost<e.cost;}
};class Solution {vector<Edge> edges;vector<int> parent;int ans=0;int cnt;
public:int minCostConnectPoints(vector<vector<int>>& points) {int n=points.size();cnt=n;parent.resize(n);for(int i=0;i<n;i++){parent[i]=i;}for(int i=0;i<n;i++){for(int j=i+1;j<n;j++){edges.push_back(Edge(i,j,abs(points[i][0]-points[j][0])+abs(points[i][1]-points[j][1])));}}sort(edges.begin(),edges.end());for(auto& edge:edges){if(!isConnected(edge.v,edge.u)){Union(edge.v,edge.u);ans+=edge.cost;if(cnt==1)break;}}return ans;}

刷题心得04 图论基础和一些经典算法相关推荐

  1. 看了这篇 LeetCode 的刷题心得,再也不用抄别人代码了

    作者:VioletJack 原文:<LeetCode 算法题刷题心得>https://www.jianshu.com/p/8876704ea9c8 花了十几天,把<算法>看了一 ...

  2. 记录周三12.8的刷题心得ODAY

    记录本周周三的刷题心得ODAY PTA地址 Python获取每一位的数字,并返回到列表 三种方法 通过计算 通过自带方法 通过字符串 Python从键盘输入多行文本数据的方法 链接

  3. 算法刷题系列(四)蓝桥杯python算法训练3(下)

    上一次的节点选择算法由于春节过年耽搁了,现在重新补上 上篇链接:算法刷题系列(四)蓝桥杯python算法训练3 - 经验教训 在纷繁复杂的使用了列表来暂存数据之后,发现其实可以利用笔者自己不太常用的字 ...

  4. 企业面试之LeetCode刷题心得

    谈起刷LeetCode的心得,想要先扯点别的,说实话我是比较自虐的人,大学时候本专业从来不好好上,一直觊觎着别人的专业,因为自己文科生,总觉得没有项技术在身出门找工作都没有底气,然后看什么炫学什么,简 ...

  5. PAT乙级刷题心得和常用函数总结 (c++实现)

    开始先说最重要心得体会: 写代码前,先在纸上写写画画,写下伪码,理清思路,别上来就敲代码,效率极低还易出现bug. 2019-12-12到2020-01-17,用C++刷完了PAT乙级95道题目,第6 ...

  6. leetcode刷题心得

    本人以前大概搞过半年的算法,不是什么大佬,学得也不怎么样,一般般.leetcode只刷了200左右(没有水题),leetcode简单.中等级别的题目大部分都可以做.大部分公司的笔试题也还行,当然了像字 ...

  7. python题库刷题训练软件_Python基础练习100题 ( 11~ 20)

    刷题继续 上一期和大家分享了前10道题,今天继续来刷11~20 Question 11: Write a program which accepts a sequence of comma separ ...

  8. 前端牛客网刷题总结【 JS基础变量、数据类型、数据类型转换、运算符等】

    1.JavaScript是解释性语言.正确.体现在JS文件在完成之后,不会经过任何的编译.而是在运行时去解释执行.最好的例子就是,JS中一个函数有语法的错误,但是不会因为这个局部的错误而影响其他函数运 ...

  9. 力扣刷题心得(设计类题目)

    设计类题目基本考察的是你对现实事物的抽象能力,一般会遇到一些类的设计.字符串切分.集合的使用(list.map.set.stack.deque)等,结束后我会更新一些关于这些集合的常见使用方法和场景. ...

最新文章

  1. 【通俗理解线性代数】 -- 特殊的矩阵
  2. ganglia安装与配置
  3. 2345电脑管家_如何彻底清除流氓的2345安全卫士及2345SafeCenterSvc服务?
  4. vue通过class获取dom_.NET Core通过Json或直接获取图形验证码(务必收藏备用)
  5. linux rsync 安装教程,linux下的rsync配置和使用教程
  6. Seesaw Loss:一种面向长尾目标检测的平衡损失函数
  7. Xcode 项目忽略警告
  8. 蓝桥杯省赛真题C++java2013-2019
  9. python在线编辑器可视化_python软件——wxpython可视化编辑器 v4.1附使用教程
  10. oracle12c cdb修改,Oracle 12c 配置和修改 CDB 和 PDB 参数
  11. flutter 上滑悬浮吸顶
  12. HDU - 5773 贪心 + LIS
  13. hadoop fs,hadoop dfs以及hdfs dfs区别
  14. 华为鸿蒙HarmonyOS 简介
  15. 灰色简约大学生小组作业展示PPT模板
  16. matlab 离群值去除方法,数据清洗中异常值(离群值)的判别和处理方法
  17. Cadence Allegro 17.4 IBS文件处理(IBIS模型)
  18. Demo:校验_SAP刘梦_新浪博客
  19. 运动补偿和运动估计总结(MEMC)
  20. web连接蓝牙电子秤navigator.bluetooth

热门文章

  1. mysql里平均工资最高怎么做_SQL数据库 计算出每个部门的平均工资 最高工资和最低工资 语法怎么写?...
  2. 汇川660C系列CANOPEN 伺服通过HT3S-PNS-COP网关实现数据传输到西门子ProfiNet (S7-300/400/1200/1500)PLC
  3. python-OpenCV图像,像素说明(二)
  4. 人之间的尊重是相互的_人与人之间相互尊重的句子
  5. 你还在为你的妹子奋斗么
  6. 一周学会爬取网页-网易云课堂
  7. 签到题-1 装13 (10 分)
  8. 全球智能经济峰会暨第八届智博会在浙江省宁波市召开
  9. 关于新手对接单问题的几大建议
  10. 第1章 Python机器学习的生态系统