【递归】7段码(回溯 + 结束条件通过并查集进行相邻节点约束判断)
文章目录
问题描述
试题链接:7段码
参考资料:E题.七段码 - dfs+并查集,七段码,【算法与数据结构】— 并查集,百度百科 - 并查集
总体思想
采用回溯的思想,回溯探索整个树空间,并在结束条件使用并查集结构判断并查集中是否只有唯一一个集合
- 先构造邻接矩阵,表示7个节点间的边的关系
- 采用回溯的思想,每个节点都有选择和不选择两种情况,进而建立整棵高为
7
的完全二叉树搜索空间。 - 由于是带约束的回溯法,则需要在结束条件(到达叶子节点的孩子节点)下,通过并查集结构判断求出来的解是否符合约束(如果符合,则方案个数
+1
)。 - 结束条件下的并查集操作步骤如下(每次通过边的关系,更新这个全局集合,这个全局集合可以简化成并查集数组
unionSet
):
1)初始化unionSet
:把已选择的节点所在的集合初始化为其自身。通常来说,这个步骤在每次使用该数据结构时只需要执行一次,无论何种实现方式,时间复杂度均为 O ( n ) O(n) O(n)。
2)查找:查找元素所在的集合,即根节点。
3)合并:将两个元素所在的集合合并为一个集合。通常来说,合并之前,应先判断两个元素是否属于同一集合,这可用上面的“查找”操作实现。
因此该算法的核心是在结束条件下如何实现并查集,并查集的实现方法并不唯一,这里要特别强调:
- 并查集是一个用数组维护的数据结构,每次通过边的关系,更新这个数组。参考百度百科 - 并查集
- 这个数组通过查询和合并的操作,维护“并查集”这个逻辑结构,通过并查集,我们可以知道图中有多少个连通子图。
- 并查集不同实现形式的主要区别在于要维护的并查集究竟是什么样子的:
- 保证每个连通子图中孩子节点都连向子图中的根结点(树的高度为2)
- 不要求每个连通子图中孩子节点都连向子图中的根结点(树的高度可以大于2)
两种方式均可实现,个人喜欢第二种,因为方式1要维护一个高度最大为2的树,需要每次遍历整个数组,并将相应的值修改为根结点的值,如果节点数比较多的话,是比较耗时的;
因此如果x
和y
相连,但root_x
和root_y
不同,此时不如直接将root_y
作为root_x
的子节点,而不用将root_y
及其所有的孩子节点都作为root_x
子节点。
并查集实现思路1(在数组中维护一个高度最大为2的树)
1、实现步骤
假设现在有一个维护并查集逻辑结构的数组unionSet[N]
,并且要求在要维护的并查集数组中,孩子节点都指向根节点,即树的最大高度为2
;
- 查找操作:为了想知道当前节点
x
的根结点(前驱节点)是谁,由于要维护的并查集中所有的孩子节点都指向了根节点,则可以直接通过unionSet[x]
获取即可。参考代码如下://并查集unionSet的查询操作:查询根节点 int find(int unionSet[N], int x){ return unionSet[x]; }
- 合并操作:如果两个节点
x
和y
,它们之间有边直接相连,但它们的根结点unionSet[x]
和unionSet[y]
不同,需要通过合并join()
操作,将并查集中所有根结点为unionSet[y]
的节点全部替换为unionSet[x]
,这样保证了x
和y
及其它们的根结点为同一个阵营的,都归顺于天子unionSet[x]
,这样也完成了路径压缩。参考代码如下:
Note:在使用//并查集unionSet的合并操作,将x及其孩子节点挂载到根节点root上 void join(int unionSet[N],int x,int root){ int i; for(i = 1; i < N; ++i){ if(unionSet[i] == x){ unionSet[i] = root; } } } void func(...){ ... //遍历所有边集,为每个节点分配根节点 for(i = 1; i < N; ++i){ for(j = 1; j < N; ++j){ //每读取一条边,更新一次并查集(以i为起点,如果这两节点UnionSet值不同,说明这两个节点并没有合并) if(graph[i][j] == 1 && unionSet[i] != 0 && unionSet[j] != 0){ if(unionSet[i] != unionSet[j]){ //这两节点虽然是邻居,但是双方的根结点却不同,需要用一方的根结点更新另一方的根结点 int root_i = unionSet[i]; int root_j = unionSet[j]; join(unionSet,root_j,root_i); //将root_j及其孩子节点,挂到root_i节点下,完成所有路径的压缩; } } } } ... }
join()
时可以直接用下标,不需要使用find()
查询节点的根结点 - 连通图个数判断操作:在通过并查集计算图中连通图个数时,通过对并查集中元素进行频数统计,频数
>0
的索引号即可作为根结点,参考代码如下:
Note:该题目中要求图中只有一个连通图//通过unionSet查询连通子图个数 int get_setCount_by_unionSet(int unionSet[N],int connected[N]){ int set_count = 0, i; for(i = 1; i < N; ++i){ connected[unionSet[i]] += 1; } for(i = 1; i < N; ++i){ if(connected[i] != 0){ set_count += 1; } connected[i] = 0; //清0 } return set_count; } int func(...){ ... for(){ for(){ } } ... int connected[N] = {0}; //连通子图的个数 int set_count = get_setCount_by_unionSet(unionSet,connected); if(set_count == 1){ return 1; }else{ return 0; } }
2、参考代码1(C语言)
#include<stdio.h>
#define N 8
enum code {a=1,b,c,d,e,f,g};
//把7段码中的每个字符看做是一个节点,共10条边,绘制无向图的邻接矩阵
void init(int graph[N][N]){
graph[a][b] = graph[b][a] = 1;
graph[a][f] = graph[f][a] = 1;
graph[f][g] = graph[g][f] = 1;
graph[b][g] = graph[g][b] = 1;
graph[f][e] = graph[e][f] = 1;
graph[b][c] = graph[c][b] = 1;
graph[e][g] = graph[g][e] = 1;
graph[c][g] = graph[g][c] = 1;
graph[e][d] = graph[d][e] = 1;
graph[c][d] = graph[d][c] = 1;
}
//并查集unionSet的查询操作:查询根节点
int find(int unionSet[N], int x){
return unionSet[x];
}
//并查集unionSet的合并操作,将x及其孩子节点挂载到根节点root上
void join(int unionSet[N],int x,int root){
int i;
for(i = 1; i < N; ++i){
if(unionSet[i] == x){
unionSet[i] = root;
}
}
}
//通过unionSet查询连通子图个数
int get_setCount_by_unionSet(int unionSet[N],int connected[N]){
int set_count = 0, i;
for(i = 1; i < N; ++i){
connected[unionSet[i]] += 1;
}
for(i = 1; i < N; ++i){
if(connected[i] != 0){
set_count += 1;
}
connected[i] = 0; //清0
}
return set_count;
}
int func(int graph[N][N], int use[N], int code, int count){
int result = 0;
//结束条件
if(code == N){
int i,j;
int unionSet[N] = {0}; //用数组表示并查集
//并查集初始化(新加入节点需要重新初始化并查集)
for(i = 1; i < N; ++i){
if(use[i] != 0){
unionSet[i] = i;
}
}
//遍历所有边集,为每个节点分配根节点
for(i = 1; i < N; ++i){
for(j = 1; j < N; ++j){
//每读取一条边,更新一次并查集(以i为起点,如果这两节点UnionSet值不同,说明这两个节点并没有合并)
if(graph[i][j] == 1 && unionSet[i] != 0 && unionSet[j] != 0){
if(unionSet[i] != unionSet[j]){ //这两节点虽然是邻居,但是双方的根结点却不同,需要用一方的根结点更新另一方的根结点
int root_i = unionSet[i];
int root_j = unionSet[j];
join(unionSet,root_j,root_i); //将root_j及其孩子节点,挂到root_i节点下,完成所有路径的压缩;
}
}
}
}
int connected[N] = {0}; //连通子图的个数
int set_count = get_setCount_by_unionSet(unionSet,connected);
if(set_count == 1){
return 1;
}else{
return 0;
}
}
if(code < N){
//对于当前code节点,有两操作: 开/关
use[code] = 1; //使用当前code
result += func(graph,use,code + 1,count);
use[code] = 0; //不使用(回溯)
result += func(graph,use,code + 1,count);
}
return result;
}
int main(int argc, char const *argv[])
{
/* code */
int graph[N][N] = {0};
int use[N] = {0}; //保存已选择的节点
init(graph);
int result = func(graph,use,a,0);
printf("可以表达的字符个数: %d\n",result);
return 0;
}
---
80
并查集实现思路2(不要求并查集中所有连通图孩子节点都能指向同一个根结点)
1、实现步骤
假设现在有一个维护并查集逻辑结构的数组unionSet[N]
,不要求并查集中所有连通图孩子节点都能指向同一个根结点,即树的最大高度可以>2
;
- 查找操作:要想查找
x
的根结点,则需要依次遍历数组,直到unionSet[x‘] == x‘
时,即x‘
为x
的根结点。参考代码如下:
Note://并查集unionSet的查询操作:查询根节点,并同时进行路径压缩(并不能保证连通子图中所有路径上的节点都连向根结点,但能保证unionSet[x] == x位置上为根节点) int find(int unionSet[N], int x){ if(unionSet[x] != x){ //查找到根节点结束 unionSet[x] = find(unionSet,unionSet[x]); } return unionSet[x]; //返回根节点 }
- 这里使用
unionSet[x] = find(unionSet,unionSet[x])
压缩路径的方式,简化了每次find
的查找次数。 - 压缩路径并不能保证连通子图中所有路径上的节点都连向根结点(不能维护连通子图中高度为
2
的树),但能保证unionSet[x] == x
位置上为根节点。
- 这里使用
- 合并操作:如果节点
x
和y
,它们的根结点find(x)
和find(y)
不同(这里不能直接通过unionSet[x]
,unionSet[y]
获取根节点,因为没有维护高度为2
的树),则将任意一个根结点归顺到另一个根结点上,比如root_y
归顺于root_x
,但root_y
的孩子节点并直接归于root_x
。参考代码如下://并查集unionSet的合并操作:两节点的根结点并非同一个,让两个根结点进行决策,让其中一个根结点作为另一个的下级 void join(int unionSet[N],int i,int j){ int root_i = find(unionSet,i); //找到i节点的根结点 int root_j = find(unionSet,j); //找到j节点的根结点 if(root_i != root_j){ //两节点的根结点并非同一个 unionSet[root_j] = root_i; //只需要将root_j挂到root_i节点下 } }
- 连通图个数判断操作:
unionSet[x] == x
位置上为根节点,直接统计频数即可。int count = 0; //计算根结点的个数 for(int i = 1;i <= 7;i++) if(use[i] && unionSet[i] == i) count++; if(count == 1) result++;//如果所有亮灯都属于同一个集合
2、参考代码2(C语言)
#include<stdio.h>
#define N 8
enum code {a=1,b,c,d,e,f,g};
//把7段码中的每个字符看做是一个节点,共10条边,绘制无向图的邻接矩阵
void init(int graph[N][N]){
graph[a][b] = graph[b][a] = 1;
graph[a][f] = graph[f][a] = 1;
graph[f][g] = graph[g][f] = 1;
graph[b][g] = graph[g][b] = 1;
graph[f][e] = graph[e][f] = 1;
graph[b][c] = graph[c][b] = 1;
graph[e][g] = graph[g][e] = 1;
graph[c][g] = graph[g][c] = 1;
graph[e][d] = graph[d][e] = 1;
graph[c][d] = graph[d][c] = 1;
}
//并查集unionSet的查询操作:查询根节点,并同时进行路径压缩(并不能保证连通子图中所有路径上的节点都连向根结点,但能保证unionSet[x] == x位置上为根节点)
int find(int unionSet[N], int x){
if(unionSet[x] != x){ //查找到根节点结束
unionSet[x] = find(unionSet,unionSet[x]);
}
return unionSet[x]; //返回根节点
}
//并查集unionSet的合并操作:两节点的根结点并非同一个,让两个根结点进行决策,让其中一个根结点作为另一个的下级
void join(int unionSet[N],int i,int j){
int root_i = find(unionSet,i); //找到i节点的根结点
int root_j = find(unionSet,j); //找到j节点的根结点
if(root_i != root_j){ //两节点的根结点并非同一个
unionSet[root_j] = root_i; //只需要将root_j挂到root_i节点下
}
}
int func(int graph[N][N], int use[N], int code, int count){
int result = 0;
//结束条件
if(code == N){
int i,j;
int unionSet[N] = {0}; //用数组表示并查集
//并查集初始化(新加入节点需要重新初始化并查集)
for(i = 1; i < N; ++i){
if(use[i] != 0){
unionSet[i] = i;
}
}
//遍历所有边集,为每个节点分配根节点
for(i = 1; i < N; ++i){
for(j = 1; j < N; ++j){
//每读取一条边,更新一次并查集(以i为起点,如果这两节点UnionSet值不同,说明这两个节点并没有合并)
if(graph[i][j] == 1 && unionSet[i] != 0 && unionSet[j] != 0){
join(unionSet,i,j);
}
}
}
int count = 0;
//计算根结点的个数
for(int i = 1;i <= 7;i++)
if(use[i] && unionSet[i] == i) count++;
if(count == 1) result++;//如果所有亮灯都属于同一个集合
}
if(code < N){
//对于当前code节点,有两操作: 开/关
use[code] = 1; //使用当前code
result += func(graph,use,code + 1,count);
use[code] = 0; //不使用(回溯)
result += func(graph,use,code + 1,count);
}
return result;
}
int main(int argc, char const *argv[])
{
/* code */
int graph[N][N] = {0};
int use[N] = {0}; //保存已选择的节点
init(graph);
int result = func(graph,use,a,0);
printf("可以表达的字符个数: %d\n",result);
return 0;
}
---
80