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

本日主要内容是树与图。

1.树

    • 树的性质
    • 树的遍历
    • 树的LCA
    • 树上前缀和

树的基本性质:

对于一棵有n个节点的树,必定有n-1条边。任意两个点之间的路径是唯一确定的。

回到题目上,如果题目读入的是树上所有的边,则我们应该想到:

    1. 每个点的父亲是谁
    2. 每个点的深度
    3. 每个点距离根节点的距离
    4. 其他的附加信息(例如:子树和,子树最大值。。)

遍历整个树的代码如下:

 1 void dfs(int now)
 2 {
 3     deep[now]=deep[fa[now]]+1;
 4     sum[now]=value[now]; maxx[now]=value[now];
 5     for 遍历从now出发的每一条边,边到达的点是v
 6     if (v != fa[now])
 7     {
 8         fa[v]=now;
 9         dfs(v);
10         sum[now]+=sum[v];  maxx[now]=max(maxx[now], maxx[v]);
11     }
12 }

实际上,树也有类似于图的邻接表存储结构。关于树的邻接表存法本来应该是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 = 0;
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!=0;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。。。

 1 #include<iostream>
 2 #include<cstring>
 3 #include<cstdio>
 4 #include<vector>
 5 #define maxn 2333
 6 using namespace std;
 7 int f[maxn][maxn];
 8 int father[maxn];
 9 int deep[maxn];
10 vector<int> tree;
11
12 void dfs(int x){
13     f[x][0] = father[x];
14     for (int i=1;i<=n;i++)
15         f[x][i] = f[f[x][i-1]][i-1];
16     for (int i=0;i<tree[x].size();i++){
17         if (tree[x][i]!=father[x]){
18             int y = tree[x][i];
19             father[y] = x;
20             deep[y] = deep[x]+1;
21             dfs(y);
22         }
23     }
24 }//从根节点开始dfs,预处理f数组
25
26 //查询LCA:
27 int lca(int x,int y){
28     if (deep[x]<deep[y])
29         swap(x,y);
30     for (int i=n;i>=0;i--)
31         if (deep[y] <= deep[f[x][i]])
32             x = f[x][i];
33     if (x==y)
34         return x;
35     for (int i=n;i>=0;i--)
36         if (f[x][i]!=f[y][i]){
37             x = f[x][i];
38             y = f[y][i];
39         }
40     return f[x][0];
41 }
42
43 int main(){
44     //do something
45     return 0;
46 }

树的前缀和

类似线性表的前缀和。

还记得线性表的前缀和吗?

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.邻接表:应用最广泛的存图方式,它的本质是若干串链表。

虽说是链表,但实际使用中一般使用数组模拟链表进行存储。以每个节点作为一串链表中的头结点,其后继点代表着与这个点相连的点。

优点:速度快,支持的数据范围较为广泛

缺点:如果对链表不是很理解的话理解代码时会存在一定困难

链表按难度应该是基础班的东西。我相信看我这些随笔的人应该都具有数据结构基础班或数据结构基础班以上的实力,我就不再写链表是怎么一回事了。。

实现方式:手写

 1 struct Edge{
 2     long long int from,to,dis;
 3 };
 4 Edge edge[maxn];
 5 long long int head[maxn];
 6 long long int cnt = 0;
 7 void add_edge(long long int from,long long int to,long long int dis){
 8     edge[++cnt].from = head[from];
 9     edge[cnt].to = to;
10     edge[cnt].dis = dis;
11     head[from] = cnt;
12 }

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

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

求最短路径

图上求最短路的算法大体分三种。

1.floyed算法,可求任意两点之间的最短路径,一般配合邻接矩阵食用。时间复杂度O(n3),在一些数据量较小的题中还是有些作用的。

2.Dijkstra算法。基于贪心思想的只能处理无负权图的单源最短路算法。未优化时的复杂度是O(n2),使用堆(一般使用优先队列)进行优化后时间复杂度降为O((n+m)log(n+m)),n是点数,m是边数。

给出堆优化之后的代码:

 1 #include<cstdio>
 2 #include<cstring>
 3 #include<iostream>
 4 #include<queue>
 5 #include<vector>
 6 #include<algorithm>
 7 #define ll long long
 8 #define INF 2147483647
 9 using namespace std;
10 int n,m,s,head[50010],cnt;
11 ll dis[10010];
12 bool used[10010];
13 struct Edge{
14     int to,from,dis;
15 }edge[500010];
16
17 void add_edge(int u,int v,int dis){
18     edge[cnt].to=v;
19     edge[cnt].from=head[u];
20     edge[cnt].dis=dis;
21     head[u]=cnt++;
22 }
23 typedef pair<int,int> P;
24 void dijkstra(int s){
25     priority_queue<P,vector<P>,greater<P> > q;
26     fill(dis,dis+n+1,INF);
27     fill(used,used+n+1,false);
28     dis[s]=0;
29     q.push(P(0,s));
30     while(!q.empty()){
31         P p=q.top();q.pop();
32         int u=p.second;
33         if(used[u]) continue;
34         used[u]=true;
35         int pp=head[u];
36         while(pp!=-1){
37             int v=edge[pp].to;
38             if(!used[v]&&dis[v]>dis[u]+edge[pp].dis){
39                 dis[v]=dis[u]+edge[pp].dis;
40                 q.push(P(dis[v],v));
41             }
42             pp=edge[pp].from;
43         }
44     }
45 }
46 int main(){
47     memset(head,-1,sizeof(head));
48     cin>>n>>m>>s;
49     for(int i=1;i<=m;i++){
50         int u,v,d;
51         scanf("%d%d%d",&u,&v,&d);
52         add_edge(u,v,d);
53     }
54     dijkstra(s);
55     for(int i=1;i<=n;i++) printf("%lld ",dis[i]);
56     return 0;
57 }

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

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

