本日主要内容是树与图。
 
1.树
    • 树的性质
    • 树的遍历
    • 树的LCA
    • 树上前缀和
 
树的基本性质:
对于一棵有n个节点的树,必定有n-1条边。任意两个点之间的路径是唯一确定的。
 
回到题目上,如果题目读入的是树上所有的边,则我们应该想到:
    1. 每个点的父亲是谁
    2. 每个点的深度
    3. 每个点距离根节点的距离
    4. 其他的附加信息(例如:子树和,子树最大值。。)
遍历整个树的代码如下:
 void dfs(int now)
{
deep[now]=deep[fa[now]]+;
sum[now]=value[now]; maxx[now]=value[now];
for 遍历从now出发的每一条边,边到达的点是v
if (v != fa[now])
{
fa[v]=now;
dfs(v);
sum[now]+=sum[v]; maxx[now]=max(maxx[now], maxx[v]);
}
}
 
实际上,树也有类似于图的邻接表存储结构。关于树的邻接表存法本来应该是Day5 zhn讲到的,我为了方便把这一块放在了Day 3的整理上。
我们用u[i],v[i]表示第i条边的两个端点,在有根树中一般认为v是儿子,u是父亲。w[i]代表边权,first[i]代表当前以i为端点的最后一条边,next[i]代表第i号点的下一条边。
struct Edge_tree{
int u,v,w;
int next; };
Edge_tree edge[maxn];
int cnt = ;
int first[maxn];
void add_edge(int from,int to,int dis){
edge[++cnt].u = from;
edge[cnt].v = to;
edge[cnt].w = dis;
edge[cnt].next = fisrt[from];
first[from] =cnt; /*
作为一棵无向树,还需要反向进行加边操作。
图的邻接表不也是这样吗?
*/
edge[++cnt].v = from;
edge[cnt].u = to;
edge[cnt].w = dis;
edge[cnt].next = first[to];
first[to] = cnt;
/*
这超酷,是不是?
以前我还从来没有想过可以使用邻接表存一棵树!
这可以说是最新操作了。
*/
} void dfs_tree(int x,int fa){
//cout << x << " ";
for (int i = first[x];i!=;i = edge[i].next)
if (edge[i].v != fa)
dfs_tree(edge[i].v,x);
}
 
 
树的LCA与倍增思想
LCA指树上两个节点的「最近公共祖先」。
一个比较简单的求法:我们找到这两个点到达根节点的路径,然后去寻找这两条路径的交集,交集上深度最大的点便是LCA。
具体到实现,我们可以先让深度较大的点先蹦到与深度较小的点的同深度位置,然后这两个点一起向上蹦,直到重合。
这个操作的时间复杂度是O(n)的,在数据量不大的时候可以使用,或者用来对拍。
正常的方法是倍增处理。
何为倍增?倍增是根据已经得到的信息,将考虑的范围扩大一倍,从而加速操作的一种思想,它在变化规则相同的情况下,加速状态转移。
运用倍增思想的算法有:倍增查找LCA,归并排序,快速幂,基于ST表的RMQ(这个今年没讲),当然还有FFT啊后缀数组啊等不在NOIP考纲范围的奇怪的东西。。。
 
