小白的计蒜客算法笔记——图和树基础

1. 图是由一系列顶点和若干连结顶点集合内两个顶点的边组成的数据结构。
G(V,E)V表示点集,E表示边集。
2. 一般而言,我们在数据结构中所讨论的图都是有向图。
3. 图的分类
e是边,n是点。
e<n*logn稀疏图,反之为稠密图
边集为空的是零图
任何一对顶点都有边相连称为完全图
有向完全图任意两个顶点之间有两条边。
(注意:完全图是任意两点之间右边,连通图是任意两点之间有路径)
4. 出度和入度 顶点的度数总和为边数的两倍。
5. 度序列:在无向图中,度序列的定义为:将图G中所有顶点的度数排成一个序列s。
6.一个序列是可图的
在这里插入图片描述
7. 路径:在无向图 GG 中,如果从顶点vi出发,沿着图中的边经过一些顶点 vp1 vp2, …vpm…到达顶点v j ,则称顶点序列为从顶点 v i 到顶点v j 的一条 路径.
路径中边的数量称为 路径长度。如果路径中的顶点均不重复,则称为简单路径。如果始点与终点相同,则称为回路
8. 子图:对于两个图G1(V1,E1),G2(V2,E2)如果V1包含于V2,E1包含于E2,则G1是G2的子图。
9. 连通性:在无向图中,如果从顶点u到顶点v有路径,则称点u和点v是连通的。如果无向图中任意一对顶点都是连通的,那么这个无向图是连通图。如果一个无向图G的子图是连通图,则称该子图为图G的连通子图。
如果一个连通子图加上任何一个不在子图中的顶点后都不能称为连通子图,则称其为极大连通子图,又名连通分量
10. 在有向图中,如果每对顶点之间都互相有路径可达,则称此图为强连通图。
如果一个有向图G的子图是强连通图,则称该子图为图G的强连通子图。
如果一个强连通子图加上任何一个不在子图中的顶点后都不能称为强连通子图,则称其为极大强连通子图,又名强连通分量。
11. 生成树:对一个无向图G(V,E)来说,它的一个生成树指的是包含**|V|个顶点,|V|-1条边的连通子图**。
12.邻接矩阵 n代表结点数
就用一个n*n的M矩阵来表示各顶点的相邻关系,如果vi和vj之间存在边,则M[i][j] = 1,否则会等于0;
13. 显然在构造邻接矩阵的时候,我们需要实现一个整型的二维数组。由于当前的图还是空的,因此我们还要把这个二维数组中的每个元素都初始化为 0。
在构造好了一个图的结构后,我们需要把图中各边的情况对应在邻接矩阵上。实际上,这一步的实现非常简单,当从顶点 x 到 y 上存在边时,我们只要把二维数组对应的位置置为 1 就好了。

const int maxn = 100;
int mat[maxn][maxn];
void init(){
   memset(mat,0,sizeof(mat));
}
void insert(int u,int v){
mat[u][v] = 1;
}
//随机访问一条边看是否存在
cout<<mat[u][v];
//输出从u连出的所有边,顶点从0开始编号。
for(int i = 0;i<n;i++){
if(mat[u][i]){
cout<<”(”<<u<<”,”<<i<<”)”<<endl;
}
}

14.邻接表是图的一种顺序存储与链式存储相结合的存储方法。所有单链表的表头结点都存储在一个一维数组中,以便于顶点的访问。
例如:
有向图的邻接表
无向图的邻接表亦可以知道。如果这是个无向图,那就是
1-2-3-4
2-1-4
3-1-4
4-1-2-3
15.输出邻接表
A.

const int maxn = 100;
vector<int> adj[maxn];
void insert (int u,int v){
 adj[u].push_back(v);
}
for(int i = 0;i<adj[u].size;i++){
cout<<”(”<<u<<”,”<<adj[u][i]<<”)”<<endl;
}

B.

const int MAX_N = 100;
const int MAX_M = 10000;
struct edge {
    int v, next;
} e[MAX_M];
int p[MAX_N], eid;
void init() {
    memset(p, -1, sizeof(p));
    eid = 0;
}
void insert(int u, int v) {
    e[eid].v = v;
    e[eid].next = p[u];
    p[u] = eid++;
}

// 输出从 u 连出的所有边,顶点从 0 开始编号
for (int i = p[u]; i != -1; i = e[i].next) {
    cout << "(" << u << ", " << e[i].v << ")" << endl;
}