3.SPFA算法,即经过队列优化的bellman-ford算法,也是我个人最喜欢使用的求最短路的算法,它支持存在负边的图,并且可以在判定出负环后及时退出。时间复杂度是O(kE) O(RP),其中E代表边数,k是一个玄学常数,均值为2。

(k的大小与当前写代码的人有没有穿女装有很大关系

但SPFA也是存在缺点的。当你试图在一个稠密图(点少边巨多)的图上跑SPFA时,它将会变得非常慢,这与它的扩展方式有密切关系。

 1 #include<iostream>
 2 #include<cstdio>
 3 #include<cstring>
 4 #include<queue>
 5 #define maxn 5000015
 6 #define INF 2147483647
 7 #define ms(x) memset(x,0,sizeof(x));
 8 using namespace std;
 9 struct Edge{
10     long long int from,to,dis;
11 };
12 Edge edge[maxn];
13 long long int n,m,s,u,v,d;
14 long long int head[maxn];
15 long long int dis[maxn];
16 bool inq[maxn];
17 long long int cnt = 0;
18 void add_edge(long long int from,long long int to,long long int dis){
19     edge[++cnt].from = head[from];
20     edge[cnt].to = to;
21     edge[cnt].dis = dis;
22     head[from] = cnt;
23 }
24
25 void spfa(void){
26     queue<long long int> q;
27     q.push(s);
28     inq[s] = true;
29     ms(inq);
30     for (int i=1;i<=n;i++)
31         dis[i] = INF;
32     dis[s] = 0;
33     while (!q.empty()){
34         long long int u = q.front();
35         q.pop();
36         inq[s] = false;
37         for (int i=head[u];i!=0;i=edge[i].from){
38             long long int v = edge[i].to;
39             long long int w = edge[i].dis;
40             if (dis[u]+w < dis[v]){
41                 dis[v] = w+ dis[u];
42                 if (!inq[v]){
43                     q.push(v);
44                     inq[v] = true;
45                 }
46             }
47         }
48     }
49
50 }
51
52 int main(){
53     cin >> n >> m >> s;
54     for (int i=1;i<=m;i++){
55         cin >> u >> v >> d;
56         add_edge(u,v,d);
57     }
58     spfa();
59     for (int i=1;i<=n;i++)
60         cout << dis[i] << " ";
61     return 0;
62 }

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

特殊的方式:

如果一个图的边权都是1,那么我们有什么简便方法求最短路呢?

BFS与SPFA算法的扩展非常类似,并且由于BFS算法的性质,找到的第一个解必定最优,也就是最短。

最小生成树

昨天卖的关子现在该揭开了。

最小生成树的官方定义比较麻烦,用通俗一点的话来说就是在这个图中找到“一棵连接了所有节点的树”,并且树上的所有边权值是最小的。

最小生成树有两种算法。第一种是基于贪心思想的prim,我平常不怎么用。。

第二种就是我现在要说的这个kruskal。

首先,我们把所有边按照边权从小到大排序,并认为每个点在初始状态时都是孤立的。然后我们桉顺序枚举每条边,若当前枚举的这条边连接两个不同的集合,那么这条边就一定属于最小生成树,同时将这两个集合合并。若当前枚举的这条边连接相同的集合,则不选这条边。

根据树的基本性质,一个拥有n个节点的树有n-1条边,则使用kruskal找到n-1条边后终止就好了。

给出伪代码:

 1 1.初始化 father[x] = [x],tot = 0
 2 2.对所有边进行边权排序,设边数为m
 3 3.for (int i=1;i<=m;i++){
 4   if 当前的这条边连接的两个点不属于同一集合{
 5     合并两集合,并把边(u,v)加入最小生成树
 6     tot += w(u,v),k++
 7     if (k==n-1)
 8       break;
 9   }
10 }

关于这个正确性的证明。。我不太会。。反正它肯定是对的就是了。。

tips:有的题目没有很明显的字眼要求你使用图论算法,但是看起来必须要用图论,那么我们就要考虑适当的建模,把原题转化为图论问题。

(这个想法在做某些数学题的时候也适用23333

例题:

T1:华容道

待续

T2:货车运输

待续

T3:关押罪犯

待续

T4:最优交♂ 易

待续

时间: 2024-10-12 16:45:08

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

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

今年没有发纸质讲义是最气的.还好我留了点课件. 第一次用这个估计也不怎么会用,但尝试一下新事物总是好的. 前四天gty哥哥讲的内容和去年差不多,后三天zhn大佬讲的内容有点难,努力去理解吧. 毕竟知识还是需要消化的. 这里我只整理知识点,每天上午评测的题目我会单独处理. 嗯大概就是这样了. 写完后我就会考虑发到博客园里.

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

DP专场.. 动态规划是运筹学的一个分支, 求解决策过程最优化的数学方法. 我们一般把动态规划简称为DP(Dynamic Programming) 1.动态规划的背包问题 有一个容量为m的背包,有n个物品,每一个物品i的重量为w[i],价值为v[i]. 要求选择一些物品放入背包中,每种物品只能最多使用一次,使得在不超重的情况下让背包中所有物品价值总和最大. 正常向解法:设状态数组f[i][j]为把前i个物品放入一个容量为j的背包中所能获得的最大价值(以下同设),则状态转移方程为: f[i][j]

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

本日主要内容就是搜索(打暴力 搜索可以说是OIer必会的算法,同时也是OI系列赛事常考的算法之一. 有很多的题目都可以通过暴力搜索拿到部分分,而在暴力搜索的基础上再加一些剪枝优化, 就有可能会拿到更多的分数. 有句话说的好嘛,骗分过样例,暴力出奇迹. 真的可以出奇迹的,只要你用得好. 1.搜索的概念 在一个给定的空间内,运用一定的查找(遍历)方式,直到找到目标解(状态)的过程,我们称之为搜索. 搜素是尝试性的,搜索是无脑的,搜索是朴素的,搜索在很多时候是显然的,搜索应该总是暴力的.但搜索也是很常

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

主要内容是栈和队列. 1.  栈 运算受到限制的线性表.只允许从一端进行插入和删除等操作.这一端便是栈顶,另一端便是栈底. 其实可以把栈想象层任何有底无盖的柱状的容器...毕竟栈满足后进先出的特性.计算机当中调用函数时,中间结果便会保存到「系统栈」中.递归过程也需要栈的协助 . 实现:STL or 手写(请参照一本通 or 课件) 一般操作:判断栈空/满.入栈.出栈,判断栈的大小(请参照一本通 or 课件) 1.1 单调栈 顾名思义,保证内部元素单调(单增或单减)的栈.我们只要在插入新元素的时候

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

Day6讲了三个大部分的内容. 1.STL 2.初等数论 3.倍增 Part1主要与STL有关. 1.概述 STL的英文全名叫Standard Template Library,翻译成中文就叫标准模板库. 它有点类似于一个大型的工具箱,里面包含许多实用工具,可以拿过来直接用而大部分情况下无需去深入探究其内部原理. 不知道从什么时候开始,CCF不再限制选手使用STL,所以在OI赛事中STL被广泛应用. 它分为六个大部分: 1)容器 containers 2)迭代器 iterators 3)空间配置

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