这次我们重点考虑LCA。
我们用一个数组f[i][j]表示i这个点向上跳2^j次,会跳到什么地方。
我们j从小到大枚举,用j较小的更新j较大的:
就有f[i][j]=f[f[i][j-1]][j-1],边界f[i][0]=fa[i]
有了这一步预处理,接下来的操作就和朴素的做法类似了。
分成两个阶段。第一阶段把让两个点处在同一深度,第二阶段我们就让这两个点跳到重合位置。
但因为我们每次是跳2^j次(倍增,加速处理嘛,所以有些时候会“跳过头”,怎么办?
试探法。试着去跳2^j个格子,如果不跳过头,就可以跳,否则不这样跳。
现在我们让两个点处在同一深度了,然后我们从大到小枚举j,看跳2^j步会不会跳到一个地方。如果会,不跳;如果不会,跳。这样跳完了以后,x和y这两个点将在距离lca仅一步的位置。再跳一步就好了。。
我在我的电脑里找到了我大概半年之前写的这个LCA。。。
 #include<iostream>
#include<cstring>
#include<cstdio>
#include<vector>
#define maxn 2333
using namespace std;
int f[maxn][maxn];
int father[maxn];
int deep[maxn];
vector<int> tree; void dfs(int x){
f[x][] = father[x];
for (int i=;i<=n;i++)
f[x][i] = f[f[x][i-]][i-];
for (int i=;i<tree[x].size();i++){
if (tree[x][i]!=father[x]){
int y = tree[x][i];
father[y] = x;
deep[y] = deep[x]+;
dfs(y);
}
}
}//从根节点开始dfs,预处理f数组 //查询LCA:
int lca(int x,int y){
if (deep[x]<deep[y])
swap(x,y);
for (int i=n;i>=;i--)
if (deep[y] <= deep[f[x][i]])
x = f[x][i];
if (x==y)
return x;
for (int i=n;i>=;i--)
if (f[x][i]!=f[y][i]){
x = f[x][i];
y = f[y][i];
}
return f[x][];
} int main(){
//do something
return ;
}
 
 
树的前缀和
类似线性表的前缀和。
还记得线性表的前缀和吗?
sum[i]表示a[1]~a[i]的和
用处1:求i~j的和sum[j]-sum[i-1]
用处2:区间修改。设置一个change数组。当区间[i,j]上要加k时,我们令change[i]+=k,令change[j+1]-=k。如果我们对change数组求前缀和的话,前缀和sum_change[i]就是i这个位置变动的值
 
树的前缀和有两种。第一种是子树前缀和sum1[i],指i的子树(包括i本身)所有节点的权值之和。第二种是根路径前缀和sum2[i],指i到根节点所有节点的权值之和。
用处1:用来求路径节点的前缀和
用处2:路径修改
 
    • 根路径前缀和
我们要求x点到y点的前缀和,设z = lca(x,y),则有Sx-y = sum[x] + sum[y] - 2sum[z] + val[z].
    • 子树前缀和
可用来做路径修改。设定一个修改数组change。如果要对x到y路径上的所有点权值+k,lca为z。那么change[x]+=k,change[y]+=k,change[z]-=k,change[fa[z]]-=k。这样如果最后对change[i]求前缀和的话,最后得到的结果就是i权值的修改量
特点:可以O(1)修改,但是只能一次查询(因为要求前缀和O(n))
 
 
    • 邻接矩阵 && 邻接表
    • 图的最短路径算法
    • 最小生成树
    • 拓扑排序(我之前好像总结过
 
存图方式
1.邻接矩阵:比较直观的一种存图方式,使用二维数组存图,下表i,j表示的是节点,而记录的值就代表这两个点的关系。
优点:直观好写,理解简单
缺点:无法适用于太大的数据范围
2.邻接表:应用最广泛的存图方式,它的本质是若干串链表。
虽说是链表,但实际使用中一般使用数组模拟链表进行存储。以每个节点作为一串链表中的头结点,其后继点代表着与这个点相连的点。
优点:速度快,支持的数据范围较为广泛
缺点:如果对链表不是很理解的话理解代码时会存在一定困难
 
链表按难度应该是基础班的东西。我相信看我这些随笔的人应该都具有数据结构基础班或数据结构基础班以上的实力,我就不再写链表是怎么一回事了。。
 
实现方式:手写
 struct Edge{
long long int from,to,dis;
};
Edge edge[maxn];
long long int head[maxn];
long long int cnt = ;
void add_edge(long long int from,long long int to,long long int dis){
edge[++cnt].from = head[from];
edge[cnt].to = to;
edge[cnt].dis = dis;
head[from] = cnt;
}

这是我最常用的邻接表。开一个struct存边。

加边操作其实就是往链表里塞一个节点罢了。那个head数组表示第i个点连接的下一条边的编号。

 
求最短路径
图上求最短路的算法大体分三种。
1.floyed算法,可求任意两点之间的最短路径,一般配合邻接矩阵食用。时间复杂度O(n3),在一些数据量较小的题中还是有些作用的。
2.Dijkstra算法。基于贪心思想的只能处理无负权图的单源最短路算法。未优化时的复杂度是O(n2),使用堆(一般使用优先队列)进行优化后时间复杂度降为O((n+m)log(n+m)),n是点数,m是边数。
给出堆优化之后的代码:
 #include<cstdio>
#include<cstring>
#include<iostream>
#include<queue>
#include<vector>
#include<algorithm>
#define ll long long
#define INF 2147483647
using namespace std;
int n,m,s,head[],cnt;
ll dis[];
bool used[];
struct Edge{
int to,from,dis;
}edge[]; void add_edge(int u,int v,int dis){
edge[cnt].to=v;
edge[cnt].from=head[u];
edge[cnt].dis=dis;
head[u]=cnt++;
}
typedef pair<int,int> P;
void dijkstra(int s){
priority_queue<P,vector<P>,greater<P> > q;
fill(dis,dis+n+,INF);
fill(used,used+n+,false);
dis[s]=;
q.push(P(,s));
while(!q.empty()){
P p=q.top();q.pop();
int u=p.second;
if(used[u]) continue;
used[u]=true;
int pp=head[u];
while(pp!=-){
int v=edge[pp].to;
if(!used[v]&&dis[v]>dis[u]+edge[pp].dis){
dis[v]=dis[u]+edge[pp].dis;
q.push(P(dis[v],v));
}
pp=edge[pp].from;
}
}
}
int main(){
memset(head,-,sizeof(head));
cin>>n>>m>>s;
for(int i=;i<=m;i++){
int u,v,d;
scanf("%d%d%d",&u,&v,&d);
add_edge(u,v,d);
}
dijkstra(s);
for(int i=;i<=n;i++) printf("%lld ",dis[i]);
return ;
}

用了一个pair,对组,用来存放最短路径

priority_queue<P,vector<P>,greater<P> > q;这样声明,便成了小根堆,我们每次都要取最小的边。

3.SPFA算法,即经过队列优化的bellman-ford算法,也是我个人最喜欢使用的求最短路的算法,它支持存在负边的图,并且可以在判定出负环后及时退出。时间复杂度是O(kE) O(RP),其中E代表边数,k是一个玄学常数,均值为2。
(k的大小与当前写代码的人有没有穿女装有很大关系
但SPFA也是存在缺点的。当你试图在一个稠密图(点少边巨多)的图上跑SPFA时,它将会变得非常慢,这与它的扩展方式有密切关系。
 #include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
#define maxn 5000015
#define INF 2147483647
#define ms(x) memset(x,0,sizeof(x));
using namespace std;
struct Edge{
long long int from,to,dis;
};
Edge edge[maxn];
long long int n,m,s,u,v,d;
long long int head[maxn];
long long int dis[maxn];
bool inq[maxn];
long long int cnt = ;
void add_edge(long long int from,long long int to,long long int dis){
edge[++cnt].from = head[from];
edge[cnt].to = to;
edge[cnt].dis = dis;
head[from] = cnt;
} void spfa(void){
queue<long long int> q;
q.push(s);
ms(inq);
inq[s] = true;
for (int i=;i<=n;i++)
dis[i] = INF;
dis[s] = ;
while (!q.empty()){
long long int u = q.front();
q.pop();
inq[s] = false;
for (int i=head[u];i!=;i=edge[i].from){
long long int v = edge[i].to;
long long int w = edge[i].dis;
if (dis[u]+w < dis[v]){
dis[v] = w+ dis[u];
if (!inq[v]){
q.push(v);
inq[v] = true;
}
}
}
} } int main(){
cin >> n >> m >> s;
for (int i=;i<=m;i++){
cin >> u >> v >> d;
add_edge(u,v,d);
}
spfa();
for (int i=;i<=n;i++)
cout << dis[i] << " ";
return ;
}

如果要判断负环的话,再加一个数组记录每个点入队的次数,如果在入队操作时发现一个点的入队次数超过n,则一定存在负环。

 
特殊的方式:
如果一个图的边权都是1,那么我们有什么简便方法求最短路呢?
BFS与SPFA算法的扩展非常类似,并且由于BFS算法的性质,找到的第一个解必定最优,也就是最短。
 
 
最小生成树
昨天卖的关子现在该揭开了。
最小生成树的官方定义比较麻烦,用通俗一点的话来说就是在这个图中找到“一棵连接了所有节点的树”,并且树上的所有边权值是最小的。
最小生成树有两种算法。第一种是基于贪心思想的prim,我平常不怎么用。。
第二种就是我现在要说的这个kruskal。
首先,我们把所有边按照边权从小到大排序,并认为每个点在初始状态时都是孤立的。然后我们桉顺序枚举每条边,若当前枚举的这条边连接两个不同的集合,那么这条边就一定属于最小生成树,同时将这两个集合合并。若当前枚举的这条边连接相同的集合,则不选这条边。
根据树的基本性质,一个拥有n个节点的树有n-1条边,则使用kruskal找到n-1条边后终止就好了。
给出伪代码:
 .初始化 father[x] = [x],tot =
.对所有边进行边权排序,设边数为m
.for (int i=;i<=m;i++){
  if 当前的这条边连接的两个点不属于同一集合{
    合并两集合,并把边(u,v)加入最小生成树
    tot += w(u,v),k++
    if (k==n-)
      break;
  }
}
 
关于这个正确性的证明。。我不太会。。反正它肯定是对的就是了。。
 
 
tips:有的题目没有很明显的字眼要求你使用图论算法,但是看起来必须要用图论,那么我们就要考虑适当的建模,把原题转化为图论问题。
(这个想法在做某些数学题的时候也适用23333
 

夏令营讲课内容整理 Day 3.的更多相关文章

  1. 夏令营讲课内容整理 Day 7.

    Day7是夏令营的最后一天,这一天主要讲了骗分技巧和往年经典的一些NOIP试题以及比赛策略. 这天有个小插曲,上午的day7T3是一道和树有关的题,我是想破脑袋也想不出来,正解写不出来就写暴力吧,暴力 ...

  2. 夏令营讲课内容整理Day 0.

    今年没有发纸质讲义是最气的.还好我留了点课件. 第一次用这个估计也不怎么会用,但尝试一下新事物总是好的. 前四天gty哥哥讲的内容和去年差不多,后三天zhn大佬讲的内容有点难,努力去理解吧. 毕竟知识 ...

  3. 夏令营讲课内容整理 Day 6 Part 3.

    第三部分主要讲的是倍增思想及其应用. 在Day3的整理中,我简要提到了倍增思想,我们来回顾一下. 倍增是根据已经得到的信息,将考虑的范围扩大一倍,从而加速操作的一种思想,它在变化规则相同的情况下,加速 ...

  4. 夏令营讲课内容整理 Day 6 Part 2.

    Day 6的第二部分,数论 数论是纯粹数学的分支之一,主要研究整数的性质   1.一些符号: a mod b 代表a除以b得到的余数 a|b a是b的约数 floor(x) 代表x的下取整,即小于等于 ...

  5. 夏令营讲课内容整理 Day 6 Part 1.

    Day6讲了三个大部分的内容. 1.STL 2.初等数论 3.倍增   Part1主要与STL有关. 1.概述 STL的英文全名叫Standard Template Library,翻译成中文就叫标准 ...

  6. 夏令营讲课内容整理 Day 5.

    DP专场.. 动态规划是运筹学的一个分支, 求解决策过程最优化的数学方法. 我们一般把动态规划简称为DP(Dynamic Programming)   1.动态规划的背包问题 有一个容量为m的背包,有 ...

  7. 夏令营讲课内容整理 Day 4.

    本日主要内容就是搜索(打暴力 搜索可以说是OIer必会的算法,同时也是OI系列赛事常考的算法之一. 有很多的题目都可以通过暴力搜索拿到部分分,而在暴力搜索的基础上再加一些剪枝优化, 就有可能会拿到更多 ...

  8. 夏令营讲课内容整理 Day 2.

    本日主要内容是并查集和堆. 并查集 并查集是一种树型的数据结构,通常用来处理不同集合间的元素之间的合并与查找问题.一个并查集支持三个基本功能:合并.查找和判断.举一个通俗的例子,我和lhz认识,lhz ...

  9. 夏令营讲课内容整理Day 1.

    主要内容是栈和队列. 1.  栈 运算受到限制的线性表.只允许从一端进行插入和删除等操作.这一端便是栈顶,另一端便是栈底. 其实可以把栈想象层任何有底无盖的柱状的容器...毕竟栈满足后进先出的特性.计 ...

随机推荐

  1. 移动App,AJAX异步请求,实现简单的增、删、改、查

    用ajax发异步请求时,要注意url."AppServer"为后台项目名,"LoginServlet.action"为web.xml中的<url-patt ...

  2. 调用webService的几种方式

    转自:http://blog.csdn.net/u011165335/article/details/51345224 一.概览 方式1: HttpClient:可以用来调用webservie服务,也 ...

  3. mysql 恢复数据

    前提:保存了需要恢复数据库的文件 .frm 和 .ibd 文件 条件:InnoDB 类型的 恢复表结构1.新建一个数据库--新建一个表,表名和列数和需要恢复数据库相同2.停止mysql服务器 serv ...

  4. C#历年来最受欢迎功能

    不定时更新翻译系列,此系列更新毫无时间规律,文笔菜翻译菜求各位看官老爷们轻喷,如觉得我翻译有问题请挪步原博客地址 本博文翻译自: http://www.dotnetcurry.com/csharp/1 ...

  5. .29-浅析webpack源码之Resolver.prototype.resolve

    在上一节中,最后返回了一个resolver,本质上就是一个Resolver对象: resolver = new Resolver(fileSystem); 这个对象的构造函数非常简单,只是简单的继承了 ...

  6. 图tp delDataById问题

  7. phpmyadmin设置密码,不用登录直接进入

    版权声明:本文为博主原创文章,未经博主允许不得转载. 1.config.sample.inc.PHP改为config.inc.php 2.加入或更改代码: [php] view plain copy ...

  8. c#动态编译并执行字符串

    比较简单,步骤是这样的 string -> compiler -> assembly -> reflection -> execution 直接上代码: using Syste ...

  9. 解决前端页面a标签嵌套a标签bug

    在前端页面中,一般二级导航栏会出现a嵌套a标签出现重复的父元素a标签bug 比如: <nav class="nav"> <ul> <li> &l ...

  10. CSDN博客测试目录

    经常写博客,但是一般没怎么注意些目录,最近看别人写的博客都有目录,所以我也想在以后写好目录,这样子也方便阅读. 这里就写一个实验: 这里一级目录 这里是一级目录下的文本.林肯公园 这里是1.1目录 这 ...