文章目录
31.省份数量
有 n 个城市,其中一些彼此相连,另一些没有相连。如果城市 a 与城市 b 直接相连,且城市 b 与城市 c 直接相连,那么城市 a 与城市 c 间接相连。
省份 是一组直接或间接相连的城市,组内不含其他没有相连的城市。
给你一个 n x n 的矩阵 isConnected ,其中isConnected[i][j] = 1
表示第 i 个城市和第 j 个城市直接相连,而 isConnected[i][j] = 0
表示二者不直接相连。
返回矩阵中 省份 的数量。
与15题重复,见15题
32.旋转盒子
给你一个 m x n 的字符矩阵 box ,它表示一个箱子的侧视图。箱子的每一个格子可能为:
‘#’ 表示石头
‘*’ 表示固定的障碍物
‘.’ 表示空位置
这个箱子被 顺时针旋转 90 度 ,由于重力原因,部分石头的位置会发生改变。每个石头会垂直掉落,直到它遇到障碍物,另一个石头或者箱子的底部。重力 不会 影响障碍物的位置,同时箱子旋转不会产生惯性 ,也就是说石头的水平位置不会发生改变。
题目保证初始时 box 中的石头要么在一个障碍物上,要么在另一个石头上,要么在箱子的底部。
请你返回一个 n x m的矩阵,表示按照上述旋转后,箱子内的结果。
解法:简单模拟
算法思路:
-
首先将二维数组先进行翻转,然后按照列进行处理
-
从每列的末尾开始遍历,计算空格的个数
-
如果遇到障碍物,空格的个数就减1
-
如果遇到的了石头,让他和他的位置+空格个数所在的位置进行交换
即和最底下的空格交换,如果没有障碍物就是最底下的空格,有障碍物就是最靠近障碍物的空格
代码:
class Solution {
public:
vector<vector<char>> rotateTheBox(vector<vector<char>>& box) {
int row=box.size();
int col=box[0].size();
vector<vector<char>>ans;
for(int i=0;i<col;i++)
{
vector<char>tmp;
for(int j=row-1;j>=0;j--){
tmp.push_back(box[j][i]);
}
ans.push_back(tmp);
}
int size=row;
int newRow=col;
for(int j=0;j<size;j++){
int i=newRow-1;
int emptyCnt=0;
while(i>=0){
if(ans[i][j]=='.')
{
emptyCnt++;
}
else if(ans[i][j]=='*'){
emptyCnt=0;
}
else {
if(emptyCnt>0){
ans[i+emptyCnt][j]='#';
ans[i][j]='.';
}
}
i--;
}
}
return ans;
}
};
执行结果:
时间复杂度:O(mn)
空间复杂度:O(nm)
33.统计封闭岛屿的数目
二维矩阵 grid
由 0
(土地)和 1
(水)组成。岛是由最大的4个方向连通的 0
组成的群,封闭岛是一个 完全
由1包围(左、上、右、下)的岛。
请返回 封闭岛屿 的数目。
解法一:DFS深度优先搜索
这是一道典型的深度优先搜索的题目,当一个节点为陆地时,我们需要从上下左右四个方向进行深度优先搜索判断是否是岛屿。
在dfs过程中可能会遇到四种情况:
1.当前区域是一个陆地但是已经访问过了
2.当前区域是一个水域
3.当前区域是一个陆地还没访问过
4.当前岛屿已经超过边界
超过边界肯定返回false,如果是一个陆地并且没有访问过,则继续递归查找它的四个方向,如果是已经访问过的陆地,那么则直接返回true。 注意陆地表示为0,海洋为1,那么可以省去访问标记,将访问过的陆地直接标记为海洋即可。
最最重要的是,必须注意这个DFS不是在某个方向上检测到边界就返回false,因为有可能其他方向上还有未被检测到的陆地。
因为在每一次的递归过程中,我们都需要将该点由0变为1,检测到边界的时候,当前岛屿并不一定都已经染色了,如果直接返回false,那么当前岛屿的部分点可能还是0,就会继续被当做起始点做递归,导致答案错误。正确的做法应该是设置一个标志位flag,在检测到边界时仅仅是改变该标志位,不能提前终止递归,在循环中通过判断标志位来确认该岛屿是不是封闭岛屿。
代码:
class Solution {
public:
pair<int,int>direction[4]={{-1,0},{1,0},{0,-1},{0,1}};
int closedIsland(vector<vector<int>>& grid) {
int row=grid.size();
int col=grid[0].size();
int ans=0;
if(row<3||col<3)
return ans;
for(int i=0;i<row;i++)
for(int j=0;j<col;j++){
if(grid[i][j]==0)
{
if(dfs2(i,j,row,col,grid))
ans++;
}
}
return ans;
}
bool dfs2(int x,int y,int row, int col,vector<vector<int>>&grid){
if(x<0||x>=row||y<0||y>=col)
return false;
if(grid[x][y]==1)
return true;
grid[x][y]=1;
bool flag= true;
for (int i=0;i<4;i++){
if(!dfs2(x+direction[i].first,y+direction[i].second,row,col,grid))
flag= false;
}
return flag;
}
};
执行结果:
时间复杂度:O(mn)
空间复杂度:O(mn)递归栈的深度
34.皇位继承顺序
一个王国里住着国王、他的孩子们、他的孙子们等等。每一个时间点,这个家庭里有人出生也有人死亡。
这个王国有一个明确规定的皇位继承顺序,第一继承人总是国王自己。我们定义递归函数 Successor(x, curOrder) ,给定一个人 x 和当前的继承顺序,该函数返回 x 的下一继承人。
Successor(x, curOrder):
如果 x 没有孩子或者所有 x 的孩子都在 curOrder 中:
如果 x 是国王,那么返回 null
否则,返回 Successor(x 的父亲, curOrder)
否则,返回 x 不在 curOrder 中最年长的孩子
比方说,假设王国由国王,他的孩子 Alice 和 Bob (Alice 比 Bob 年长)和 Alice 的孩子 Jack 组成。
一开始, curOrder 为 [“king”].
调用 Successor(king, curOrder) ,返回 Alice ,所以我们将 Alice 放入 curOrder 中,得到 [“king”, “Alice”] 。
调用 Successor(Alice, curOrder) ,返回 Jack ,所以我们将 Jack 放入 curOrder 中,得到 [“king”, “Alice”, “Jack”] 。
调用 Successor(Jack, curOrder) ,返回 Bob ,所以我们将 Bob 放入 curOrder 中,得到 [“king”, “Alice”, “Jack”, “Bob”] 。
调用 Successor(Bob, curOrder) ,返回 null 。最终得到继承顺序为 [“king”, “Alice”, “Jack”, “Bob”] 。
通过以上的函数,我们总是能得到一个唯一的继承顺序。
请你实现 ThroneInheritance 类:
ThroneInheritance(string kingName) 初始化一个 ThroneInheritance 类的对象。国王的名字作为构造函数的参数传入。
void birth(string parentName, string childName) 表示 parentName 新拥有了一个名为 childName 的孩子。
void death(string name) 表示名为 name 的人死亡。一个人的死亡不会影响 Successor 函数,也不会影响当前的继承顺序。你可以只将这个人标记为死亡状态。
string[] getInheritanceOrder() 返回 除去 死亡人员的当前继承顺序列表。
解法:先序遍历:即DFS递归
虽然题目很长,但是读完题之后,会发现其实继承的顺序就是从根节点开始进行先序遍历,即DFS递归遍历。
构造函数中需要保存根节点king,类中还需要有根据birth而产生的类似图的关联管理,即每个父结点和他所有的孩子节点,同时需要记录当前死亡的节点,如果递归时此节点死亡,那么就不加入最后的继承结果集中。
需要注意的一点:一开始的时候,我是用vector容器去保存死亡的节点,但是在查找当前节点是否是死亡节点时,使用的是find函数,时间复杂度是O(n),在48个案例时超出时间限制。
之后将其改成set容器,set容器的底层是使用红黑树实现的,查找效率是O(logn),不会超时。
STL知识 C++:move函数
https://blog.csdn.net/chengjian168/article/details/107809308
std::move并不能移动任何东西,它唯一的功能是将一个左值强制转化为右值引用,继而可以通过右值引用使用该值,以用于移动语义。从实现上讲,std::move基本等同于一个类型转换:static_cast<T&&>(lvalue);
C++ 标准库使用比如vector::push_back 等这类函数时,会对参数的对象进行复制,连数据也会复制.这就会造成对象内存的额外创建, 本来原意是想把参数push_back进去就行了,通过std::move,可以避免不必要的拷贝操作。
std::move是为性能而生。
std::move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存的搬迁或者内存拷贝。
代码:
class ThroneInheritance {
public:
unordered_map<string,vector<string>>graph;
unordered_set<string> dead;
string king;
ThroneInheritance(string kingName) {
this->king=move(kingName);
}
void birth(string parentName, string childName) {
graph[move(parentName)].push_back(move(childName));
}
void death(string name) {
dead.insert(move(name));
}
vector<string> getInheritanceOrder() {
vector<string>ans;
dfs(king,ans);
return ans;
}
void dfs(string name,vector<string>&ans){
if(!dead.count(name))
{
ans.push_back(name);
}
for(auto child:graph[name]){
dfs(child,ans);
}
}
};
执行结果:
时间复杂度:
ThroneInheritance(kingName):O(1);
birth(parentName, childName):O(1);
death(name):O(1);
getInheritanceOrder():O(n),其中 n 是当前树中的总人数。我们需要对整棵树进行一次前序遍历,时间复杂度为 O(n)。
空间复杂度:
n 个节点的树包含n−1 条边,因此我们需要 O(n) 的空间(graph)存储整棵树;
我们需要 O(n) 的空间(即哈希集合)存储所有的死亡人员;
在getInheritanceOrder() 中前序遍历的过程中,我们使用的是递归,需要一定的栈空间,栈空间的大小与树的高度成正比。由于树的高度不会超过树中的节点个数,因此栈空间最多为 O(n)。
35.带阈值的图的连通性
有 n 座城市,编号从 1 到 n 。编号为 x 和 y 的两座城市直接连通的前提是: x 和 y 的公因数中,至少有一个 严格大于 某个阈值 threshold 。更正式地说,如果存在整数 z ,且满足以下所有条件,则编号 x 和 y 的城市之间有一条道路:
x % z == 0
y % z == 0
z > threshold
给你两个整数 n 和 threshold ,以及一个待查询数组,请你判断每个查询 queries[i] = [ai, bi] 指向的城市 ai 和 bi 是否连通(即,它们之间是否存在一条路径)。
返回数组 answer ,其中answer.length == queries.length 。如果第 i 个查询中指向的城市 ai 和 bi 连通,则 answer[i] 为 true ;如果不连通,则 answer[i] 为 false 。
解法:并查集
连通性自然会想到并查集模板
代码:
class Solution {
public:
vector<bool> areConnected(int n, int threshold, vector<vector<int>>& queries) {
int fa[n+1],rank[n+1];
iota(fa,fa+n+1,0);
for(int i=1;i<n+1;i++){
rank[i]=1;
}
for(int i=threshold+1;i<=n;i++)
for(int j=2*i;j<=n;j+=i){
merge(i,j,fa,rank);
}
vector<bool>ans;
for(auto q:queries){
if(find(q[0],fa)==find(q[1],fa)){
ans.push_back(1);
}else{
ans.push_back(0);
}
}
return ans;
}
//并查集
//查找根节点
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]++;
}
}
};
执行结果:
- 时间复杂度O*(NlogN+Q)
- 空间复杂度O(N)
36.细分图中的可到达节点
给你一个无向图(原始图),图中有 n 个节点,编号从 0 到 n - 1 。你决定将图中的每条边 细分 为一条节点链,每条边之间的新节点数各不相同。
图用由边组成的二维数组 edges 表示,其中 edges[i] = [ui, vi, cnti] 表示原始图中节点 ui 和 vi 之间存在一条边,cnti 是将边 细分 后的新节点总数。注意,cnti == 0 表示边不可细分。
要 细分 边 [ui, vi] ,需要将其替换为 (cnti + 1) 条新边,和 cnti 个新节点。新节点为 x1, x2, …, xcnti ,新边为 [ui, x1], [x1, x2], [x2, x3], …, [xcnti+1, xcnti], [xcnti, vi] 。
现在得到一个 新的细分图 ,请你计算从节点 0 出发,可以到达多少个节点?如果节点间距离是 maxMoves 或更少,则视为 可以到达 。
给你原始图和 maxMoves ,返回 新的细分图中从节点 0 出发 可到达的节点数 。
解法:Dijkstra+点数记录
根据题目可知,需要求得点0可到达的节点个数,细分节点可以暂时看作时两个节点之间的权值,那么可以先求得节点0到各节点的最短路径,单源最短路径用Dijkstra算法求得。
-
首先构建双向图,求得节点0到各节点的最短路径数组dist
-
使用优先队列进行堆优化的Dijkstra算法模板:注意若把两边之间的小点个数当作权值时,计算最小距离需要
dist[v]=dist[u]+w+1;
因为是否能在maxMoves此移动下到达另一个节点,需要加上1,如0-1的权值为4,需要5步移动。 -
先通过dist数组求得在maxMoves阈值下可以到达的大点数目,然后在以边为单位,记录两边中可以到达的小点数目。
for(auto &edge:edges){ int start=edge[0]; int end=edge[1]; int cnt=edge[2]; int sum=0; //记录从start出发还能够走几个小点 sum+=dist[start]<=maxMoves?maxMoves-dist[start]:0; //记录从end出发还能够走几个小点 sum+=dist[end]<=maxMoves?maxMoves-dist[end]:0; //最多两边只能是cnt个 res+=min(sum,cnt); }
以示例1为例:可知dist[0]=0;dist[1]=5,dist[2]=2;
如【0,1,10】 从点0开始则还可以走6步,从点1开始还可以走1步,sum=6+1=7;
而【0,1】之间只有10个小点,7<10,则[0,1]中可到达的小点个数为4.
37.最大化一张图的路径价值
给你一张 无向 图,图中有 n 个节点,节点编号从 0 到 n - 1 (都包括)。同时给你一个下标从 0 开始的整数数组 values ,其中 values[i] 是第 i 个节点的 价值 。同时给你一个下标从 0 开始的二维整数数组 edges ,其中 edges[j] = [uj, vj, timej] 表示节点 uj 和 vj 之间有一条需要 timej 秒才能通过的无向边。最后,给你一个整数 maxTime 。
合法路径 指的是图中任意一条从节点 0 开始,最终回到节点 0 ,且花费的总时间 不超过 maxTime 秒的一条路径。你可以访问一个节点任意次
。一条合法路径的 价值 定义为路径中 不同节点 的价值 之和 (每个节点的价值 至多 算入价值总和中一次)。
请你返回一条合法路径的 最大 价值。
注意:每个节点 至多 有 四条 边与之相连。
解法一:DFS深度优先遍历
首先根据题目底下的提示,每个节点最多有四条边,而time和maxTime的范围为10-100。
如果time取最小10,maxTime取最大100,那么也最多也只有10条边就可以返回。
即最大递归的深度为10,最多有4^10种结果。所以可以使用深度优先遍历
当递归节点等于初始节点时,可以更新结果。虽然节点可任意访问,但是节点的价值只计算一次,所以使用visited数组,如果节点已经访问过,递归时总价值不做变动。
注意递归的时候要注意visited数组在dfs完成时的回溯
代码:
class Solution {
public:
int ans=0;
int maximalPathQuality(vector<int>& values, vector<vector<int>>& edges, int maxTime) {
vector<vector<pair<int,int>>>graph(values.size());
for(auto edge:edges){
graph[edge[0]].emplace_back(edge[1],edge[2]);
graph[edge[1]].emplace_back(edge[0],edge[2]);
}
vector<bool>visited(values.size());
visited[0]=true;
dfs(0,0,maxTime,values[0],values,visited,graph);
return ans;
}
void dfs(int u,int time,int maxTime,int value,vector<int>&values,vector<bool>&visited,vector<vector<pair<int,int>>>&graph){
if(time>maxTime)
return;
if(u==0)
ans=max(ans,value);
for(auto &node:graph[u]){
int v=node.first;
int w=node.second;
if(!visited[v]){
visited[v]=1;
dfs(v,time+w,maxTime,value+values[v],values,visited,graph);
visited[v]=0;
}else{
dfs(v,time+w,maxTime,value,values,visited,graph);
}
}
}
};
执行结果:
时间复杂度:O(n + m + d^k)),其中 m 是数组 edges 的长度,d 是图中每个点度数的最大值,k 是最多经过的边的数量,在本题中 d = 4, k = 10
将 edges 存储成邻接表的形式需要的时间为 O(n + m)
搜索需要的时间为 O(d^k)
空间复杂度:O(n+m+k)。
邻接表需要的空间为 O(n+m)。
记录每个节点是否访问过的数组需要的空间为O(n)。
搜索中栈需要的空间为 O(k)。
38.有向图中最大颜色值
给你一个 有向图 ,它含有 n 个节点和 m 条边。节点编号从 0 到 n - 1 。
给你一个字符串 colors ,其中 colors[i] 是小写英文字母,表示图中第 i 个节点的 颜色 (下标从 0 开始)。同时给你一个二维数组 edges ,其中 edges[j] = [aj, bj] 表示从节点 aj 到节点 bj 有一条 有向边 。
图中一条有效 路径 是一个点序列 x1 -> x2 -> x3 -> … -> xk ,对于所有 1 <= i < k ,从 xi 到 xi+1 在图中有一条有向边。路径的 颜色值 是路径中 出现次数最多 颜色的节点数目。
请你返回给定图中有效路径里面的 最大颜色值 。如果图中含有环,请返回 -1
解法一:DFS深度优先遍历+dp
解法:因为每个节点的颜色可能是1-26种颜色中的一种,所以可以使用类似hash表的dp[i][j]
表示以节点i为起点的有效路径中颜色j的最大的值。
将边的关系以邻接表的形式保存,同时设置visited数组,visited[i]=0表示改节点未被访问,visited[i]=1,表示改节点已经被访问过,visited[i]=-1表示当前dfs结束,可以返回。
dfs过程中,如果visited[i]==1,说明成环,直接返回false。
之后每次循环后都需要继承子节点中每个颜色的最大值,然后再将自身的颜色的最大值加一,res与每次更新后的值进行比较,最后返回的res是最大值。
关于对每个点dfs时,是否需要将visited和dp清空?
是不需要的,因为
dp[i][j]
相当于记忆化搜索,如果dfs的邻接节点已经访问结束,直接可以通过for循环继承它的邻居节点的各个颜色的最大值
代码:
class Solution {
public:
int res=0;
vector<vector<int>>dp;
int largestPathValue(string colors, vector<vector<int>>& edges) {
vector<vector<int>>graph(colors.size());
for(auto edge:edges){
graph[edge[0]].emplace_back(edge[1]);
}
dp=vector<vector<int>>(colors.size(),vector<int>(26,0));
vector<int>visited(colors.size(),0);
for(int i=0;i<colors.size();i++)
{
if(!dfs(i,visited,colors,graph))
return -1;
}
return res;
}
bool dfs(int u,vector<int>&visited,string &color,vector<vector<int>>&graph){
if(visited[u]==-1)
return true;
if(visited[u]==1)
return false;
visited[u]=1;
for(auto neigh:graph[u]){
if(!dfs(neigh,visited,color,graph))
return false;
for(int k=0;k<26;k++)
dp[u][k]=max(dp[u][k],dp[neigh][k]);
}
visited[u]=-1;
dp[u][color[u]-'a']++;
res=max(res,dp[u][color[u]-'a']);
return true;
}
};
执行结果:
时间复杂度:O(n^2)
空间复杂度:O(n*m)m为字符串的大小,n为节点数
解法二:拓扑排序+dp
STL: max_element 一种比较方便的返回数组中最大值迭代的函数。返回遇到的第一个最大值迭代器。max_element(begin,end)
本题的图是一个有向图,且可以排除有环的情况,故可以使用拓扑排序,按拓扑序进行动态规划。
拓扑排序流程
①将所有入度为0的点加入队列中
②每次出队一个入度为0的点,然后将该点删除(意思是将所有与该点相连的边都删掉,即将边另一端对应的点的入度减1),若删除该点后与该点相连的点入度变为了0,则将该点加入队列。
③重复②过程直到队列中的元素被删完
排除有环的情况
因为只有入度为 0 的点才能入队,故若存在环,环上的点一定无法入队。
所以只需统计入过队的点数之和是否等于点的总数 n 即可。
算法思路:拓扑序DP
拓扑序可以理解为上述队列的出队顺序,一个点 x 拓扑序小于 y 当且仅当 x在 y之前出队,在拓扑序列上DP就是拓扑序DP
实现上,可以在步骤②中,删除一个点时进行动态规划转移。
本题中DP状态设为 f[i][j]
表示到点 i 的所有路径中,颜色为 j 的点的个数的最大值。
设 u 为拓扑序在 i 后面且存在边 )(i,u) , 则:
f[u][j]
= max(f[u][j]
, f[i][j]
+ (colors[u] - ‘a’ == j))
ans = max(f[i][j]
), i=0,…,n-1,j=0,…,25;
代码:
class Solution {
public:
int res=0;
int largestPathValue(string colors, vector<vector<int>>& edges) {
vector<vector<int>>graph(colors.size());
vector<int>indeg(colors.size());//拓扑排序记录入度
for(auto edge:edges){
indeg[edge[1]]++;
graph[edge[0]].emplace_back(edge[1]);
}
vector<vector<int>>f(colors.size(),vector<int>(26,0));
int found=0;
queue<int>que;
for(int i=0;i<colors.size();i++)
{
if(indeg[i]==0){
que.push(i);
}
}
while(!que.empty()){
found++;
int node=que.front();
que.pop();
f[node][colors[node]-'a']++;
for(int neigh:graph[node]){
indeg[neigh]--;
//状态转换方程
for(int i=0;i<26;i++)
{
f[neigh][i]=max(f[neigh][i],f[node][i]);
}
if(indeg[neigh]==0)
que.push(neigh);
}
}
if(found!=colors.size())
return -1;
for(int i=0;i<colors.size();i++)
res=max(res,*max_element(f[i].begin(),f[i].end()));
return res;
}
};
39.一个图中连通三元组的最小度数
给你一个无向图,整数 n 表示图中节点的数目,edges 数组表示图中的边,其中 edges[i] = [ui, vi] ,表示 ui 和 vi 之间有一条无向边。
一个 连通三元组 指的是 三个 节点组成的集合且这三个点之间 两两 有边。
连通三元组的度数 是所有满足此条件的边的数目:一个顶点在这个三元组内,而另一个顶点不在这个三元组内。
请你返回所有连通三元组中度数的 最小值 ,如果图中没有连通三元组,那么返回 -1 。
解法一:暴力搜索
因为n最大为400,可以考虑通过暴力搜索得到。
必须得到题目中三元组的度数其实就是三元组中三个节点的度数之和-6
只要找到三元组,就可以更新最小度数的值。
代码:
class Solution {
public:
int minTrioDegree(int n, vector<vector<int>>& edges) {
const int INF=0X3f3f3f3f;
vector<int>indegree(n+1,0);
int res=INF;
vector<vector<int>>graph(n+1,vector<int>(n+1,0));
for(auto &edge:edges){
indegree[edge[0]]++;
indegree[edge[1]]++;
graph[edge[0]][edge[1]]=1;
graph[edge[1]][edge[0]]=1;
}
for(int i=1;i<=n;i++){
//最小为1,可以不需要再寻找其他了
if(res==0)
{
break;
}
for(int j=1;j<=n;j++){
if(graph[i][j]==1) {
for(int k=1;k<=n;k++)
if(graph[i][k]==1&&graph[j][k]==1){
res=min(res,indegree[i]+indegree[j]+indegree[k]-6);
}
}
}
}
return res==INF?-1:res;
}
};
执行结果:
时间复杂度:O(n^3)
空间复杂度:O(n^2)
解法二:bitset
c++ bitset使用
https://www.cnblogs.com/magisk/p/8809922.html
C++的 bitset 在 bitset 头文件中,它是一种类似数组的结构,它的每一个元素只能是0或1,每个元素仅用1bit空间。
bitset<4> bitset1; //无参构造,长度为4,默认每一位为0 bitset<8> bitset2(12); //长度为8,二进制保存,前面用0补充 string s = "100101"; bitset<10> bitset3(s); //长度为10,前面用0补充 char s2[] = "10101"; bitset<13> bitset4(s2); //长度为13,前面用0补充 cout << bitset1 << endl; //0000 cout << bitset2 << endl; //00001100 cout << bitset3 << endl; //0000100101 cout << bitset4 << endl; //0000000010101
注意:
用字符串构造时,字符串只能包含 ‘0’ 或 ‘1’ ,否则会抛出异常。
构造时,需在<>中表明bitset 的大小(即size)。
在进行有参构造时,若参数的二进制表示比bitset的size小,则在前面用0补充(如上面的栗子);若比bitsize大,参数为整数时取后面部分,参数为字符串时取前面部分
bitset有意思的函数:
bitset<8> foo ("10011011"); cout << foo.count() << endl; //5 (count函数用来求bitset中1的位数,foo中共有5个1 cout << foo.size() << endl; //8 (size函数用来求bitset的大小,一共有8位 cout << foo.test(0) << endl; //true (test函数用来查下标处的元素是0还是1,并返回false或true,此处foo[0]为1,返回true cout << foo.test(2) << endl; //false (同理,foo[2]为0,返回false cout << foo.any() << endl; //true (any函数检查bitset中是否有1 cout << foo.none() << endl; //false (none函数检查bitset中是否没有1 cout << foo.all() << endl; //false (all函数检查bitset中是全部为1 bitset<8> foo ("10011011"); cout << foo.flip(2) << endl; //10011111 (flip函数传参数时,用于将参数位取反,本行代码将foo下标2处"反转",即0变1,1变0 cout << foo.flip() << endl; //01100000 (flip函数不指定参数时,将bitset每一位全部取反 cout << foo.set() << endl; //11111111 (set函数不指定参数时,将bitset的每一位全部置为1 cout << foo.set(3,0) << endl; //11110111 (set函数指定两位参数时,将第一参数位的元素置为第二参数的值,本行对foo的操作相当于foo[3]=0 cout << foo.set(3) << endl; //11111111 (set函数只有一个参数时,将参数下标处置为1 cout << foo.reset(4) << endl; //11101111 (reset函数传一个参数时将参数下标处置为0 cout << foo.reset() << endl; //00000000 (reset函数不传参数时将bitset的每一位全部置为0
算法思路:
对于一个由节点a,b,c构成的连通三元组,他的度即为degree[a]+degree[b]+degree[c]-6,所以我们只要找出所有的连通三元组即可,这里使用的方法是对于每一条边的两个端点,通过搜索他们是否有公共邻居来实现,公共邻居使用bitset来记录,两个bitset的and即为公共邻居。
将节点与其他节点的连接关系有bitset保存,若当前节点和某节点有边,则在对应的位置置1.
例如1,2,3为一个三元组
对于1节点:bitset[1]=0110 2节点:bitset[2]=0101 3节点: bitset[3]=0011
若从【1,2】边入手,如果1,2有公共邻居,则可以组成三元组,bitset[1]&bitset[2]=0100,可以发现1和2的公共邻居的二进制位为1。这样就找到公共邻居了。
代码:
class Solution {
public:
int minTrioDegree(int n, vector<vector<int>>& edges) {
const int INF=0x3f3f3f3f;
int res=INF;
bitset<401>graph[n+1];
int degree[n+1];
memset(degree,0, sizeof(degree));
for(int i=1;i<=n;i++)
{
bitset<401>temp{0};
graph[i]=temp;
}
for(auto e:edges){
graph[e[0]].set(e[1]);
graph[e[1]].set(e[0]);
degree[e[0]]++;
degree[e[1]]++;
}
for(auto e:edges){
bitset<401>neigh=graph[e[0]]&graph[e[1]];
for(int i=1;i<=n;i++){
if(neigh[i]){
res=min(res,degree[e[0]]+degree[e[1]]+degree[i]-6);
}
}
}
return res==INF?-1:res;
}
};
执行结果:
时间复杂度:O(n^2)
空间复杂度:O(n^2)
40.保证图的可完全遍历
Alice 和 Bob 共有一个无向图,其中包含 n 个节点和 3 种类型的边:
类型 1:只能由 Alice 遍历。
类型 2:只能由 Bob 遍历。
类型 3:Alice 和 Bob 都可以遍历。
给你一个数组 edges ,其中 edges[i] = [typei, ui, vi] 表示节点 ui 和 vi 之间存在类型为 typei 的双向边。请你在保证图仍能够被 Alice和 Bob 完全遍历的前提下,找出可以删除的最大边数。如果从任何节点开始,Alice 和 Bob 都可以到达所有其他节点,则认为图是可以完全遍历的。
返回可以删除的最大边数,如果 Alice 和 Bob 无法完全遍历图,则返回 -1 。
解法:并查集
思路与算法
我们称类型 1,2,3 的边分别为「Alice 独占边」「Bob 独占边」以及「公共边」。
首先我们需要思考什么样的图是可以被 Alice 和 Bob 完全遍历的。对于 Alice 而言,她可以经过的边是「Alice 独占边」以及「公共边」,由于她需要能够从任意节点到达任意节点,那么就说明:
当图中仅有「Alice 独占边」以及「公共边」时,整个图是连通的,即整个图只包含一个连通分量。
同理,对于 Bob 而言,当图中仅有「Bob 独占边」以及「公共边」时,整个图也要是连通的。
由于题目描述中希望我们删除最多数目的边,这等价于保留最少数目的边。换句话说,我们可以从一个仅包含 n 个节点(而没有边)的无向图开始,逐步添加边
,使得满足上述的要求。
那么我们应该按照什么策略来添加边呢?直觉告诉我们,「公共边」的重要性大于「Alice 独占边」以及「Bob 独占边」,因为「公共边」是 Alice 和 Bob 都可以使用的,而他们各自的独占边却不能给对方使用
。「公共边」的重要性也是可以证明的:
对于一条连接了两个不同的连通分量的「公共边」而言,如果我们不保留这条公共边,那么 Alice 和 Bob 就无法往返这两个连通分量,即他们分别需要使用各自的独占边。因此,Alice 需要一条连接这两个连通分量的独占边,Bob 同样也需要一条连接这两个连通分量的独占边,那么一共需要两条边,这就严格不优于直接使用一条连接这两个连通分量的「公共边」了。
因此,我们可以遵从优先添加「公共边」的策略。具体地,我们遍历每一条「公共边」,对于其连接的的两个节点:
如果这两个节点在同一个连通分量中,那么添加这条「公共边」是无意义的;
如果这两个节点不在同一个连通分量中,我们就可以(并且一定)添加这条「公共边」,然后合并这两个节点所在的连通分量。
这就提示了我们使用并查集来维护整个图的连通性,上述的策略只需要用到并查集的「查询」和「合并」这两个最基础的操作。
在处理完了所有的「公共边」之后,我们需要处理他们各自的独占边,而方法也与添加「公共边」类似。我们将当前的并查集复制一份,一份交给 Alice,一份交给 Bob。随后 Alice 不断地向并查集中添加「Alice 独占边」,Bob 不断地向并查集中添加「Bob 独占边」。在处理完了所有的独占边之后,如果这两个并查集都只包含一个连通分量,那么就说明 Alice 和 Bob 都可以遍历整个无向图。
细节
在使用并查集进行合并的过程中,我们每遇到一次失败的合并操作(即需要合并的两个点属于同一个连通分量),那么就说明当前这条边可以被删除,将答案增加 1。
代码:
class UnionFind{
public:
vector<int>father;//父亲节点
vector<int>rank;//秩的数
int n;//节点个数
int setCount;//连通分量数
public:
//查询父结点
UnionFind(int _n):n(_n),setCount(_n),rank(_n,1),father(_n){
//iota函数,令father[i]=i;
iota(father.begin(),father.end(),0);
}
int find(int x,vector<int>&father){
if(x==father[x])
return x;
else{
father[x]=find(father[x],father);
return father[x];
}
}
//并查集合并
bool merge(int i,int j){
int x=find(i,father);
int y=find(j,father);
if(x==y){
return false;
}
if(rank[x]<=rank[y]){
father[x]=y;
}
else{
father[y]=x;
}
if(rank[x]==rank[y]&&x!=y)
rank[y]++;
setCount--;
return true;
}
bool isConnected(int x,int y){
x=find(x,father);
y=find(y,father);
return x==y;
}
};
class Solution {
public:
int maxNumEdgesToRemove(int n, vector<vector<int>>& edges) {
//让序号可以从0开始 注意这里必须用引用,否则无法修改edge的值
for(auto &edge:edges){
edge[1]--;
edge[2]--;
}
UnionFind a(n);
UnionFind b(n);
int ans=0;
//先添加公共边 注意如果添加公共边时发现两点已经连通了
//那么这条边就是不必要的,可以删除,同时a和b都需要复制相同的并查集
//所以如果可以删除,ans++,b只需要做相同的操作,不需要在ans++了;
for(auto &edge:edges){
if(edge[0]==3){
if(!a.merge(edge[1],edge[2])){
ans++;
}
//如果公共边成功添加,那么在b的并查集里也需要添加
else{
b.merge(edge[1],edge[2]);
}
}
}
for(auto &edge:edges){
if(edge[0]==1){
if(!a.merge(edge[1],edge[2])){
ans++;
}
}
else if(edge[0]==2){
if(!b.merge(edge[1],edge[2])){
ans++;
}
}
}
if(a.setCount!=1||b.setCount!=1)
{
return -1;
}
return ans;
}
};
执行结果:
时间复杂度:O(m⋅α(n)),其中 m 是数组 edges 的长度,α 是阿克曼函数的反函数。
空间复杂度:O(n),即为并查集需要使用的空间。
41.访问所有节点的最短路径
存在一个由 n 个节点组成的无向连通图,图中的节点按从 0 到 n - 1 编号。
给你一个数组 graph 表示这个图。其中,graph[i] 是一个列表,由所有与节点 i 直接相连的节点组成。
返回能够访问所有节点的最短路径的长度。你可以在任一节点开始和停止,也可以多次重访节点,并且可以重用边。
解法一:BFS+状态压缩
由于题目需要我们求出「访问所有节点的最短路径的长度」,并且图中每一条边的长度均为 1,因此我们可以考虑使用广度优先搜索的方法求出最短路径。
在常规的广度优先搜索中,我们会在队列中存储节点的编号。对于本题而言,最短路径的前提是「访问了所有节点」,因此除了记录节点的编号以外,我们还需要记录每一个节点的经过情况。因此,我们使用三元组(u,mask,dist) 表示队列中的每一个元素,其中:
u 表示当前位于的节点编号;
mask 是一个长度为 n 的二进制数,表示每一个节点是否经过。如果 mask 的第 i 位是 1,则表示节点 i 已经过,否则表示节点 i 未经过;
dist 表示到当前节点为止经过的路径长度。
这样一来,我们使用该三元组进行广度优先搜索,即可解决本题。初始时,我们将所有的(i,2^i ,0) 放入队列,表示可以从任一节点开始。在搜索的过程中,如果当前三元组中的 mask包含的1的个数为n个,那么我们就可以返回 dist 作为答案。
细节
为了保证广度优先搜索时间复杂度的正确性,即同一个节点 u 以及节点的经过情况mask 只被搜索到一次,我们可以使用数组或者哈希表记录 (u,mask) 是否已经被搜索过,防止无效的重复搜索。
就比如0-2-0-2当第四搜索到2时,实际上已经重复了,这是我们可以通过二维数组seen[i][j]
来保证某个节点只被访问一次。
注意:mask使用bitset可以加快运行速度
代码:
class Solution {
public:
int shortestPathLength(vector<vector<int>>& graph) {
const int n=graph.size();
queue<tuple<int,bitset<12>,int>>que;
// bitset<12>visited[n];
vector<vector<int>> seen(n, vector<int>(1 << n));
for(int i=0;i<n;i++){
bitset<12>b{0};
b.set(i);
seen[i][1<<i]= true;
que.emplace(i,b,0);
}
int ans=0;
while(!que.empty()){
auto[u,mask,dist]=que.front();
que.pop();
if(mask.count()==n){
ans=dist;
return ans;
}
for(int v:graph[u]){
auto m=mask;
int mask_v=(int)m.set(v).to_ulong();
if(!seen[v][mask_v]){
que.emplace(v,mask_v,dist+1);
seen[v][mask_v]= true;
}
}
}
return ans;
}
};
执行结果:
时间复杂度:O(n^2)
空间复杂度: O(n*2^n)
解法二:Floyd+dp
代码:
class Solution {
public:
int shortestPathLength(vector<vector<int>>& graph) {
int n = graph.size();
vector<vector<int>> d(n, vector<int>(n, n + 1));
for (int i = 0; i < n; ++i) {
for (int j: graph[i]) {
d[i][j] = 1;
}
}
// 使用 floyd 算法预处理出所有点对之间的最短路径长度
for (int k = 0; k < n; ++k) {
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}
}
}
vector<vector<int>> f(n, vector<int>(1 << n, INT_MAX / 2));
for (int mask = 1; mask < (1 << n); ++mask) {
// 如果 mask 只包含一个 1,即 mask 是 2 的幂
if ((mask & (mask - 1)) == 0) {
//这个函数返回这个u末尾的0的个数
int u = __builtin_ctz(mask);
f[u][mask] = 0;
}
else {
for (int u = 0; u < n; ++u) {
if (mask & (1 << u)) {
for (int v = 0; v < n; ++v) {
if ((mask & (1 << v)) && u != v) {
f[u][mask] = min(f[u][mask], f[v][mask ^ (1 << u)] + d[v][u]);
}
}
}
}
}
}
int ans = INT_MAX;
for (int u = 0; u < n; ++u) {
ans = min(ans, f[u][(1 << n) - 1]);
}
return ans;
}
};
执行结果:
42.猫和老鼠
两位玩家分别扮演猫和老鼠,在一张 无向 图上进行游戏,两人轮流行动。
图的形式是:graph[a] 是一个列表,由满足 ab 是图中的一条边的所有节点 b 组成。
老鼠从节点 1 开始,第一个出发;猫从节点 2 开始,第二个出发。在节点 0 处有一个洞。
在每个玩家的行动中,他们 必须 沿着图中与所在当前位置连通的一条边移动。例如,如果老鼠在节点 1 ,那么它必须移动到 graph[1] 中的任一节点。
此外,猫无法移动到洞中(节点 0)。
然后,游戏在出现以下三种情形之一时结束:
如果猫和老鼠出现在同一个节点,猫获胜。
如果老鼠到达洞中,老鼠获胜。
如果某一位置重复出现(即,玩家的位置和移动顺序都与上一次行动相同),游戏平局。
给你一张图 graph ,并假设两位玩家都都以最佳状态参与游戏:
如果老鼠获胜,则返回 1;
如果猫获胜,则返回 2;
如果平局,则返回 0 。
这题实在太难了
解法一:DFS+记忆化搜索
代码:
class Solution {
public:
int catMouseGame(vector<vector<int>>& graph) {
int n=graph.size();
int memo[2*55*55][55][55];
memset(memo,-1,sizeof(memo));
return dfs(0,1,2,n,graph,memo);
}
int dfs(int k,int a,int b,int n,vector<vector<int>>&graph,int memo[][55][55]){
int ans=memo[k][a][b];
if(a==0) ans= 1;//老鼠获胜
else if(a==b)ans=2;//猫获胜
else if(k>=2*n*n) ans= 0;//平局
else if(ans==-1){
//老鼠走
if(k%2==0){
//老鼠先走 win设置最坏的结果败 draw设置是否平局
bool win=false;
bool draw= false;
for(auto &ne:graph[a]){
int t=dfs(k+1,ne,b,n,graph,memo);
if(t==0) draw= true;
else if(t==1)
{
win= true;
break;
}
}
if(win)
ans=1;
else if(draw) ans=0;
else ans=2;
}
else{
//猫走
bool win=false;
bool draw= false;
for(auto ne:graph[b]){
if (ne == 0) continue;
int t=dfs(k+1,a,ne,n,graph,memo);
if(t==0) draw= true;
else if(t==2){
win= true;
break;
}
}
if(win)
ans=2;
else if(draw)
ans=0;
else
ans=1;
}
}
memo[k][a][b]=ans;
return ans;
}
};
执行结果:
时间复杂度:状态的数量级为 n^4,可以确保每个状态只会被计算一次。复杂度为 O(n^4)
空间复杂度:O(n^4)处。
解法二:官方解法拓扑排序
https://leetcode-cn.com/problems/cat-and-mouse/solution/mao-he-lao-shu-by-leetcode-solution-444x/
43.尽量减少恶意传播I
给出了一个由 n 个节点组成的网络,用 n × n 个邻接矩阵图 graph 表示。在节点网络中,当 graph[i][j] = 1
时,表示节点 i 能够直接连接到另一个节点 j。
一些节点 initial 最初被恶意软件感染。只要两个节点直接连接,且其中至少一个节点受到恶意软件的感染,那么两个节点都将被恶意软件感染。这种恶意软件的传播将继续,直到没有更多的节点可以被这种方式感染。
假设 M(initial) 是在恶意软件停止传播之后,整个网络中感染恶意软件的最终节点数。
如果从 initial 中移除某一节点能够最小化 M(initial), 返回该节点。如果有多个节点满足条件,就返回索引最小的节点。注意这个索引最小的意思是返回initial中的值最小的点
请注意,如果某个节点已从受感染节点的列表 initial 中删除,它以后仍有可能因恶意软件传播而受到感染。
解法一:DFS计算连通分量
首先,把图中所有的连通分量各自标上不同的颜色,这可以用深度优先搜索来实现。
如题所述,如果 initial 中的两个节点的颜色相同(即属于同一个连通分量),那移除这种节点是不会减少 M(initial) 的,因为恶意软件会感染同一个连通分量中的所有节点。
因此,对于 initial 中颜色唯一的节点,从中选择一个移除来最大限度地减少被感染节点数。(如果有多个节点都可以达成最优解,就选择下标最小的节点。另外,如果没有颜色唯一的节点,就直接返回下标最小的节点。)
算法
算法包括以下几个部分:
给连通分量上色: 遍历每个节点,如果它还没有颜色,就用深度优先搜索去遍历它所在的连通分量,同时给这个连通分量标上新的颜色。
计算每个连通分量的大小: 数一下每个颜色的节点各有多少个。
找到唯一的颜色: 找到 initial 中颜色唯一的节点。
选择答案: 对于 initial 中颜色唯一的节点,计算这个颜色节点的个数。从中选出最大节点个数的那个,如果有多个最优解,选择其中节点下标最小的。
如果没有颜色唯一的节点,直接返回 min(initial)。
代码:
class Solution {
public:
int minMalwareSpread(vector<vector<int>>& graph, vector<int>& initial) {
int n=graph.size();
vector<int>colors(n,-1);
vector<vector<int>>g(n);
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
if(graph[i][j]==1&&i!=j){
g[i].push_back(j);
}
}
}
for(int i=0;i<n;i++)
{
if(colors[i]==-1)
dfs(i,i,colors,g);
}
//计算每个颜色的连通分量的大小
vector<int>size(n,0);
for(int color:colors){
size[color]++;
}
//找到initial中连通分量为1的颜色节点
//计算节点中存在的所有颜色,找到颜色唯一的点
vector<int>count(n);
for(int node:initial){
count[colors[node]]++;
}
int ans=INT_MAX;
for(int node:initial){
int c=colors[node];
if(count[c]==1){
if(ans==INT_MAX)
ans=node;
else if(size[c]>size[colors[ans]]){
ans=node;
}
else if(size[c]==size[colors[ans]]&&node<ans){
ans=node;
}
}
}
if(ans==INT_MAX){
for(int node:initial){
ans=min(node,ans);
}
}
return ans;
}
//dfs染色
void dfs(int node,int color,vector<int>&colors,vector<vector<int>>graph){
colors[node]=color;
for(auto neigh:graph[node]){
if(colors[neigh]==-1)
dfs(neigh,color,colors,graph);
}
}
};
执行结果:
- 时间复杂度: O(N^2),其中 N 是
graph
的大小。 - 空间复杂度: O(N)。
解法二:并查集
同 方法一 一样,也得找出图中所有的连通分量,不同的是这一步用并查集来做。
在并查集中会额外计算连通分量的大小,当合并两个连通分量的时候,会把它们的大小进行累加。
借助并查集,可以用 方法一 中一样的思路处理:对于 initial 中每个颜色唯一的节点,都去计算连通分量的大小,从中找到最优解。如果 initial 中没有颜色唯一的节点,直接返回 min(initial)。
注意计算连通分量的时候,合并需要按秩合并,否则有可能超出int长度的限制,出错
代码:
class UnionFind2{
public:
vector<int>father;//父亲节点
int n;//节点个数
vector<int>size;//连通分量数
public:
//查询父结点
UnionFind2(int _n):n(_n),father(_n),size(_n,1){
//iota函数,令father[i]=i;
iota(father.begin(),father.end(),0);
}
int find(int x){
if(x==father[x])
return x;
else{
father[x]=find(father[x]);
return father[x];
}
}
//并查集合并
void merge(int i,int j){
int x=find(i);
int y=find(j);
if (x != y)
{
// 始终用更大的size作为父节点
if (size[x] < size[y])
{
swap(x, y);
}
father[y] = x;
size[x] += size[y];
}
}
bool isConnected(int x,int y){
x=find(x);
y=find(y);
return x==y;
}
};
class Solution {
public:
int minMalwareSpread(vector<vector<int>>& graph, vector<int>& initial) {
int n = graph.size();
UnionFind2 unionFind2(n);
for (int i = 0; i < n; i++)
for (int j = i + 1; j < n; j++) {
if (graph[i][j] == 1) {
unionFind2.merge(i, j);
}
}
vector<int> count(n);
for (int node:initial) {
count[unionFind2.find(node)]++;
}
int ans = INT_MAX;
for (int node:initial) {
int f = unionFind2.find(node);
if (count[f] == 1) {
if (ans == INT_MAX)
ans = node;
else if (unionFind2.size[f] > unionFind2.size[unionFind2.find(ans)]) {
ans = node;
} else if (unionFind2.size[f] == unionFind2.size[unionFind2.find(ans)] && node < ans) {
ans = node;
}
}
}
if(ans==INT_MAX){
for(int node:initial){
ans=min(node,ans);
}
}
return ans;
}
};
执行结果:
时间复杂度:O(n^2)
空间复杂度:O(n)
44.尽量减少恶意传播II
给定一个由 n 个节点组成的网络,用 n x n 个邻接矩阵 graph 表示。在节点网络中,只有当 graph[i][j] = 1
时,节点 i 能够直接连接到另一个节点 j。
一些节点 initial 最初被恶意软件感染。只要两个节点直接连接,且其中至少一个节点受到恶意软件的感染,那么两个节点都将被恶意软件感染。这种恶意软件的传播将继续,直到没有更多的节点可以被这种方式感染。
假设 M(initial) 是在恶意软件停止传播之后,整个网络中感染恶意软件的最终节点数。
我们可以从 initial 中删除一个节点,并完全移除该节点以及从该节点到任何其他节点的任何连接。
请返回移除后能够使 M(initial) 最小化的节点。如果有多个节点满足条件,返回索引 最小的节点 。
解法一:DFS
因为这题删除的感染源是删除这个节点和对应的边,必须破坏子图内部的连接关系。
- 使用dfs计算每个感染源节点可以感染的正常节点个数
- 然后统计每个正常节点可以被哪些感染源节点感染
- 如果正常节点可以被多个感染源感染,那么删除其中一个感染源是无用的,因为该正常节点
还会被其他感染源节点感染,不会影响M的大小 - 所以我们只要关注只被一个感染源感染的正常节点即可。
- 最后统计一下只被一个感染源感染的正常节点中,每个感染源节点分别感染了哪些正常节点。
- 最后取得其中感染正常节点最火的感染源索引,如果没有则返回最小索引
代码:
class Solution {
public:
int minMalwareSpread(vector<vector<int>>& graph, vector<int>& initial) {
//使用dfs计算每个感染源节点可以感染的正常节点个数
//然后统计每个正常节点可以被哪些感染源节点感染
//如果正常节点可以被多个感染源感染,那么删除其中一个感染源是无用的,因为该正常节点
//还会被其他感染源节点感染,不会影响M的导小
//最后统计一下只被一个感染源感染的正常节点中,感染源节点分别感染了哪些正常节点,取得最大轴
int n=graph.size();
vector<int>infectSorce(n);
sort(initial.begin(),initial.end());
for(int node:initial){
infectSorce[node]=1;
}
//记录正常节点可以被哪些感染源感染
vector<vector<int>>infectedBy(n);
for(int n:initial){
set<int>infected;
dfs(graph,infectSorce,infected,n);
for(int v:infected){
infectedBy[v].push_back(n);
}
}
//统计只被一个感染源节点感染的正常节点中,感染源节点可以感染的节点个数
vector<int>count(n,-1);
for(auto c:infectedBy){
if(c.size()==1)
count[c[0]]++;
}
int min=-1;
int ans=initial[0];
for(int i=0;i<n;i++)
{
if(count[i]>min){
ans=i;
min=count[i];
}
}
return ans;
}
void dfs(vector<vector<int>>&graph,vector<int>&infectSorce,set<int>&infected,int u){
for(int j=0;j<graph.size();j++){
if(graph[u][j]==1&&infectSorce[j]==0&&!infected.count(j))
{
infected.insert(j);
dfs(graph,infectSorce,infected,j);
}
}
}
};
执行结果:
时间复杂度:O(n^2)
空间复杂度:O(n)
解法二:逆向并查集
1)前言
问:本题为什么不能用上题题解【并查集解法】中的方法二呢?
答:因为在本题中,当删除一个感染节点之后,与其相连的边也会跟着删除,这样原本的一个连通分量可能因此而一分为二,这样原本的连通分量中的感染节点数可能产生变化,所以单单通过连通分量中的感染节点数来判断,对于本题是不够的。
提到 拆分 连通图,是否还记得打砖块那道题,我们之前提过可以采用【逆向并查集】。因为并查集本身只适合用作集合的合并,并不适合用作集合的拆分
当碰到拆分 并查集的题干,应该想到 逆向思维 地利用并查集
当然,本题也不例外
这样,我们采用逆向思维并结合本题的背景,将问题由删除一个感染节点能减少多个节点受到感染转换成添加一个感染节点会增加多少个被感染节点,具体看下推理过程:
推理过程:
题目说了 删除一个节点要使得最后的被感染节点数最少,所以,删除这个节点之后,该节点能帮我们恢复正常的节点应该尽可能多。
意思是说,要找出一个感染节点,仅仅受到该节点感染的正常节点数 相对于 仅仅受到其他感染节点影响的节点数 最多。
逆向思考,即添加一个感染节点,使得该节点能感染的节点最多。
2)步骤
忽略所有感染节点,只考虑正常节点。针对所有正常节点之间的连接关系,构建初始并查集 ds(此时并查集中的所有连通分量均未受到感染)
依次遍历感染节点数组 initial ,当判断到节点 i 时, 计算初始并查集 ds 中有哪些连通分量只与感染节点i相连,不与其他任何感染节点相连。计算满足条件的所有连通分量的节点数之和 counts,加入到数组 infectNums中。
遍历 infectNums ,找出最大值,返回节点编号
代码:
// 比较常规的并查集模版
class DJset {
public:
vector<int>father;//父亲节点
int n;//节点个数
vector<int>size;//连通分量数
vector<int>rank;
public:
//查询父结点
DJset(int _n):n(_n),father(_n),size(_n,1),rank(_n,1){
//iota函数,令father[i]=i;
iota(father.begin(),father.end(),0);
}
int find(int x){
if(x==father[x])
return x;
else{
father[x]=find(father[x]);
return father[x];
}
}
//并查集合并
void merge(int i,int j){
int x=find(i);
int y=find(j);
if(x!=y){
if(rank[x]<rank[y]){
swap(x,y);
}
father[y]=x;
size[x]+=size[y];
if (rank[x] == rank[y])
rank[x] += 1;
}
}
};
class Solution {
public:
// 说明: 感染节点 意为 initial 中的节点
int minMalwareSpread(vector<vector<int>>& graph, vector<int>& initial) {
int n = graph.size();
sort(initial.begin(), initial.end());
int maxM = -1; // 记录最少感染节点数
int idx = initial[0]; // 记录产生最少感染节点数的 删除节点
DJset ds(n);
// 先对所有正常节点, 构建并查集
for (int i = 0; i < n; i++) {
if (count(initial.begin(), initial.end(), i)) continue;
for (int j = i + 1; j < n; j++) {
if (count(initial.begin(), initial.end(), j)) continue;
if (graph[i][j] == 1) ds.merge(i, j);
}
}
// infectRoot : 记录 感染来源 个数
// 主要记录连通分量的感染来源,此处只有 并查集连通分量中的根节点存在值,其他下标均为 -1
// -1 :该下标出节点不是连通分量根节点
// -2 :该下标处节点为连通分量根节点,但有 多 个感染来源
// 其他值 : 该下标处节点为连通分量,且该连通分量只有 1 个感染来源
vector<int> infectRoot(n, -1);
for (auto& i : initial) {
for (int j = 0; j < n; j++) {
if (count(initial.begin(), initial.end(), j)) continue;
int rj = ds.find(j);
if (graph[i][j] == 0 || infectRoot[rj] == -2) continue;
if (infectRoot[rj] == -1) {
infectRoot[rj] = i;
} else if(infectRoot[rj] != i) {
// 如果本连通分量已存在感染源,且感染源不是节点i
// 说明该分量存在了多个不同的感染源
infectRoot[rj] = -2;
}
}
}
// infectNums : 记录每个 感染节点 能感染的节点数
vector<int> infectNums(n);
for (int i = 0; i < n; i++) {
if (infectRoot[i] == -1 || infectRoot[i] == -2) continue;
infectNums[infectRoot[i]] += ds.size[i];
}
for (auto& i : initial) {
if (infectNums[i] > maxM) {
maxM = infectNums[i];
idx = i;
}
}
return idx;
}
};
执行结果:
时间复杂度:O(n^2)
空间复杂度:O(n)
45.冗余连接II
在本问题中,有根树指满足以下条件的 有向 图。该树只有一个根节点,所有其他节点都是该根节点的后继。该树除了根节点之外的每一个节点都有且只有一个父节点,而根节点没有父节点。
输入一个有向图,该图由一个有着 n 个节点(节点值不重复,从 1 到 n)的树及一条附加的有向边构成。附加的边包含在 1 到 n 中的两个不同顶点间,这条附加的边不属于树中已存在的边。
结果图是一个以边组成的二维数组 edges 。 每个元素是一对 [ui, vi],用以表示 有向 图中连接顶点 ui 和顶点 vi 的边,其中 ui 是 vi 的一个父节点。
返回一条能删除的边,使得剩下的图是有 n 个节点的有根树。若有多个答案,返回最后出现在给定二维数组的答案。
解法:并查集 找环
为此设计算法如下:
-
先统计每一个结点的入度,如果有入度为 2 的结点,将其他节点通过并查集进行合并
然后因为是有根树+附加边,那么入度为2的节点数一定是2.所以可以尝试将其中一条边通过并查集进行合并,如果合并失败,就是这两边的两个节点的父结点相同,那么说明会形成环,返回这条边。否则返回另外一条边。
-
如果不存在入度为2的点,那么有可能是示例2中,有环的存在,那么直接使用并查集进行合并所有的边,如果有一条边合并失败,返回这条边即可。
代码:
class UnionFind3{
public:
vector<int>father;//父亲节点
vector<int>rank;//秩的数
int n;//节点个数
public:
//查询父结点
UnionFind3(int _n):n(_n),rank(_n,1),father(_n){
//iota函数,令father[i]=i;
iota(father.begin(),father.end(),0);
}
int find(int x){
if(x==father[x])
return x;
else{
father[x]=find(father[x]);
return father[x];
}
}
//并查集合并
bool merge(int i,int j){
int x=find(i);
int y=find(j);
if(x==y){
return false;
}
if(rank[x]<=rank[y]){
father[x]=y;
}
else{
father[y]=x;
}
if(rank[x]==rank[y]&&x!=y)
rank[y]++;
return true;
}
bool isConnected(int x,int y){
x=find(x);
y=find(y);
return x==y;
}
};
class Solution {
public:
vector<int> findRedundantDirectedConnection(vector<vector<int>>& edges) {
int n=edges.size();
vector<int>in(n+1,0);
int target=-1;
for(int i=0;i<n;i++){
in[edges[i][1]]++;
if(in[edges[i][1]]==2){
target=edges[i][1];
break;
}
}
//说明没有入度为2的点,有可能成环,找到成环的点
if(target==-1){
UnionFind3 unionFind3(n+1);
for(auto &e:edges){
int x=unionFind3.find(e[0]);
int y=unionFind3.find(e[1]);
if(x!=y){
unionFind3.merge(x,y);
}
else{
return vector<int>{e[0],e[1]};
}
}
}
else{
vector<vector<int>>two;
UnionFind3 unionFind3(n+1);
for(int i=0;i<n;i++){
if(edges[i][1]==target){
two.push_back({edges[i][0],edges[i][1]});
}else {
unionFind3.merge(edges[i][0],edges[i][1]);
}
}
for(auto &edge:two){
// cout<<unionFind3.merge(edge[0],edge[1])<<endl;
if(unionFind3.merge(edge[0],edge[1]))
return two[1];
else
return two[0];
}
}
return vector<int>{};
}
};
执行结果:
时间复杂度:O(nlogn),其中 n 是图中的节点个数。需要遍历图中的 n 条边,对于每条边,需要对两个节点查找祖先,如果两个节点的祖先不同则需要进行合并,需要进行 2 次查找和最多 1 次合并。一共需要进行 2n 次查找和最多 n 次合并,因此总时间复杂度是O(2nlogn)=O(nlogn)。这里的并查集使用了路径压缩,最坏情况下的时间复杂度是 O(nlogn),平均情况下的时间复杂度依然是 O(nα(n)),其中α 为阿克曼函数的反函数,α(n) 可以认为是一个很小的常数。
空间复杂度:O(n),其中 n 是图中的节点个数。使用数组 father 记录每个节点的父节点,并查集使用数组记录每个节点的祖先。
46.并行课程II
给你一个整数 n 表示某所大学里课程的数目,编号为 1 到 n ,数组 dependencies 中, dependencies[i] = [xi, yi] 表示一个先修课的关系,也就是课程 xi 必须在课程 yi 之前上。同时你还有一个整数 k 。
在一个学期中,你 最多 可以同时上 k 门课,前提是这些课的先修课在之前的学期里已经上过了。
请你返回上完所有课最少需要多少个学期。题目保证一定存在一种上完所有课的方式。
解法:拓扑排序+贪心 错误
这题一开始我想用拓扑排序,不管是正向拓扑排序+BFS还是逆向的,加上贪心算法,先上出度最大的,最后都是错误的。
因为这题的本质是NPC问题,任何贪心的思想都是错的。
解法:n<16,状态压缩 dp
状态压缩常用操作
预备知识
首先分享一些状态压缩常用的操作(本题不一定会用到)批量统计i的二进制数中1的个数
for (int i = 0; i < totalState; ++i) { cnt[i] = cnt[i >> 1] + (i & 1); }
统计单个数num的二进制数中1的个数,32可以为其他数,取决于num的二进制数长度
bit_set<32>(num).count()
遍历state的子集,比如state是011,那么子集是{001, 010, 011}
for (int subMask = state; subMask; subMask = (subMask - 1) & state) { operate subMask... }
取a的最低位1,比如a = 00110100,则lowBit = 100,原理是一个数的负数是它的反码加1,即从右到左数(低位到高位)保留第一个1,其他都取反
int lowBit = a & -a
判断x是否是y的子集,表达式为true的话,说明x是y的子集
(x & y) == x
判断x的二进制数中是否有连续的1,表达式为true的话,说明没有
(x & (x >> 1)) == 0
判断x的二进制数中,第i位是否是1,从左到右数
1 & (x >> i)
算法思路:
-
首先使用二进制记录每门课程的先修课程
-
设置状态dp[1<<n-1]为最后返回的结果,dp[i]表示修到i二进制中所有为1的课程后,最小学期数
-
dp[0]=0;
-
只有从0开始更新状态,在上过taken的基础上,还有哪些课可以上,要满足两个条件
-
((taken & (1 << j)) == 0) 表示这个课在taken中没上过
-
requisite[j] & taken) == prerequisite[j]) 表示这个课的先修课已经上完了
-
-
注意枚举这学期可以上的所有课程的子集,然后才更新最小值
-
最后返回dp[totalCount-1]
代码:
class Solution { public: int minNumberOfSemesters(int n, vector<vector<int>>& relations, int k) { vector<int>preCource(n,0); //记录每个课程的先修课程,用二进制|可以得到 //例如pre[1]=0110,说明1的先修课程是2和3 for(auto &r:relations){ int p=r[0]-1; int a=r[1]-1; preCource[a]|=1<<p; } int totalCount=1<<n; //0为不需要上任何课,学期数为0 vector<int>dp(totalCount,16); dp[0]=0; vector<int>cnt(totalCount); //可以计算出每个i的二进制中1的个数 cnt[0]=0; for(int i=1;i<totalCount;i++){ cnt[i]=cnt[i>>1]+(i&1); } //taken表示已经上过的课程 take=0111,表示课程1 2 3 已经上过了 for(int take=0;take<totalCount;take++){ if(dp[take]>n) continue; int cur=0; // 在上过taken的基础上,还有哪些课可以上,要满足两个条件 // 1. ((taken & (1 << j)) == 0) 表示这个课在taken中没上过 // 2. ((prerequisite[j] & taken) == prerequisite[j]) 表示这个课的先修课已经上完了 for(int j=0;j<n;j++) { if((take&(1<<j))==0&&((preCource[j]&take)==preCource[j])) { // 存这学期可以上的课,注意,可以上不代表一定要上,也不一定要上满 // 这题的本质是NPC问题,任何贪心的思想都是错的,选择cur中的课来上的这个操作,用下面枚举子集的方法实现 cur|=(1<<j); } } //枚举cur的子集,比如cur=111 它的子mask集合就是{111, 110, 101 011, 100, 010, 001} for(int subMask=cur;subMask!=0;subMask=subMask-1&cur){ //这学期学的课程数量不超过k if(cnt[subMask]<=k){ dp[take|subMask]=min(dp[take|subMask],dp[take]+1); } } } return dp[totalCount-1]; } };
执行结果:
时间复杂度:O(n^2)
空间复杂度:O(n)
47.找到最小生成树的关键边和伪关键边
给你一个 n 个点的带权无向连通图,节点编号为 0 到 n-1 ,同时还有一个数组 edges ,其中 edges[i] = [fromi, toi, weighti] 表示在 fromi 和 toi 节点之间有一条带权无向边。最小生成树 (MST) 是给定图中边的一个子集,它连接了所有节点且没有环,而且这些边的权值和最小。
请你找到给定图中最小生成树的所有关键边和伪关键边。如果从图中删去某条边,会导致最小生成树的权值和增加,那么我们就说它是一条关键边。伪关键边则是可能会出现在某些最小生成树中但不会出现在所有最小生成树中的边。
请注意,你可以分别以任意顺序返回关键边的下标和伪关键边的下标。 注意返回的下标是边的索引号,示例1中的<3,2> 3为第三条边,边索引,2为边长度
解法:Kruskal 并查集
关于最小生成树的算法,可见https://leetcode-cn.com/problems/min-cost-to-connect-all-points/solution/prim-and-kruskal-by-yexiso-c500/
如示例1:
可以看到,最小生成树一共有四种,最小生成树的权值伪7,图中标记红色的是每种最小生成树中都存在的边,也就是关键边,如果删除关键边,那么边集形成的最小生成树的权值会发生改变。
图中标记黄色的边,在有些最小生成树中存在,有些最小生成树中没有,这就是伪关键边,因此为关键边的存在与否不会影响到最小生成树的权值大小。
所谓关键边,就是原图中去掉了这条边,图的最小生成树权重值就会变化;
所谓伪关键边,就是排除关键边,树中提前加上这条边,图的最小生成树权重值不会发生变化;
根据上面这个思路,就很好做了,第一步,只需要先求出所有边存在情况下的最小生成树权重;
第二步,求分别去掉每一条边时,最小生成树的权重是否会发生变化,如果发生变化了,那它肯定是关键边;
第三步,非关键边中,求树上分别提前加上每一条边时,最小生成树的权重是否会发生变化,如果没有发生变化了,那它肯定是伪关键边。
为什么在判断伪关键边的时候,需要排除关键边呢?
因为我们是通过求树上分别提前加上每一条边时,最小生成树的权重是否会发生变化的依据,来判断伪关键边的,而关键边当然也包含了这个性质,但是伪关键边不包含第二步的性质,所以,先判断得出关键边,然后再第三步判断时剔除关键边。
代码:
class UnionFind4{
public:
vector<int>father;//父亲节点
vector<int>rank;//秩的数
int n;//节点个数
int setCount;//连通分量数
public:
//查询父结点
UnionFind4(int _n):n(_n),setCount(_n),rank(_n,1),father(_n){
//iota函数,令father[i]=i;
iota(father.begin(),father.end(),0);
}
int find(int x,vector<int>&father){
if(x==father[x])
return x;
else{
father[x]=find(father[x],father);
return father[x];
}
}
//并查集合并
bool merge(int i,int j){
int x=find(i,father);
int y=find(j,father);
if(x==y){
return false;
}
if(rank[x]<=rank[y]){
father[x]=y;
}
else{
father[y]=x;
}
if(rank[x]==rank[y]&&x!=y)
rank[y]++;
setCount--;
return true;
}
bool isConnected(int x,int y){
x=find(x,father);
y=find(y,father);
return x==y;
}
};
class Solution {
public:
vector<vector<int>> findCriticalAndPseudoCriticalEdges(int n, vector<vector<int>>& edges) {
//kruskal计算最小生成树权值
int m=edges.size();
for(int i=0;i<m;i++){
edges[i].push_back(i);
}
sort(edges.begin(),edges.end(),[](const vector<int>&a,const vector<int>&b){
return a[2]<b[2];
});
UnionFind4 unionFind4(n);
//计算最小生成树权值
int value=0;
for(int i=0;i<m;i++){
if(unionFind4.merge(edges[i][0],edges[i][1])){
value+=edges[i][2];
}
}
vector<vector<int>>ans(2);
//判断是否为关键边,就是如果关键边不加入并查集中合并,最小生成树的权值发生变化
//就是关键边
//如果是关键边,那么一定不是非关键边,跳过
//如果不是关键边,那么重新创建一个并查集,这个并查集首先将这条边合并,
// 如果最后的权值不变,说明是伪关键边
//循环遍历m次,每次删除一条边验证是否关键边
for(int i=0;i<m;i++)
{
UnionFind4 u1(n);
int v=0;
for(int j=0;j<m;j++){
if(i!=j&&u1.merge(edges[j][0],edges[j][1]))
v+=edges[j][2];
}
if(u1.setCount!=1||(u1.setCount==1&&v>value)){
ans[0].push_back(edges[i][3]);
continue;//关键边一定不是伪关键边,所以直接跳过伪关键边的判断
}
UnionFind4 u2(n);
u2.merge(edges[i][0],edges[i][1]);
v=edges[i][2];
for(int j=0;j<m;j++){
if(j!=i&&u2.merge(edges[j][0],edges[j][1])){
v+=edges[j][2];
}
}
if(v==value){
ans[1].push_back(edges[i][3]);
}
}
return ans;
}
};
执行结果:
时间复杂度:O(m^2⋅α(n)),其中 n 和 m 分别是图中的节点数和边数。我们首先需要对所有的边进行排序,时间复杂度为O(mlogm)。一次 Kruskal 算法的时间复杂度为O(m⋅α(n)),其中 \alphaα 是阿克曼函数的反函数。我们最多需要执行 2m+1 次 Kruskal 算法,时间复杂度为O(m^2α(n)),在渐进意义下大于排序的时间复杂度,因此前者可以忽略不计,总时间复杂度为 O(m^2⋅α(n))。
空间复杂度:O(m+n)。在进行排序时,我们必须要额外存储每条边原始的编号,用来返回答案,空间复杂度为 O(m))。Kruskal 算法中的并查集需要使用 O(n) 的空间,因此总空间复杂度为 O(m+n)。
48.并行课程III
给你一个整数 n ,表示有 n 节课,课程编号从 1 到 n 。同时给你一个二维整数数组 relations ,其中 relations[j] = [prevCoursej, nextCoursej] ,表示课程 prevCoursej 必须在课程 nextCoursej 之前 完成(先修课的关系)。同时给你一个下标从 0 开始的整数数组 time ,其中 time[i] 表示完成第 (i+1) 门课程需要花费的 月份 数。
请你根据以下规则算出完成所有课程所需要的 最少 月份数:
如果一门课的所有先修课都已经完成,你可以在 任意 时间开始这门课程。
你可以 同时 上 任意门课程 。
请你返回完成所有课程所需要的 最少 月份数。
注意:测试数据保证一定可以完成所有课程(也就是先修课的关系构成一个有向无环图)。
解法:拓扑排序+动态规划
本题相当于给拓扑排序的每个节点加上了完成时间,在保证按照完成课程的顺序的情况下,寻找最早时间。问题可以转化为求一个DAG图中的关键路径的长度。
思路
关键路径就是DAG图中长度最长的路径。注意到,一门课程必须要等待其所有先修课程完成之后,该门课程才能开始,只要有一门先修课程没有完成,该门课程就不能开始。所以该课程的开始时间是其所有先修课程完成时间的最大值!课程的完成时间就在此基础上加上time[i]。基于问题的特点,每一个节点的完成时间都与其前置节点有关,考虑动态规划解法。
dp数组表示的含义:dp[i]表示节点i的最短完成时间。
状态转移方程:
dp[i]等于节点i的所有前置节点的dp最大值加上time[i],最终我们取dp数组中的最大值。(为什么要取最大值?题目中不是让求最少月份数吗?注意我们要求的是关键路径的长度,关键路径是图中长度最长的路径)。
遍历顺序
注意本题中不能从0开始遍历到n-1,因为课程的顺序是不确定的,而必须按照拓扑排序的顺序给dp数组赋值。
初始化
对于所有入度为0的节点,也就是开始拓扑排序的节点,令其dp[i] = time[i].
拓扑排序
首先进行准备工作,创建邻接表和入度数组用于进行拓扑排序,创建逆邻接表用于dp数组的取值。
然后将入度为0的节点入队。队列不空则持续运行循环。每一次循环时,对于队列中的每个元素,遍历其逆邻接表,进行动态规划公式递推。
下面给一个实例,红色字体表示节点的编号,蓝色字体表示该节点的time,绿色字体表示该节点dp数组的值,最终输出19
代码:
class Solution {
public:
int minimumTime(int n, vector<vector<int>>& relations, vector<int>& time) {
int dp[n+1];
memset(dp,0, sizeof(dp));
int indegree[n+1];
memset(indegree,0, sizeof(indegree));
vector<vector<int>>adjact(n+1);
vector<vector<int>>revadjact(n+1);
for(auto r:relations){
indegree[r[1]]++;
adjact[r[0]].push_back(r[1]);
revadjact[r[1]].push_back(r[0]);
}
queue<int>que;
for(int i=1;i<=n;i++){
if(indegree[i]==0){
dp[i]=time[i-1];
que.push(i);
}
}
int res=0;
while(!que.empty()){
int len=que.size();
for(int i=0;i<len;i++){
int node=que.front();
que.pop();
int m=0;
//当前节点的最短完成时间,等于其依赖的节点完成时间的最大值+自身
for(int n:revadjact[node]){
m=max(m,dp[n]);
}
dp[node]=m+time[node-1];
res=max(res,dp[node]);
//让邻接表中该节点的邻居节点的入度-1
for(int n:adjact[node]){
indegree[n]--;
if(indegree[n]==0)
{
que.push(n);
}
}
}
}
return res;
}
};
执行结果:
时间复杂度:O(n^2+ne)
空间复杂度:O(n)
49.按公因数计算最大组件的大小
给定一个由不同正整数的组成的非空数组 nums ,考虑下面的图:
有 nums.length 个节点,按从 nums[0] 到 nums[nums.length - 1] 标记;
只有当 nums[i] 和 nums[j] 共用一个大于 1 的公因数时,nums[i] 和 nums[j]之间才有一条边。
返回 图中最大连通组件的大小 。
解法:并查集+约数枚举
根据题目的意思,两个点之间有除了1之外的公因数时,两个节点有边相连,遍历所有节点,每两个节点去寻找是否有公因数的方法,明显是超时的。
根据数学中枚举因数的方法,对于一个数字n,不需要从2一直枚举到n,其实只需要从2枚举到sqrt
(n),因为对于每一个小于sqrt(n)的因数i,一定有一个大于sqrt(n)的因数j与它相对应,也就是i*j=n;
算法思路:
- 找到nums中的最大值,引用并查集的代码,设置大小为num最大值
- 然后对于nums中的每一个num,使用遍历方法从sqrt(num)遍历到2,找到它的所有因数,使用并查集合并num和它的因数。因此如果两个num中有公因数,他们在并查集中就属于同一个连通分量,有相同的根节点。
- 最后使用map去统计nums数组中,nums所属的父结点的集合数。返回最大的集合数。即nums数组中所形成的最大连通分量的大小。
class UnionFind5{
public:
vector<int>father;//父亲节点
vector<int>rank;//秩的数
int n;//节点个数
int setCount;//连通分量数
public:
//查询父结点
UnionFind5(int _n):n(_n),setCount(_n),rank(_n,1),father(_n){
//iota函数,令father[i]=i;
iota(father.begin(),father.end(),0);
}
int find(int x){
if(x==father[x])
return x;
else{
father[x]=find(father[x]);
return father[x];
}
}
//并查集合并
void merge(int i,int j){
int x=find(i);
int y=find(j);
if(x!=y){
if(rank[x]<=rank[y]){
father[x]=y;
}
else{
father[y]=x;
}
if(rank[x]==rank[y]&&x!=y)
rank[y]++;
setCount--;
}
}
bool isConnected(int x,int y){
x=find(x);
y=find(y);
return x==y;
}
};
class Solution {
public:
int largestComponentSize(vector<int>& nums) {
int maxNum=0;
for(int num:nums){
maxNum=max(num,maxNum);
}
int n=nums.size();
UnionFind5 unionFind5(maxNum+1);
for(int num:nums){
for(int i=sqrt(num);i>=2;i--){
if(num%i==0){
unionFind5.merge(num,i);
unionFind5.merge(num,num/i);
}
}
}
int res=0;
unordered_map<int,int>map;
for(int num:nums){
map[unionFind5.find(num)]++;
}
for(auto it=map.begin();it!=map.end();it++){
res=max(res,it->second);
}
return res;
}
};
时间复杂度: O(N*sqrtW),其中 N 是 A 的长度 W=max(A[i])。
空间复杂度: O(N)
50.统计点对的数目
给你一个无向图,无向图由整数 n ,表示图中节点的数目,和 edges 组成,其中 edges[i] = [ui, vi] 表示 ui 和 vi 之间有一条无向边。同时给你一个代表查询的整数数组 queries 。
第 j 个查询的答案是满足如下条件的点对 (a, b) 的数目:
a < b
cnt 是与 a 或者 b 相连的边的数目,且 cnt 严格大于 queries[j] 。
请你返回一个数组 answers ,其中 answers.length == queries.length 且 answers[j] 是第 j 个查询的答案。
请注意,图中可能会有 重复边 。
解法:容斥原理+双指针
设图中的点数为 n,边数为 m。
根据容斥原理,与点对 a,b 相连的边数等于与 a 相连的边数加上与 b 相连的边数,再减去同时与 a,b 同时 相连的边数。我们的目标,就是求出这两个部分,随后二者的差就是相连的边数。
首先来看第一部分。为了求与 a,b 同时相连的边数,不难想到利用一个二维数组来维护。然而,由于 n 的数据范围较大,二维数组的方式会占用过大的空间与时间(初始化时需要与数组大小等大的时间)。考虑到 m 只有 10^5 ,因此可以使用哈希表(散列表)来节省空间占用。
具体而言,对于任意一条边 (p_1,p_2),我们可以将这条边映射到一个唯一的整数。设
p[max]=max(p_1,p_2)
p[min]=min(p_1,p_2)
则可以将这条边映射到整数 pmax*§+pmin的散列表上。
其中,要首先取最大值和最小值的原因在于,需要处理重边(如 (1,2)和 (2,1) 的情况。)
现在来看第二部分。记与点 i 相连的边数为 deg[i],则可以通过一次遍历 edges 数组求出 deg 数组的取值。因此对于任意的点对,我们可以轻松地求出 deg[a]+deg[b] 的值。
但是注意,这种暴力枚举的每个点对的方法在数据量很大的时候会初吻日
记与 a,b 同时相连的边数为 overlap[a][b]
(再次注意:实际实现中要使用哈希表 即map[a*P+b]=边数)。回过头来再看最开始列出的条件,我们需要求出点对 a,b 的数量:使得
deg[a]+deg[b]−map[a*P-b]>query。
我们首先求出deg[a]+deg[b]>query 的数量。如果给deg 数组排序,则问题等价于在有序数组中,求出所有数字对的数目,使得它们的和大于给定的值。(使用双指针,左指针指向1,右指针指向n)
随后,我们再求出满足 deg[a]+deg[b]−map[a*P-b]≤query 的数量。此时,我们无需再遍历所有的点对,因为满足这样条件的点对,一定出现在edges 数组中!因此,我们只需再遍历一次 edges 数组即可。这样做的时间复杂度为 O(m),相比于枚举点对的 O(n^2)
在求出这两部分后,二者的差值即为满足 deg[a]+deg[b]−map[a*P-b]>query的点对数量。
算法思路:
- 统计每个点的度数;
- 计算S=io[a]+io[b]>queries[i]的总数;
- 遍历边,计算T=(io[a]+io[b]>queries[i] && io[a]+io[b]-e(a,b)<=queries[i])的数量;
- ans[i] = S-T;
代码:
class Solution {
public:
const int P=100000;
vector<int> countPairs(int n, vector<vector<int>>& edges, vector<int>& queries) {
int m=queries.size();
//记录度
vector<int>io(n+1);
//对度进行排序,用双指针求deq[a]+deq[b]>query的个数
vector<int>ans(m,0);
//使用map记录两条边之间相连的边的数目
unordered_map<int,int>map;
for(auto &edge:edges){
//为了防止重边,如【1,2】和【2,1】这种边
int u=min(edge[0],edge[1]);
int v=max(edge[0],edge[1]);
io[u]++;
io[v]++;
int key=u*P+v;
map[key]++;
}
vector<int>arr(io.begin(),io.end());
sort(arr.begin(),arr.end());
for(int i=0;i<m;i++)
{
int l=1;
int r=n;
while(l<r){
while(l<r&&arr[l]+arr[r]<=queries[i])
l++;
if(l<r)
{
ans[i]+=r-l;
}
r--;
}
if(ans[i]==0)
continue;
for(auto it=map.begin();it!=map.end();it++){
auto[key,connect]=*it;
int u=key/P;
int v=key%P;
if(io[u]+io[v]>queries[i]&&io[u]+io[v]-connect<=queries[i])
{
ans[i]--;
}
}
}
return ans;
}
};
执行结果:
预处理时间复杂度:O(m+nlogn),即遍历一遍 }edges 数组,并排序 deg 数组的复杂度。
单次查询时间复杂度:O(n+m)。首先利用双指针找出第一部分,O(n) 时间;随后遍历 edges 数组求解第二部分,O(m) 时间。