16.
优缺点
17.图的遍历
从图的某个顶点出发,沿图中的路径依次访问图中所有的点,并且使得图中所有的顶点都恰好被访问一次。
18.图的遍历最常见的算法:
深度优先搜索(DFS)和广度优先搜索(BFS)
DFS
A. 假设图上所有顶点都未被访问,选择图中任一顶点
B. 访问当前顶点v,并将顶点标为已经访问
C. 遍历与顶点v相邻的所有顶点c,然后对顶点v所有尚未被访问的相邻顶点c,递归的执行第一步操作,如果当前顶点已经没有被访问的相邻顶点了,则说明该分支搜索结束,沿通路回溯到顶点v。此时如果还有相邻顶点未被访问,则从该顶点继续开始深度优先搜索,直到所有顶点都被访问。
19.仔细分析后可以发现,对一个连通图进行深度优先搜索,其实是在对该图的一颗生成树进行搜索,这里我们把这棵生成树称为深度优先搜索树
20.为了方便,我们通常以递归的形式实现深度优先搜索。
21.

const int maxn = 100;
const int maxm = 10000;
struct edge{
int v,pre;
}e[maxm];
int p[maxn],eid;
void init(){
memset(p,-1,sizeof(p));
eid = 0;
}
void insert(int u,int v){
e[eid].v = v;
e[eid].pre = p[u];
p[u] = eid ++;
}
bool vst[maxn];
//在每次dfs之前需要将vst数组中的元素全部初始化为false
void dfs(int u){
cout<<”visting”<<u<<endl;//访问u
vst[u] = true;
for(int i = p[u];i!=-1;i = e[i].pre){
if(!vst[e[i].v]){
dfs(e[i].v);
}
}
}

22.
广度优先搜索,BFS
通常用于求起点到各点的最短路径,以及求两点之间的最优路径。
具体办法:
A选择图中任意一个顶点v作为起点进行访问,并将顶点v标为已访问
B 遍历并访问与顶点v相邻且未被访问的所有顶点c1,c2,…ck,接着遍历并访问与顶点c1,c2…ck相邻且未被访问的顶点,也就是依次访问所有相邻顶点的相邻顶点,以此类推,直到所有顶点均被访问。
23.借助队列实现广搜
1.任意选择一个顶点v作为起点,加入队列。
2.访问队首元素v并标记,将其从队列中删除。
3.遍历与顶点v相邻且未被访问的所有顶点c1,c2,…ck,并依次加入到队列中
4.重复第二步和第三步操作,直到队列为空。
24.仔细分析后可以发现,对一个连通图进行广度优先搜索,其实也是在对该图的一颗生成树进行搜索,这里我们把这棵生成树称为广度优先搜索树
25.
A.

const int maxn = 100;
const int maxm = 10000;
struct edge{
int v,next;
}e[maxm];
int p[maxn].eid;
void init(){
memset(p,-1,sizeof(p));
eid = 0;
}
void insert(int u,int v){
e[eid].v = v;
e[eid].next = p[u];
p[u] = eid++;
}
bool vst[maxn];
int q[maxn],h,r;//用数组实现队列。
void bfs(int v){
memset(vst,0,sizeof(vst));
h = 0;r = -1;
q[++r] = v;
vst[v] = 1;
while(h<=r){
int u =q[h++];
cout<<”visiting”<<u<<endl;
for(int i = p[u];i!=-1;i = e[i].next ){
if(!vst[e[i].v]){
vst[e[i].v] = 1;
q[++r] = e[i].v;
}
}
}
}

B

#include <iostream>
#include <string.h>
#include <queue>
using namespace std;
const int MAX_N = 100;
const int MAX_M = 10000;
struct edge{
    int v,next;
    int len;
}E[MAX_M];
int p[MAX_N],eid;
void init(){
    memset(p,-1,sizeof(p));
    eid = 0;
}
void insert(int u,int v,int len){
    E[eid].v = v;
    E[eid].len = len;
    E[eid].next = p[u];
    p[u] = eid++;
}
bool vst[MAX_N];
void bfs(int v){
    memset(vst,false,sizeof(vst));
    queue<int>q;
    q.push(v);
    vst[v] = 1;
    while(!q.empty()){
        int u = q.front();
        q.pop();
        cout<<"visiting "<<u<<endl;
        for(int i = p[u];i+1;i = E[i].next){
            if(!vst[E[i].v]){
                vst[E[i].v] = 1;
                q.push(E[i].v);
            }
        }
    }
}


