本日主要内容是树与图。
 
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. c语言中标识符的作用域

    1.代码块作用域(block scope) 位于一对花括号之间的所有语句称为一个代码块,在代码块的开始位置声明的标识符具有代码块作用域,表示它们可以被这个代码中的所有语句访问.函数定义的形式参数在函数 ...

  2. FineReport调用存储过程

    "总结一下本人在项目中遇到的问题,如何在数据库表名未知且作为一种查询条件的情况下查询出数据集,仅能通过FineReport+Oracle实现. 首先分析这个问题的条件和要求: 条件:只有一个 ...

  3. java 跳出多层循环

    lableB: for(int i=0;i<10;i++){ lableA: for(int j=0;j<10;j++){ System.out.println(j); if(j==1){ ...

  4. Sqoop导入导出的几个例子

    Sqoop导入导出的几个例子 http://sqoop.apache.org/docs/1.4.6/SqoopUserGuide.html#_importing_data_into_hive   no ...

  5. redis常见使用场景下PHP实现

    基于redis字符串string类型的简单缓存实现 <?php //简单字符串缓存 $redis = new \Redis(); $redis->connect('127.0.0.1',6 ...

  6. J.U.C JMM. pipeline.指令重排序,happen-before(续MESI协议)

    缓存(Cache)       CPU的读/写(以及取指令)单元正常情况下甚至都不能直接访问内存——这是物理结构决定的:CPU都没有管脚直接连到内存.相反,CPU和一级缓存(L1 Cache)通讯,而 ...

  7. 直接请求转发(Forward)和间接请求转发(Redirect)两种区别?

    用户向服务器发送了一次HTTP请求,该请求肯能会经过多个信息资源处理以后才返回给用户,各个信息资源使用请求转发机制相互转发请求,但是用户是感觉不到请求转发的.根据转发方式的不同,可以区分为直接请求转发 ...

  8. 【fail2ban】使用fail2ban进行攻击防范

    使用fail2ban进行攻击防范 转自:https://kyle.ai/blog/6215.html 最近总有一些无聊的人,会来扫描一下我的服务器,看有没有啥漏洞可以利用的... 可以看到类似这样的4 ...

  9. 前后端分离之CORS和WebApi

    目前的项目是前端mv*+api的方式进行开发的,以前都是没有跨域的方案,前后端人员在同一个解决方案里边进行开发,前端人员要用IIS或VS来开发和调试Api,这样就很不方便,迫切需要跨域访问Api. 评 ...

  10. Linux指令--chgrp

    在lunix系统里,文件或目录的权限的掌控以拥有者及所诉群组来管理.可以使用chgrp指令取变更文件与目录所属群组,这种方式采用群组名称或群组识别码都可以.Chgrp命令就是change group的 ...