紫书刷题进行中,题解系列【GitHub|CSDN】
例题6-19 UVA1572 Self-Assembly(39行AC代码)
题目大意
锻炼思维的好题,注重分析思路,等价条件转换推理哦
给定n种正方形,每种正方形有4条边,每条边有2个字符构成,包含以下两种模式:
- 第一个为
A-Z
的大写字母,第二个为+/-
,如A+或Z-
00
当两条边的第一个字符相同,而第二个字符相反时,两边可相连,00
不可与任何边相连。
现假设每种正方形无限供应,问是否存在无限拼接图像?
思路分析
一拿到题目,毫无头绪,但可以知道它是一个判定性问题,即只需回答是/否
。
而细看要求,要求判定是否存在无限延展的图像,若是存在的话,计算机是没法直接构造出无限的图像的,那么它在暗示我们肯定可以找到一个判定无限的等价条件(类似于循环节的寻找,比如循环小数UVA202和死循环判定UVA12108)
按着循环节思路继续分析下去,发现既然是图像,那么能不能用图论方式解决?
- 尝试1:将正方形当成顶点,遍历查找可用正方形判断是否有能够连接的。但存在一个致命问题,可用正方形个数无数,组合方式爆炸,除非我能穷举每种情况,才能证明它不存在无限的情况,又绕回了思路分析的源点
- 尝试2:将每个正方形的边当成顶点,将正方形作为沟通各个点的边(这里需要一点想象力,和直觉违背),若通过以上方式构建的图存在有向环(循环节),说明存在无限的可能。举个构建图的例子:
图一共有A-Z(+/-)52个顶点
输入:K+K-Q+Q-
对于边K+,构造出一点顶点K1,可想而知,只有另一个正方形的边为K-时才能与K+相邻,间接与剩余的3条边相邻,转换为图即是K-这个点能单向到达K-,Q+,Q-这3个点。
> 思考:想想为何不构造K+到K-的边?
同理,对于边K-,构造顶点K+到K+,Q+,Q-单向边
对于边Q+,构造顶点Q-到K+,K-,Q-的单向边
对于边Q-,构造顶点Q+到K+,K-,Q+的单向边
至此,思路基本明了,将复杂问题转换为有向图的有向环判断问题,有两种思路处理有向环,可以说图的算法核心就两种:bfs和dfs。这里也一样
- 拓扑排序(bfs):拓扑排序过程中计算出队的顶点个数,若不为52,说明存在有向环
- dfs标记法:关键在于访问状态设计,用
0:违访问, -1:当前正在访问, 1:已访问
来标记状态,在遍历过程中碰到-1的状态,表示遇到环。以下为二者对求环问题的对比:
对比项目\处理方法 | bfs(拓扑排序) | dfs(活用标记) |
---|---|---|
适用性 | 有向图 | 任意图 |
打印环 | 否 | 能 |
效率 | 高 | 低(递归用栈消耗大,防止爆栈) |
算法设计
为了便于处理A+和A-
的转换关系,定义以下转换函数(类似哈希函数),令二者处于相邻位置,到真正访问与1异或即可完成变换,例如ID(A+)=0,ID(A-)=1
,那么将ID(A+)^1=1
即可得到ID(A-)
int getId(char c1, char c2) { // 获取字符节点的编号
return (c1 - 'A')*2 + ((c2 == '+') ? 0 : 1); // 注意?:优先级很低,要用括号
}
图用邻接表方式存储,若是用dfs方式判环,必须注意去除重复邻边,bfs方式不影响
其余细节参见详细的代码注释,以下给出两种思路的AC代码(二者仅判环过程不同,其余均一致)
AC代码(C++11)
DFS(标记活用)
#include<bits/stdc++.h>
using namespace std;
vector<set<int> > adj; // 邻接表,注意用set存储去重
int vis[52]={0}; // 标记访问数组
int getId(char c1, char c2) { // 获取字符节点的编号
return (c1 - 'A')*2 + ((c2 == '+') ? 0 : 1); // 注意?:优先级很低,要用括号
}
void connect(char a1, char a2, char b1, char b2) { // 参数分别表示两点的第一二个字符
if (a1 == '0' || b1 == '0') return; // 任意一点为0均不连接
adj[getId(a1,a2)^1].insert(getId(b1,b2)); // 有向图构建;异或含义-> (B+)^1=B-
}
bool dfs(int u) { // 判断以顶点u开始是否存在有向图
vis[u] = -1; // 当前遍历的标记
for (int v : adj[u]) // 邻边
if (vis[v] == -1 || (vis[v] == 0 && dfs(v))) return true; // 碰见当前已访问节点,表示存在有向环
vis[u] = 1; // 回溯时标记为1,表示已访问过,必须在return false之前
return false; // 到此处表示当前连通块不存在有向环
}
bool find_cycle() { // 检查图是否存在有向环
memset(vis, 0, sizeof(vis)); // 初始化为0,表示未访问
for (int i=0; i < 52; i ++) // 遍历所有连通块
if (vis[i] == 0 && dfs(i)) return true; // 存在一个环
return false;
}
int main() {
int n; string s;
while (cin >>n) {
adj.clear(); adj.resize(52); // 初始化
for (int i=0; i < n; i ++) {
cin >>s;
for (int i=0; i < 4; i ++) { // 考虑旋转翻转
for (int j=0; j < 4; j ++)
if (i != j) connect(s[i*2],s[i*2+1],s[j*2],s[j*2+1]); // 构建有向图
}
}
printf("%s\n", !find_cycle() ? "bounded" : "unbounded");
}
return 0;
}
BFS(拓扑排序)
#include<bits/stdc++.h>
using namespace std;
vector<vector<int> > adj; // 邻接表
int getId(char c1, char c2) { // 获取字符节点的编号
return (c1 - 'A')*2 + ((c2 == '+') ? 0 : 1); // 注意?:优先级很低,要用括号
}
void connect(char a1, char a2, char b1, char b2) { // 参数分别表示两点的第一二个字符
if (a1 == '0' || b1 == '0') return; // 任意一点为0均不连接
adj[getId(a1,a2)^1].push_back(getId(b1,b2)); // 有向图构建;异或含义-> (B+)^1=B-
}
bool bfs() { // 拓扑排序检测是否存在有向环
int indegree[52]={0}, num=0; // 访问数组, 入度表,入队计算
for (int i=0; i < 52; i ++)
for (auto v : adj[i]) indegree[v] ++; // 入度计算
queue<int> q;
for (int i=0; i < 52; i ++) if (indegree[i] == 0) q.push(i); // 队列初始化
while (!q.empty()) {
int u=q.front(); q.pop(); num ++; // 统计出队个数
for (int v : adj[u]) {
indegree[v] --; // 更新入度表
if (indegree[v] == 0) q.push(v); // 入度=0则进队
}
}
return num == 52; // 不存在有向环num=52
}
int main() {
int n; string s;
while (cin >>n) {
adj.clear(); adj.resize(52); // 初始化
for (int i=0; i < n; i ++) {
cin >>s;
for (int i=0; i < 4; i ++) { // 考虑旋转翻转
for (int j=0; j < 4; j ++)
if (i != j) connect(s[i*2],s[i*2+1],s[j*2],s[j*2+1]); // 构建有向图
}
}
printf("%s\n", bfs() ? "bounded" : "unbounded");
}
return 0;
}