一、树和图的存储
树是特殊的图,因此我们只要学会了图的存储方式,自然也就知道了树的存储。图分为有向图和无向图两种。采用的方式也有邻接矩阵和邻接表两种方式。
1、邻接矩阵
邻接矩阵,顾名思义,就是采用矩阵来存储信息。无向图的矩阵是对称的,所以可以只存储上三角矩阵或者是下三角矩阵;而有向图则需要挨个边进行存储。
2、邻接表
邻接表就是每一个结点的位置开一个单链表,假设有n个结点,可以用一个数组来记录这n个元素。每一个结点创建一个链表,用next指向该结点所连接的各个结点。插入方式采用头插法。
一、最短路径地杰斯特拉算法及其优化
Dijkstra算法
首先,定义一个dist数组,记录起点到其他个点的距离最小值。然后将dist数组除了起点以外的所有元素都赋成无限大。然后开始扫描与起点相连的点,找出一个直接距离最短的点,加入已生成的图中,并将连接它们的这条边加入最小生成树中。
然后继续,从已有的最小生成树中的所有点出发,找到一个距离最近的,继续加入生成树中。
优化:
可以使用由优先队列,每次弹出的都是最小的元素,而且时间开销很少(由于采用了最小堆)
if(dis[j]>dis[k]+map[k][j])
{
dis[j]=dis[k]+map[k][j];
path[j]=k;
}
二、最小生成树算法
1.Kruskal算法
此算法可以称为“加边法”,初始最小生成树边数为0,每迭代一次就选择一条满足条件的最小代价边,加入到最小生成树的边集合里。
- 把图中的所有边按代价从小到大排序;
- 把图中的n个顶点看成独立的n棵树组成的森林;
- 按权值从小到大选择边,所选的边连接的两个顶点ui,viui,vi,应属于两颗不同的树,则成为最小生成树的一条边,并将这两颗树合并作为一颗树。
- 重复(3),直到所有顶点都在一颗树内或者有n-1条边为止。
///克鲁斯卡尔
struct edge
{
int m;
int n;
int d;
}a[5010];
int cmp(const void *a,const void *b) ///按升序排列
{
return ((struct edge *)a)->d>((struct edge *)b)->d;
}
int main(void)
{
int n,t,num,minn,x[100];
cin>>n;///顶点数
t=n*(n-1)/2;
for(int i=1;i<=n;i++)
x[i]=i;
printf("请输入每条边的起始端点、权值:/n");
for(int i=0;i<t;i++)///输入每条边的权值
cin>>a[i].m>>a[i].n>>a[i].d;
qsort(a,t,sizeof(a[0]),cmp);
minn=num=0;
for(int i=0;i<t && num<n-1;i++)
{
for(int k=a[i].m;x[k]!=k;k=x[k]) ///判断线段的起始点所在的集合
x[k]=x[x[k]];
for(int g=a[i].n;x[g]!=g;g=x[g]) ///判断线段的终点所在的集合
x[g]=x[x[g]];
if(k!=g) ///如果线段的两个端点所在的集合不一样
{
x[g]=k;
minn+=a[i].d;
num++;
}
}
cout<<minn<<endl;
return 0;
}
2.Prim算法
此算法可以称为“加点法”,每次迭代选择代价最小的边对应的点,加入到最小生成树中。算法从某一个顶点s开始,逐渐长大覆盖整个连通网的所有顶点。
图的所有顶点集合为VV;初始令集合u={s},v=V−uu={s},v=V−u;
在两个集合u,vu,v能够组成的边中,选择一条代价最小的边(u0,v0)(u0,v0),加入到最小生成树中,并把v0v0并入到集合u中。
重复上述步骤,直到最小生成树有n-1条边或者n个顶点为止。
由于不断向集合u中加点,所以最小代价边必须同步更新;需要建立一个辅助数组closedge,用来维护集合v中每个顶点与集合u中最小代价边信息。
三、并查集原理以及应用
并查集,在一些有N个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中。这一类问题近几年来反复出现在信息学的国际国内赛题中,其特点是看似并不复杂,但数据量极大,若用正常的数据结构来描述的话,往往在空间上过大,计算机无法承受;即使在空间上勉强通过,运行的时间复杂度也极高,根本就不可能在比赛规定的运行时间(1~3秒)内计算出试题需要的结果,只能用并查集来描述。
///并查集
int findx(int x) /// 查找根节点
{
if(x == fa[x])
return x;
return findx(fa[x]);
}
int findx(int x)
{
if(fa[x] != x)
fa[x] = findx(fa[x]); /// 路径压缩优化
return fa[x];
}
void judge(int a,int b){///判断是否在同一个集合里
if(findx(a)!=findx(b)){
printf("No\n");
}else{
printf("Yes\n");
}
return;
}
void unionn(int a,int b){
fa[b]=a;///将两棵树连接起来
return;
}
四、深度优先搜索DFS
深度优先搜索的实质就是穷举,按照一定的顺序和规则不断地去尝试,直到找到问题的解。
对于一个问题的第一个状态叫做初始状态,最后要求的状态叫做目的状态。
在搜索的过程中,对当前状态进行检测,如果当前状态满足目的状态,那么这个当前状态就是结果之一。
可求所有解
深度优先搜索(Depth First Search,DFS)——求出可行解(所有解)
void DFS(int start)
{
visited[start] = 1;
for (int i = 1; i <= N; i++)
{
if (!visited[i] && maze[start - 1][i - 1] == 1)
DFS(i);
}
cout << start << " ";
}
五、广度优先搜索BFS
广度优先搜索——求出最优一个解
void BFS(int start)
{
queue<int> Q;
Q.push(start);
visited[start] = 1;
while (!Q.empty())
{
int front = Q.front();
cout << front << " ";
Q.pop();
for (int i = 1; i <= N;i++) {
if (!visited[i] && maze[front - 1][i - 1]== 1)
{
visited[i] = 1;
Q.push(i);
}
}
}
}
六、可行性剪枝
剪枝算法按照其判断思路可大致分成两类:可行性剪枝及最优性剪枝.
1、可行性剪枝
该方法判断继续搜索能否得出答案,如果不能直接回溯。
2、最优性剪枝
最优性剪枝,又称为上下界剪枝,是一种重要的搜索剪枝策略。它记录当前得到的最优值,如果当前状态已经无法产生比当前最优解更优的解时,可以提前回溯。