广义表的深度

广义表的定义

本周的数据结构与算法继续学习数组和广义表。主要讲了广义表。广义表是一种比较抽象的数据结构,它包含两个部分:原子。相较于链表和容器而言,它的元素不止一种,而且由于表可以嵌套,一张广义表是递归定义的。广义表主要应用于LISP语言(实际上LISP语言也只有广义表)。

广义表的表示

通常,表用大写字母表示,而原子用小写字母表示,并用括号来体现层次。几个例子如下图:

image-20221017005212710

注意,广义表的递归可以没有出口,比如图中E表。

广义表的深度

将广义表的深度定义为所有元素深度的最大值,直观体现就是括号嵌套的最大层数,对于上面的例子,深度如下:A 1, B 1, C 2, D 3, E inf,而这周的编程题便是求广义表的深度。题目如下:

image-20221019232208025

首先考虑每个表中包含的表都在之前输入,那么,我们只需要遍历输入,同时维护一个depth,遇到左括号+1,遇到右括号-1,遇到其他表先加后减,同时在每一步取MAX,最终结果就是这张表的深度,存下来为后面做准备。

然而,未必恰好是这样输入的。很有可能内部的表要在后面输入,比如下面的情况。

1
2
3
A=(B,C)
C=(a)
B=(a,B)

应该输出

1
2
3
inf
1
inf

然而,当我们得到A=(B,C)时,并不知道B和C是什么,也就是说,简单地立即计算行不通了。又该怎么办呢?

方法一——不断遍历

既然不能立即遍历,我们可以这样做:将所有的输入存下来,作为memo,同时构建一张表-深度映射表,每次遍历memo,并尝试计算,如果在计算过程中,发现里面有还没有计算出来的表,说明现在还不能计算,再尝试下一个。如果计算出来,就在memo中删除它,同时将结果存入到表-深度表中。

循环何时结束?每次遍历memo时,我们至少要计算出一个,才有可能继续算下去,否则说明尚未计算的全部算不了了,或者已经计算完毕,这时就可以终止了。尚未计算的,全部都是inf

具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include <bits/stdc++.h>
using namespace std;
int main()
{
vector<char> input; //保存输入顺序
vector<int> ans(26); //保存每个表的结果
vector<string> memo(26); //保存每个输入
int n;
string s;
cin >> n;
for (int i = 1; i <= n; ++i)
{
cin >> s;
input.push_back(s[0]);
memo[s[0] - 'A'] = s.substr(2);
}
bool flag = true;
while (flag)
{
flag = false;
for (char c = 'A'; c <= 'Z'; ++c) //遍历memo,判断是否可以计算并计算之
{
bool canSolve = true;
if (memo[c - 'A'] == "") //没输入它,跳过
continue;
for (char d : memo[c - 'A'])
if (d >= 'A' && d <= 'Z' && ans[d - 'A'] == 0)
{
canSolve = false;
break;
}
if (!canSolve)
continue;
//判断为可以计算了,计算括号嵌套的层数
int res = 0, depth = 0;
for (char d : memo[c - 'A'])
{
depth += d == '(';
depth -= d == ')';
depth += d >= 'A' && d <= 'Z' ? ans[d - 'A'] : 0;
res = max(res, depth);
depth -= d >= 'A' && d <= 'Z' ? ans[d - 'A'] : 0;
}
ans[c - 'A'] = res;
memo[c - 'A'] = "";
flag = true;
}
}
for (char c : input)
if (ans[c - 'A'])
cout << ans[c - 'A'] << endl;
else
cout << "infity" << endl;
return 0;
}

算法分析:这样的计算方法,最坏的情况是每次只计算出最后一个,时间复杂度是O(N^2)的,不过由于大写字母就那么多,实际上运行时间很短,完全可以接受(如果超时,肯定是死循环了)。而写这个代码时,让我想到了大一上学期的消消乐的消去判断逻辑(如何处理连续消去):

  1. 每次遍历表格,找到所有可以进行消去的地方,然后消去
  2. 让上面的棋子下落,补充空格,最顶上随机刷新。

重复这个过程,直到某次遍历时,发现没有可以消去的地方,就说明不会再可以消去了。这两个问题的判断逻辑是十分相似的。

方法二——递归求解

同样,我们仍然将输入存入memo,然而,这次我们不是重复尝试,而是遇到谁算谁。比如说,我们看到A=(B,a),就直接去尝试计算B,这样的思路可以用递归体现。而计算方法与上面是相同的。

