在这个炎热而忙碌的夏季,我们进行了一场二十多天的ACM集训。其实从大一上学期接触ACM来,我都对这个比赛充满了兴趣,对于AC一道题充满了喜悦。通过一个学期的学习,我掌握了一些基本的ACM算法知识,并能够解决一些简单的题。当听说暑假可以参加集训,我就马上报名参加了这次集训,也收获了很多东西。
在集训刚开始的时候,我们还不适应,老师就让我们复习一下搜索和图论的知识,并且做了一些搜索和图论的题。练习一下编程能力和熟悉一下做题的节奏。
通过复习的练习,大约巩固了一下关于搜索的知识点。
1.深度搜索与广度搜索
深度搜索与广度搜索的控制结构和产生系统很相似,唯一的区别在于对扩展节点选取上。由于其保留了所有的前继节点,所以在产生后继节点时可以去掉一部分重复 的节点,从而提高了搜索效率。这两种算法每次都扩展一个节点的所有子节点,而不同的是,深度搜索下一次扩展的是本次扩展出来的子节点中的一个,而广度搜索 扩展的则是本次扩展的节点的兄弟节点。在具体实现上为了提高效率,所以采用了不同的数据结构.
2.回溯算法
回溯算法是所有搜索算法中最为基本的一种算法,其采用了一种“走不通就掉头”思想作为其控制结构,其相当于采用了先根遍历的方法来构造解答树,可用于找解或所有解以及最优解。评价:回溯算法对空间的消耗较少,当其与分枝定界法一起使用时,对于所求解在解答树中层较深的问题 有较好的效果。但应避免在后继节点可能与前继节点相同的问题中使用,以免产生循环。
当然还有其他知识点就不一一介绍了。
学习图论的知识,巩固了下面的知识点。
一、求出最短路径的长度
以下没有特别说明的话,dis[u][v]表示从u到v最短路径长度,w[u][v]表示连接u,v的边的长度。
1.Floyed-Warshall算法 O(N3)
简称Floyed(弗洛伊德)算法,是最简单的最短路径算法,可以计算图中任意两点间的最短路径。Floyed的时间复杂度是O (N3),适用于出现负边权的情况。
算法描述:
初始化:点u、v如果有边相连,则dis[u][v]=w[u][v]。
如果不相连则dis[u][v]=0x7fffffff
For (k = 1; k <= n; k++)
For (i = 1; i <= n; i++)
For (j = 1; j <= n; j++)
If (dis[i][j] >dis[i][k] + dis[k][j])
dis[i][j] = dis[i][k] + dis[k][j];
算法结束:dis[i][j]得出的就是从i到j的最短路径。
2.Dijkstra算法O (N2)
用来计算从一个点到其他所有点的最短路径的算法,是一种单源最短路径算法。也就是说,只能计算起点只有一个的情况。
Dijkstra的时间复杂度是O (N2),它不能处理存在负边权的情况。
算法描述:
设起点为s,dis[v]表示从s到v的最短路径,pre[v]为v的前驱节点,用来输出路径。
a)初始化:dis[v]=∞(v≠s); dis[s]=0; pre[s]=0;
b)For (i = 1; i <= n ; i++)
1.在没有被访问过的点中找一个顶点u使得dis[u]是最小的。
2.u标记为已确定最短路径
3.For 与u相连的每个未确定最短路径的顶点v
if (dis[u]+w[u][v] < dis[v])
{
dis[v] = dis[u] + w[u][v];
pre[v] = u;
}
c)算法结束:dis[v]为s到v的最短距离;pre[v]为v的前驱节点,用来输出路径。
3.Bellman-Ford算法O(NE)
Bellman-Ford算法的思想很简单。一开始认为起点是白点(dis[1]=0),每一次都枚举所有的边,必然会有一些边,连接着白点和蓝点。因此每次都能用所有的白点去修改所有的蓝点,每次循环也必然会有至少一个蓝点变成白点。
虽然Bellman-Ford算法可以求出存在负边权情况下的最短路径,却无法解决存在负权回路的情况。
算法实现:
设s为起点,dis[v]即为s到v的最短距离,pre[v]为v前驱。w[j]是边j的长度,且j连接u、v。
初始化:dis[s]=0,dis[v]=∞(v≠s),pre[s]=0
For (i = 1; i <= n-1; i++)
For (j = 1; j <= E; j++)//注意要枚举所有边,不能枚举点。
if (dis[u]+w[j]<dis[v]) //u、v分别是这条边连接的两个点。
{
dis[v] =dis[u] + w[j];
pre[v] = u;
}
二. 最小生成树
1.Prim算法采用与Dijkstra、Bellman-Ford算法一样的“蓝白点”思想:白点代表已经进入最小生成树的点,蓝点代表未进入最小生成树的点。
算法描述:
以1为起点生成最小生成树,min[v]表示蓝点v与白点相连的最小边权。
MST表示最小生成树的权值之和。
a)初始化:min[v]= ∞(v≠1); min[1]=0;MST=0;
b)for (i = 1; i<= n; i++)
1.寻找min[u]最小的蓝点u。
2.将u标记为白点
3.MST+=min[u]
4.for 与白点u相连的所有蓝点v
if (w[u][v]<min[v])
min[v]=w[u][v];
c)算法结束: MST即为最小生成树的权值之和。
2.Kruskal算法
算法描述:
初始化并查集。father[x]=x。
tot=0
将所有边用快排从小到大排序。
计数器 k=0;
for (i=1; i<=M; i++) //循环所有已从小到大排序的边
if 这是一条u,v不属于同一集合的边(u,v)(因为已经排序,所以必为最小),
begin
①合并u,v所在的集合,相当于把边(u,v)加入最小生成树。
②tot=tot+W(u,v)
③k++
④如果k=n-1,说明最小生成树已经生成,则break;
end;
6. 结束,tot即为最小生成树的总权值之和。
在集训期间还学习了一些新知识。
一.单调队列。
单调队列:队列中元素之间的关系具有单调性,而且,队首和队尾都可以进行出队操作,只有队尾可以进行入队操作。。
单调队列的常用操作如下:
(1)插入:若新元素从队尾插入后会破坏单调性,则删除队尾元素,直到插入后不再破坏单调性为止,再将其插入单调队列。
(2)获取最优(最大、最小)值:访问首尾元素。
有一个区间求极值问题。
先枚举起始元素ax,然后求ax到ax+k-1的最大(小)值。我们得到了一个复杂度为o(nk)的算法。
怎么用单调队列求解那?
以最大值为例
对任意l<=i<j<=r,如果ai<aj,那么,在区间向右移动的过程中,最大值永远也不会落在ai上,因为ai比aj先失效,能用ai一定能用aj ,此时,我们便不再需要ai了。
这个性质似乎与单调队列的性质重合了。
当我们将区间从(l,r)移动到(l+1,r+1)时,我们将ar+1插入单调队列,若队首元素不在(l,r)区间当中,删除它。
二.树状数组
首先要分清a[] c[] sum[] 他们各自所代表的意思;
a[]就是输入的数组;
c[]就是建立的树状数组;
c[i] = a[i - 2^k +1] + ...... + a[i];
a有多少个c就有多少个,而且c[i]肯定包含相应的a[i];
lowbit(i) = 2^k 表示i的二进制数表示形式留下左右边的1其余为取0得到的数
sum[k] = c[N1] + c[N2] + c[N3].......+ c[Nm];
Ni-1 = Ni - lowbit(i);
求和的话,就是有c[Nm] c[Nm-1] c[Nm-2] .... c[N1]的过程 Ni - lowbit(i)的过程是将Ni的二进制的最右边的1去掉的过程,所以求和的时间复杂度为O(log(k));
而更新也是如果a[i]更新了 那么c[N1] c[N2] ..... C[Nm]也要更新N1= i; 就是从 c[N1] c[N2] ... c[Nm]的过程 Ni = Ni-1 + lowbit(i);就是在Ni - 1的二进制最右边1后边填1的过程,就是在时间复杂度也是O(log(n))级别。
树状数组的基本操作。
1.区间更新,单点求和。
当[a,b]区间加1时。
可以 add(a,1);从这之后的sum()都会加1。
add(b+1,-1); 从这之后的sum()都会减1。也就是从这之后原来的加一被抵消了。
这样原来的求和数组已经不是原来的意义了,这已经不是为了求和了。
这时候在区间[a,b]内求一个值sum(i)会比原先加1。
2.单点更新,区间区和。
当需要a[i]增加d时,
可以 add(a[i],d);
这样 当 x > i 时,sum(x)求和都增大了d。
求和时,求区间[ j , k ] 的和,sum(k)- sum(j-1);
3.离散化
离散化一般都要定义结构体,结构体里一般包含两个数,一个数是输入的原值 v ,一个数是记录输入的先后次序的 id。
1。在输入数据时,要便输入边对结构体的 id 进行赋值,从1到n。(输入的先后顺序)
2。根据结构体中数据的大小进行排序。(排序)
3。新建立一个数组b,结构体的 id 作为数组下表,然后,根据排序的先后顺序,给数组b赋从1到n的值代表他们原来的大小关系。(大小的代换)
离散化完毕,现在这些不集中的数据,已经变得集中了。在处理数据大,数据量不大的数据时可以采用离散化。
在我们学习中遇到的这些算法的类型还有很多,这里就不一一写下来了,在这里出现的都是一些算法的基础内容和重要知识点。这些我们一定要掌握牢固,毕竟只有基础打得牢,楼房才能建的好。
以上就是我们暑假集训的学习成果。
在集训刚开始的时候,我们还不适应,老师就让我们复习一下搜索和图论的知识,并且做了一些搜索和图论的题。练习一下编程能力和熟悉一下做题的节奏。
通过复习的练习,大约巩固了一下关于搜索的知识点。
1.深度搜索与广度搜索
深度搜索与广度搜索的控制结构和产生系统很相似,唯一的区别在于对扩展节点选取上。由于其保留了所有的前继节点,所以在产生后继节点时可以去掉一部分重复 的节点,从而提高了搜索效率。这两种算法每次都扩展一个节点的所有子节点,而不同的是,深度搜索下一次扩展的是本次扩展出来的子节点中的一个,而广度搜索 扩展的则是本次扩展的节点的兄弟节点。在具体实现上为了提高效率,所以采用了不同的数据结构.
2.回溯算法
回溯算法是所有搜索算法中最为基本的一种算法,其采用了一种“走不通就掉头”思想作为其控制结构,其相当于采用了先根遍历的方法来构造解答树,可用于找解或所有解以及最优解。评价:回溯算法对空间的消耗较少,当其与分枝定界法一起使用时,对于所求解在解答树中层较深的问题 有较好的效果。但应避免在后继节点可能与前继节点相同的问题中使用,以免产生循环。
当然还有其他知识点就不一一介绍了。
学习图论的知识,巩固了下面的知识点。
一、求出最短路径的长度
以下没有特别说明的话,dis[u][v]表示从u到v最短路径长度,w[u][v]表示连接u,v的边的长度。
1.Floyed-Warshall算法 O(N3)
简称Floyed(弗洛伊德)算法,是最简单的最短路径算法,可以计算图中任意两点间的最短路径。Floyed的时间复杂度是O (N3),适用于出现负边权的情况。
算法描述:
初始化:点u、v如果有边相连,则dis[u][v]=w[u][v]。
如果不相连则dis[u][v]=0x7fffffff
For (k = 1; k <= n; k++)
For (i = 1; i <= n; i++)
For (j = 1; j <= n; j++)
If (dis[i][j] >dis[i][k] + dis[k][j])
dis[i][j] = dis[i][k] + dis[k][j];
算法结束:dis[i][j]得出的就是从i到j的最短路径。
2.Dijkstra算法O (N2)
用来计算从一个点到其他所有点的最短路径的算法,是一种单源最短路径算法。也就是说,只能计算起点只有一个的情况。
Dijkstra的时间复杂度是O (N2),它不能处理存在负边权的情况。
算法描述:
设起点为s,dis[v]表示从s到v的最短路径,pre[v]为v的前驱节点,用来输出路径。
a)初始化:dis[v]=∞(v≠s); dis[s]=0; pre[s]=0;
b)For (i = 1; i <= n ; i++)
1.在没有被访问过的点中找一个顶点u使得dis[u]是最小的。
2.u标记为已确定最短路径
3.For 与u相连的每个未确定最短路径的顶点v
if (dis[u]+w[u][v] < dis[v])
{
dis[v] = dis[u] + w[u][v];
pre[v] = u;
}
c)算法结束:dis[v]为s到v的最短距离;pre[v]为v的前驱节点,用来输出路径。
3.Bellman-Ford算法O(NE)
Bellman-Ford算法的思想很简单。一开始认为起点是白点(dis[1]=0),每一次都枚举所有的边,必然会有一些边,连接着白点和蓝点。因此每次都能用所有的白点去修改所有的蓝点,每次循环也必然会有至少一个蓝点变成白点。
虽然Bellman-Ford算法可以求出存在负边权情况下的最短路径,却无法解决存在负权回路的情况。
算法实现:
设s为起点,dis[v]即为s到v的最短距离,pre[v]为v前驱。w[j]是边j的长度,且j连接u、v。
初始化:dis[s]=0,dis[v]=∞(v≠s),pre[s]=0
For (i = 1; i <= n-1; i++)
For (j = 1; j <= E; j++)//注意要枚举所有边,不能枚举点。
if (dis[u]+w[j]<dis[v]) //u、v分别是这条边连接的两个点。
{
dis[v] =dis[u] + w[j];
pre[v] = u;
}
二. 最小生成树
1.Prim算法采用与Dijkstra、Bellman-Ford算法一样的“蓝白点”思想:白点代表已经进入最小生成树的点,蓝点代表未进入最小生成树的点。
算法描述:
以1为起点生成最小生成树,min[v]表示蓝点v与白点相连的最小边权。
MST表示最小生成树的权值之和。
a)初始化:min[v]= ∞(v≠1); min[1]=0;MST=0;
b)for (i = 1; i<= n; i++)
1.寻找min[u]最小的蓝点u。
2.将u标记为白点
3.MST+=min[u]
4.for 与白点u相连的所有蓝点v
if (w[u][v]<min[v])
min[v]=w[u][v];
c)算法结束: MST即为最小生成树的权值之和。
2.Kruskal算法
算法描述:
初始化并查集。father[x]=x。
tot=0
将所有边用快排从小到大排序。
计数器 k=0;
for (i=1; i<=M; i++) //循环所有已从小到大排序的边
if 这是一条u,v不属于同一集合的边(u,v)(因为已经排序,所以必为最小),
begin
①合并u,v所在的集合,相当于把边(u,v)加入最小生成树。
②tot=tot+W(u,v)
③k++
④如果k=n-1,说明最小生成树已经生成,则break;
end;
6. 结束,tot即为最小生成树的总权值之和。
在集训期间还学习了一些新知识。
一.单调队列。
单调队列:队列中元素之间的关系具有单调性,而且,队首和队尾都可以进行出队操作,只有队尾可以进行入队操作。。
单调队列的常用操作如下:
(1)插入:若新元素从队尾插入后会破坏单调性,则删除队尾元素,直到插入后不再破坏单调性为止,再将其插入单调队列。
(2)获取最优(最大、最小)值:访问首尾元素。
有一个区间求极值问题。
先枚举起始元素ax,然后求ax到ax+k-1的最大(小)值。我们得到了一个复杂度为o(nk)的算法。
怎么用单调队列求解那?
以最大值为例
对任意l<=i<j<=r,如果ai<aj,那么,在区间向右移动的过程中,最大值永远也不会落在ai上,因为ai比aj先失效,能用ai一定能用aj ,此时,我们便不再需要ai了。
这个性质似乎与单调队列的性质重合了。
当我们将区间从(l,r)移动到(l+1,r+1)时,我们将ar+1插入单调队列,若队首元素不在(l,r)区间当中,删除它。
二.树状数组
首先要分清a[] c[] sum[] 他们各自所代表的意思;
a[]就是输入的数组;
c[]就是建立的树状数组;
c[i] = a[i - 2^k +1] + ...... + a[i];
a有多少个c就有多少个,而且c[i]肯定包含相应的a[i];
lowbit(i) = 2^k 表示i的二进制数表示形式留下左右边的1其余为取0得到的数
sum[k] = c[N1] + c[N2] + c[N3].......+ c[Nm];
Ni-1 = Ni - lowbit(i);
求和的话,就是有c[Nm] c[Nm-1] c[Nm-2] .... c[N1]的过程 Ni - lowbit(i)的过程是将Ni的二进制的最右边的1去掉的过程,所以求和的时间复杂度为O(log(k));
而更新也是如果a[i]更新了 那么c[N1] c[N2] ..... C[Nm]也要更新N1= i; 就是从 c[N1] c[N2] ... c[Nm]的过程 Ni = Ni-1 + lowbit(i);就是在Ni - 1的二进制最右边1后边填1的过程,就是在时间复杂度也是O(log(n))级别。
树状数组的基本操作。
1.区间更新,单点求和。
当[a,b]区间加1时。
可以 add(a,1);从这之后的sum()都会加1。
add(b+1,-1); 从这之后的sum()都会减1。也就是从这之后原来的加一被抵消了。
这样原来的求和数组已经不是原来的意义了,这已经不是为了求和了。
这时候在区间[a,b]内求一个值sum(i)会比原先加1。
2.单点更新,区间区和。
当需要a[i]增加d时,
可以 add(a[i],d);
这样 当 x > i 时,sum(x)求和都增大了d。
求和时,求区间[ j , k ] 的和,sum(k)- sum(j-1);
3.离散化
离散化一般都要定义结构体,结构体里一般包含两个数,一个数是输入的原值 v ,一个数是记录输入的先后次序的 id。
1。在输入数据时,要便输入边对结构体的 id 进行赋值,从1到n。(输入的先后顺序)
2。根据结构体中数据的大小进行排序。(排序)
3。新建立一个数组b,结构体的 id 作为数组下表,然后,根据排序的先后顺序,给数组b赋从1到n的值代表他们原来的大小关系。(大小的代换)
离散化完毕,现在这些不集中的数据,已经变得集中了。在处理数据大,数据量不大的数据时可以采用离散化。
在我们学习中遇到的这些算法的类型还有很多,这里就不一一写下来了,在这里出现的都是一些算法的基础内容和重要知识点。这些我们一定要掌握牢固,毕竟只有基础打得牢,楼房才能建的好。
以上就是我们暑假集训的学习成果。