树形结构不仅能表示数据间的指向关系,还能表示出数据的层次关系,而有很明显的递归性质。因此,我们可以利用树的性质解决更多种类的问题。
但是在平常的使用中,我们并不需要使用这么复杂的结构,只需要建立一个包含int right和int left的结构体即可,left和right用于指示该节点的左儿子和右儿子。
一、【P4913】二叉树深度(递归/层次遍历)
本题的重点在于二叉树的存储和二叉树的层次遍历。
1. 二叉树存储:考虑一个二叉树的每个节点都有两个子节点,所以我们可以考虑用一个结构体来存储。其中,left
和right
分别代表节点的左节点编号和右节点编号。
struct node {
int left, right;
};
node tree[MAXN];
2. 二叉树层次遍历:我们可以从根节点出发,先递归遍历该节点的左节点,再递归遍历该节点的右节点。对于每个父亲节点,先比较左右子树的高度,然后选取较高的子树,将其深度加1,每次遍历更新子树的高度。当遍历到叶子节点后返回0(叶子节点的左右子树高度均为0)。
int maxDepth(int id)
{
if (id == 0) return 0;
return 1 + max(maxDepth(Tree[id].left), maxDepth(Tree[id].right));
}
AC代码:
#include <iostream>
#include <string>
#include <algorithm>
#include <cmath>
using namespace std;
struct node
{
int left;
int right;
}Tree[1000005];
int maxDepth(int id)
{
if (id == 0) return 0;
return 1 + max(maxDepth(Tree[id].left), maxDepth(Tree[id].right));
}
int main()
{
int n; cin >> n;
for (int i = 1; i <= n; i++)
{
int a, b; cin >> a >> b;
Tree[i].left = a;
Tree[i].right = b;
}
cout << maxDepth(1);
}
二、【P1827】美国血统(前中后序遍历)
前序遍历、中序遍历和后序遍历是三种利用深度优先搜索遍历二叉树的方式。它们是在对节点访问的顺序有一点不同,其它完全相同。
需要注意的是下图中伪代码并未给出base case,即递归返回的条件。
AC代码:
先序遍历的特点是优先遍历根节点,所以整棵树的根应该会在inorder字符串的最开始出现;中序遍历的特点是优先遍历左子树,然后遍历根节点,所以我们可以根据中序遍历序列判断左右子树中包含哪些点。
以本题为例:
中序(inorder)遍历:ABEDFCHG;先序(preorder)遍历:CBADEFGH。
- 首先可以根据preorder判断C为根节点,然后依据inorder判断ABEDF为左子树中的节点,HG为右子树中的节点。
- 回到preorder序列,第二个节点是B,所以左子树的根节点为B,依据inorder序列可知A构成次级左子树,DEF构成次级右子树。
- 回到preorder序列,第三个节点是A,刚好是以B为根的子树的左子树的根;第四个节点是D,所以以B为根的子树的右子树的根为D。
- 以此类推,可以还原整棵树的情况。
#include <iostream>
#include <string>
#include <algorithm>
#include <cmath>
#include <vector>
#include <map>
#include <stack>
#include <set>
using namespace std;
struct TreeNode //建树
{
char val;
TreeNode* left;
TreeNode* right;
TreeNode() :val(0), left(nullptr), right(nullptr) {}
TreeNode(char x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(char x, TreeNode* left, TreeNode* right) : val(x), left(left), right(right) {}
};
//对preorder中的[s0,e0]部分操作,s1用于跟踪preorder序列中某个父亲节点
TreeNode* buildTree(string& preorder, map<char, int>& mp, int s0, int e0, int s1)
{
if (s0 > e0) return nullptr;
char mid = preorder[s1];
int index = mp[mid]; //找到当前父亲节点在中序遍历inorder中的位置
int left_length = index - s0; //左子树大小
TreeNode* node = new TreeNode(mid);
node->left = buildTree(preorder, mp, s0, index - 1, s1 + 1);
node->right = buildTree(preorder, mp, index + 1, e0, s1 + 1 + left_length);
return node;
}
void PostOrder(TreeNode* tree)
{
if (!tree) return;
PostOrder(tree->left);
PostOrder(tree->right);
cout << tree->val;
}
int main()
{
string preorder, inorder;
cin >> inorder >> preorder;
map<char, int> mp;
//使用map存储,便于搜索
for (int i = 0; i < inorder.length(); i++)
mp[inorder[i]] = i;
TreeNode* res = buildTree(preorder, mp, 0, inorder.length() - 1, 0);
PostOrder(res);
}
由于需要通过preorder序列的节点,查询该点在inorder序列的位置,所以将inorder序列用map存储,实现O(1)量级的查询。
使用buildTree函数递归还原整棵树,然后使用postorder函数对树进行后序遍历,得到答案。
三、【P1229】遍历问题(前中后序遍历)
思路:给定inorder能确定树的具体结构,是因为可以确定子树是左子树还是右子树;而只给定preorder和postorder,则不能确定,这就是会出现不同树结构的原因。
只有前和后,那么主要问题就是没有办法处理只有一个子树的情况,因为这种情况下不知道子树究竟是这个节点的左子树还是右子树,也就是说其实这道题要判断遍历中存在着多少个只有一棵子树的情况。对于前,如果一个结点的下个结点等于后中对应结点的前一个结点的话,那么这个结点就是根节点且其只有一个子树。sum初始化为1,出现一个只有一棵子树的情况,就把sum*2(每次会出现两种不同的情况,分别是出现左子树和出现右子树)。
AC代码:
#include <iostream>
#include <string>
#include <algorithm>
#include <cmath>
#include <vector>
#include <map>
#include <cstring>
#include <queue>
using namespace std;
const int INF = 0x7fffffff;
int main()
{
string preorder, postorder;
cin >> preorder >> postorder;
int sum = 1;
for (int i = 0; i <= preorder.length() - 2; i++)
{
for (int j = 0; j <= postorder.length() - 1; j++)
{
if (preorder[i] == postorder[j] && preorder[i + 1] == postorder[j - 1])
{
sum *= 2;
}
}
}
cout << sum << endl;
}
四、【P1030】求先序序列(前中后序遍历)
首先,一点基本常识,给你一个后序遍历,那么最后一个就是根(如ABCD,则根为D),因为题目求先序,意味着要不断找根。
那么我们来看这道题方法:(示例)
中序ACGDBHZKX,后序CDGAHXKZB,首先可找到主根B;
那么我们找到中序遍历中的B,由这种遍历的性质,可将中序遍历分为ACGD和HZKX两棵子树,那么对应可找到后序遍历CDGA和HXKZ(从头找即可);从而问题就变成求:
- 中序遍历ACGD,后序遍历CDGA的树;
- 中序遍历HZKX,后序遍历HXKZ的树;
接着递归,按照原先方法,找到
1.子根A,再分为两棵子树;
2.子根Z,再分为两棵子树。
就按这样一直做下去(先输出根,再递归)。
模板概括为:
- step1:找到根并输出
- step2:将中序,后序各分为左右两棵子树;
- step3:递归,重复step1,2;
AC代码:
#include <iostream>
#include <string>
#include <algorithm>
#include <cmath>
#include <vector>
#include <map>
#include <cstring>
#include <queue>
using namespace std;
const int INF = 0x7fffffff;
string inorder, postorder;
void preorder(string in,string after)
{
if (in.size() > 0)
{
char ch = after[after.size() - 1];
cout << ch;
int k = in.find(ch);
preorder(in.substr(0, k), after.substr(0, k)); //左子树
preorder(in.substr(k + 1), after.substr(k, in.size() - k - 1));//从k开始,截取这么多个数
}
}
int main()
{
cin >> inorder >> postorder;
preorder(inorder, postorder);
}
将2,3,4三题进行比较,2的解题思路具有普适性,但是建树的过程相对复杂;4的解题思路较为清晰,但是缺点在于只能输出序列,并不能得到完整的树。
五、【P5076】简化二叉树(二叉搜索树)
参考:https://www.cnblogs.com/do-while-true/p/13566274.html
但值得强调的是,二叉搜索树是一种低级的平衡二叉树,因为在最差情况下,可以会呈现链状结构,导致时间复杂度为O(n),而不是所需的O(logN)。因此,只需要了解即可,重点需要掌握的是平衡二叉树。
splay搜索树的几种操作:
(1)插入:x 是当前节点的下标,v 是要插入的值。要在树上插入一个 v 的值,就要找到一个合适 v 的位置,如果本身树的节点内有代表 v 的值的节点,就把该节点的计数器加 11 ,否则一直向下寻找,直到找到叶子节点,这个时候就可以从这个叶子节点连出一个儿子,代表 v 的节点。具体向下寻找该走左儿子还是右儿子是根据二叉搜索树的性质来的。
void add(int x,int v)
{
tree[x].siz++;
//如果查到这个节点,说明这个节点的子树里面肯定是有v的,所以siz++
if(tree[x].val==v){
//如果恰好有重复的数,就把cnt++,退出即可,因为我们要满足第四条性质
tree[x].cnt++;
return ;
}
if(tree[x].val>v){//如果v<tree[x].val,说明v实在x的左子树里
if(tree[x].ls!=0)
add(tree[x].ls,v);//如果x有左子树,就去x的左子树
else{//如果不是,v就是x的左子树的权值
cont++;//cont是目前BST一共有几个节点
tree[cont].val=v;
tree[cont].siz=tree[cont].cnt=1;
tree[x].ls=cont;
}
}
else{//右子树同理
if(tree[x].rs!=0)
add(tree[x].rs,v);
else{
cont++;
tree[cont].val=v;
tree[cont].siz=tree[cont].cnt=1;
tree[x].rs=cont;
}
}
}
(2)找前驱:x 是当前的节点的下标,val 是要找前驱的值,ans 是目前找到的比 val 小的数的最大值。找前驱的方法也是不断的在树上向下爬找具体节点,具体爬的方法可以参考代码注释部分。
int queryfr(int x, int val, int ans) {
if (tree[x].val>=val)
{//如果当前值大于val,就说明查的数大了,所以要往左子树找
if (tree[x].ls==0)//如果没有左子树就直接返回找到的ans
return ans;
else//如果不是的话,去查左子树
return queryfr(tree[x].ls,val,ans);
}
else
{//如果当前值小于val,就说明我们找比val小的了
if (tree[x].rs==0)//如果没有右孩子,就返回tree[x].val,因为走到这一步时,我们后找到的一定比先找到的大(参考第二条性质)
return (tree[x].val<val) ? tree[x].val : ans
//如果有右孩子,,我们还要找这个节点的右子树,因为万一右子树有比当前节点还大并且小于要找的val的话,ans需要更新
if (tree[x].cnt!=0)//如果当前节数的个数不为0,ans就可以更新为tree[x].val
return queryfr(tree[x].rs,val,tree[x].val);
else//反之ans不需要更新
return queryfr(tree[x].rs,val,ans);
}
}
(3)找后继:与找前驱同理,只不过需要反过来。
int queryne(int x, int val, int ans) {
if (tree[x].val<=val)
{
if (tree[x].rs==0)
return ans;
else
return queryne(tree[x].rs,val,ans);
}
else
{
if (tree[x].ls==0)
return (tree[x].val>val)? tree[x].val : ans;
if (tree[x].cnt!=0)
return queryne(tree[x].ls,val,tree[x].val);
else
return queryne(tree[x].ls,val,ans);
}
}
(4)按值找排名:这里我们就要用到 siz 了,排名就是比这个值要小的数的个数再 +1,所以我们按值找排名,就可以看做找比这个值小的数的个数,最后加上 1 即可。
int queryval(int x,int val)
{
if(x==0) return 0;//没有排名
if(val==tree[x].val) return tree[tree[x].ls].siz;
//如果当前节点值=val,则我们加上现在比val小的数的个数,也就是它左子树的大小
if(val<tree[x].val) return queryval(tree[x].ls,val);
//如果当前节点值比val大了,我们就去它的左子树找val,因为左子树的节点值一定是小的
return queryval(tree[x].rs,val)+tree[tree[x].ls].siz+tree[x].cnt;
//如果当前节点值比val小了,我们就去它的右子树找val,同时加上左子树的大小和这个节点的值出现次数
//因为这个节点的值小于val,这个节点的左子树的各个节点的值一定也小于val
}
//注:这里最终返回的是排名-1,也就是比val小的数的个数,在输出的时候记得+1
(5)按排名找值:排名为 n 的数在BST上是第 n 靠左的数。或者说排名为 n 的数的节点在BST中,它的左子树的 siz 与它的各个祖先的左子树的 siz 相加恰好 =n (这里相加是要减去重复部分)。
int queryrk(int x,int rk)
{
if(x==0) return INF;
if(tree[tree[x].ls].siz>=rk)//如果左子树大小>=rk了,就说明答案在左子树里
return queryrk(tree[x].ls,rk);//查左子树
if(tree[tree[x].ls].siz+tree[x].cnt>=rk)//如果左子树大小加上当前的数的多少恰好>=k,说明我们找到答案了
return tree[x].val;//直接返回权值
return queryrk(tree[x].rs,rk-tree[tree[x].ls].siz-tree[x].cnt);
//否则就查右子树,同时减去当前节点的次数与左子树的大小
}
AC代码:
#include <iostream>
#include <string>
#include <algorithm>
#include <cmath>
#include <vector>
#include <map>
#include <stack>
#include <set>
using namespace std;
const int INF = 0x7fffffff;
struct node
{
int val; //节点值
int left, right; //左右子树
int cnt; //计数器
int siz; //子树大小
}tree[100005];
int cont = 0;//记录节点数量
//初始化
void init()
{
for (int i = 0; i < 100005; i++)
{
tree[i].val = 0;
tree[i].left = 0;
tree[i].right = 0;
tree[i].cnt = 0;
tree[i].siz = 0;
}
}
//添加新点
void add(int x, int value) //当前节点为x,需要插入value
{
tree[x].siz++; //肯定在x子树上,规模加一
if (tree[x].val == value) //如果value刚好等于节点值,计数器加一即可
{
tree[x].cnt++;
return;
}
if (tree[x].val > value) //如果x的值大于value,value只能放在x的左子树上
{
if (tree[x].left != 0)//如果左子树上有节点,递归查找
add(tree[x].left, value);
else//如果左子树上没有点,直接添加
{
cont++;
tree[cont].val = value;
tree[cont].siz = tree[cont].cnt = 1;
tree[x].left = cont;
}
}
else if (tree[x].val < value)//如果x的值小于value,value要放到x的右子树上
{
if (tree[x].right != 0)//如果右子树上有节点,递归查找
add(tree[x].right, value);
else//如果右子树上没有点,直接添加
{
cont++;
tree[cont].val = value;
tree[cont].siz = tree[cont].cnt = 1;
tree[x].right = cont;
}
}
}
//查找前驱
int query_front(int x, int value, int ans) //x 是当前的节点的下标,val 是要找前驱的值,ans 是目前找到的比val小的数的最大值。
{
if (cont == 0) return -INF;
if (tree[x].val >= value)//当前节点val大于所查value,到左子树查找
{
if (tree[x].left == 0) //左子树不存在,那么前驱为ans
return ans;
else
return query_front(tree[x].left, value, ans); //否则遍历左子树
}
else
{
if (tree[x].right == 0)//若无右孩子,说明当前点满足条件
return (tree[x].val < value) ? tree[x].val : ans;
else
{
if (tree[x].cnt != 0) //如果当前节点的个数不为0,ans就可以更新为tree[x].val
return query_front(tree[x].right, value, tree[x].val);
else
return query_front(tree[x].right, value, ans);
}
}
}
//查找后继
int query_next(int x, int value, int ans)
{
if (cont == 0) return INF;
if (tree[x].val <= value)
{
if (tree[x].right == 0)
return ans;
else
return query_next(tree[x].right, value, ans);
}
else
{
if (tree[x].left == 0)
return (tree[x].val > value) ? tree[x].val : ans;
else
{
if (tree[x].cnt != 0)
return query_next(tree[x].left, value, tree[x].val);
else
return query_next(tree[x].left, value, ans);
}
}
}
//按值找排名
int val_to_rank(int x, int value)
{
if (x == 0)
return 0;
if (value == tree[x].val)
return tree[tree[x].left].siz;//比它小的数
if (value < tree[x].val)
return val_to_rank(tree[x].left, value);
if (value > tree[x].val)
return val_to_rank(tree[x].right, value) + tree[tree[x].left].siz + tree[x].cnt;//左子树上的所有点加上根上的点都小于value
}
//按排名找值
int rank_to_val(int x, int rank)
{
if (x == 0)
return INF;
if (tree[tree[x].left].siz >= rank) //如果小于x.val的数多于rank,那么该值一定在左子树上,递归查找
return rank_to_val(tree[x].left, rank);
if (tree[tree[x].left].siz + tree[x].cnt >= rank) //如果左子树上的点数+根上的节点数大于rank,且左子树上的点数小于rank,那么点x的值为所需值
return tree[x].val;
return rank_to_val(tree[x].right, rank - tree[tree[x].left].siz - tree[x].cnt); //否则在右子树上,要先将左子树+根上的点数减去
}
int main()
{
int q; cin >> q;
init();
for (int i = 1; i <= q; i++)
{
int op, v;
cin >> op >> v;
if (op == 1)
cout << val_to_rank(1, v) + 1 << endl;
else if (op == 2)
cout << rank_to_val(1, v) << endl;
else if (op == 3)
cout << query_front(1, v, -INF) << endl;
else if (op == 4)
cout << query_next(1, v, INF) << endl;
else
{
if (cont == 0)
{
cont++;
tree[cont].cnt = tree[cont].siz = 1;
tree[cont].val = v;
}
else
add(1, v);
}
}
}
六、【P1364】医院设置(BFS广搜)
广度优先搜索(breadth-first search, BFS)不同与深度优先搜索,它是一层层进行遍历的,因此需要用先入先出的队列而非先入后出的栈进行遍历。由于是按层次进行遍历,广度优先搜索时按照“广”的方向进行遍历的,也常常用来处理最短路径等问题。
在本题中,由于每个点都可以是原点(树根),因此采用树结构存储是行不通的,因为指针只能单方向指向。这里采用邻接矩阵的方式来存储树。
AC代码:
本题中只考虑每个点到根节点的距离,因此采用DFS和BFS均可。
由于BFS操作起来更直观,因此本题用普通图的形式存储树,枚举每个节点为根节点的情况,进行n遍BFS,然后每遍都进行答案的比较更新。
#include <iostream>
#include <string>
#include <algorithm>
#include <cmath>
#include <vector>
#include <map>
#include <cstring>
#include <queue>
using namespace std;
const int INF = 0x7fffffff;
bool graph[105][105] = { 0 };
bool visit[105] = { 0 };
int value[105] = { 0 };
struct node
{
int num;
int step; //用于存储某点到根节点的距离
};
int n;
int BFS(int x) //以x为根对树进行BFS遍历
{
memset(visit, 0, sizeof(visit));
queue<node> q;
visit[x] = 1;
node gen;
gen.num = x, gen.step = 0;
q.push(gen); //将树根节点入队
int sum = 0;
while (!q.empty())
{
node now = q.front();
q.pop();
for (int i = 1; i <= n; i++) //依次取出队列中的点,并找到与该点距离为1且未被访问过的点
{
if (graph[now.num][i] && !visit[i])
{
node next;
next.num = i, next.step = now.step + 1; //更新选中点到根的距离
visit[i] = 1;
q.push(next);
sum += value[i] * next.step; //该点的权重乘以与根的距离
}
}
}
return sum;
}
int main()
{
cin >> n;
for (int i = 1; i <= n; i++)
{
int w, u, v;
cin >> w >> u >> v;
value[i] = w;
if (u != 0)
graph[i][u] = graph[u][i] = 1;
if (v != 0)
graph[i][v] = graph[v][i] = 1;
}
int MinDis = INF;
for (int i = 1; i <= n; i++)
{
int tmp = BFS(i);
if (tmp < MinDis)
MinDis = tmp;
}
cout << MinDis << endl;
}
由于bfs每遍都是O(V+E)的,而这里特殊之处在于本身是个树,所以是n个节点和n-1条边
所以总复杂度近似为.
七、【P3884】二叉树问题(Floyd算法)
树其实一种特殊的图。
即没有环路,一定联通,而且有且有只有一个节点没有入度的图。
- 树上任意两点间的距离可以直接通过最短路算法获得。
- 树的深度就是距离根节点最远的那个点的距离。
- 树的宽度是节点最多的一层,同一层的节点距离根节点的距离都是相同的,所以也可以直接枚举获得。
AC代码:
Floyd:使用邻接矩阵存储树,先将邻接矩阵的对角线位置初始化为0(某点到自己的距离为0),其他位置初始化为INT_MAX/4(不相连的节点理论上距离为无限大,但是在计算是要考虑可能发生的溢出情况,所以除以4)
三层嵌套循环,列举每两个点 i 和 j 之间的距离,并且要考虑有中继点 k 存在的情况下,两点间的距离。(由于树是无环图,所以只需要一个中继点即可覆盖所有情况)
#include <iostream>
#include <string>
#include <algorithm>
#include <cmath>
#include <vector>
#include <map>
#include <cstring>
#include <queue>
using namespace std;
const int INF = 0x7fffffff / 4; //若直接为INT_MAX,则使用floyed时会发生溢出
int a[105][105] = { 0 };
int b[10005] = { 0 };
void init()
{
for (int i = 0; i < 105; i++)
{
for (int j = 0; j < 105; j++)
{
if (i != j)
a[i][j] = INF;
}
}
}
int main()
{
init();
int n; cin >> n;
for (int i = 1; i <= n - 1; i++)
{
int u, v; cin >> u >> v;
a[u][v] = 1;
a[v][u] = 2;
}
for (int k = 1; k <= n; k++)
{
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= n; j++)
{
a[i][j] = min(a[i][j], a[i][k] + a[k][j]);
}
}
}
int maxdepth = 0;
for (int i = 1; i <= n; i++)
{
if (maxdepth < a[1][i])
maxdepth = a[1][i];
b[a[1][i]]++;
}
int maxd = 0;
for (int i = 1; i <= maxdepth; i++)
{
maxd = max(maxd, b[i]);
}
int u, v;
cin >> u >> v;
cout << maxdepth + 1 << endl;
cout << maxd << endl;
cout << a[u][v] << endl;
}