Day 6的第二部分,数论 数论是纯粹数学的分支之一,主要研究整数的性质 1.一些符号: a mod b 代表a除以b得到的余数 a|b a是b的约数 floor(x) 代表x的下取整,即小于等于x的最大整数,也可以认为是直接舍去小数部分 (这个应该是一个符号,但我不知道怎么打出来..下面那个ceil也是) ceil(x) 代表x的上取整,即大于等于x的最小整数,也可以认为是直接舍去小数部分再+1. gcd(a,b) 表示a与b的最大公约数 lcm(a,b) 表示a与b的最小公倍数 累加符号∑

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

本日主要内容是并查集和堆. 并查集 并查集是一种树型的数据结构,通常用来处理不同集合间的元素之间的合并与查找问题.一个并查集支持三个基本功能:合并.查找和判断.举一个通俗的例子,我和lhz认识,lhz和hzc认识,那么也就可以断定我和hzc认识. 依照并查集的思想,我们把所有要待处理的元素a1,a2,a3....an这n个元素都看作是一个单独的集合,初始状态每个集合都只有一个元素.我们就可以把并查集的合并操作理解为集合之间的取并集操作. 作为一个树形结构,在一个由许多这样的集合构成的森林中,每个

Google C++ 风格指南内容整理

之前一直没有全面的看过Google C++风格指南,现在很多公司进行C++开发都要求按照Google C++风格.在这个网站 http://zh-google-styleguide.readthedocs.org/en/latest/contents/  有人已经把其翻译成中文.为了便于以后查看,下面的内容完全是来自于这个网站,只是把多个网页的内容整理放在了一起. 1.      头文件: 通常每一个.cc文件都有一个对应的.h文件.也有一些常见例外,如单元测试代码和只包含main()函数的.c

网页格式化排版代码,专用信息采集后的内容整理

public static string ClearHtml(string content) { Regex regex = new Regex(""); //首先把p标签的属性去掉,只留<p> regex = new Regex(@"<p.*?>", RegexOptions.IgnoreCase | RegexOptions.Singleline); content = regex.Replace(content, "<p