本日主要内容是树与图。
 
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. Wolf and Rabbit

    http://acm.hdu.edu.cn/showproblem.php?pid=1222 Wolf and Rabbit Time Limit: 2000/1000 MS (Java/Others ...

  2. vuex的使用及持久化state的方式

    Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式.它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化. 当我们接触vuex的时候,这是我们最先看到 ...

  3. MFC中菜单的命令响应顺序

    响应只可以由Doc,View,MainFrame以及APP四个类完成. 响应顺序是: 点击某菜单项,框架类最先接到菜单命令消息. 框架类把接收到得这个消息交给它的子窗口,即视图类. 视图类根据命令消息 ...

  4. ASP.NET Core下发布网站

    一.windows下发布到IIS 1.前奏:IIS上的准备 (1)IIS 必须安装AspNetCoreModule 模块 下载地址:(DotNetCore.2.0.3-WindowsHosting-a ...

  5. mysql 性能优化常见命令

    mysql 性能优化常见命令: 一: 当发现mysql程序运行缓慢时,在排除sql主机问题之后,可以尝试在schema,table,和sql上进一步进行考查: 1:mysql> show ful ...

  6. shopnc验证码显示不了

    data/config文件编码问题,要utf-8无bom

  7. 20170510 mysql导入导出csv

    一开始没加FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"' LINES TERMINATED BY '\n' 导致导出的csv没有进行分 ...

  8. video.js不能控制本地视频或者音频播放时长

    问题: 把视频放到本地,然后对视频进行测试,想要控制视频或者音频的播放时长,没办法做到,每次拉动进度条,都会使得本地视频重新播放 原因: 所有浏览器默认js无法访问本地地址,也就是说js不能对本地文件 ...

  9. strstr()与find()

  10. Android ui 透明度设置

    格式如#00FFFFFF,前两位代表不透明度的十六进制.00表示完全透明,FF就是全不透明.依次递增. <?xml version="1.0" encoding=" ...