最近写代码遇到一个问题。如图一个3*3*3的Cube
当两个立方体之间共享一个面,称为面邻居(R-6);当两个立方体仅共享一条边,称为边邻居(R-18);仅共享一个点,称为点邻居(R-26)。在细化骨架的算法中,判断一个体素是否为简单体素的时候要判断,删除体素前后欧拉示性数、连通分量数是否发生改变。这个连通分量数,把立方体当成图中的点,两个立方体之间点相邻代表他们之间有边,可以转换成图深搜、广搜去判断没有中心体素前后的连通分量数。在体网格模型里,从点邻接关系独立抽出中心体素的R-26有点麻烦,所以改变角度,从八分圆的邻接关系去判断。八分圆就是中心体素八个顶点为中心的2*2*2的Cube,当两个八分圆之间有重叠部分代表两个八分圆相邻。
因此问题还是回到图的连通判断。判断图的连通性可以深搜、广搜、并查集。
下面这道题目的场景非常相似。
班上有 N 名学生。其中有些人是朋友,有些则不是。他们的友谊具有是传递性。如果已知 A 是 B 的朋友,B 是 C 的朋友,那么我们可以认为 A 也是 C 的朋友。所谓的朋友圈,是指所有朋友的集合。
给定一个 N * N 的矩阵 M,表示班级中学生之间的朋友关系。如果M[i][j] = 1,表示已知第 i 个和 j 个学生互为朋友关系,否则为不知道。你必须输出所有学生中的已知的朋友圈总数。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/friend-circles
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
第一版可以AC的代码。结果:52ms,14.1mb。
//Union_Find
class solver {
public:
solver(int n) {
count = n;
root.resize(n);
for (int i = 0; i < n; i++)
root[i] = i;
}
//connect x and y
void connect(int x, int y) {
int xroot = find(x);
int yroot = find(y);
if (xroot == yroot) {
return;
}
root[yroot] = xroot;
count--;
}
//judge two point is neighbor
bool isConnected(int x, int y) {
int xroot = find(x);
int yroot = find(y);
return xroot == yroot;
}
//return count
int count_num() {
return count;
}
private:
int count;
vector<int> root;
//get root of x
int find(int x) {
while (root[x] != x)
x = root[x];
return x;
}
};
//LeetCode内置的判题系统太复杂,上面写了一个求解器来包含并查集的函数,简单易懂
class Solution {
public:
int findCircleNum(vector<vector<int>>& M) {
int m = M.size();
if (M.size() == 0)
return 0;
solver* s = new solver(m);
for (int i = 0; i < M[0].size(); i++) {
for (int j = i + 1; j < m; j++) {//对称矩阵
//有邻接关系
if (M[i][j] != 0)
s->connect(i, j);
}
}
return s->count_num();
}
};
并查集中连接两个节点可以当成是树的构造,树最怕的就是退化成一串链表,上述连接过程不好的地方在于没有避免这点。优化的方式在于优化树的高度,让树尽量平衡。
可以考虑引入一个重量数组,记录树有多重,也就是每一个节点下有多少个节点。这样就可以在考虑是要root[yroot] = xroot;
还是root[xroot] = yroot;
的时候可以根据树的重量来选择。这一步叫做平衡性优化。O(N)->O(logN)。但是呢,加了这一步之后,运行时间反倒边久了,可能要在数据量大的时候才能体现出来。
最后一步优化也叫做路径压缩,在每一次查root的过程中,让每一个节点都能往上走,和最root的节点连接。
如下:
//Union_Find
class solver{
public:
solver(int n){
count = n;
root.resize(n);
size.resize(n);
for(int i=0;i<n;i++){
root[i]=i;
size[i]=1;
}
}
//connect x and y
void connect(int x, int y){
int xroot = find(x);
int yroot = find(y);
if(xroot == yroot){
return;
}
//平衡性优化
if(size[xroot] < size[yroot]){
root[xroot] = yroot;
size[yroot] += size[xroot];
}else{
root[yroot] = xroot;
size[xroot] += size[yroot];
}
//root[yroot] = xroot;
count--;
}
bool isConnected(int x, int y){
int xroot = find(x);
int yroot = find(y);
return xroot == yroot;
}
//return count
int count_num(){
return count;
}
private:
int count;
vector<int> root;
vector<int> size;
//get root of x
int find(int x){
while(root[x]!=x){
//路径压缩
root[x] = root[root[x]];
x = root[x];
}
return x;
}
};
class Solution {
public:
int findCircleNum(vector<vector<int>>& M) {
int m=M.size();
if(M.size() == 0)
return 0;
solver* s = new solver(m);
for(int i=0;i<M[0].size();i++){
//这里和深搜的范围不一样,这里的意思是把点相邻当成有向图。
for(int j = i+1; j < m; j++){//对称矩阵
if(M[i][j]!=0)
s->connect(i, j);
}
}
return s->count_num();
}
};
这下面是深搜解法。
class Solution {
public:
void dfs(vector<vector<int>>& M, vector<int>& visited, int i){
visited[i]=1;
//注意这里j的遍历范围,矩阵每一行、每一列代表该下标的邻接关系,不能像并查集那样
for(int j=0; j<M.size(); j++){
if(M[i][j]==1 && visited[j]==0){
dfs(M, visited, j);
}
}
}
int findCircleNum(vector<vector<int>>& M) {
int m = M.size();
vector<int> visited(m, 0);//1-visited
int count = 0;
for(int i=0;i<m;i++){
visited[i]=0;
}
for(int i=0;i<m;i++){
if(visited[i] == 0){
dfs(M, visited, i);
count++;
}
}
return count;
}
};
这下面是广搜的解法。
class Solution {
public:
int findCircleNum(vector<vector<int>>& M) {
int m = M.size();
vector<int> visited(m, 0);//1-visited
int count = 0, temp;
queue<int> bfs;
for(int i=0;i<m;i++){
if(visited[i]==0){
count++;
bfs.push(i);
while(!bfs.empty()){
temp = bfs.front();
bfs.pop();
//访问过的出队
visited[temp]=1;
for(int j=0;j<m;j++)
//没访问的入队
if(M[temp][j]==1 && visited[j]==0)
bfs.push(j);
}
}
}
return count;
}
};