需要注意的是计算失败的判断:在递归过程中,重复出现了某张表,并且这张表没被算出来过,那么就不可能被算出来了。这样我们返回-1,代表inf。

具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include <bits/stdc++.h>
using namespace std;
vector<char> input; //保存输入顺序
vector<int> ans(26); //保存每个表的结果
vector<int> vis(26); //判断某张表是否用过了
vector<string> memo(26); //保存每个输入
int solve(char c)
{
if (ans[c - 'A']) //如果已经计算过这张表了,直接返回
return ans[c - 'A'];
if (vis[c - 'A'])
return -1;
vis[c - 'A'] = true;
int res = 0, depth = 0;
for (char d : memo[c - 'A'])
{
depth += d == '(';
depth -= d == ')';
if (d >= 'A' && d <= 'Z')
{
int child = solve(d);
if (child != -1)
res = max(res, depth + child);
else
return -1;
vis[d - 'A'] = false;
}
res = max(res, depth);
}
ans[c - 'A'] = res;
memo[c - 'A'] = "";
vis[c - 'A'] = false;
return ans[c - 'A'];
}
int main()
{
int n;
string s;
cin >> n;
for (int i = 1; i <= n; ++i)
{
cin >> s;
input.push_back(s[0]);
memo[s[0] - 'A'] = s.substr(2);
}
for (char c : input)
{
int res = solve(c);
if (res == -1)
cout << "infity" << endl;
else
cout << res << endl;
}
return 0;
}

算法分析:这样的方法,时间复杂度应当是O(N)的(因为每张表最多计算一次),时间复杂度较上面有所改善,不过多了vis数组和递归栈的空间开销。

方法三——拓扑排序

如同最开始所说,如果输入顺序保证了子表在母表之前输入,就省去了繁琐的判断过程了,可以很方便地求解。那么有没有一种方法,可以帮助我们人为地排出计算顺序,来满足上述条件呢?其实,这就是拓扑排序的过程。

拓扑排序最经典的例子就是课程表问题。如果不懂,可以去学习一下,基本上都是这个模板。简单来说,拓扑排序可以解决前置条件问题,即每个问题有若干前置条件,如何安排解决这些问题的顺序,使每个问题都能解决?这道题也符合这个性质:我们可以将母表中包含的子表视为前置条件(因为必须先算出子表),依次进行拓扑排序,最后得到一个计算顺序。注意,不在最终顺序里的,也就是排序失败的,说明出现了互为前提,最后都是inf。

具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include <bits/stdc++.h>
using namespace std;
int main()
{
vector<char> input; //保存输入顺序
vector<int> ans(26); //保存每个表的结果
vector<string> memo(26); //保存每个输入
vector<vector<char>> graph(26); //建图
int n;
string s;
cin >> n;
unordered_map<char, int> indegree; //入度表,长度设为n
vector<char> sorted; //拓扑排序结果
for (int i = 1; i <= n; ++i)
{
cin >> s;
input.push_back(s[0]);
indegree[s[0]]; //创建位置
memo[s[0] - 'A'] = s.substr(2);
for (char c : memo[s[0] - 'A']) //建图并保存入度
if (c >= 'A' && c <= 'Z')
{
++indegree[s[0]];
graph[c - 'A'].push_back(s[0]);
}
}
queue<char> q; //辅助队列
for (auto &p : indegree)
if (p.second == 0)
q.push(p.first);
while (!q.empty())
{
char now = q.front();
q.pop();
sorted.push_back(now);
for (char c : graph[now - 'A'])
{
--indegree[c];
if (indegree[c] == 0)
q.push(c);
}
}
for (char c : sorted) //按序计算即可,剩下的都是inf
{
int res = 0, depth = 0;
for (char d : memo[c - 'A'])
{
depth += d == '(';
depth -= d == ')';
depth += d >= 'A' && d <= 'Z' ? ans[d - 'A'] : 0;
res = max(res, depth);
depth -= d >= 'A' && d <= 'Z' ? ans[d - 'A'] : 0;
}
ans[c - 'A'] = res;
}
for (char c : input)
if (ans[c - 'A'])
cout << ans[c - 'A'] << endl;
else
cout << "infity" << endl;
return 0;
}

算法分析:拓扑排序时间复杂度是O(n+e),其中n为节点数,e为边数,后续计算时间复杂度为O(N),空间上多了一个辅助栈的消耗。