文章目录
- 1.找出星型图的中心节点
- 2.寻找图中是否存在路径
- 3.二进制链表转整数
- 4.旅行终点站
- 5.分发饼干
- 6.图片平滑器
- 7.克隆图
- 8.判断二分图
- 9.格雷编码
- 10.第K个语法符号
- 11.祖父节点值为偶数的节点和
- 12.所有可能的路径
- 13.找到最终的安全状态
- 14.节点间的通路
- 15.省份数量
- 16.课程表IV
- 17.到达目的地的方案数
- 18.通知所有员工所需的时间
- 解法一:普通DFS深度优先搜索
- 19.猜数字大小II
- 20.二分图
- 21.颜色交替最短路径
- 21.冗余连接
- 22.可以到达所有点的最小点数目
- 解法:寻找入度为0的节点
- 23.从第一个节点出发到最后一个节点的受限路径数
- 24.最佳直线
- 25.对链表进行插入排序
- 26.K站中转内最便宜的航班
- 27.概率最大的路径
- 28.最小高度树
- 29.阈值距离内邻居最少的城市
- 30.子树中标签相同的节点数
- 解法:DFS深度优先遍历+hash表
1.找出星型图的中心节点
有一个无向的 星型 图,由 n 个编号从 1 到 n 的节点组成。星型图有一个 中心 节点,并且恰有 n - 1 条边将中心节点与其他每个节点连接起来。
给你一个二维整数数组 edges ,其中 edges[i] = [ui, vi] 表示在节点 ui 和 vi 之间存在一条边。请你找出并返回 edges 所表示星型图的中心节点。
示例 1:
输入:edges = [[1,2],[2,3],[4,2]]
输出:2
解释:如上图所示,节点 2 与其他每个节点都相连,所以节点 2 是中心节点。
示例 2:
输入:edges = [[1,2],[5,1],[1,3],[1,4]]
输出:1
解法1:
可以看出,中心节点的度为n-1,所有找到度为n-1的节点就是中心节点
注意,c++的vector<int>a 如果没有赋予初始空间,a[i]会报错
代码:
class Solution {
public:
int findCenter(vector<vector<int>>& edges) {
int n=edges.size();
vector<int>nums(n+2,0);
for(int i=0;i<n;i++)
{
nums[edges[i][0]]++;
nums[edges[i][1]]++;
}
for(int i=1;i<=nums.size();i++){
if(nums[i]==n)
return i;
}
return 0;
}
};
执行结果:
时间复杂度:O(n)
空间复杂度:O(n)
2.寻找图中是否存在路径
有一个具有 n个顶点的 双向 图,其中每个顶点标记从 0 到 n - 1(包含 0 和 n - 1)。图中的边用一个二维整数数组 edges 表示,其中 edges[i] = [ui, vi] 表示顶点 ui 和顶点 vi 之间的双向边。 每个顶点对由 最多一条 边连接,并且没有顶点存在与自身相连的边。
请你确定是否存在从顶点 start 开始,到顶点 end 结束的 有效路径 。
给你数组 edges 和整数 n、start和end,如果从 start 到 end 存在 有效路径 ,则返回 true,否则返回 false 。
解法1:BFS
本题是双向图的BFS遍历,如果直接用二维数组来表示图,当顶点比较稀疏,顶点数量多的时候会超时。
顶点的编号是从0到n-1, 要减少搜索的计算量。
所以只记录顶点到它可以到达的顶点集合, 即采用vector<vector<pair<int, bool>>> matrix(n), 其中pair对的第一个元素是边的另一个顶点, 下标是边的起始顶点, bool表示当前顶点边是否访问过, 避免无限循环。
例:如示例1所示,0的邻接节点有1和2,即matrix[0]=[[(1,false)],[(2,false)]]。
访问一个顶点时,把它所有没有访问过的边都入队, 如果发现某条边的另一个顶点为要寻找的end顶点, 说明存在合法路径。全部遍历完成后,若队列为空, 还没有找到, 说明不存在, 返回false。
代码:
class Solution {
public:
bool validPath(int n, vector<vector<int> > &edges, int start, int end) {
if(n==1||start==end)
return true;
vector<vector<pair<int, bool>>>matrix(n);
for(int i=0;i<edges.size();i++)
{
matrix[edges[i][0]].push_back(make_pair(edges[i][1],false));
matrix[edges[i][1]].push_back(make_pair(edges[i][0],false));
}
queue<int>que;
que.push(start);
while(!que.empty()){
int start=que.front();
que.pop();
for(int i=0;i<matrix[start].size();i++)
{
if(matrix[start][i].second)
continue;
if(matrix[start][i].first!=end)
{
matrix[start][i].second= true;
que.push(matrix[start][i].first);
}else{
return true;
}
}
}
return false;
}
};
执行结果:
时间复杂度:O(n)
空间复杂度:O(n^2)
解法2 :并查集
因为是无向图,所以要判断起点和终点是否可达,可以判断起点和终点是否在一个集合下,采用并查集的算法框架,如果起点和终点的根节点坐标相同,那么说明可达。
其中采取了合并采取了路径压缩和按秩合并
具体并查集算法链接:https://zhuanlan.zhihu.com/p/93647900/
代码:
class Solution {
public:
int find(int x, int *fa) {
if(x==fa[x])
return x;
else{
fa[x]=find(fa[x],fa);//将沿途的所有节点的父结点都设置为根节点
return fa[x];
}
}
void merge(int i, int j, int *fa, int *rank) {
int x=find(i,fa);
int y=find(j,fa);
//将高度低的合并到高度高的树上,不会增加树得深度
if(rank[x]<=rank[y])
fa[x]=y;
else
fa[y]=x;
if(rank[x]==rank[y]&&x!=y){
rank[y]++;
}
}
bool validPath(int n, vector<vector<int> > &edges, int start, int end) {
if(n==1||start==end)
return true;
int fa[n],rank[n];
iota(fa,fa+n,0);
memset(rank,1, sizeof(rank));
for(auto edge:edges){
merge(edge[0],edge[1],fa,rank);
}
return find(start,fa)==find(end,fa);
}
};
执行结果:
时间复杂度:O(n)
空间复杂度:O(n)
3.二进制链表转整数
给你一个单链表的引用结点 head。链表中每个结点的值不是 0 就是 1。已知此链表是一个整数数字的二进制表示形式。
请你返回该链表所表示数字的 十进制值 。
解法一:栈+累加
用栈将链表的值一一保存,然后弹栈,使用math.h 的pow函数进行平方计算,得到最终结果
代码:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
int getDecimalValue(ListNode* head) {
stack<int> stack;
ListNode *tmp=head;
while(tmp!= nullptr)
{
stack.push(tmp->val);
tmp=tmp->next;
}
int sum=0;
int size=stack.size();
for(int i=0;i<size;i++)
{
int n=stack.top();
stack.pop();
sum=sum+pow(2,i)*n;
}
return sum;
}
};
执行结果:
时间复杂度:O(n)
空间复杂度:O(n)
解法二:不需要栈,二进制的带移位的加法可以用或运算代替
转为二进制的过程,就是从head指针将数字不断左移并相加的过程,因为左移后不包含进位,可以用或运算来代替加运算,节省时间
代码:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
int getDecimalValue(ListNode* head) {
int res=0;
while(head!=nullptr)
{
res<<=1;
res=res|head->val;
head=head->next;
}
return res;
}
};
执行结果:
时间复杂度:O(n)
空间复杂度:O(1)
4.旅行终点站
给你一份旅游线路图,该线路图中的旅行线路用数组 paths 表示,其中 paths[i] = [cityAi, cityBi] 表示该线路将会从 cityAi 直接前往 cityBi 。请你找出这次旅行的终点站,即没有任何可以通往其他城市的线路的城市。
题目数据保证线路图会形成一条不存在循环的线路,因此恰有一个旅行终点站。
解法一:hash表法
根据终点站的定义,重点在不会出现在在cityAi中,因为存在从cityAi出发的线路,所以终点站只会出现在cityBi中。可以将cityAi存于set结构中,遍历cityBi,返回不在数组中的元素,就是答案。
代码:
class Solution {
public:
string destCity(vector<vector<string>>& paths) {
set<string>sources;
for(auto &path:paths){
sources.insert(path[0]);
}
for(auto&path:paths){
if(sources.find(path[1])==sources.end())
{
return path[1];
}
}
return "";
}
};
执行结果:
时间复杂度:O(nm),其中 n** 是数组 paths 的长度,m 是城市名称的最大长度。
空间复杂度:O(nm)
5.分发饼干
假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。
对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。
示例 1:
输入: g = [1,2,3], s = [1,1]
输出: 1
解释: 你有三个孩子和两块小饼干,3个孩子的胃口值分别是:1,2,3。
虽然你有两块小饼干,由于他们的尺寸都是1,你只能让胃口值是1的孩子满足。
所以你应该输出1。
示例 2:
输入: g = [1,2], s = [1,2,3]
输出: 2
解释:
你有两个孩子和三块小饼干,2个孩子的胃口值分别是1,2。
你拥有的饼干数量和尺寸都足以让所有孩子满足。
所以你应该输出2.
解法一:贪心算法
首先一个小朋友只吃一个饼干,先对g和s进行一个排序操作,首先对数组 g 和 s 排序,然后从小到大遍历 g 中的每个元素,对于每个元素找到能满足该元素的 s 中的最小的元素。具体而言,令 i 是 g 的下标,j 是 s 的下标,初始时 i 和 j 都为 00,进行如下操作。
对于每个元素 g[i],找到未被使用的最小的 j 使得 g[i]≤s[j],则 s[j] 可以满足 g[i]。由于 g 和 s 已经排好序,因此整个过程只需要对数组 g 和 s 各遍历一次。当两个数组之一遍历结束时,说明所有的孩子都被分配到了饼干,或者所有的饼干都已经被分配或被尝试分配(可能有些饼干无法分配给任何孩子),此时被分配到饼干的孩子数量即为可以满足的最多数量。
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
代码:
class Solution {
public:
int findContentChildren(vector<int>& g, vector<int>& s) {
sort(g.begin(),g.end());
sort(s.begin(),s.end());
int i=0;
int j=0;
int numG=g.size();
int numS=s.size();
int cout=0;
while(i<numG&&j<numS)
{
if(g[i]>s[j])
{
j++;
}
else{
i++;
j++;
cout++;
}
}
return cout;
}
};
执行结果:
时间复杂度:O(mlogm+nlogn)
空间复杂度:O(mlogm+nlogn)
6.图片平滑器
包含整数的二维矩阵 M 表示一个图片的灰度。你需要设计一个平滑器来让每一个单元的灰度成为平均灰度 (向下舍入) ,平均灰度的计算是周围的8个单元和它本身的值求平均,如果周围的单元格不足八个,则尽可能多的利用它们。
示例 1:
输入:
[[1,1,1],
[1,0,1],
[1,1,1]]
输出:
[[0, 0, 0],
[0, 0, 0],
[0, 0, 0]]
解释:
对于点 (0,0), (0,2), (2,0), (2,2): 平均(3/4) = 平均(0.75) = 0
对于点 (0,1), (1,0), (1,2), (2,1): 平均(5/6) = 平均(0.83333333) = 0
对于点 (1,1): 平均(8/9) = 平均(0.88888889) = 0
解法:遍历矩阵
对于矩阵中的每一个单元格,只要找到9个包括它自身在内的紧邻的格子里的数组,将其相加取平困即可。然后取得的结果向下取整保存在和矩阵相同的二维数组中即可。
假设一个矩阵的节点为(x,y)则以它为中心的9个节点为
(x-1,y-1) | (x-1,y) | (x-1,y+1) |
---|---|---|
(x,y-1) | (x,y) | (x,y+1) |
(x+1,y-1) | (x+1,y) | (x+1,y+1) |
同时需要判断这九个节点是否都在这个矩阵的范围内,如果是则count技术加1,这九个节点的和都相加保存导result二维数组中。
代码:
class Solution {
public:
vector<vector<int>> imageSmoother(vector<vector<int>>& img) {
int x=img.size();
int y=img[0].size();
vector<vector<int>> result(x,vector<int>(y,0));
for(int i=0;i<x;i++)
for(int j=0;j<y;j++)
{
int count=0;
for(int nx=i-1;nx<=i+1;nx++)
for(int ny=j-1;ny<=j+1;ny++)
{
if(nx>=0&&nx<x&&ny>=0&&ny<y)
{
result[i][j]+=img[nx][ny];
count++;
}
}
result[i][j]=result[i][j]/count;
}
return result;
}
};
执行结果:
时间复杂度:O(N)其中 N 是图片中像素的数目。我们需要将每个像素都遍历一遍。
空间复杂度:O(N)我们答案的大小。
7.克隆图
给你无向 连通 图中一个节点的引用,请你返回该图的 深拷贝(克隆)。
图中的每个节点都包含它的值 val(int) 和其邻居的列表(list[Node])。
class Node {
public int val;
public List<Node> neighbors;
}
测试用例格式:
简单起见,每个节点的值都和它的索引相同。例如,第一个节点值为 1(val = 1),第二个节点值为 2(val = 2),以此类推。该图在测试用例中使用邻接列表表示。
邻接列表 是用于表示有限图的无序列表的集合。每个列表都描述了图中节点的邻居集。
给定节点将始终是图中的第一个节点(值为 1)。你必须将 给定节点的拷贝 作为对克隆图的引用返回。
解法一:深度优先搜索
c++STL库知识
C++ STL库小知识
unorderd_map 和map
需要引入的头文件不同
map: #include < map >
unordered_map: #include < unordered_map >内部实现机理不同
map: map内部实现了一个红黑树(红黑树是非严格平衡二叉搜索树,而AVL是严格平衡二叉搜索树),红黑树具有自动排序的功能,因此map内部的所有元素都是有序的,红黑树的每一个节点都代表着map的一个元素。因此,对于map进行的查找,删除,添加等一系列的操作都相当于是对红黑树进行的操作。map中的元素是按照二叉搜索树(又名二叉查找树、二叉排序树,特点就是左子树上所有节点的键值都小于根节点的键值,右子树所有节点的键值都大于根节点的键值)存储的,使用中序遍历可将键值按照从小到大遍历出来。
unordered_map: unordered_map内部实现了一个哈希表(也叫散列表,通过把关键码值映射到Hash表中一个位置来访问记录,查找的时间复杂度可达到O(1),其在海量数据处理中有着广泛应用)。因此,其元素的排列顺序是无序的。哈希表详细介绍优缺点以及适用处
map:优点:
有序性,这是map结构最大的优点,其元素的有序性在很多应用中都会简化很多的操作
红黑树,内部实现一个红黑书使得map的很多操作在lgn的时间复杂度下就可以实现,因此效率非常的高
缺点: 空间占用率高,因为map内部实现了红黑树,虽然提高了运行效率,但是因为每一个节点都需要额外保存父节点、孩子节点和红/黑性质,使得每一个节点都占用大量的空间适用处:对于那些有顺序要求的问题,用map会更高效一些
unordered_map:
优点: 因为内部实现了哈希表,因此其查找速度非常的快
缺点: 哈希表的建立比较耗费时间
适用处:对于查找问题,unordered_map会更加高效一些,因此遇到查找问题,常会考虑一下用unordered_mapvector的emplace_back和push_back
在
C++11
之后,vector
容器中添加了新的方法:emplace_back()
,和push_back()
一样的是都是在容器末尾添加一个新的元素进去,不同的是emplace_back()
在效率上相比较于push_back()
有了一定的提升。push_back():
首先需要调用构造函数构造一个临时对象,然后调用拷贝构造函数将这个临时对象放入容器中,然后释放临时变量。
emplace_back():
这个元素原地构造,不需要触发拷贝构造和转移构造。
对于本题而言,我们需要明确图的深拷贝是在做什么,对于一张图而言,它的深拷贝即构建一张与原图结构,值均一样的图,但是其中的节点不再是原来图节点的引用。因此,为了深拷贝出整张图,我们需要知道整张图的结构以及对应节点的值。
由于题目只给了我们一个节点的引用,因此为了知道整张图的结构以及对应节点的值,我们需要从给定的节点出发,进行「图的遍历」,并在遍历的过程中完成图的深拷贝。
为了避免在深拷贝时陷入死循环,我们需要理解图的结构。对于一张无向图,任何给定的无向边都可以表示为两个有向边,即如果节点 A 和节点 B 之间存在无向边,则表示该图具有从节点 A 到节点 B 的有向边和从节点 B 到节点 A 的有向边。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4j6NQZNh-1651402801859)(C:\Users\gcc\AppData\Roaming\Typora\typora-user-images\image-20220125154046500.png)]
为了防止多次遍历同一个节点,陷入死循环,我们需要用一种数据结构记录已经被克隆过的节点。
算法
使用一个哈希表存储所有已被访问和克隆的节点。哈希表中的 key 是原始图中的节点,value 是克隆图中的对应节点。
从给定节点开始遍历图。如果某个节点已经被访问过,则返回其克隆图中的对应节点。
如下图,我们给定无向边边 A - B,表示 A 能连接到 B,且 B 能连接到 A。如果不对访问过的节点做标记,则会陷入死循环中。
3.如果当前访问的节点不在哈希表中,则创建它的克隆节点并存储在哈希表中。注意:在进入递归之前,必须先创建克隆节点并保存在哈希表中。如果不保证这种顺序,可能会在递归中再次遇到同一个节点,再次遍历该节点时,陷入死循环。
4.递归调用每个节点的邻接点。每个节点递归调用的次数等于邻接点的数量,每一次调用返回其对应邻接点的克隆节点,最终返回这些克隆邻接点的列表,将其放入对应克隆节点的邻接表中。这样就可以克隆给定的节点和其邻接点。
代码:
/*
// Definition for a Node.
class Node {
public:
int val;
vector<Node*> neighbors;
Node() {
val = 0;
neighbors = vector<Node*>();
}
Node(int _val) {
val = _val;
neighbors = vector<Node*>();
}
Node(int _val, vector<Node*> _neighbors) {
val = _val;
neighbors = _neighbors;
}
};
*/
class Solution {
public:
unordered_map <Node*,Node*>visited;
Node* cloneGraph(Node* node) {
if(node== nullptr)
return node;
// 如果该节点已经被访问过了,则直接从哈希表中取出对应的克隆节点返回
if(visited.find(node)!=visited.end())
{
return visited[node];
}
// 克隆节点,注意到为了深拷贝我们不会克隆它的邻居的列表
Node* copynode=new Node(node->val);
/// 哈希表存储
visited[node]=copynode;
// 遍历该节点的邻居并更新克隆节点的邻居列表
for(auto neigh:node->neighbors)
{
copynode->neighbors.push_back(cloneGraph(neigh));
}
return copynode;
}
};
执行结果:
时间复杂度:O(N)其中 N 表示节点数量。深度优先搜索遍历图的过程中每个节点只会被访问一次。
空间复杂度:O(N) 存储克隆节点和原节点的哈希表需要 O(N) 的空间,递归调用栈需要 O(H)的空间,其中 H 是图的深度,经过放缩可以得到 O(H) = O(N),因此总体空间复杂度为 O(N)。
解法二:广度优先搜索
算法
使用一个哈希表 visited 存储所有已被访问和克隆的节点。哈希表中的 key 是原始图中的节点,value 是克隆图中的对应节点。
将题目给定的节点添加到队列。克隆该节点并存储到哈希表中。
每次从队列首部取出一个节点,遍历该节点的所有邻接点。如果某个邻接点已被访问,则该邻接点一定在 visited 中,那么从 visited 获得该邻接点,否则创建一个新的节点存储在 visited 中,并将邻接点添加到队列。将克隆的邻接点添加到克隆图对应节点的邻接表中。重复上述操作直到队列为空,则整个图遍历结束。
代码:
/*
// Definition for a Node.
class Node {
public:
int val;
vector<Node*> neighbors;
Node() {
val = 0;
neighbors = vector<Node*>();
}
Node(int _val) {
val = _val;
neighbors = vector<Node*>();
}
Node(int _val, vector<Node*> _neighbors) {
val = _val;
neighbors = _neighbors;
}
};
*/
class Solution {
public:
unordered_map <Node*,Node*>visited;
Node* cloneGraph(Node* node) {
if(node== nullptr)
return node;
queue<Node*>queue;
queue.push(node);
//将第一个节点加入队列并且复制
Node *copynode=new Node(node->val);
visited[node]=copynode;
while(!queue.empty())
{
Node* node=queue.front();
queue.pop();
//复制队列的邻居节点,如果未访问过就加入队列,构造节点的邻居关系
for(auto neigh:node->neighbors)
{
if(visited.find(neigh)==visited.end())
{
Node * n=new Node(neigh->val);
visited[neigh]=n;
queue.push(neigh);
}
visited[node]->neighbors.push_back(visited[neigh]);
}
}
return visited[node];
}
};
执行结果:
时间复杂度:O(N),其中 NN 表示节点数量。广度优先搜索遍历图的过程中每个节点只会被访问一次。
空间复杂度:O(N)。哈希表使用 O(N) 的空间。广度优先搜索中的队列在最坏情况下会达到 O(N)的空间复杂度,因此总体空间复杂度为 O(N)
8.判断二分图
存在一个 无向图 ,图中有 n 个节点。其中每个节点都有一个介于 0 到 n - 1 之间的唯一编号。给你一个二维数组 graph ,其中 graph[u] 是一个节点数组,由节点 u 的邻接节点组成。形式上,对于 graph[u] 中的每个 v ,都存在一条位于节点 u 和节点 v 之间的无向边。该无向图同时具有以下属性:
不存在自环(graph[u] 不包含 u)。
不存在平行边(graph[u] 不包含重复值)。
如果 v 在 graph[u] 内,那么 u 也应该在 graph[v] 内(该图是无向图)
这个图可能不是连通图,也就是说两个节点 u 和 v 之间可能不存在一条连通彼此的路径。
二分图 定义:如果能将一个图的节点集合分割成两个独立的子集 A 和 B ,并使图中的每一条边的两个节点一个来自 A 集合,一个来自 B 集合,就将这个图称为 二分图 。
如果图是二分图,返回 true ;否则,返回 false 。
解法一:DFS
对于图中的任意两个节点 u 和 v,如果它们之间有一条边直接相连,那么 u 和 v 必须属于不同的集合。
如果给定的无向图连通,那么我们就可以任选一个节点开始,给它染成红色。随后我们对整个图进行遍历,将该节点直接相连的所有节点染成绿色,表示这些节点不能与起始节点属于同一个集合。我们再将这些绿色节点直接相连的所有节点染成红色,以此类推,直到无向图中的每个节点均被染色。
如果我们能够成功染色,那么红色和绿色的节点各属于一个集合,这个无向图就是一个二分图;如果我们未能成功染色,即在染色的过程中,某一时刻访问到了一个已经染色的节点,并且它的颜色与我们将要给它染上的颜色不相同,也就说明这个无向图不是一个二分图。
算法的流程如下:
我们任选一个节点开始,将其染成红色,并从该节点开始对整个无向图进行遍历;
在遍历的过程中,如果我们通过节点 u 遍历到了节点 v(即 u 和 v 在图中有一条边直接相连),那么会有两种情况:
如果 v 未被染色,那么我们将其染成与 u 不同的颜色,并对 v 直接相连的节点进行遍历;
如果 v 被染色,并且颜色与 u 相同,那么说明给定的无向图不是二分图。我们可以直接退出遍历并返回 False 作为答案。
当遍历结束时,说明给定的无向图是二分图,返回 True 作为答案。
注意:题目中给定的无向图不一定保证连通,因此我们需要进行多次遍历,直到每一个节点都被染色,或确定答案为}False 为止。每次遍历开始时,我们任选一个未被染色的节点,将所有与该节点直接或间接相连的节点进行染色。
代码:
class Solution {
public:
static const int NCOLOR=0;
static const int RED=1;
static const int GREEN=2;
vector<int>color;
bool isValid;
void dfs(int node,int c, vector<vector<int>>&graph)
{
color[node]=c;
int cNext=(c==RED?GREEN:RED);
for(int neigh:graph[node])
{
if(color[neigh]==NCOLOR)
{
dfs(neigh,cNext,graph);
if(!isValid)
{
return;
}
}
else if(cNext!=color[neigh]){
isValid= false;
return;
}
}
}
bool isBipartite(vector<vector<int>>& graph) {
int n=graph.size();
isValid= true;
color.assign(n,NCOLOR);
for(int i=0;i<n;i++)
{
if(color[i]==NCOLOR)
{
dfs(i,RED,graph);
if(!isValid)
{
return isValid;
}
}
}
return isValid;
}
};
const int Solution::NCOLOR;
const int Solution::RED;
const int Solution::GREEN;
执行结果:
时间复杂度:O(N+M),其中 N 和 M 分别是无向图中的点数和边数。
空间复杂度:O(N),存储节点颜色的数组需要 O(N) 的空间,并且在深度优先搜索的过程中,栈的深度最大为 N,需要 O(N) 的空间。
C++库相关知识
注意:C++类内声明的静态常量,必须在C++类外在声明一遍,就是.h文件中声明的,.cpp文件必须声明,不然编译链接报错:undefined reference to `Solution7::NCOLOR’
解法二:BFS
思路同上:
代码:
class Solution {
public:
static const int NCOLOR=0;
static const int RED=1;
static const int GREEN=2;
vector<int>color;
bool isBipartite(vector<vector<int>>& graph) {
int n=graph.size();
vector<int>color(n,NCOLOR);
for(int i=0;i<n;i++)
{
if(color[i]==NCOLOR)
{
queue<int>queue;
queue.push(i);
color[i]=RED;
while(!queue.empty())
{
int node=queue.front();
int cNext=(color[node]==RED?GREEN:RED);
queue.pop();
for(int nei:graph[node])
{
if(color[nei]==NCOLOR)
{
queue.push(nei);
color[nei]=cNext;
}
else if(color[nei]!=cNext)
{
return false;
}
}
}
}
}
return true;
}
};
const int Solution::NCOLOR;
const int Solution::RED;
const int Solution::GREEN;
执行结果:
时间复杂度:O(N+M),其中 N 和 M 分别是无向图中的点数和边数。
空间复杂度:O(N),存储节点颜色的数组需要 O(N) 的空间,并且在广度优先搜索的过程中,队列中最多有 N-1个节点,需要 O(N) 的空间。。
9.格雷编码
n 位格雷码序列 是一个由 2n 个整数组成的序列,其中:
每个整数都在范围 [0, 2n - 1] 内(含 0 和 2n - 1)
第一个整数是 0
一个整数在序列中出现 不超过一次
每对 相邻 整数的二进制表示 恰好一位不同 ,且
第一个 和 最后一个 整数的二进制表示 恰好一位不同
给你一个整数 n ,返回任一有效的 n 位格雷码序列 。
解法一:对称法
解释:格雷码是有对称规律的,0位数格雷码G0=[0]
一位格雷码:[0,1]
二位格雷码:[00,01,11,10]
三位格雷码:[000,001,011,010,110,111,101,100]
…
可以得到规律:比如二位格雷码就是首先对1位格雷码对称翻转[1,0]
然后对1位格雷码加上2^1得到[11,10],然后和1位格雷码[0,1]拼接
得到[00,01,11,10]
按照这个规律可模拟代码
代码:
class Solution {
public:
vector<int> grayCode(int n) {
vector<int>ret;
ret.reserve(n);
//G[0]=0
ret.push_back(0);
for(int i=1;i<=n;i++)
{
//m是动态变化的
int m=ret.size();
for(int j=m-1;j>=0;j--)
{
//下一位是前一位加上1左移2^n-1
ret.push_back(ret[j]|(1<<i-1));
}
}
return ret;
}
};
执行结果:
解法二:二进制转格雷码
解法:因为二进制数字和格雷码中间是有转换关系的
例如n=3是的格雷码序列为[000,001,011,010,110,111,101,100]
序号为6的格雷码为101
如何得来的呢?
数字6的二进制为110,那么将第一位补一个0,组成[0,1,1,0]
从最后一位开始异或,1^0=1, 1^1=0, 0^1=1,即为101为相应格雷码
所以101=[110]1,就是这个二进制数异或它右移1为后的结果
代码:
class Solution {
public:
vector<int> grayCode(int n) {
vector<int>ret;
ret.reserve(n);
int size=1<<n;
for(int i=0;i<size;i++)
ret.push_back((i>>1)^i);
return ret;
}
};
执行结果:
时间复杂度:O(2^n)其中 n 为格雷码序列的位数。每个整数转换为格雷码的时间复杂度为 O(1),总共有 2^n
空间复杂度:O(1).注意返回值不计入空间复杂度。
10.第K个语法符号
在第一行我们写上一个 0
。接下来的每一行,将前一行中的0
替换为01
,1
替换为10
。
给定行数 N
和序数 K
,返回第 N
行中第 K
个字符。(K
从1开始)
解法:递推找规律
总体思路可以采用递归的方式,因为求每第N行的第K个位置,可以转换为求第N-1行的第K个位置,或者第N-1行的第K-length/2的位置的数字取反,最后递推到第一个位置一定是返回0的。
代码:
class Solution {
public:
int kthGrammar(int n, int k) {
if(n==1)
return 0;
int length=1<<(n-1);
//如果超过一半,需要求它在n-1行的第k-length/2位置的数字取反
if(k>length/2){
int val=kthGrammar(n-1,k-length/2);
return val==0?1:0;
}
else{
//不超过那么直接求第n-1行的第k个位置
return kthGrammar(n-1,k);
}
}
};
执行结果:
时间复杂度:O(n)
空间复杂度:O(1)
11.祖父节点值为偶数的节点和
给你一棵二叉树,请你返回满足以下条件的所有节点的值之和:
该节点的祖父节点的值为偶数。(一个节点的祖父节点是指该节点的父节点的父节点。)
如果不存在祖父节点值为偶数的节点,那么返回 0 。
解法一:深度优先搜索
我们可以通过深度优先搜索找出所有满足题目要求的节点。
具体地,在进行搜索时,搜索状态除了当前节点之外,还需要存储该节点的祖父节点和父节点,即三元组 (grandparent, parent, node)。如果节点 grandparent 的值为偶数,那么就将节点 node 的值加入答案。在这之后,我们继续搜索节点 node 的左孩子 (parent, node, node.left) 以及右孩子 (parent, node, node.right),直到搜索结束。
代码:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
int count=0;
void dfs(TreeNode* grandparent ,TreeNode* parent,TreeNode*node){
if(node== nullptr)
{
return;
}
if(grandparent->val%2==0)
{
count+=node->val;
}
dfs(parent,node,node->left);
dfs(parent,node,node->right);
}
int sumEvenGrandparent(TreeNode* root) {
if(root->left)
{
dfs(root,root->left,root->left->left);
dfs(root,root->left,root->left->right);
}
if(root->right)
{
dfs(root,root->right,root->right->left);
dfs(root,root->right,root->right->right);
}
return count;
}
};
执行结果:
- 时间复杂度:O(N),其中 N 是树中的节点个数。
- 空间复杂度:O(H),其中 H是树的高度。
解法二:广度优先搜索
我们也可以换一种思考方式。既然要找出祖父节点的值为偶数的节点,我们不如找到所有值为偶数的节点,并对这些节点的孙子节点(即子节点的子节点)统计答案。
这样我们就可以使用广度优先搜索遍历整棵树,当我们找到一个值为偶数的节点时,我们将该节点的所有孙子节点的值加入答案。如果它的子节点不为空,那么就将它的子节点加入队列中,直到队列为空,说明所有节点已经遍历完了,可以将结果返回。
代码:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
int count=0;
int sumEvenGrandparent(TreeNode* root) {
queue<TreeNode*>que;
que.push(root);
while(!que.empty())
{
TreeNode * node =que.front();
que.pop();
if(node->val%2==0)
{
if(node->left)
{
if(node->left->left)
{
count+=node->left->left->val;
}
if(node->left->right)
{
count+=node->left->right->val;
}
}
if(node->right)
{
if(node->right->left)
{
count+=node->right->left->val;
}
if(node->right->right)
{
count+=node->right->right->val;
}
}
}
if(node->left)
que.push(node->left);
if(node->right)
que.push(node->right);
}
return count;
}
};
执行结果:
- 时间复杂度:O(N),其中 N是树中的节点个数。
- 空间复杂度:O(N)
12.所有可能的路径
给你一个有 n 个节点的 有向无环图(DAG),请你找出所有从节点 0 到节点 n-1 的路径并输出(不要求按特定顺序)
graph[i] 是一个从节点 i 可以访问的所有节点的列表(即从节点 i 到节点
g
r
a
p
h
[
i
]
[
j
]
graph[i][j]
graph[i][j]
存在一条有向边)。
解法一:深度优先搜索
因为所要求的路径是从0到n-1节点的所有路径,可以从0号节点出发,采用深度优先遍历算法遍历节点,如果遍历到n-1号,就将结果路径记录到答案中。记录路径的过程中要注意到result的回溯
即当一个节点加入路径后,要及时进行弹出
同时注意题目中给出的是有向无环图(DAG),搜索的过程中肯定不会反复遍历节点,素以不需要判断当前节点是否遍历过。
代码:
class Solution {
public:
vector<vector<int>> allpath;
vector<int> result;
void dfs(int i, int n, vector<vector<int>> &graph) {
if (i == n) {
allpath.push_back(result);
return;
}
for (auto neigh:graph[i]) {
result.push_back(neigh);
dfs(neigh, n, graph);
result.pop_back();
}
}
vector<vector<int>> allPathsSourceTarget(vector<vector<int>> &graph) {
int n = graph.size();
result.push_back(0);
dfs(0, n - 1, graph);
return allpath;
}
};
执行结果:
时间复杂度:O(n×2^n),其中 n 为图中点的数量。我们可以找到一种最坏情况,即每一个点都可以去往编号比它大的点。此时路径数为 O(2^n)且每条路径长度为 O(n),因此总时间复杂度为 O(n*2^n)
空间复杂度:O(n),其中 n 为点的数量。主要为栈空间的开销。注意返回值不计入空间复杂度。
解法二:广度优先搜索
利用广度优先搜索也是可以解决的,就是没有深度优先搜索方便,因为需要在搜索每一层时记录从源节点到这个节点的路径,所以队列中需要保存的是具体的路径,如示例1中的例子,刚开始为{0},遍历0的邻居的节点的时候,应该把路径{0,1},{0,2}保存到队列中,这样才能保证当遍历到最终节点的时候可以有一整条的路径,并且也要主要bfs中的搜索和回溯。
代码:
class Solution {
public:
vector<vector<int>> allpath;
vector<vector<int>> allPathsSourceTarget(vector<vector<int>> &graph) {
queue<vector<int>>que;
que.push({0});
int n=graph.size();
while(!que.empty()){
vector<int>path=que.front();
que.pop();
int cur=path.back();
if(cur==n-1){
allpath.push_back(path);
continue;
}
for(int neigh:graph[cur])
{
path.push_back(neigh);
que.push(path);//加入队列搜索
path.pop_back();//回溯
}
}
return allpath;
}
};
执行结果:
13.找到最终的安全状态
在有向图中,以某个节点为起始节点,从该点出发,每一步沿着图中的一条有向边行走。如果到达的节点是终点(即它没有连出的有向边),则停止。
对于一个起始节点,如果从该节点出发,无论每一步选择沿哪条有向边行走,最后必然在有限步内到达终点,则将该起始节点称作是 安全 的。
返回一个由图中所有安全的起始节点组成的数组作为答案。答案数组中的元素应当按 升序
排列。
该有向图有 n 个节点,按 0 到 n - 1 编号,其中 n 是 graph 的节点数。图以下述形式给出:graph[i] 是编号 j 节点的一个列表,满足 (i, j) 是图的一条有向边。
解法一:三色标记法DFS
根据题意,若起始节点位于一个环内,或者能到达一个环,则该节点不是安全的。否则,该节点是安全的。
我们可以使用深度优先搜索来找环,并在深度优先搜索时,用三种颜色对节点进行标记,标记的规则如下:
白色(用 0 表示):该节点尚未被访问;
灰色(用 1 表示):该节点位于递归栈中,或者在某个环上;
黑色(用 2 表示):该节点搜索完毕,是一个安全节点。
当我们首次访问一个节点时,将其标记为灰色,并继续搜索与其相连的节点。
如果在搜索过程中遇到了一个灰色节点,则说明找到了一个环,此时退出搜索,栈中的节点仍保持为灰色,这一做法可以将「找到了环」这一信息传递到栈中的所有节点上。
如果搜索过程中没有遇到灰色节点,则说明没有遇到环,那么递归返回前,我们将其标记由灰色改为黑色,即表示它是一个安全的节点。
代码:
class Solution {
public:
vector<int>visited;
bool dfs(int i, vector<vector<int>>&graph){
if(visited[i]==1)//访问过的顶点
return false;
else if(visited[i]==2)//安全的顶点
return true;
visited[i]=1;
for(int neigh:graph[i]){
if(!dfs(neigh,graph)){
return false;
}
}
visited[i]=2;
return true;
}
vector<int> eventualSafeNodes(vector<vector<int>>& graph) {
vector<int>result;
int n=graph.size();
visited.assign(n,0);
for(int i=0;i<n;i++){
if(dfs(i,graph))
result.push_back(i);
}
return result;
}
};
执行结果:
时间复杂度:O(n+m),其中 n是图中的点数,m 是图中的边数。
空间复杂度:O(n)。存储节点颜色以及递归栈的开销均为 O(n)
解法二:反向图+拓扑排序
图论知识:拓扑排序
在图论中,拓扑排序(Topological Sorting)是一个有向无环图(DAG, Directed Acyclic Graph)的所有顶点的线性序列。且该序列必须满足下面两个条件:
- 每个顶点出现且只出现一次。
- 存在一条从顶点 A 到顶点 B 的路径,那么在序列中顶点 A 出现在顶点 B 的前面。
有向无环图(DAG)才有拓扑排序,非DAG图没有拓扑排序一说。在一个有向图中,对所有的节点进行排序,要求没有一个节点指向它前面的节点。
先统计所有节点的入度,对于入度为0的节点就可以分离出来,然后把这个节点指向的节点的入度减一。
一直做改操作,直到所有的节点都被分离出来。
如果最后不存在入度为0的节点,那就说明有环,不存在拓扑排序
告诉咱一个图,图中都是有向边,要我们找出所有一定会走死的路(笑)
比较标准的拓扑排序了,大伙离散课上都学过。
讲一下拓扑排序是咋回事吧。
一个点是否走死和它的出度有关,即如果一个点的出度为零,它无路可走,则它一定走死。
反推,如果一个点所到达的都是出度为0的点,则这个点也一定会走死。
再推,如果一个点到达的所有点都是走死的点,则这个点一定走死。
以上,则我们找出所有出度为0的点,并让所有到达此点的点出度-1,循坏往复,直至找出所有会走死的点
算法思路:
根据这一性质,我们可以将图中所有边反向,得到一个反图,然后在反图上运行拓扑排序。
具体来说,首先得到反图rg 及其入度数组inDeg。将所有入度为 0 的点加入队列,然后不断取出队首元素,将其出边相连的点的入度减一,若该点入度减一后为 0,则将该点加入队列,如此循环直至队列为空。循环结束后,所有入度为 0 的节点均为安全的。我们遍历入度数组,并将入度为 0 的点加入答案列表。
代码:
class Solution {
public:
vector<int> eventualSafeNodes(vector<vector<int>>& graph) {
int n=graph.size();
vector<vector<int>>rerverse(n);
vector<int>rudu(n);
for(int i=0;i<n;i++)
{
for(int neigh:graph[i]){
rerverse[neigh].push_back(i);
}
rudu[i]=graph[i].size();
}
queue<int>que;
for(int i=0;i<n;i++){
if(rudu[i]==0){
que.push(i);
}
}
while(!que.empty()){
int node=que.front();
que.pop();
for(int x:rerverse[node]){
if(--rudu[x]==0)
que.push(x);
}
}
vector<int>result;
for(int i=0;i<n;i++){
if(rudu[i]==0){
result.push_back(i);
}
}
return result;
}
};
执行结果:
时间复杂度:O(n+m),其中 n 是图中的点数,m 是图中的边数。
空间复杂度:O(n+m)。需要 O(n+m)的空间记录反图。
14.节点间的通路
节点间通路。给定有向图,设计一个算法,找出两个节点之间是否存在一条路径。
示例1:
输入:n = 3, graph = [[0, 1], [0, 2], [1, 2], [1, 2]], start = 0, target = 2
输出:true
示例2:
输入:n = 5, graph = [[0, 1], [0, 2], [0, 4], [0, 4], [0, 1], [1, 3], [1, 4], [1, 3], [2, 3], [3, 4]], start = 0, target = 4
输出 true
解法一:BFS宽度优先搜索
利用map<int,vector>的结构记录邻接表关系,然后进行广度优先遍历,找到节点返回true
代码:
class Solution {
public:
bool findWhetherExistsPath(int n, vector<vector<int>>& graph, int start, int target) {
//创建邻接表
unordered_map<int ,vector<int>>map(n);
for(vector<int> &vec:graph){
map[vec[0]].push_back(vec[1]);
}
vector<bool>visited(n, false);
if(start==target)
return true;
queue<int> que;
que.push(start);
visited[start]= true;
while(!que.empty()){
int node=que.front();
que.pop();
if(node==target)
return true;
visited[node]=true;
for(int neigh:map[node]){
if(!visited[neigh])
que.push(neigh);
}
}
return false;
}
};
执行结果:
时间复杂度:O(N)
空间复杂度:O(1)
解法二:深度优先遍历
也是构造邻接表,采用深度优先遍历的递归形式,若找到答案返回true。
代码:
class Solution {
public:
unordered_map<int ,vector<int>>map;
vector<bool>visited;
bool dfs(int start,int target){
if(visited[start]== true)
return false;
if(start==target)
return true;
for(int neigh:map[start])
{
if(dfs(neigh,target))
return true;
}
return false;
}
bool findWhetherExistsPath(int n, vector<vector<int>>& graph, int start, int target) {
if(start==target)
return true;
for(vector<int> &vec:graph){
map[vec[0]].push_back(vec[1]);
}
visited.assign(n, false);
return dfs(start,target);
}
};
执行结果:
时间复杂度:O(N)
空间复杂度:O(1)
15.省份数量
有 n 个城市,其中一些彼此相连,另一些没有相连。如果城市 a 与城市 b 直接相连,且城市 b 与城市 c 直接相连,那么城市 a 与城市 c 间接相连。
省份 是一组直接或间接相连的城市,组内不含其他没有相连的城市。
给你一个 n x n 的矩阵 isConnected ,其中 isConnected[i][j]
= 1 表示第 i 个城市和第 j 个城市直接相连,而 isConnected[i][j] = 0
表示二者不直接相连。
返回矩阵中 省份 的数量
解法一:深度优先遍历
可以把 n 个城市和它们之间的相连关系看成图,城市是图中的节点,相连关系是图中的边,给定的矩阵isConnected 即为图的邻接矩阵,省份即为图中的连通分量。
计算省份总数,等价于计算图中的连通分量数,可以通过深度优先搜索或广度优先搜索实现,也可以通过并查集实现。
深度优先搜索的思路是很直观的。遍历所有城市,对于每个城市,如果该城市尚未被访问过,则从该城市开始深度优先搜索,通过矩阵isConnected 得到与该城市直接相连的城市有哪些,这些城市和该城市属于同一个连通分量,然后对这些城市继续深度优先搜索,直到同一个连通分量的所有城市都被访问到,即可得到一个省份。遍历完全部城市以后,即可得到连通分量的总数,即省份的总数。处。
代码:
class Solution {
public:
vector<bool>visited;
int count;
void dfs(int i, vector<vector<int>>&isConnected){
if(visited[i]){
return;
}
visited[i]= true;
for(int j=0;j<isConnected.size();j++){
if(isConnected[i][j]==1&&!visited[j]){
dfs(j,isConnected);
}
}
}
int findCircleNum(vector<vector<int>>& isConnected) {
int n=isConnected.size();
visited.assign(n,false);
for(int i=0;i<n;i++){
if(!visited[i]){
dfs(i,isConnected);
count++;
}
}
return count;
}
};
执行结果:
时间复杂度:O(n^2),其中 n 是城市的数量。需要遍历矩阵 n 中的每个元素。
空间复杂度:O(n),其中 n 是城市的数量。需要使用数组 visited 记录每个城市是否被访问过,数组长度是 n,递归调用栈的深度不会超过 n。
解法二:宽度优先搜索
对于每个城市,如果该城市尚未被访问过,则从该城市开始广度优先搜索,直到同一个连通分量中的所有城市都被访问到,即可得到一个省份。
代码:
class Solution {
public:
vector<bool>visited;
int count;
int findCircleNum(vector<vector<int>>& isConnected) {
int n=isConnected.size();
queue<int>que;
visited.assign(n,false);
for(int i=0;i<n;i++){
if(!visited[i]){
que.push(i);
while(!que.empty()){
int node=que.front();
que.pop();
visited[node]= true;
for(int j=0;j<n;j++){
if(isConnected[node][j]==1&&!visited[j]){
que.push(j);
}
}
}
count++;
}
}
return count;
}
};
执行结果:
时间复杂度:O(n^2),其中 n 是城市的数量。需要遍历矩阵isConnected 中的每个元素。
空间复杂度:O(n),其中 n 是城市的数量。需要使用数组 visited 记录每个城市是否被访问过,数组长度是 n,广度优先搜索使用的队列的元素个数不会超过 n。
解法三:并查集
计算连通分量数的另一个方法是使用并查集。初始时,每个城市都属于不同的连通分量。遍历矩阵 isConnected,如果两个城市之间有相连关系,则它们属于同一个连通分量,对它们进行合并。
遍历矩阵isConnected 的全部元素之后,计算连通分量的总数,即为省份的总数
代码:
class Solution {
public:
vector<int>father;
//查询某个节点的父结点
int find(int x){
if(father[x]==x)
return x;
else
return find(father[x]);
}
//合并某两个节点
void merge(int i,int j){
father[find(i)]=find(j);
}
//并查集
int findCircleNum(vector<vector<int>>& isConnected) {
int n=isConnected.size();
father.reserve(n);
//初始化
for(int i=0;i<n;i++){
father[i]=i;
}
for(int i=0;i<n;i++)
for(int j=i+1;j<n;j++)
{
if(isConnected[i][j]==1)
merge(i,j);
}
int count=0;
for(int i=0;i<n;i++)
{
if(father[i]==i)
count++;
}
return count;
}
};
执行结果:
时间复杂度:O(n^2)
空间复杂度:O(n)
16.课程表IV
你总共需要上 n 门课,课程编号依次为 0 到 n-1 。
有的课会有直接的先修课程,比如如果想上课程 0 ,你必须先上课程 1 ,那么会以 [1,0] 数对的形式给出先修课程数对。
给你课程总数 n 和一个直接先修课程数对列表 prerequisite 和一个查询对列表 queries 。
对于每个查询对 queries[i] ,请判断 queries[i][0] 是否是 queries[i][1] 的先修课程。
请返回一个布尔值列表,列表中每个元素依次分别对应 queries 每个查询对的判断结果。
注意:如果课程 a 是课程 b 的先修课程且课程 b 是课程 c 的先修课程,那么课程 a 也是课程 c 的先修课程。
解法一:记忆化DFS(需要记录中间结果)
单纯的使用DFS深度优先搜索,会超出时间限制,例如1-2-3-4,由1可以到达4,那么由2和3都可以到达4,如果只使用DFS深度优先搜索,那么就需要从头开始,可以选择使用二维数组,在一次深搜时记录中间结果,即memory[2][3]=1
表示2可达3,memory[2][4]=1
表示2可达4.
代码:
class Solution {
public:
vector<vector<int>>memory;
bool dfs(vector<vector<int>>&prerequsites,int start,int target){
if(memory[start][target]==1)//start是target的先修磕碜
return true;
if(memory[start][target]==-1)//不是先修课程
return false;
for(auto neigh:prerequsites[start]){
if(dfs(prerequsites,neigh,target)){
memory[start][target]=1;
return true;
}
}
memory[start][target]=-1;
return false;
}
vector<bool> checkIfPrerequisite(int numCourses, vector<vector<int>>& prerequisites, vector<vector<int>>& queries) {
vector<bool>result;
memory.assign(numCourses,vector<int>(numCourses));
vector<vector<int>>adjact(numCourses);
for(auto i:prerequisites){
adjact[i[0]].push_back(i[1]);
memory[i[0]][i[1]]=1;
}
for(auto i:queries){
result.push_back(dfs(adjact,i[0],i[1]));
}
return result;
}
};
执行结果:
时间复杂度:O(n)遍历图
空间复杂度:O(n^2)
解法二:宽度优先搜索
在选择宽度优先搜索的时候,也需要存储中间结果,否则会造成时间超时
代码:
class Solution {
public:
vector<vector<int>>memory;
vector<bool> checkIfPrerequisite(int numCourses, vector<vector<int>>& prerequisites, vector<vector<int>>& queries) {
vector<bool>result;
memory.assign(numCourses,vector<int>(numCourses));
vector<vector<int>>adjact(numCourses);
for(auto i:prerequisites){
adjact[i[0]].push_back(i[1]);
}
for(int i=0;i<numCourses;i++)
{
queue<int>que;
que.push(i);
while(!que.empty()){
int node=que.front();
que.pop();
for(int neigh:adjact[node]){
if(memory[i][neigh]!=1){
memory[i][neigh]=1;
que.push(neigh);
}
}
}
}
for(auto q:queries){
result.push_back(memory[q[0]][q[1]]);
}
return result;
}
};
执行结果:
时间复杂度:O(n)
空间复杂度:O(n^2)
解法三:Floyd算法
Floyd算法可以求解两个节点之间的最短路径,可以改造Floyd算法,只要两个节点之间有边可达,那么dp[i][j]=1
代码:
class Solution {
public:
vector<bool> checkIfPrerequisite(int numCourses, vector<vector<int>>& prerequisites, vector<vector<int>>& queries) {
vector<bool> result;
int n=numCourses;
vector<vector<bool>> dp(n,vector<bool>(n,0));
for(auto p:prerequisites){
dp[p[0]][p[1]]= true;
}
for(int k=0;k<n;k++){
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
dp[i][j]=dp[i][j]||(dp[i][k]&&dp[k][j]);
}
}
}
for(auto &q:queries){
result.push_back(dp[q[0]][q[1]]);
}
return result;
}
};
执行结果:
时间复杂度:O(n^3)
空间复杂度:O(n^2)
17.到达目的地的方案数
你在一个城市里,城市由 n 个路口组成,路口编号为 0 到 n - 1 ,某些路口之间有 双向 道路。输入保证你可以从任意路口出发到达其他任意路口,且任意两个路口之间最多有一条路。
给你一个整数 n 和二维整数数组 roads ,其中 roads[i] = [ui, vi, timei] 表示在路口 ui 和 vi 之间有一条需要花费 timei 时间才能通过的道路。你想知道花费 最少时间 从路口 0 出发到达路口 n - 1 的方案数。
请返回花费 最少时间 到达目的地的 路径数目 。由于答案可能很大,将结果对 109 + 7 取余 后返回。
解法一:迪杰斯特拉或者Floyd求最短路径+有向无环图
方法一:最短路 + 有向无环图上的动态规划
记 dist[i] 表示从起点 0 到节点 i 的最短路径的长度。
如果我们希望花费最短的时间从起点 0到达终点 n-1,那么我们走过的每一条边 u→v 都需要满足:
dist[v]−dist[u]=length(u→v)
其中length(u→v) 表示连接 u 与 v 的这条边的长度。
假设我们当前位于节点 u。要想走出从起点 0 经过节点 u 到达终点 n-1 的最短路径,我们首先一定要在最短的时间内到达节点 u,也就是 dist[u]。
那么我们如何选择下一个节点呢?假设某一节点 v 与节点 u 相邻,即存在一条 u→v 的边。经过这条边后,我们花费的时间变为 dist[u]+length(u→v),那么与 dist[v] 本身对比,会有下面的几种情况:
如果dist[u]+length(u→v)<dist[v],那这是不可能的。因为dist[v] 表示从起点 0 到节点 v 的最短路径长度,这就与我们求出的答案相矛盾了。
如果dist[u]+length(u→v)=dist[v],那么我们在最短的时间内到达了节点 v,即目前我们还是有可能在最短的时间内到达终点 n-1的。
如果dist[u]+length(u→v)>dist[v],难么我们就没有机会在最短的时间内(在经过节点 v 的前提下)到达终点 n-1 了。
我们对于给定的城市路线的「无向图」建立一个新的「有向图」G:
「无向图」与「有向图」的节点相同;
「有向图」中有一条u→v 的有向边,当且仅当dist[v]−dist[u]=length(u→v) 成立。
可以发现 G 一定是一个无环图,因为每一条有向边总是从 dist 值较小的节点指向 dist 值较大的节点,因此图 G 中不可能存在环。
这样一来,我们在图 G 上从起点 0 开始,顺便边的方法任意地进行移动,只要到达终点 n−1,就一定是一条最短路径。我们就可以使用动态规划
的方法,求出从起点 0 到终点 n-1 的路径数目了。
思路与算法
记 f[i]表示在图 G 上从节点 i 走到终点 n-1 的路径数目。在进行动态规划时,我们可以考虑下一个走到的节点 j,那么有:
特别地,当 i=n-1时,我们已经到达了终点,那么有:
f[i] = 1
最终的答案即为 f[0]
细节
上述的状态转移方程使用自顶向下的记忆化搜索编写起来较为容易。本质上来说,在有向无环图上的动态规划的求解顺序可以为该图的任意一种拓扑排序(或者拓扑排序的逆序)。「记忆化搜索」对应着「深度优先搜索」求解拓扑排序,而「动态规划」对应着「广度优先搜索」求解拓扑排序。
dist 的值可以通过任一最短路径算法求得。在本题中,节点个数不超过 200,因此可以使用 Floyd 算法在 O(n^3) 的时间内求出 dist。当然,我们也可以使用Dijkstra 算法在 O(n^2)求出dist。
代码:
class Solution {
private:
static const int mod=1000000007;
public:
//dfs递归求最短路径的个数
int dfs(int n,int u,vector<vector<int>>&graph, vector<int>&f){
if(u==n-1)
return 1;
if(f[u]!=-1)
return f[u];
f[u]=0;
for(int i:graph[u]){
f[u]+=dfs(n,i,graph,f);
if(f[u]>=mod)
f[u]-=mod;
}
return f[u];
}
int countPaths(int n, vector<vector<int>>& roads) {
vector<vector<long long>>dist(n,vector<long long>(n,LLONG_MAX/2));
for(int i=0;i<n;i++)
dist[i][i]=0;
for(auto &road:roads) {
dist[road[0]][road[1]]=road[2];
dist[road[1]][road[0]]=road[2];
}
//可以采用Floyd或者Dijksta求单源最短路径
vector<int>visieted(n);
for(int i=0;i<n;i++){
int u=-1;
for(int j=0;j<n;j++){
if(!visieted[j]&&(u==-1||dist[0][j]<dist[0][u])){
u=j;
}
}
visieted[u]=true;
for(int k=0;k<n;k++) {
dist[0][k] = min(dist[0][k], dist[0][u] + dist[u][k]);
}
}
//求单源最短路径之后,构建有向图,图的边从dis小的指向dis大的位置,保证其无环
//当f(n-1)==1。由动态规划可以得到f[0]为最终答案。
vector<vector<int>>graph(n);
for(auto &road:roads){
int x=road[0];
int y=road[1];
int z=road[2];
if(dist[0][x]-dist[0][y]==z){
graph[y].push_back(x);
}
else if(dist[0][y]-dist[0][x]==z){
graph[x].push_back(y);
}
}
vector<int>f(n,-1);
return dfs(n,0,graph,f);
}
};
执行结果:
时间复杂度:O(n^3)。由于动态规划需要的时间为 O(n^2),因此算法的瓶颈在于求解dist。如果使用 Floyd 算法,那么时间复杂度为 O(n^3);如果使用Dijkstra 算法,那么时间复杂度为 O(n^2)
空间复杂度:O(n^2)。在使用 Floyd 算法时,我们需要将图以邻接矩阵的形式进行存储,因此需要 O(n^2)空间。而对于 Dijkstra 算法而言,使用的空间正比于图中的边数,而本题的数据范围中的边数最多为n(n-1)/2,也是O(n^2)
18.通知所有员工所需的时间
公司里有 n 名员工,每个员工的 ID 都是独一无二的,编号从 0 到 n - 1。公司的总负责人通过 headID 进行标识。
在 manager 数组中,每个员工都有一个直属负责人,其中 manager[i] 是第 i 名员工的直属负责人。对于总负责人,manager[headID] = -1。题目保证从属关系可以用树结构显示。
公司总负责人想要向公司所有员工通告一条紧急消息。他将会首先通知他的直属下属们,然后由这些下属通知他们的下属,直到所有的员工都得知这条紧急消息。
第 i 名员工需要 informTime[i] 分钟来通知它的所有直属下属(也就是说在 informTime[i] 分钟后,他的所有直属下属都可以开始传播这一消息)。
返回通知所有员工这一紧急消息所需要的 分钟数 。
解法一:普通DFS深度优先搜索
按照题目意思,可以使用二维数组作为邻接矩阵记录节点之间的父子关系,而题目可以转化成领导之间的一个树形关系,求通知时间,实际上就是求这棵树的最大深度即可。那么需要一个结构去存储父子关系,可以使用二维数组保存父子关系,利用深度优先搜索求最深深度。
代码:
class Solution {
public:
vector<vector<int>>graph;
int ans=0;
void dfs(int pos,vector<int>&informTime,int step){
for(int i:graph[pos]){
dfs(i,informTime,step+informTime[pos]);
}
ans=max(ans,step);
}
int numOfMinutes(int n, int headID, vector<int>& manager, vector<int>& informTime) {
graph=vector<vector<int>>(n);
for(int i=0;i<n;i++)
{
if(manager[i]!=-1)
{
graph[manager[i]].push_back(i);
}
}
dfs(headID,informTime,0);
return ans;
}
};
执行结果:
时间复杂度:O(n)
空间复杂度:O(n^2)
解法二:广度优先搜索遍历
注意:也可以使用广度优先遍历的方式,但是需要注意必须保存路径中的权值变量,而且必须是严格按照层次进行遍历
代码:
class Solution {
public:
vector<vector<int>>graph;
int ans=0;
int numOfMinutes(int n, int headID, vector<int>& manager, vector<int>& informTime) {
graph=vector<vector<int>>(n);
for(int i=0;i<n;i++)
{
if(manager[i]!=-1)
{
graph[manager[i]].push_back(i);
}
}
queue<vector<int>>que;
que.push({headID,informTime[headID]});
while(!que.empty()){
int size=que.size();
for(int i=0;i<size;i++){
auto node=que.front();
que.pop();
int id= node[0];
int step=node[1];
for(int i:graph[id]){
ans=max(ans,step+informTime[i]);
que.push({i,step+informTime[i]});
}
}
}
return ans;
}
};
执行结果:
时间复杂度:O(n)
空间复杂度:O(n^2)
19.猜数字大小II
我们正在玩一个猜数游戏,游戏规则如下:
我从 1 到 n 之间选择一个数字。
你来猜我选了哪个数字。
如果你猜到正确的数字,就会 赢得游戏 。
如果你猜错了,那么我会告诉你,我选的数字比你的 更大或者更小 ,并且你需要继续猜数。
每当你猜了数字 x 并且猜错了的时候,你需要支付金额为 x 的现金。如果你花光了钱,就会 输掉游戏 。
给你一个特定的数字 n ,返回能够 确保你获胜 的最小现金数,不管我选择那个数字 。
解法一:DFS递归:记忆化搜索
比较容易想到的做法为使用「递归」进行求解。
设计递归函数为 int dfs(int l, int r) 传入参数 l 和 r 代表在范围 [l, r][l,r] 内进行猜数,返回值为在 [l, r][l,r] 内猜中数字至少需要多少钱。
我们可决策的部分为「选择猜哪个数 x」,而不可决策的是「选择某个数 x 之后(假设没有猜中),真实值会落在哪边」。
因此为求得「最坏情况下最好」的结果,我们应当取所有的 x 中的最小值。
最后,为减少重复计算,我们需要在「递归」基础上加入记忆化搜索。用于保存递归过程中得到的中间结果。
思路:
首先定义[i,j][i,j]的最小支付现金为F(i,j),对于F(1,n),若第一次猜x获胜,则会出现3种结果:
猜对,支付0
猜错,正确数字比x小,然后问题转化为求[1,x-1]的最小现金数,支付现金数= x + F(1,x-1)
猜错,正确数字比xx大,然后问题转化为求[x+1,n]的最小现金数,支付现金数= x + F(x+1,n)
而若要确保获胜,必须要取上述3种情况的最大值,否则不能满足所有的获胜情况。我们再定义猜x获胜的金额为M(x)M(x),则M(x)= x + max{F(1,x-1),F(x+1,n)
以[1,10]为例,假如我们第一次猜6,则上述分析过程如下图:
实际上对于[1,n],从1到n的所有数都可以猜,每个数都可以得到一个M(x),即M(1),M(2)…M(n),取出其中的最小值,就是我们应该猜的数,也是最优的策略,即F(1,n)=min{M(x)} = min{x + max{F(1,x-1),F(x+1,n)}}
按上述过程进行递推,则可得F(i,j)F(i,j)的递推公式为:
F(i,j) = min{x + max{F(i,x-1),F(x+1,j)}} , 其中 1 <= x <= n
最后考虑初始条件,对于F(i,j),随着子问题越来越小,i与j也越来越接近,可发现:
若i=j,一定能猜对,支付0
若i>j,超出范围,所以i>=j为递归退出条件
代码:
class Solution {
public:
vector<vector<int>>cache;
int dfs(int l,int r) {
if(l>=r)
return 0;
if(cache[l][r]!=0)
return cache[l][r];
int ans=INT_MAX;
for(int x=l;x<=r;x++) {
int cur=x+max(dfs(l,x-1),dfs(x+1,r));
ans=min(ans,cur);
}
cache[l][r]=ans;
return ans;
}
int getMoneyAmount(int n) {
cache=vector<vector<int>>(n+1,vector<int>(n+1));
return dfs(1,n);
}
};
执行结果:
- 时间复杂度:O(n^3)
- 空间复杂度:忽略递归带来的额外空间开销,复杂度为 O(n^2)
解法二:DP动态规划
实际上,记忆化搜索就是DP的一种实现方式,一般情况下,DP的问题都具有2个要素:
最优子结构,一个大问题的最优解可以拆分成多个子问题的最优解,当子问题足够小,就可以很容易得到结果
子问题重叠,即会反复得求解相同的子问题,如上述的重复计算(分治也是求解子问题,但分治法的子问题是不重叠的)
而解决DP问题的方式也就变成了:
定义最优子结构,上述分析中F(i,j)即为定义的最优子结构
将子问题的解记录在表中,重叠的子问题只需计算一次,再次求解时查表即可
再回到题目中来,上面的记忆化搜索是DP自顶向下的一种递归实现方式,而更经典的方式方式是自底向上的计算,先计算子问题,然后再不断得计算更大的问题,得到最终的解。
通过状态转移公式,可以看到,区间 [i,j] 依赖于更小的区间 [i,x-1] 和 [x+1,j],所以,我们可以先计算长度小的区间,再计算长度大的区间,这里有两种写法:
一种是枚举长度,从小到大;
代码:
class Solution {
public:
int getMoneyAmount(int n) {
//定义dp函数
vector<vector<int>>dp(n+2,vector<int>(n+2,0));
//区间长度的范围是2-n
for(int len=2;len<=n;len++)
for(int i=1;i<=n-len+1;i++)
{
int j=i+len-1;
int m=INT_MAX;
for(int k=i;k<=j;k++){
m=min(m,max(dp[i][k-1],dp[k+1][j])+k);
}
dp[i][j]=m;
}
return dp[1][n];
}
};
执行结果:
时间复杂度:O(n^3),其中 n 是给定的参数。状态数量是 O(n^2),需要对每个状态使用 O(n)的时间计算状态值,因此总时间复杂度是 O(n^3)
空间复杂度:O(n^2)需要创建 n + 1行 n + 1 列的二维数组 f。
20.二分图
存在一个 无向图 ,图中有 n 个节点。其中每个节点都有一个介于 0 到 n - 1 之间的唯一编号。
给定一个二维数组 graph ,表示图,其中 graph[u] 是一个节点数组,由节点 u 的邻接节点组成。形式上,对于 graph[u] 中的每个 v ,都存在一条位于节点 u 和节点 v 之间的无向边。该无向图同时具有以下属性:
不存在自环(graph[u] 不包含 u)。
不存在平行边(graph[u] 不包含重复值)。
如果 v 在 graph[u] 内,那么 u 也应该在 graph[v] 内(该图是无向图)
这个图可能不是连通图,也就是说两个节点 u 和 v 之间可能不存在一条连通彼此的路径。
二分图 定义:如果能将一个图的节点集合分割成两个独立的子集 A 和 B ,并使图中的每一条边的两个节点一个来自 A 集合,一个来自 B 集合,就将这个图称为 二分图 。
如果图是二分图,返回 true ;否则,返回 false 。
与第八题重复,详情可见第八题
21.所有路径
给定一个有 n 个节点的有向无环图,用二维数组 graph 表示,请找到所有从 0 到 n-1 的路径并输出(不要求按顺序)。
graph 的第 i 个数组中的单元都表示有向图中 i 号节点所能到达的下一些结点(译者注:有向图是有方向的,即规定了 a→b 你就不能从 b→a ),若为空,就是没有下一个节点了。
与12题重复,详情见12题
21.颜色交替最短路径
在一个有向图中,节点分别标记为 0, 1, ..., n-1
。这个图中的每条边不是红色就是蓝色,且存在自环或平行边。
red_edges
中的每一个 [i, j]
对表示从节点 i
到节点 j
的红色有向边。类似地,blue_edges
中的每一个 [i, j]
对表示从节点 i
到节点 j
的蓝色有向边。
返回长度为 n
的数组 answer
,其中 answer[X]
是从节点 0
到节点 X
的红色边和蓝色边交替出现的最短路径的长度。如果不存在这样的路径,那么 answer[x] = -1
。
解法一:深度优先遍历
注意到题目中存在自环或者平行边,不能使用最简单的深度优先遍历,有可能会产生死循环,因为0到1可能有两条边,一条是红色的,一条是蓝色的,可能存在双向边,0-1红色,1-0蓝色
所以使用DFS深度搜索的时候,需要分别从红色和蓝色开始,交替寻找最短路径
使用一个二维数组记录从0到该节点的两种颜色的路径长度res,取两种颜色最小值
dfs实现时:遍历下一个可达的节点,若找到了更短的节点,那么需要递归查找下去,去更新与其有关系的其他节点。
最后遍历二维数组res求每个节点的最小值
代码:
class Solution {
static const int RED=0;
static const int BlUE=1;
public:
void dfs(int cur,int color,vector<vector<vector<int>>>&graph,vector<vector<int>>&distance){
for(auto item:graph[color][cur]) {
if(distance[cur][color]+1<distance[item][!color]){
distance[item][!color]=distance[cur][color]+1;
dfs(item,!color,graph,distance);
}
}
}
vector<int> shortestAlternatingPaths(int n, vector<vector<int>>& redEdges, vector<vector<int>>& blueEdges) {
vector<vector<vector<int>>> graph(2, vector<vector<int>>(n, vector<int>()));
for (vector<int>& edge : redEdges)
{
graph[0][edge[0]].push_back(edge[1]);
}
for (vector<int>& edge : blueEdges)
{
graph[1][edge[0]].push_back(edge[1]);
}
vector<vector<int>> distances(n, {INT_MAX, INT_MAX});
// 对于起点0初始化为0,0
distances[0] = {0, 0};
// 考虑两种情况去遍历
dfs(0, RED, graph, distances);
dfs( 0,BlUE, graph, distances);
// 再次遍历res去获取更小的值即是结果
vector<int> res(n, -1);
res[0] = 0;
for (int i = 1; i < n; ++i)
{
int curr = min(distances[i][0], distances[i][1]);
if (curr != INT_MAX)
{
res[i] = curr;
}
}
return res;
}
};
执行结果:
时间复杂度:O(n)
空间复杂度:O(n^2)
解法二:广度优先遍历 红蓝交替搜索
广度优先遍历可以得到相同的结果,同样从0点开始,因为存在平行边,可能是红边开始,也可能是蓝边开始,两种开始存在不同排序:红蓝红,或者蓝红蓝。
广度优先遍历按照层次遍历,遍历的过程中更新结果数组ans,因为ans[node]的可能是0点的红边开始的,也可能是0点的蓝边开始的,可能会得到两种结果,选最小的。
优化:在广度优先遍历时不设置visited数组,因为使用graph三维数组去整合保存红边和蓝边数组时,遍历过的红边或者蓝边要保证不会再次遍历。所以在遍历完边之后,使用
grah[color][node]
.clear()直接将其清空,防止再次被访问
代码:
class Solution {
static const int RED=0;
static const int BlUE=1;
public:
vector<int> shortestAlternatingPaths(int n, vector<vector<int>>& redEdges, vector<vector<int>>& blueEdges) {
vector<vector<vector<int>>> graph(2, vector<vector<int>>(n, vector<int>()));
for (vector<int>& edge : redEdges)
{
graph[0][edge[0]].push_back(edge[1]);
}
for (vector<int>& edge : blueEdges)
{
graph[1][edge[0]].push_back(edge[1]);
}
vector<int>ans(n,INT_MAX);
queue<pair<int,int>>queue;
queue.push(make_pair(0,0));
queue.push(make_pair(0,1));
int step=0;
while(!queue.empty()){
int size=queue.size();
for(int i=0;i<size;i++){
int node=queue.front().first;
int color=queue.front().second;
queue.pop();
ans[node]=min(ans[node],step);
vector<int>&next=graph[color][node];
for(int neigh:next){
queue.push(make_pair(neigh,!color));
}
graph[color][node].clear();
}
step++;
}
ans[0]=0;
for(int i=1;i<n;i++)
{
ans[i]=ans[i]==INT_MAX?-1:ans[i];
}
return ans;
}
};
执行结果:
时间复杂度:O(n*m),n为点,m为边
空间复杂度:O(n)
21.冗余连接
树可以看成是一个连通且 无环
的 无向
图。
给定往一棵 n 个节点 (节点值 1~n) 的树中添加一条边后的图。添加的边的两个顶点包含在 1 到 n 中间,且这条附加的边不属于树中已存在的边。图的信息记录于长度为 n 的二维数组 edges ,edges[i] = [ai, bi] 表示图中在 ai 和 bi 之间存在一条边。
请找出一条可以删去的边,删除后可使得剩余部分是一个有着 n 个节点的树。如果有多个答案,则返回数组 edges 中最后出现的边。
注意,题目的意思就是返回某一条删除的边
解法:并查集
在一棵树中,边的数量比节点的数量少 1。如果一棵树有 n 个节点,则这棵树有 n-1 条边。这道题中的图在树的基础上多了一条附加的边,因此边的数量也是 n
。
树是一个连通且无环的无向图,在树中多了一条附加的边之后就会出现环,因此附加的边即为导致环出现的边。
可以通过并查集寻找附加的边。初始时,每个节点都属于不同的连通分量。遍历每一条边,判断这条边连接的两个顶点是否属于相同的连通分量。
如果两个顶点属于不同的连通分量,则说明在遍历到当前的边之前,这两个顶点之间不连通,因此当前的边不会导致环出现,合并这两个顶点的连通分量。
如果两个顶点属于相同的连通分量,则说明在遍历到当前的边之前,这两个顶点之间已经连通,因此当前的边导致环出现,为附加的边,将当前的边作为答案返回。
并查集模板见:https://zhuanlan.zhihu.com/p/93647900/
优化:利用路径压缩以及按秩合并
代码:
class Solution {
public:
int find(int x,vector<int>&fa){
if(x==fa[x])
return x;
else{
fa[x]=find(fa[x],fa);
return fa[x];
}
}
void merge(int i,int j,vector<int>&fa,vector<int>&rank){
int x=find(i,fa);
int y=find(j,fa);
if(rank[x]<=rank[y]){
fa[x]=fa[y];
}else{
fa[y]=fa[x];
}
if(rank[x]==rank[y]&&x!=y){
rank[y]++;
}
}
vector<int> findRedundantConnection(vector<vector<int>>& edges) {
//注意原本一棵树有n个节点,n-1条边,题目中添一条边之后变成n条边
int n=edges.size()+1;
vector<int>fa(n);
for(int i=0;i<n;i++)
fa[i]=i;
vector<int>rank(n,1);
for(auto edge:edges){
int n1=edge[0];
int n2=edge[1];
if(find(n1,fa)!=find(n2,fa)){
merge(n1,n2,fa,rank);
}else{
return edge;
}
}
return vector<int>{};
}
};
执行结果:
时间复杂度:O(nlogn),其中 n 是图中的节点个数。需要遍历图中的 n 条边,对于每条边,需要对两个节点查找祖先,如果两个节点的祖先不同则需要进行合并,需要进行 2 次查找和最多 1 次合并。一共需要进行 2n次查找和最多 n 次合并,因此总时间复杂度是 O(2nlogn)=O(nlogn)。这里的并查集使用了路径压缩,但是没有使用按秩合并,最坏情况下的时间复杂度是 O(nlogn)。
空间复杂度:O(n),其中 n 是图中的节点个数。使用数组fa 记录每个节点的祖先。
22.可以到达所有点的最小点数目
给你一个 有向无环图
, n 个节点编号为 0 到 n-1 ,以及一个边数组 edges ,其中 edges[i] = [fromi, toi] 表示一条从点 fromi 到点 toi 的有向边。
找到最小的点集使得从这些点出发能到达图中所有点。题目保证解存在且唯一。
你可以以任意顺序返回这些节点编号。
解法:寻找入度为0的节点
对于任意节点 x,如果其入度不为零,则一定存在节点 y 指向节点 x,从节点 y 出发即可到达节点 y 和节点 x,因此如果从节点 y出发,节点 x 和节点 y 都可以到达,且从节点 y 出发可以到达的节点比从节点 x 出发可以到达的节点更多。
由于给定的图是有向无环图
,基于上述分析可知,对于任意入度不为零的节点 x,一定存在另一个节点 z,使得从节点 z 出发可以到达节点 x。为了获得最小的点集,只有入度为零的节点才应该加入最小的点集。
由于入度为零的节点必须从其自身出发才能到达该节点,从别的节点出发都无法到达该节点,因此最小的点集必须包含所有入度为零的节点。
因为入度不为零的点总可以由某个入度为零的点到达,所以这些点不包括在最小的合法点集当中。
因此,我们得到「最小的点集使得从这些点出发能到达图中所有点」就是入度为零的所有点的集合。
如何判断一个节点的入度是否为零呢?在有向图中,一个节点的入度等于以该节点为终点的有向边的数量,因此一个节点的入度为零,当且仅当对于任何有向边,该节点都不是有向边的终点。
因此,可以遍历所有的边,使用集合存储所有有向边的终点,集合中的所有节点即为入度不为零的节点,剩下的所有节点即为入度为零的节点。
代码:
class Solution {
public:
vector<int> findSmallestSetOfVertices(int n, vector<vector<int>>& edges) {
bool hasInDegree[n];
vector<int>res;
memset(hasInDegree, false, sizeof(hasInDegree));
for(auto edge:edges){
hasInDegree[edge[1]]= true;
}
for(int i=0;i<n;i++){
if(hasInDegree[i]== false){
res.push_back(i);
}
}
return res;
}
};
执行结果:
时间复杂度:O(m+n),其中 m 是图中的边数量,n 是图中的节点数量。需要遍历所有的边获得入度不为零的节点,然后遍历所有的节点保留入度为零的节点。
空间复杂度:O(n),其中 n 是图中的节点数量。需要使用一个集合存储入度不为零的节点,集合中的节点数不会超过 n。
23.从第一个节点出发到最后一个节点的受限路径数
现有一个加权无向连通图
。给你一个正整数 n ,表示图中有 n 个节点,并按从 1 到 n 给节点编号;另给你一个数组 edges ,其中每个 edges[i] = [ui, vi, weighti] 表示存在一条位于节点 ui 和 vi 之间的边,这条边的权重为 weighti 。
从节点 start 出发到节点 end 的路径是一个形如 [z0, z1, z2, …, zk] 的节点序列,满足 z0 = start 、zk = end 且在所有符合 0 <= i <= k-1 的节点 zi 和 zi+1 之间存在一条边。
路径的距离定义为这条路径上所有边的权重总和。用 distanceToLastNode(x) 表示节点 n 和 x 之间路径的最短距离。受限路径 为满足 distanceToLastNode(zi) > distanceToLastNode(zi+1) 的一条路径,其中 0 <= i <= k-1 。
返回从节点 1 出发到节点 n 的 受限路径数 。由于数字可能很大,请返回对 109 + 7 取余 的结果。
题目意思解读,蓝色的标识为各个节点到5号节点的最短路径,而从起点到终点的受限路径的意思是,在路径中的所有节点的最短路径呈现递减形式
如1(4)–>2(2)–>5(0)
1(4)–>2(2)–>3(1)–>5(0)
STL库:priority_queue优先队列用法
详细见链接:https://blog.csdn.net/weixin_36888577/article/details/79937886
普通的队列是一种先进先出的数据结构,元素在队列尾追加,而从队列头删除。
在优先队列中,元素被赋予优先级。当访问元素时,具有最高优先级的元素最先删除。优先队列具有最高级先出 (first in, largest out)的行为特征。
首先要包含头文件#include<queue>
, 他和queue
不同的就在于我们可以自定义其中数据的优先级, 让优先级高的排在队列前面,优先出队。
优先队列具有队列的所有特性,包括队列的基本操作,只是在这基础上添加了内部的一个排序,它本质是一个堆实现的。
和队列基本操作相同:
- top 访问队头元素
- empty 队列是否为空
- size 返回队列内元素个数
- push 插入元素到队尾 (并排序)
- emplace 原地构造一个元素并插入队列
- pop 弹出队头元素
- swap 交换内容
定义
:
priority_queue<Type, Container, Functional>
Type 就是数据类型,Container 就是容器类型(Container必须是用数组实现的容器,比如vector,deque等等,但不能用 list。STL里面默认用的是vector),Functional 就是比较的方式。
当需要用自定义的数据类型时才需要传入这三个参数,使用基本数据类型时,只需要传入数据类型,默认是大顶堆。
一般是:
//升序队列,小顶堆
priority_queue <int,vector<int>,greater<int> > q;
//降序队列,大顶堆
priority_queue <int,vector<int>,less<int> >q;
//greater和less是std实现的两个仿函数(就是使一个类的使用看上去像一个函数。其实现就是类中实现一个operator(),这个类就有了类似函数的行为,就是一个仿函数类了)
重定义小于号:即less是大顶堆,是默认的 降序
重定义大于号:即greater,是小顶堆 升序
算法思路:
1.首先根据边和权值构建图模型
2.很明显这个是单源最短路径,可以使用迪杰斯特拉算法求得单源最短路径。求得每个节点的单源最短路径之后
以示例1为例:
1到5的受限路径长度为:
path(1)=path(2)+path(3)
path(2)=path(3)+path(5)
path(3)=path(5)
path(5)=1
很明显这是一个递归的形式,path(5)=1是递归的出口,path(3)=1,path(2)=2
所以path(1)=3;
注意两点,递归的过程中,path存在重复计算,比如path(3),所以可以使用记忆化递归,用数组dp[]记录中间过程,否则会在测试时可能超过测试用例。
第二:递归的过程中,每一步都需要取余操作,否则数据越大溢出。
第三:dist数组初始化的时候必须使用INT_MAX(大值),不能使用INT_MAX/2,否则数据用例过大会超过INT_MAX/2,造成结果出错。
代码:
class Solution {
public:
//堆优化的迪捷斯特拉+记忆化dfs
const int kMod=1e9+7;
int countRestrictedPaths(int n, vector<vector<int>>& edges) {
vector<vector<pair<int,int>>>g(n+1);
//建图
for(auto edge:edges){
g[edge[0]].emplace_back(edge[1],edge[2]);
g[edge[1]].emplace_back(edge[0],edge[2]);
}
//dijkstra
vector<int>dist(n+1,INT_MAX);
dist[n]=0;
//优先队列三个参数,数据类型,容器类型:默认vector,比较方式,stl中有greater和less
//优先队列中的pair的first为距离,second为n点到某一点的距离,dis[n]=0;
priority_queue<pair<int,int>,vector<pair<int,int>>,greater<pair<int,int>>>q;
q.emplace(0,n);
while(!q.empty()){
pair<int,int>node=q.top();
q.pop();
int d=node.first;
int u=node.second;
if(d>dist[u]){
continue;
}
for(auto neigh:g[u]){
int v=neigh.first;
int w=neigh.second;
if(dist[v]>dist[u]+w)
q.emplace(dist[v]=dist[u]+w,v);
}
}
//迪杰斯特拉求除最短路径后,记忆化dfs
vector<int>dp(n+1,INT_MAX);
return dfs(1,n,dist,g,dp);
}
//记忆化dfs
int dfs(int u,int n,vector<int>&dist, vector<vector<pair<int,int>>>&g,vector<int>&dp){
if(u==n)
return 1;
if(dp[u]!=INT_MAX)
return dp[u];
int ans=0;
for(auto node:g[u]){
if(dist[node.first]<dist[u]){
ans=(ans+dfs(node.first,n,dist,g,dp))%kMod;
}
}
dp[u]=ans;
return dp[u];
}
};
执行结果:
时间复杂度:O(ElogV+V+E) E是边的数目,V是节点的数目,Elogv是迪杰斯特拉复杂度,V+E是递归复杂度
空间复杂度:O(V+E)
24.最佳直线
给定一个二维平面及平面上的 N 个点列表Points,其中第i个点的坐标为Points[i]=[Xi,Yi]。请找出一条直线,其通过的点的数目最多。
设穿过最多点的直线所穿过的全部点编号从小到大排序的列表为S,你仅需返回[S[0],S[1]]作为答案,若有多条直线穿过了相同数量的点,则选择S[0]值较小的直线返回,S[0]相同则选择S[1]值较小的直线返回。
解法:斜率常规计算
如果节点的斜率相同,那么可以说明节点在同一条直线上,而斜率使用(y1-y2)/(x1-x2)就可以计算,当然必须考虑x1=x2的情况,此时斜率为无穷大。
因为题目要求只返回一条直线上节点最多,并且按照索引顺序的前两个节点的序号,如果有两个相同节点数的直线,那么返回的是S[0]的值较小的,或者S[0]相同,S[1]的值较小的。
利用hash表,以斜率为key,计算相同斜率的节点说,采用二重循环从左到右遍历,这样本身就是从小到大遍历,保存斜率的两个节点必然是最小的两个节点。当有多个相同数量的直线时,也能保证返回节点最小。
最重要的是:这个hash表在每次外层循环时,都需要重新创建一个hash表,因为为了避免斜率相同的平行直线被当成同一条线计算节点数。
因为双重循环固定了一个顶点i,然后循环顶点j,此时如果斜率相同那么一定是一条直线,不可能是平行的线。
代码:
class Solution {
public:
vector<int> bestLine(vector<vector<int>>& points) {
int maxCnt=2;
vector<int>res{0,1};
for(int i=0;i<points.size();i++)
{
unordered_map<double,pair<int,vector<int>>>hashmap;
int x1=points[i][0];
int y1=points[i][1];
for(int j=i+1;j<points.size();j++){
int x2=points[j][0];
int y2=points[j][1];
double k=(x1==x2)?DBL_MAX:(double)(y1-y2)/(double)(x1-x2);
if(!hashmap.count(k)){
hashmap[k]=make_pair(2,vector<int>{i,j});
}else{
hashmap[k].first++;
}
if(hashmap[k].first>maxCnt){
maxCnt=hashmap[k].first;
res=hashmap[k].second;
}
}
}
return res;
}
};
执行结果:
时间复杂度:O(n^2)
空间复杂度:O(1)
25.对链表进行插入排序
给定单个链表的头 head ,使用 插入排序 对链表进行排序,并返回 排序后链表的头 。
插入排序 算法的步骤:
插入排序是迭代的,每次只移动一个元素,直到所有元素可以形成一个有序的输出列表。
每次迭代中,插入排序只从输入数据中移除一个待排序的元素,找到它在序列中适当的位置,并将其插入。
重复直到所有输入数据插入完为止。
下面是插入排序算法的一个图形示例。部分排序的列表(黑色)最初只包含列表中的第一个元素。每次迭代时,从输入数据中删除一个元素(红色),并就地插入已排序的列表中。
对链表进行插入排序。
解法:找到插入点,修改指针
插入排序的基本思想是,维护一个有序序列,初始时有序序列只有一个元素,每次将一个新的元素插入到有序序列中,将有序序列的长度增加 1,直到全部元素都加入到有序序列中。
如果是数组的插入排序,则数组的前面部分是有序序列,每次找到有序序列后面的第一个元素(待插入元素)的插入位置,将有序序列中的插入位置后面的元素都往后移动一位,然后将待插入元素置于插入位置。
对于链表而言,插入元素时只要更新相邻节点的指针即可,不需要像数组一样将插入位置后面的元素往后移动,因此插入操作的时间复杂度是 O(1),但是找到插入位置需要遍历链表中的节点,时间复杂度是 O(n),因此链表插入排序的总时间复杂度仍然是 O(n^2),其中 n 是链表的长度。
对于单向链表而言,只有指向后一个节点的指针,因此需要从链表的头节点开始往后遍历链表中的节点,寻找插入位置。
对链表进行插入排序的具体过程如下。
首先判断给定的链表是否为空,若为空,则不需要进行排序,直接返回。
创建亚节点preHead,令 preHead.next = head。引入哑节点是为了便于在 head 节点之前插入节点。
维护 lastSorted 为链表的已排序部分的最后一个节点,初始时 lastSorted = head。
维护 curr 为待插入的元素,初始时 curr = head.next。
比较 lastSorted 和 curr 的节点值。
若 lastSorted.val <= curr.val,说明 curr 应该位于 lastSorted 之后,将 lastSorted 后移一位,curr 变成新的 lastSorted。
否则,从链表的头节点开始往后遍历链表中的节点,寻找插入 cur 的位置。令 pre 为插入 cur 的位置的前一个节点,进行如下操作,完成对 cur 的插入:
lastSorted.next = cur.next
cur.next = pre.next
pre.next = cur
令 cur = lastSorted.next,此时 cur 为下一个待插入的元素。
重复第 5 步和第 6 步,直到 curr 变成空,排序结束。
返回 preHead.next,为排序后的链表的头节点。
代码:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* insertionSortList(ListNode* head) {
if(head== nullptr)
return head;
ListNode* preHead=new ListNode();
preHead->next=head;
ListNode *lastSorted=head;
ListNode * cur=head->next;
while(cur!= nullptr){
if(lastSorted->val<=cur->val){
lastSorted=cur;
}else{
ListNode * pre=preHead;
while(pre->next->val<cur->val){
pre=pre->next;
}
lastSorted->next=cur->next;
cur->next=pre->next;
pre->next=cur;
}
cur=lastSorted->next;
}
return preHead->next;
}
};
执行结果:
时间复杂度:O(n^2)
因此插入操作的时间复杂度是 O(1),但是找到插入位置需要遍历链表中的节点,时间复杂度是 O(n),因此链表插入排序的总时间复杂度仍然是 O(n^2),其中 n 是链表的长度。
空间复杂度:O(1)
26.K站中转内最便宜的航班
有 n 个城市通过一些航班连接。给你一个数组 flights ,其中 flights[i] = [fromi, toi, pricei] ,表示该航班都从城市 fromi 开始,以价格 pricei 抵达 toi。
现在给定所有的城市和航班,以及出发城市 src 和目的地 dst,你的任务是找到出一条最多经过 k 站中转的路线,使得从 src 到 dst 的 价格最便宜 ,并返回该价格。 如果不存在这样的路线,则输出 -1。
解法一:DFS深度优先遍历
算法的思路是将从src到dst的所有路径遍历一遍,但是第一次写的时候没有考虑到超时问题,如果不爆粗中间结果,很可能造成用例非常大时的路径超时问题。
因此在DFS深度优先遍历的基础上,需要加上缓存memory数组,即保存遍历过的中间结果。
加记忆化搜索就是添加dfs函数参数中变的部分,memory[src][k]
表示从src节点出发,走k步到dst所花费的最小费用。
代码:
class Solution {
public:
const int INF=10e7;
vector<vector<vector<int>>>graph;
int dfs(int src,int dst,int k,vector<vector<int>>&memory){
if(k<0){
return INF;
}
if(src==dst){
return 0;
}
if(memory[src][k]!=0)
return memory[src][k];
int mini=INF;
for(auto neigh:graph[src]){
int temp=dfs(neigh[0],dst,k-1,memory)+neigh[1];
mini= min(temp,mini);
}
memory[src][k]=mini;
return mini;
}
int findCheapestPrice(int n, vector<vector<int>>& flights, int src, int dst, int k) {
graph=vector<vector<vector<int>>>(n);
for(auto flight:flights){
graph[flight[0]].push_back(vector<int>{flight[1],flight[2]});
}
vector<vector<int>>memory(n,vector<int>(k+2));
return dfs(src,dst,k+1,memory)==INF?-1:dfs(src,dst,k+1,memory);
}
};
执行结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QIRkKI7o-1651402912655)(https://s2.loli.net/2022/02/09/PwXSsdTELycrVOm.png)]
时间复杂度:O(k*n^2) 需要遍历k层,每层都需要遍历graph数组
空间复杂度:O(k*n) memory数组。
解法二:动态规划
既然记忆化DFS的已经写出,那么动态规划的方法就是它的修改
假设dp[src][k]
表示的是从原节点走k步到达dst节点的最小价格
由此得到状态转移方程
d
p
[
i
]
[
k
]
=
m
i
n
j
=
0
n
(
d
p
[
j
]
[
k
−
1
]
+
p
r
i
c
e
[
i
]
[
j
]
)
其
中
j
是
i
的
下
一
个
节
点
dp[i][k]=min_{j=0}^{n}(dp[j][k-1]+price[i][j]) 其中j是i的下一个节点
dp[i][k]=minj=0n(dp[j][k−1]+price[i][j])其中j是i的下一个节点
初 始 值 : 初 始 时 , d s t 到 d s t 走 0 步 的 最 少 价 格 为 0 , 即 d p [ d s t ] [ 0 ] = 0 , 其 余 为 I N F 初始值:初始时,dst到dst走0步的最少价格为0,即dp[dst][0]=0,其余为INF 初始值:初始时,dst到dst走0步的最少价格为0,即dp[dst][0]=0,其余为INF
返 回 值 : 返 回 的 是 m i n k = 0 K d p [ s r c ] [ k ] 中 的 最 小 值 返回值:返回的是min_{k=0}^{K}dp[src][k]中的最小值 返回值:返回的是mink=0Kdp[src][k]中的最小值
class Solution {
public:
const int INF=10e7;
int findCheapestPrice(int n, vector<vector<int>>& flights, int src, int dst, int k) {
vector<vector<int>>dp(n,vector<int>(k+2,INF));
dp[dst][0]=0;
for(int m=1;m<=k+1;m++)
{
for(auto flight:flights){
dp[flight[0]][m]=min(dp[flight[0]][m],dp[flight[1]][m-1]+flight[2]);
}
}
int ans=INF;
for(int i=1;i<=k+1;i++){
ans=min(ans,dp[src][i]);
}
return ans==INF?-1:ans;
}
};
执行结果:
时间复杂度:O((m+n)k),其中 m 是数组 }flights 的长度。状态的数量为 O(nk),对于固定的 ,我们需要 O(m)O(m) 的时间计算出所有 f[t][…]f[t][…] 的值,因此总时间复杂度为 O((m+n)k)。
空间复杂度:O(nk) 或 O(n),即为存储状态需要的空间
解法三:BFS广度优先遍历
思路:广度优先遍历是按层次进行的遍历,因此,需要将flights结构转换为邻接表形式存储每个节点的邻居。同时,因为部数不超过k,在使用队列的时候,循环结果的条件除了非空还需要加上步数的限制。
使用一位数组ans记录,ans[i]从节点src到节点i的最少价格,ans初始化全为INF。
最后返回ans[dst],如果ans[dst]==INF,则说明找不到路径,返回-1。
代码:
class Solution {
public:
const int INF=10e7;
vector<vector<vector<int>>>graph;
int findCheapestPrice(int n, vector<vector<int>>& flights, int src, int dst, int k) {
graph=vector<vector<vector<int>>>(n);
for(auto flight:flights){
graph[flight[0]].push_back(vector<int>{flight[1],flight[2]});
}
vector<int>ans(n,INF);
queue<vector<int>>que;
que.push(vector<int>{src,0});
while(!que.empty()&&k+1>0){
int size=que.size();
for(int i=0;i<size;i++){
auto node=que.front();
que.pop();
for(auto neigh:graph[node[0]]){
int dis=node[1]+neigh[1];
if(dis<ans[neigh[0]]){
ans[neigh[0]]=dis;
que.push(vector<int>{neigh[0],dis});
}
}
}
k--;
}
return ans[dst]==INF?-1:ans[dst];
}
};
执行结果:
时间复杂度:O(N)
空间复杂度:O(N)
27.概率最大的路径
给你一个由 n 个节点(下标从 0 开始)组成的无向加权图
,该图由一个描述边的列表组成,其中 edges[i] = [a, b] 表示连接节点 a 和 b 的一条无向边,且该边遍历成功的概率为 succProb[i] 。
指定两个节点分别作为起点 start 和终点 end ,请你找出从起点到终点成功概率最大的路径,并返回其成功概率。
如果不存在从 start 到 end 的路径,请 返回 0 。只要答案与标准答案的误差不超过 1e-5 ,就会被视作正确答案。
解法一:BFS广度优先遍历
因为求源节点到目标节点的最大概率,可以从源节点开始进行广度优先遍历,使用一位数组ans保存源节点到其余各节点的最大概率。ans[i]表示从源节点到i节点的最大路径概率为ans[i].
需要根据边的关系构建邻接表,然后进行广度优先遍历。广度优先遍历的过程中其实不需要visited数组,之前考虑的时候觉得需要,其实概率都在【0,1】之间,只可能越乘越小,所以当比ans数组来的小的时候,不加入队列。队列不会陷入死循环中。
其中要队列中要保存,{节点,到节点的最大概率}。
代码:
class Solution {
public:
double maxProbability(int n, vector<vector<int>>& edges, vector<double>& succProb, int start, int end) {
vector<vector<vector<double>>>graph(n);
for(int i=0;i<edges.size();i++)
{
graph[edges[i][0]].push_back(vector<double>{(double)edges[i][1],succProb[i]});
graph[edges[i][1]].push_back(vector<double>{(double)edges[i][0],succProb[i]});
}
queue<vector<double>>que;
vector<bool>visited(n,0);
vector<double>ans(n,0);
que.push(vector<double>{(double)start,1.0});
while(!que.empty()){
int len=que.size();
for(int i=0;i<len;i++){
auto node=que.front();
que.pop();
for(auto neigh:graph[node[0]]){
double dis=neigh[1]*node[1];
if(dis>ans[neigh[0]]){
ans[neigh[0]]=dis;
que.push(vector<double>{neigh[0],dis});
}
}
}
return ans[end];
}
};
执行结果:
时间复杂度:O(m*n)
空间复杂度:O(N)
解法二:单源最短路径算法 Dijkstra
此解法其实就是BFS遍历,通过优先队列进行优化
dijkstra算法回顾
将所有节点分成两类:已确定从起点到当前点的最短路长度的节点,以及未确定从起点到当前点的最短路长度的节点(下面简称「未确定节点」和「已确定节点」)。
每次从「未确定节点」中取一个与起点距离最短的点,将它归类为「已确定节点」,并用它「更新」从起点到其他所有「未确定节点」的距离。直到所有点都被归类为「已确定节点」。
用节点 AA「更新」节点 BB 的意思是,用起点到节点 AA 的最短路长度加上从节点 AA 到节点 BB 的边的长度,去比较起点到节点 BB 的最短路长度,如果前者小于后者,就用前者更新后者。这种操作也被叫做「松弛」。
这里暗含的信息是:每次选择「未确定节点」时,起点到它的最短路径的长度可以被确定。
可以这样理解,因为我们已经用了每一个「已确定节点」更新过了当前节点,无需再次更新(因为一个点不能多次到达)。而当前节点已经是所有「未确定节点」中与起点距离最短的点,不可能被其它「未确定节点」更新。所以当前节点可以被归类为「已确定节点」。
给定的图必须是正边权图,否则「未确定节点」有可能更新当前节点,这也是 Dijkstra 不能处理负权图的原因。
使用堆优化的迪杰斯特拉算法时,用到优先队列,默认是大顶堆,是降序的,因为概率是越乘越小的,所以每次都取出队首元素,也就是概率最大的元素,然后更新节点,如果经过队首元素始得未更新节点的值变大,那么将这个节点加入队列,进行下次更新。
代码:
class Solution {
public:
double maxProbability(int n, vector<vector<int>>& edges, vector<double>& succProb, int start, int end) {
vector<vector<pair<double,int>>>graph(n);
for(int i=0;i<edges.size();i++)
{
graph[edges[i][0]].push_back(make_pair(succProb[i],edges[i][1]));
graph[edges[i][1]].push_back(make_pair(succProb[i],edges[i][0]));
}
priority_queue<pair<double,int>>que;
vector<double>ans(n,0);
que.push(make_pair(1.0,start));
while(!que.empty()){
int len=que.size();
for(int i=0;i<len;i++){
auto node=que.top();
que.pop();
for(auto neigh:graph[node.second]){
double dis=neigh.first*node.first;
if(dis>ans[neigh.second]){
ans[neigh.second]=dis;
que.push(make_pair(dis,neigh.second));
}
}
}
}
return ans[end];
}
};
执行结果:
时间复杂度:O(mlogm),其中 mm 是图中边的数量。如果不使用任何优化,时间复杂度是 O(mn),其中 n 是图中点的数量。使用不同的数据结构优化,将会表现出不同的时间复杂度:
优先队列(例如 C++ 中的 priority_queue)优化:O(m \log m)O(mlogm);
手写堆优化:O(mlogn);
线段树优化O(mlogn);
斐波那契堆优化:O(nlogn+m)。
空间复杂度:O(m),其中 m 是图中边的数量。
28.最小高度树
树是一个无向图,其中任何两个顶点只通过一条路径连接。 换句话说,一个任何没有简单环路的连通图都是一棵树。
给你一棵包含 n 个节点的树,标记为 0 到 n - 1 。给定数字 n 和一个有 n - 1 条无向边的 edges 列表(每一个边都是一对标签),其中 edges[i] = [ai, bi] 表示树中节点 ai 和 bi 之间存在一条无向边。
可选择树中任何一个节点作为根。当选择节点 x 作为根节点时,设结果树的高度为 h 。在所有可能的树中,具有最小高度的树(即,min(h))被称为 最小高度树 。
请你找到所有的 最小高度树 并按 任意顺序 返回它们的根节点标签列表。
树的 高度 是指根节点和叶子节点之间最长向下路径上边的数量。
解法:两端烧香法:由边缘向中间寻找根节点
注意:尝试常规思路,使用bfs求得每个节点的高度,然后筛选出最小节点。
这种方法在运行第66个案例时已经超时。
直觉上,一棵树越靠「外面」的结点,我们越不可能把它作为根结点,如果这样做的话,可能树的高度是很高的,例如下面这张图。
因此,我们使用「剔除边缘结点」的策略,这里的边缘结点就是指连接其它结点最少的结点,用专业的名词来说,就是指向它的结点最少的结点,「度」最少的结点
综上所述,总结一下我们的算法:每次总是删除「入度」个数最少的结点,因为树是无向无环图,删除了它们以后,与之相连的结点的度也相应地减少 1,直到最后剩下 1 个结点或者 2 个结点,就是要求的最小高度的根节点。
算法流程:
- 需要将边的关系记录到邻接表中,并且需要一个计算每个节入度的表(因为是无向图,也可统称为度)
- 将度为1的节点先加入队列,从边缘向中间靠拢,一步步缩小节点的范围
- 每次通过队列循环都重新申明res结果集,并将队列中的节点加入结果集,这样每次保存的都是叶子节点的状态,最后一次加入结果集的叶子节点状态,就是所要求的根节点集合了。
代码:
class Solution {
public:
vector<int> findMinHeightTrees(int n, vector<vector<int>>& edges) {
if(n==1)
return vector<int>{0};
vector<vector<int>>graph(n);
vector<int>ans;
vector<int>degree(n);
for(auto edge:edges){
graph[edge[0]].push_back(edge[1]);
graph[edge[1]].push_back(edge[0]);
degree[edge[0]]++;
degree[edge[1]]++;
}
queue<int>que;
for(int i=0;i<n;i++){
if(degree[i]==1)
que.push(i);
}
while(!que.empty()){
vector<int>res;
int size=que.size();
for(int i=0;i<size;i++){
int node=que.front();
res.push_back(node);
que.pop();
for(int neigh:graph[node]){
degree[neigh]--;
if(degree[neigh]==1)
que.push(neigh);
}
}
ans=res;
}
return ans;
}
};
执行结果:
时间复杂度:O(n+e)
空间复杂度:O(n)
29.阈值距离内邻居最少的城市
有 n 个城市,按从 0 到 n-1 编号。给你一个边数组 edges,其中 edges[i] = [fromi, toi, weighti] 代表 fromi 和 toi 两个城市之间的双向加权边,距离阈值是一个整数 distanceThreshold。
返回能通过某些路径到达其他城市数目最少、且路径距离 最大 为 distanceThreshold 的城市。如果有多个这样的城市,则返回编号最大的城市。
注意,连接城市 i 和 j 的路径的距离等于沿该路径的所有边的权重之和。
解法一:Floyd算法
因为题目中要求路径最大不超过distanceThreshold,也就是说:要求得一个节点到其余所有节点的最短路径,可以利用Floyd算法,求得每两个节点的最短路径。然后使用for循环统计每个节点在阈值内可以到达的城市个数,因为使用for循环从小到大遍历,所以返回的城市编号一定会是大的。
代码:
class Solution {
public:
int findTheCity(int n, vector<vector<int>>& edges, int distanceThreshold) {
vector<vector<int>>dist(n,vector<int>(n,INT_MAX/2));
for(int i=0;i<n;i++){
dist[i][i]=0;
}
for(auto edge:edges){
dist[edge[0]][edge[1]]=edge[2];
dist[edge[1]][edge[0]]=edge[2];
}
for(int k=0;k<n;k++)
for(int i=0;i<n;i++)
for(int j=0;j<n;j++){
dist[i][j]=min(dist[i][j],dist[i][k]+dist[k][j]);
}
int index=-1;
int minCnt=INT_MAX;
for(int i=0;i<n;i++){
int cnt=0;
for(int j=0;j<n;j++){
if(dist[i][j]<=distanceThreshold){
cnt++;
}
}
if(cnt<=minCnt)
{
minCnt=cnt;
index=i;
}
}
return index;
}
};
执行结果:
时间复杂度:O(n^3)
空间复杂度:O(n^2)
解法二:Djkstra算法
因为迪杰斯特拉是解决单源最短路径的,使用优先队列优化的迪杰斯特拉算法,对每一个节点都使用一次迪杰斯特拉算法,并且每次都进行比较筛选出阈值内经过城市最少的城市编号。
代码:
class Solution {
public:
const int inf = 0x3f3f3f3f;
int findTheCity(int n, vector<vector<int>>& edges, int distanceThreshold) {
vector<vector<pair<int,int>>> graph(n);
for (auto &e : edges) { /* 邻接表 */
graph[e[0]].emplace_back(e[1], e[2]);
graph[e[1]].emplace_back(e[0], e[2]);
}
int minCnt = INT_MAX;
int index= -1;
/* 从每个节点出发, 求最短路径 */
for (int i = 0; i<n; i++) {
vector<int> dist(n, inf);
priority_queue<pair<int,int>, vector<pair<int,int>>, greater<pair<int,int>>> pq;
dist[i] = 0;
pq.emplace(0, i);
while (!pq.empty()) { /* 优先队列实现dijkstra算法 */
auto [d, u] = pq.top();
pq.pop();
if (dist[u] < d) {
continue;
}
for (auto &[v, w]: graph[u]) {
if (dist[v] > dist[u] + w) {
dist[v] = dist[u] + w;
pq.emplace(dist[v], v);
}
}
}
int cnt = 0;
for (int j = 0; j < n; j++) {
if (dist[j] <= distanceThreshold) {
cnt++;
}
}
if (cnt <=minCnt) {
minCnt = cnt;
index = i;
}
}
return index;
}
};
执行结果:
时间复杂度:O(n^3)
空间复杂度:O(n^2)
30.子树中标签相同的节点数
给你一棵树(即,一个连通的无环无向图),这棵树由编号从 0 到 n - 1 的 n 个节点组成,且恰好有 n - 1 条 edges 。树的根节点为节点 0 ,树上的每一个节点都有一个标签,也就是字符串 labels 中的一个小写字符(编号为 i 的 节点的标签就是 labels[i] )
边数组 edges 以 edges[i] = [ai, bi] 的形式给出,该格式表示节点 ai 和 bi 之间存在一条边。
返回一个大小为 n 的数组,其中 ans[i] 表示第 i 个节点的子树中与节点 i 标签相同的节点数。
树 T 中的子树是由 T 中的某个节点及其所有后代节点组成的树。
解法:DFS深度优先遍历+hash表
刚开始没有注意到节点0为根节点,所以思路上是每个节点开始dfs遍历,并且用记忆化dfs保存中间结果。但是节点0为根节点的条件,说明只需要一次dfs遍历即可,从根节点递归遍历,到叶子节点时记录结果并将结果返回传递。
这道题需要求出每个节点的子树中与该节点标签相同的节点数,即该节点的子树中,该节点标签出现的次数。由于一个节点的子树中可能包含任意的标签,因此需要对每个节点记录该节点的子树中的所有标签的出现次数。又由于标签一定是小写字母,因此只需要对每个节点维护一个长度为 26 的数组即可。
因此设置一个二维hash表,hash[i][j]
表示该节点的子树中,j标签出现的次数。
显然,一个节点的子树中的每个标签出现的次数,由该节点的左右子树中的每个标签出现的次数以及该节点自身的标签决定,因此可以使用深度优先搜索,递归地计算每个节点的子树中的每个标签出现的次数。
当得到一个节点的子树中的每个标签出现的次数之后,即可根据该节点的标签,得到子树中与该节点标签相同的节点数。
注意因为树是无向图,需要避免遍历时进入死循环,visited数组表示某个节点是否被访问
代码:
class Solution {
public:
vector<int>res;
vector<vector<int>>hash;
vector<int> countSubTrees(int n, vector<vector<int>>& edges, string labels) {
vector<vector<pair<int,char>>>graph(n);
for(int i=0;i<n-1;i++){
graph[edges[i][0]].push_back(make_pair(edges[i][1],labels[edges[i][1]]));
graph[edges[i][1]].push_back(make_pair(edges[i][0],labels[edges[i][0]]));
}
res=vector<int>(n);
hash=vector<vector<int>>(n,vector<int>(26,0));
vector<bool>visited(n,0);
dfs(0,graph,labels,visited);
return res;
}
void dfs(int index,vector<vector<pair<int,char>>>&graph, string &lables,vector<bool>&visited) {
visited[index]=1;
hash[index][lables[index]-'a']++;
for(auto neigh:graph[index]){
if(visited[neigh.first])
continue;
dfs(neigh.first,graph,lables,visited);
for(int i=0;i<26;i++)
hash[index][i]+=hash[neigh.first][i];
}
res[index]=hash[index][lables[index]-'a'];
}
};
011 ↩︎