1.一个无向连通图中不存在回路称为树
根结点,子结点,父结点,叶结点(没有子结点)
树的度:子结点的个数
二叉树:最大度为2的树
在这里插入图片描述
2.树的常用性质:
A.结点数为n,则边数一定为n-1;
B.树上任意一对结点之间有且仅有一条路径
3.祖先:对于树上的结点u,其到根结点的路径上的所有结点都是结点u的祖先
对于树上的两个结点 u,v,如果一个结点同时为 u 和 v 的祖先,则称之为 u,v 的公共祖先。
最近公共祖先(Least Common Ancestors)LCA就是离根结点最远的
4.如果一个无向图中包含了几棵互不连通的树,就称该无向图为森林。很显然,森林是一个非连通图。(树是稀疏图,所以通常用邻接表储存)
5.对树进行深度优先搜索时,有一个常用的概念,dfs序
例如:
在这里插入图片描述
6.dfs序重要的性质:子树内所有结点在dfs序中是连续的一段。例如2所在子树内的所有顶点对应着着dfs序中的2,3,3,5,5,6,6,2,这段区间,且左右两端一定是子树的根结点
在这里插入图片描述
注:有很多树上的问题都可以借助dfs序转化为数组中的问题,而和dfs序列结合最多的就是各种高级数据结构,如线段树等。
7.例题, 给定一棵树,算出每个结点所在子树中包括多少个叶结点。
解析: 我们可以用cnt[u],来记录结点u所在子树的叶结点个数。
A. 若u非叶结点,cnt[u] = ∑cnt[v].v∈childs(u)(u的子结点)
B. 若u为叶结点,cnt[u] =1;

例如以2结点所在的子树 叶结点个数
代码:

#include <iostream>
#include <string.h>
using namespace std;
const int maxn = 100;
const int maxm = 10000;
struct edge{
    int v,next;
}e[maxm];
int p[maxn],eid,o=0;
void init(){
    memset(p,-1, sizeof(p));
    eid = 0;
}
void insert(int u,int v){
    e[eid].v = v;
    e[eid].next  = p[u];
    p[u] = eid++;
}
bool vst[maxn];
int cnt[maxn];  // 统计结果
void dfs(int u) {
     vst[u] = true;
bool is_leaf = true;
    for (int i = p[u]; i != -1; i = e[i].next) {
        int v = e[i].v;
        if (!vst[v]) {
            dfs(v);
            is_leaf = false;
            cnt[u] += cnt[v];
        }
    }
    if (is_leaf) {
        cnt[u] = 1;
        o++;
        }
cout<<u<<"的cnt为:"<<cnt[u]<<endl;
}
int main(){
    init();
    int n,m;
    cin>>n>>m;
    int u,v;
    for(int i = 0;i<m;i++){
        cin>>u>>v;
        insert(u,v);
        insert(v,u);
    }
    dfs(2);
 cout<<"叶结点个数为:"<<o;
    return 0;
}

在这里插入图片描述
8.二叉树的遍历
子结点:分为左孩子和右孩子,左孩子,右孩子都有可能为空。
(也就是说二叉树的子结点之间是有序的)
正因如此,在二叉树中,除了深度优先搜索和广度优先搜索以外,还有几种特殊的遍历方法:先序遍历,中序遍历,后序遍历。
先序遍历: 在对二叉树进行遍历时,先访问当前子树的根结点,再依次访问左子树和右子树。
中序遍历: 先访问左子树,再访问根结点,最后访问右子树
后序遍历: 先访问左右子树,最后访问根结点
示例代码:
A.
int lch[maxn],rch[maxn];
void preorder(int u){
cout<<”visiting”<<u<<endl;
if(lch[u]){
preorder(lch[u]);
}
if(rch[u]){
preorder(rch[u]);
}
}
B.
int lch[MAX_N], rch[MAX_N];
void preorder(int u) {
if (lch[u]) {
preorder(lch[u]);
}
cout << "visiting " << u << endl;
if (rch[u]) {
preorder(rch[u]);
}
}
C.
int lch[MAX_N], rch[MAX_N];
void preorder(int u) {
if (lch[u]) {
preorder(lch[u]);
}
if (rch[u]) {
preorder(rch[u]);
}
cout << "visiting " << u << endl;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值