知识点小结

标签: c++  总结

1.贪心

  贪心算法应该算是最基础的算法之一了。贪心算法的策略就是在每一次总是做出当前最优的选择,即局部最优解,来求出全局最优解,因此贪心的得到的答案不一定就是全局最优解
  想要贪心的正确性往往比较难,所以最好的办法就是写一个暴力程序去对拍,如果能够通过绝大部分的数据,那么就可以比较放心的使用这一策略
  有些时候,贪心并不能够直接求出最优解,但是我们可以通过复杂度较小的贪心算法来压缩答案所在的范围,以降低程序的总的复杂度

2.枚举

  枚举类的问题一般都规模比较小,有些数据规模很大的题也能够找到一些性质来减小复杂度,例如存在循环节,或者存在某个公式等等
  枚举要注意不重不漏的找到所有符合条件的元素去进行计算,如果有重复,那就会降低程序的效率;如果有遗漏就会导致得到的答案不一定正确

3.模拟

  模拟类的题也有和枚举题类似的性质:数据规模较小或者存在一些性质来减小复杂度
  模拟题一般题目比较长,需要解释清楚模拟的原则,过程等等,而且一般也不需要用到很高深的知识点或者数据结构,但是模拟题的代码也可能会很长,期间有一些细节难以处理,出现错误也难以排查
  做模拟题的时候,一定要细心读题,不能够理解错了模拟的原则,也不能够想当然地按自己的想法去做,实在有题目没讲清楚的地方可以举手问老师
  对于一些数据规模庞大的模拟题,可以试着去发掘一些特征和性质,也要认真思考,不要觉得模拟题就只能死搞

4.排序

4.1基础排序算法

  基础的排序算法主要有冒泡排序,希尔排序,插入排序等等
  冒泡排序其实就是暴力比较,通过不断地比较,交换相邻的两个元素,是整个序列变得有序

4.2桶排序&基数排序

  桶排序是最快最简单的排序算法,代码实现也非常简单,不过相对来说空间要求也比较大,尤其是待排序数组的值域比较大的时候
  基数排序是效率比较高,同时也很稳定的排序算法,基数排序首先将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列

4.3归并排序

  归并排序其实是基于分治的思想:将目前需要排序的区间不断地二分下去,直到区间长度为1,然后从下往上操作
  对于当前区间,假设右区间的元素组成数组a,左区间的元素组成数组b,全区间的数组为q,对左右区间分别进行完操作之后(即递归下去又回来之后),a,b都变为了有序的数组
  设三个指针i,j,k分别指向a,b,q数组的头部,如果有a【i】>b【j】,那么将a【i】赋给q【k】,i,k后移;否则将b【j】赋给q【k】,然后j,k后移。直到i或j到达对应数组的末尾,然后将另一个数组中的剩余元素按次序压入q数组
  虽然c++语言中有sort函数可以直接使用,但是归并排序还可以用来解决其他问题,最普通的例子就是求序列中逆序对的个数

#include<iostream>
#include<cstdlib>
#include<cstdio>
using namespace std;
const int N=100010;
int w[N],q[N];
int n;
void sort(int l,int r)
{
    if( l==r )   return ;
    int mid=(l+r)/2;
    sort( l,mid ),sort( mid+1,r );
    int i=l,j=mid+1,k=0;
    for(;i<=mid && j<=r ;)
        if( q[i]<q[j] )   w[++k]=q[i++];
        else   w[++k]=q[j++];
    while( i<=mid )   w[++k]=q[i++];
    while( j<=r )   w[++k]=q[j++];
    for(int i=l;i<=r;i++)   q[i]=w[i—l+1];
    return ;
}
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)   scanf("%d",&q[i]);
    sort( 1,n );
    for(int i=1;i<=n;i++)   printf("%d ",q[i]);
printf("\n");
return 0;
}

4.4快速排序

  快排其实就是冒泡排序的改进,C++中直接调用sort函数即可

5.二分&三分

  二分一般是用来解决最大值最小,最小值最大之类的问题,大部分的二分问题都是整数型二分,有一小部分是小数型二分。一个问题能够二分首先就要满足它的合法解序列一定是单调的
  二分的重点一是确定二分的上下界(小数型二分还要确定二分的精度),二是check函数
  三分是用于解决凸性函数的极值问题的算法,平常没有做过这一类的问题,不过还是要有掌握

6.搜索

  搜索是一种通过枚举所有的可能的解的状态来求得题目所求的解或者是最优解的算法。搜索能够解决很多的问题,但是效率很低下,因此需要对搜索过程进行剪枝优化,或者采用不同的搜索方式或搜索顺序。
  搜索除了时间以外还要注意空间的问题,不管是采用递归方式的DFS还是采用队列方式的BFS都对空间要求比较大,因此在搜索时需要合理地设置搜索的状态,也要注意不要重复搜索一些已经搜索过的状态

6.1 DFS和BFS

  DFS,即深度优先搜索,一般采用递归的方式求得答案,所以需要较大的栈空间,同时也需要设置合理的递归结束条件,否则就会陷入无限的自我调用之中。DFS的搜索顺序类似于树的先序遍历,因此适用于解决求解的总数等问题或者是对树进行搜索遍历
  BFS,即广度优先搜索,一般采用队列的方式进行拓展。BFS常常和图论相结合,像求解最短路的spfa算法其实就是一种变形的BFS。BFS的搜索顺序是按照状态的深度来排序的,类似于按树的层次遍历。对于一些只要求得最优解,而且解的深度不深的问题来说,BFS往往是解决问题的最好方法

6.2迭代加深搜索

  迭代加深搜索是DFS的一种变形,通过限制每一次搜索的深度来进行剪枝。对于一些问题,我们无法估计解的深度,所以可以通过逐步加深来接近解的深度。
  虽然看上去前面的层会被搜索多次,提高算法的复杂度,但是实际上迭代加深通过深度的限制可以去除很大部分的没必要的深层次状态以达到优化算法的目的

6.3双向广搜

  双向广搜是BFS的一种变形。对于一些已知初始状态和目标状态的问题,我们可以同时从初始状态以及目标状态出发进行拓展,当两者的拓展相遇时的解就是最优解

6.4 A*&IDA*

  搜索算法在进行时会遍历到很多无用的状态,提高了算法的复杂度,而A*以及IDA*算法就相当于一个指路标,告知搜索应该进行的方向来降低复杂度,提高效率
  A*以及IDA*是一种启发式搜索,其中A*是DFS版本,而IDA*是BFS版本。启发式搜索在搜索之前先对每一个接下来搜索的状态进行一个估值,得到当前最好的搜索位置,然后再从这个位置开始搜索,不断重复这个过程,直到到达终点。
  A*算法的最重要的部分也就是怎么对状态进行估值,对于不同的问题有不同的估值方法。
  估值的依据是f(x),f(x)=g(x)+h(x),其中x为当前状态,f为估计的总代价,g为从初始状态到达x的花费,h为从x到达终点的估计花费。在解决问题时,如果能够将问题转化为图论问题,那么会使得估价函数直接与距离相关,这样会更方便进行判断和计算

6.5常用剪枝和技巧

6.5.1可行性剪枝

  可行性剪枝是指对于当前状态x,如果x已经不满足题目的某一个要求,那么x的子节点也一定不会满足题目要求,因此可以将x的子树从整棵搜索树中删去以到达提高效率的目的

6.5.2最优性剪枝

  最优性剪枝是指对于当前状态x,如果x以及x的后续最佳情况并不严格优于当前的解,那么x的子树就不会对答案产生影响,因此可以将x的子树从整棵搜索树中删去以到达提高效率的目的

6.5.3搜索顺序

  交换搜索顺序是对搜索的一种优化:在搜索时优先挑选分支小或者约束条件多的子树进行搜索。最经典的就是数独问题,最优的搜索顺序应当按照每一个格子能够填的数的数目从小到大进行

6.5.4 α-β剪枝术

①.基本思想

  根据倒推值的计算方法,或中取大,与中取小,在扩展和计算过程中,能剪掉不必要的分枝,提高效率

②.定义

  α值:有或后继的节点,取当前子节点中的最大倒推值为其下界,称为α值。节点倒推值>=α
  β值:有与后继的节点,取当前子节点中的最小倒推值为其上界,称为β值。节点倒推值<=β

③.α-β剪枝

  β剪枝:节点x的α值不能降低其父节点的β值,x以下的分支可停止搜索,且x的倒推值为α
  α剪枝:节点x的β值不能升高其父节点的α值,x以下的分支可停止搜索,且x的倒推值为β

7.图论

7.1定义&存图

  图是边和点的集合,用符号表示为Graph={V,E}。图分为带权图和无权图,有向图和无向图,其中带权图又有带边权和带点权之分。图论最基础的内容就是如何储存一张图
  图的储存方式分为邻接矩阵和邻接表两种,其中邻接表一般使用数组(结构体)模拟链表进行。

7.1.1邻接矩阵

  邻接矩阵指用二维数组储存图,因此需要的空间比较大,适用于稠密图或者是点数较小的图的储存。有时图的点数过多,可以采用vector进行优化

7.1.2邻接表

  邻接表是指每一个点下用一条链,链的每一个节点储存连接该点的一条边的信息(终点,权值等等),而采用数组模拟链表时还需要储存上一个节点的编号

7.2最小生成树

  生成树是指如果一棵树满足包含一张无向连通图中的所有点,且书中的边全部来自于改图的树,而图的最小生成树(MST)是指边权之和最小的生成树
  解决最小生成树问题有两种算法:Prim算法和Kruskal算法

7.2.1 Prim算法

  Prim算法的基本思想是:首先任选一个节点加入MST之中,然后选择一个MST之中的节点,然后遍历与该点直接相连的点,更新这些点的D(D[i]表示在MST中父边的权值),将未加入MST的点加入MST之中,重复操作,直到所有节点加入,各节点的D之和就是MST的边权之和(最开始的节点的D值为0)
  Prim算法的复杂度依据于图的储存方式,如果采用邻接矩阵储存,那么该算法的复杂度就是n2;如果采用邻接表储存,那么就是n*m的复杂度

#include<iostream>
#include<cstdlib>
#include<cstdio>
using namespace std;
const int INF=0x3f3f3f3f;
const int N=1010;
bool vis[N];
int g[N][N],w[N];
int n,ans;
void Prim()
{
    int x,k;
    for(int i=1;i<=n;i++)
    {
        x=INF;
        for(int j=1;j<=n;j++)
            if( !vis[j] && w[j]<x )
                x=w[j],k=j;
        vis[k]=1,ans+=x;
        for(int j=1;j<=n;j++)
            if( !vis[j] && w[j]>g[k][j] )
                w[j]=g[k][j];
    }
}
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++)
            scanf("%d",&g[i][j]);
    Prim();
    printf("%d\n",ans);
    return 0;
}

7.2.2 Kruskal算法

  Kruskal算法适用于边比较少的图中。Kruskal算法还需要用到并查集来维护顶点的连通信息
  该算法的基本思路是:将边按边权从小到大排序,按顺序取出每一条边,如果当前边连接的顶点已经连通,就删去该边,否则就加入该边,并将两点连通,直到图中所有节点连通
  Kruskal算法复杂度是严格的mlogm

#include<algorithm>
#include<iostream>
#include<cstdlib>
#include<cstdio>
using namespace std;
const int INF=0x3f3f3f3f;
const int N=1010;
struct node
{
    int u,v,w;
    bool operator < (const node &a) const
    {
        return w<a.w;
    }
}t[N];
int f[N];
int n,m,ans;
int find(int x)
{
    return x==f[x] ? f[x] : f[x]=find( f[x] ) ;
}
int main()
{
    int u,v,w;
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)   f[i]=i;
    for(int i=1;i<=m;i++)
    {
        scanf("%d%d%d",&u,&v,&w);
        t[i]=(node){ u,v,w };
    }
    sort( t+1,t+m+1 );
    for(int i=1,j=0;i<=m;i++)
    {
        u=t[i].u,v=t[i].v;
        u=find( u ),v=find( v );
        if( u==v )   continue ;
        f[u]=v;
        ans+=t[i].w;
        if( ++j==n—1 )   break ;
    }
    printf("%d\n",ans);
    return 0;
}

7.3最短路问题

  询问图中两点之间的哪一条路径的边权(点权)之和最小的问题就是最短路问题。最短路问题分为单源最短路问题以及多源最短路问题
  解决最短路问题的方法一般有:Dijkstra,spfa,Floyd

7.3.1 Dijkstra算法

  Dijkstra算法适用于求解单源不含负边权的最短路问题。设S为当前找到了最短路的顶点的集合,dis[i]表示从源点到达i的最短路径,然后选取不在S中的dis值最小的顶点v,将v加入S,然后利用v来更新与v直接相连的节点的最短路
  Dijkstra算法的复杂度同样取决于图的储存方式,如果采用邻接矩阵储存,那么该算法的复杂度就是n2;如果采用邻接表储存,那么就是n*m的复杂度
  另外,Dijkstra算法还可以进一步优化,因为Dijkstra算法的复杂度瓶颈在于每一次都要O(n)的寻找dis值最小的节点,所以我们可以通过优先队列或者是手写堆来优化这个过程,做到logn的查询。前者的好处在系统的函数库中,是能够直接使用;而后者的好处就是常数较小,能够更快地查询

7.3.2 spfa算法

  spfa算法适用于求解单源的稀疏图最短路问题。spfa的实现基于队列:设dis[i]表示从源点到达i的最短路径,vis[i]表示i是否在队列中。最开始队列中只有源点,然后每一次操作都是取出队首元素,然后用该点的dis值去更新与其直接相邻的点的dis值,如果成功更新了,并且这个点不在队列中,那就将其压入队尾。如此反复操作,直到队列为空
  spfa算法也可以解决含有负边权的问题:记录每一个点进入队列的次数,当存在某个点进入队列的次数大于n时,图中就存在负环
  spfa算法如果没有被数据专门卡掉,效率还是非常高的,为O(k*|E|),k平均为2

7.3.3 Floyd算法

  Floyd算法主要是解决点数较少的多源最短路问题。Floyd算法的复杂度很高,为O(n3),但是代码却很简单,一般只有4行,而且还有很多拓展应用
  Floyd算法是基于动态规划:对于每一对点对i和j,枚举中介点k,利用i~k的距离加上k~j的距离来更新i~j的距离

7.3.4其他

①.多源最短路

  多源最短路问题除了可以使用Floyd算法以外,也可以使用Dijkstra算法和spfa算法,其中Dijkstra算法的复杂度同样为O(n3),经过优化后可以变为O(n2logn);而spfa算法的平均复杂度就是O(k*|V|*|E|)

②.已知点对之间的距离

  对于某一些求已知点对之间的距离的问题,如果给定的点对个数不是很多,我们可以使用DFS或者BFS解决,复杂度均为O(n),但是如果个数过多,最好还是利用Dijkstra或者spfa预处理出所有点对之间的距离,做到O(1)查询

③.分层图最短路

  最短路问题还有一类特殊问题:分层图最短路。分层图最短路的主要特点就是点多边也多,重点内容就是怎么建立分层图
  分层图最短路的一般模型是:给定一张图,有k次机会可以0代价通过某条边或者是经过另一个边集之中的边,求从起点到终点的最短路
  分层图在建立时,主要遵循以下两个原则:
  ①.同一层之间的边按照题目给定的连接
  ②.两层之间的边从该层起点连向下一层的终点,若为无向边,则还需要从该层终点连向下一层起点
  由于分层图的边数很多,所以使用spfa可能会导致超时,因此推荐使用堆优化的Dijkstra算法

④.最长路

  最长路一般有两种解法:spfa算法和DP,DP在后面讲
至于spfa,我们只需要一开始将dis数组设为0,然后将松弛条件改为如果(dis[x]<dis[tmp]+t[i].w)即可

7.4割点&割边

7.4.1定义

  割点:对于一个连通图,如果删去一个点(删去一个点指把该点和与该点相连的边从图中去掉)得到的图不再联通,那么称这个点为割点
  割边:对于一个连通图,如果删去一条边之后得到的图不再连通,那么这条边被称为割边

7.4.2求法

  一般求割点或者割边采用的都是Tarjan算法:设dfn[i]表示i的dfs序,low[i]表示i以及i的子树中的点通过非父子边能到达的dfn最小的节点
  如果对于某个节点u有dfn[u]≤low[v](v为u的孩子),那么删去u之后,v就不能到达图的其他部分(除去v以及v的子树),因此u为割点
  如果对于某个节点u有dfn[u]<low[v](v为u的孩子),那么删去u-v边之后,v就不能到达图的其他部分(除去v以及v的子树),因此u-v边为割边

7.5强连通分量

  连通分量是指在无向图的某个子图满足该子图中的任意两点能够相互到达
  强连通分量(SCC)是指在有向图的某个子图满足该子图中的任意两点能够相互到达
  求解SCC的方法一般有两种:Kosaraju算法以及Tarjan算法,使用较多的一般是后者,所以这里只讲Tarjan算法
  Tarjan算法
    Tarjan算法同样是基于DFS来求解强连通分量。Tarjan算法需要利用到上文提到的dfn以及low两个数组
    Tarjan在进行DFS的时候,将当前未处理过的节点压入栈中,回溯时根据栈顶元素u的dfn和low来判断栈中的节点是否在同一个SCC之中,判断的依据是dfn[u]是否等于low[u],若相等则成立

7.6双连通分量

  双连通分量存在于无向图

7.6.1定义

①.点-双连通分量

  如果任意两点至少存在两条“点不重复”的路径,则说这个图是点-双连通的。这个要求等价于任意两条边都在同一个简单环中,即内部无割点。也就是说,不含割点的图即为点-双连通图

②.边-双连通分量

  如果任意两点之间至少存在两条“边不重复”的路径。这个要求等价于每条边都至少在一个简单环中,即所有边都不是割边。也就是说,不含割边的图即为边-双连通图。
  特殊地,孤立点所形成的图是点-双连通的,也是边-双连通的,因为内部无割点、无割边,而孤立边是点-双连通的,却不是边-双连通的。
  割边将每一个边-双连通分量分开,low[i]的意义就是low[i]所连的块能返回的最早的祖先,也就是说,low[i]相同的点即为一个边连通分量

7.6.2求法

  根据点-双联通分量和边-双联通分量的定义可知,如果要求点双,只需要求出图中的割点,而割点之间的部分就是一个点-双联通分量
  至于边双在上面已经提到过了,只需要Tarjan一次,求出各点的low,low值相等的点就在同一个边-双联通分量中

7.7拓扑排序

  一个有向无环图(DAG)的拓扑序是指给图中的每一个顶点一个序号,使得对于图中任意一条边u->v,则u的序号小于v的序号
  拓扑序的求解是基于队列的:设deg[i]表示i节点的入度,那么将deg为0的节点压入队列中。对于每一次操作,从队首取出元素u,然后删去u所连接的边,对应终点的deg也要减一,如果产生了新的deg为0的节点,就将这个节点压入队列,每一个点的取出顺序就是这个节点的拓扑序

7.8二分图

7.8.1定义

  二分图是无向图,并且满足两个条件:
  ①.对于点集V,可以将其分为两个不相交的子集X和Y
  ②.对于边集E,每一条边所连接的两点分别在X和Y中
  增广路是连接图中两个未匹配点的路径,且路径上已匹配(属于同一个匹配)和未匹配的边交替出现,那么这条路径就是该匹配的增广路

7.8.2最大匹配

  匹配是指集合内任意两条边都没有相同顶点的边集,而最大匹配就是所有匹配中边数最大的匹配
  求解最大匹配一般有两种算法:匈牙利算法和Dinic算法

①.匈牙利算法

  匈牙利算法通过寻找增广路来求得最大匹配。选取某个节点u出发,遍历与其有连边的节点v,如果v未匹配,那么u-v边就是一条增广路,否则前往v的匹配对应点x,从x出发继续找。以上过程一般通过DFS进行

bool find(Rint k) 
{ 
    for(Rint i=1;i<=n;i++)
    if( !vis[i] && g[k][i] ) 
    { 
        vis[i]=1; 
        if( !match[i] || find( match[i] ) ) 
        { 
            match[i]=k;return true ;
        } 
        } 
    return false ;
}
for(Rint i=1;i<=n;i++) 
{ 
    for(Rint j=1;j<=n;j++) vis[j]=0; 
    if( find( i ) ) cnt++; 
}
②.Dinic算法

  Dinic算法是基于网络流来求解最大匹配,因为最大匹配=最大流。具体过程在后文中会详细讲解

7.8.3最大权匹配

  最大权匹配又称为最优匹配,是指在带有权值边的二分图中,求一个匹配使得匹配边上的权值和最大。一般X和Y集合顶点个数相同,最优匹配也是一个完美匹配(如果一个图的某个匹配中,所有的顶点都是匹配点,那么它就是一个完美匹配),即每个顶点都被匹配。如果个数不相等,可以通过补点加0边实现转化。一般使用KM算法解决该问题
  KM:http://www.cnblogs.com/wenruo/p/5264235.html

7.8.4最小点覆盖

  点覆盖是图中一些点的集合,使得对于图中所有的边,至少有一个端点属于点覆盖。点数最小的覆盖就是最小点覆盖
  定理:最小点覆盖=最大匹配

7.8.5最小边覆盖

  边覆盖是图中一些边的集合,使得对于图中所有的点,至少有一条集合中的边与其相关联。边数最小的覆盖就是最小边覆盖
  定理:最小边覆盖=图中点的个数-最大匹配

7.8.6最大独立集

  独立集是图中一些点的集合,使得集合中任意两点之间都不存在边.点数最大的就是最大独立集
  定理:最大独立集=图中点的个数-最大匹配

7.8.9 DAG最小路径覆盖

  最小路径覆盖就是用最少的不相交路径覆盖所有顶点
  定理:最小路径覆盖=原图的节点数-新图最大匹配
  把原图中的每个点V拆成Vx和Vy,如果有一条有向边A->B,那么就加边Ax-By,这样就得到了一个新图

7.8.8性质

  二分图中,点覆盖数是匹配数
  ①.二分图的最大匹配数等于最小覆盖数:即求最少的点使得每条边都至少和其中的一个点相关联,很显然直接取最大匹配的一段节点即可
  ②.二分图的最大独立集等于顶点数减去最大匹配:很显然的把最大匹配两端的点都从顶点集中去掉这个时候剩余的点是独立集,这是|V|-2*|M|,同时必然可以从每条匹配边的两端取一个点加入独立集并且保持其独立集性质
  ③.最大匹配数=左边匹配点+右边未匹配点。因为在最大匹配集中的任意一条边,如果他的左边没标记,右边被标记了,那么我们就可找到一条新的增广路,所以每一条边都至少被一个点覆盖
  ④.最小边覆盖=图中点的个数-最大匹配数=最大独立集

7.8.9判定

  二分图是这样一个图:有两顶点集且图中每条边的的两个顶点分别位于两个顶点集中,每个顶点集中没有边直接相连接!
  无向图G为二分图的充分必要条件是,G至少有两个顶点,且其所有回路的长度均为偶数
  判断二分图的常见方法是染色法:开始对任意一未染色的顶点染色,之后判断其相邻的顶点中,若未染色则将其染上和相邻顶点不同的颜色,若已经染色且颜色和相邻顶点的颜色相同则说明不是二分图,若颜色不同则继续判断,bfs和dfs可以搞定!
  易知:任何无回路的的图均是二分图

7.9网络流

  网络流的定义……也不知道怎么说,参考《ACM-ICPC》的说法:
  网络流是指在一个每条边都有容量的有向图上分配流,使得每一条边的流量都不大于其容量。一道流必须满足一个顶点的进出流量相同,除非该点为源点——流流出的点或者是汇点——流流入的点。以上要素共同构成了一个网络:G={V,E,C,s,t},其中V为顶点集,E为边集,C为定义在边上的容量,s为源点,t为汇点
  网络流的题目一般不是非常难,主要考查点除了怎么求解网络流以外,主要就是考验将问题转化为模型并建立相应的网络的能力

7.9.1最大流

  最大流,顾名思义,就是指的所有可行的流中流值最大的流。求解最大流需要了解什么叫做增广路:在网络流中,增广路指的是在网络中不通过满流的边从源点到达汇点的一条路径(一条增广路的流值是指整条路径上的边中最小的残余流量,残余流量=容量-当前流量)
  因此我们可以知道,当图中不存在增广路时,此时的流值之和就是最大流
  求解最大流有三种解法:Ford-Fulkerson算法,Edmond-Karp算法以及Dinic算法

①. Ford-Fulkerson算法

  Ford-Fulkerson算法的过程就是基于上文所提到的结论:当图中不存在时,此时的流值之和就是最大流
  算法流程如下:利用dfs寻找从s到t的一条增广路,如果还存在增广路,就沿着这条路径增加流,否则就停止
  复杂度与最大流有关,为O(f*|E|),f为最大流

②. Edmond-Karp算法

  思想与Ford-Fulkerson算法基本一致,只是将寻找增广路的dfs过程改为了bfs过程,复杂度O(|V||E||E|)

③. Dinic算法

  Dinic算法的思想也与Ford-Fulkerson算法一致,不同之处在于Dinic算法能够减少增广次数
  Dinic算法将整张网络进行分层lev(利用bfs),然后在进行dfs的时候按照lev递增的路径从源点前往汇点,并且在回溯时一次性解决多条增广路
  Dinic的复杂度与|V|的相关性更大,为O(|V||V||E|)

7.9.2最小割

  割是指在网络G={V,E,C,s,t}中,从边集E中找出一个子集将网络划分为两个部分X和Y(要求X∪Y=V并且X∩Y=∅),并且有s∈X,t∈Y;割也有容量,割的容量等于Σ f(e),e={u,v},u∈X,v∈Y
  最小割,顾名思义,就是所有割中容量最小的一个割,有定理:最大流=最小割,即最小割最大流定理

7.9.3最小费用流

  费用流是基于网络G={V,E,C,W,s,t}(W为边所带的单位费用)产生的问题,是求解从s到t的特殊最短路,要求所有物品均能从s运到t并且总费用最小(一条边产生的费用=该边的单位费用×该边的流量)
  既然是求特殊最短路,所以我们可以使用spfa算法或者是Dijkstra算法。首先套用Dinic算法的模板,然后将Dinic的分层过程用spfa代替,这样的话每一次只会找到一条增广路,而且这条增广路是最短的,然后dfs求解这条路径的费用

7.9.4技巧

  前文提到过,网络流主要就是考验将问题转化为模型并建立相应的网络的能力,而大部分的问题我们都可以找到一个大概固定的模型去建立网络

①.网格问题

  问题:有一个网格,其中有一些格子不能够放置物品,放置了物品的格子附近的某些区域不能再放置物品,问最多可以放置多少个物品?
  解法:本题的重点是连边。考虑将点按照横纵坐标和的奇偶性分为左右两边,从源点向左边连边,从右边向汇点连边,然后从左边向右边能够到达的点连边然后求最小割,用总的能放的棋子数减去最小割就是最大的合法棋子数

②.拆点

  问题:有n个人,x种食物,y种饮料(每种都只有一份),每个人都有自己喜欢的食物和饮料(可以有多个),现在给每个人分配食物和饮料,使得拿到自己喜欢的食物和饮料的人尽可能多
  建图方法:将每个人拆为两个点i,j,从源点向所有食物连容量为1的边,从所有饮料向汇点连容量为1的边。然后从喜欢的食物向i连容量为1的边,从i向j连容量为1的边,从j向喜欢的饮料连容量为1的边

  问题:有n个点,m条道路,其中有一部分的点不能经过(不知道多少个),有k个点不能到达源点(S),问最少有多少个点不能经过
  建图方法:将编号为i的点拆为i,~i。如果i为源点或者i无法到达源点,就从i向~i连容量为inf的边,否则连容量为1的边。
  如果i和j有道路连接,加入边~i->j,~j->i,容量为inf
  如果i不能到达源点,加入边~i->T,容量为inf
  问题的解就是该网络的最小割

③.当前弧优化
void bfs()
{
    int tmp;
    for(int i=S;i<=T;i++) lev[i]=0; 
    while( !q.empty() ) q.pop(); 
    lev[S]=1,q.push( S ); 
    while( !q.empty() ) 
    { 
        tmp=q.front(),q.pop(); 
        if( tmp==T ) return 1; 
        for(int i=head[tmp],x; i ;i=t[i].next) 
        { 
            x=t[i].to; 
            if( !lev[x] && t[i].c ) 
            { 
                lev[x]=lev[tmp]+1; 
                q.push( x ); 
            } 
        }
    } 
    return 0;
}
int dfs(int k,int f) 
{ 
    if( k==T ) return f; 
    int tmp,fm=0; 
    for(int &i=cur[i],x; i ;i=t[i].next) 
    { 
        x=t[i].to; 
        if( t[i].c>0 && lev[x]=lev[k]+1 ) 
        { 
            tmp=dfs( x,min( f-fm,t[i].c ) );
            t[i].c-=tmp,t[i^1]+=tmp; 
            fm+=tmp;
            if( fm==f ) break ;
        }
    }
    return fm; 
} 
void Dinic() 
{ 
    int F=0;    
    while( bfs() ) 
    { 
        for(int i=S;i<=T;i++) cur[i]=head[i];//当前弧优化 
        F+=dfs( S,INF ); 
    } 
    return F; 
}

8.数学相关

  数学在联赛中考的不是非常多,一般涉及到的就是组合数学;质数相关;gcd相关;同余方程相关,更复杂的原根&离散对数以及FFT都不会涉及

8.1组合数学

8.1.1组合数

  联赛中一般就是考组合数吧,主要记住组合数的几个公式:

  组合数很少直接考,一般都是在题目中作为一个工具来使用,有时候也可以用来优化算法

8.1.2一些公式

8.1.3容斥原理

  容斥也考得比较多,一般公式是:
  集合S中不包含性质P1,P2……Pm的元素个数是:

8.2数论

8.2.1基本概念

①.整除

  如果a%b=0,那么记作b|a
  性质:
  A.b|a,c|b——>c|a
  B.b|a——>bc|ac
  C.b|a,c|a——>(mb+nc)|a,bc|a

②.余数

  如果a=b×k+c,那么称c为a除以b的余数,记作a%b=c

③.质数&合数

  这个肯定不用讲

④.最大公约数(gcd)&最小公倍数(lcm)

  正整数a,b的gcd(a,b)是指能同时整除a和b的最大正整数,所以有:gcd(a,b)|a,gcd(a,b)|b
  如果gcd(a,b)=1,则称a与b互质
  正整数a,b的lcm(a,b)是指能够同时被a和b整除的最小正整数,所以有:a|lcm(a,b),b|lcm(a,b)
  两者关系式:lcm(a,b)=a×b÷gcd(a,b)

⑤.同余

  如果m|(x-a),那么就说x和a关于m同余,记作:x(同余)a( mod m),并且称a为x模m的一个剩余,如果a∈【0,m-1】,则a为x模m的最小剩余
  剩余类(剩余系)是指与某个给定的剩余(mod m)同余的所有数的集合,显然有【0,m-1】所代表的m个剩余类

⑥.欧拉函数

⑦.唯一分解定理(算术基本定理)

  任意一个大于1的数都可以分解为若干个质数的积

8.2.2拓展欧几里得

  扩展欧几里得算法 该算法可以用来求出ax + by = gcd(a, b)的一组特解,再带入公式可以算出通解。重点在于Exgcd 可以用来证明两个整数数的线性组合一定是这两个数的gcd的倍数

void extgcd(Lint a,Lint b,Lint &x,Lint &y) ​
//扩展欧几里得所能解的ax + by = gcd(a, b) 
{ 
    if( !b ) { x=1,y=0;return ; } 
    extgcd( b,a%b,x,y ); 
    Lint tmp=x; 
    x=y,y=tmp-(a/b)*y;
    return ; 
}

8.2.3质数筛法

  这里主要讨论线性筛做法。线性筛一般用来快速打出质数表。与最普通的筛法不同的是,线性筛运算过程中避免了对一个合数的重复判断,因而加快了运行速度,时间复杂度为O(n)

#include<iostream> 
#include<cstdlib> 
#include<cstdio> 
#include<cmath> 
using namespace std; 
const int N=1000010; 
bool vis[N]; 
int prime[N];
int cnt,n; 
int main( )
{ 
    scanf("%d",&n); 
    for(int i=2;i<=n;i++) 
    { 
        if( !vis[i] ) prime[++cnt]=i; 
        for(int j=1;j<=cnt;j++) 
        { 
            if( i*prime[j]>n ) break; 
            vis[i*prime[j]]=1; 
            if( i%prime[j]==0 ) break;
        } 
    } 
    return 0; 
}

8.2.4欧拉函数

  首先我们可以利用上文中欧拉函数的第三个公式求解单个n的欧拉函数值

void get_Phi(int x) 
{ 
    int p=x; 
    for(int i=1;i<=cnt;i++) 
    { 
        if( prime[i]>x ) break ;
        if( x%prime[i]==0 ) 
        {
            p=p*(prime[i]-1)/prime[i]; 
            while( x%prime[i]==0 ) x/=prime[i]; 
        }
    } 
    if( x>1 ) p=p*(x-1)/x; 
    return p;
}

  如果要求一定范围内所有数的欧拉函数值呢?显然一个一个求是很慢的,因此同样需要利用到线性筛。与求质数的线性筛做法大致类似,利用到了欧拉函数为积性函数这一性质(上文公式二),添加了一些内容,运算过程中一样可以求出质数表:

void Prepare()
{
    phi[1]=1; 
    for(int i=2;i<=N;i++) 
    { 
        if( !vis[i] ) phi[i]=i-1,prime[++cnt]=i; 
        for(int j=1,x;j<=cnt;j++) 
        { 
            x=i*prime[j]; 
            if( x>N ) break ; 
            vis[x]=1; 
            if( i%prime[j] ) phi[x]=phi[i]*phi[prime[j]]; 
            else 
            { 
                phi[x]=phi[i]*prime[j]; break ; 
            } 
        }
    }
}

8.2.5快速幂&龟速乘

  快速幂:

int pow(int x,int k) 
{ 
    int ret=1; 
    while( k ) 
    { 
        if( k&1 ) ret=ret*x%M;
        x=x*x%M,k/=2; 
    } 
    return ret;
}

  龟速乘:

int pow(int x,int k) 
{ 
    int ret=0;
    while( k ) 
    { 
        if( k&1 ) ret=(ret+x)%M; 
        x=(x+x)%M,k/=2; 
    } 
    return ret; 
}

8.2.6矩阵

  矩阵乘法的做法非常简单,注意两个矩阵行与列的关系,判断它们是否能够相乘,时间复杂度为O(n3)

8.2.7逆元

  x 关于模数为质数意义下的逆元为xP-2(mod P),根据费马小定理易得,证明从略。
  x 关于模数为任意数意义下的逆元求法:转换成一个不定方程的形式,用 extgcd解决(前提:存在逆元)

9.动态规划

  DP是联赛中的重点,也是难点,而且DP没有什么定式,训练基本来自于平常的做题,因此考场上要认真对待DP题,且要善于发现一道题中的DP模型。需要注意的是要谨慎而大胆地去设计DP状态,转移时要考虑全面,不可遗漏特殊情况

9.1字符串DP

  在第一阶段的总结里提到过字符串DP通常记录的是A串当前到第i位,B串当前到第j位的情况,但是如果只记录这些信息的话,时间复杂度可能会比较高。因此我们可以通过增维的办法,在不超过空间限制的情况下,DP数组内多记录一些信息,来降低时间复杂度
  比如【NOIP2015】子串,若只记录f[i][j][k]表示A串到了第i位,B 串到了第j位,已经选出了k个子段,这个状态虽然可以用滚动数组来优化,但是时间上无法承受,因为转移需要枚举第i个串从前多少位转移过来。因此我们增设一维,记录f[i][j][k][b],新增的b表示A串第i位与B串第j位是否匹配。这样转移过程中每个i只会从i − 1的位置转移来,且无需枚举其它变量,因此时间复杂度大大优化,空间上也可以采用滚动数组优化

9.2背包问题

  常见的背包有:01背包、完全背包、多重背包、分组背包、混合背包……
  常见优化有:二进制分组
  注意i和j这些变量的循环顺序

9.3状态压缩

  状态压缩适用于题中所给的某些量很小且与这个量有关的状态可以用一个二进制数来表示,一般是0表示无,1表示有。状态与状态之间的转移用位运算做到,非常方便
  状压DP在一类窄棋盘的统计方案问题中出现的比较多,也适用于一个点数不多的图的遍历问题
  常见优化有:预处理出合法的状态,通常在题目中存在大量冗余的状态,我们可以在转移中不考虑这些无效状态。CDQ论文里面也提到过可以用类似BFS的方式来转移,队列实现

9.4优化

  DP的优化一般在联赛中都是使用数据结构进行优化,例如单调队列或者树状数组,线段树等等
  针对不同的题目有不同的优化方法,DP的题难就难在这种地方,只要能够突破这个瓶颈,想到优化的方法就能够做出来,否则就只能拿个暴力分了

10.字符串

  字符串在联赛中考的不是很难,如果有难题一般都是结合DP的等其他算法,例如【NOIP2015】子串等等。一般的字符串的题灵活性很大,思维要求也比较高,需要能够熟练的运用各种算法(贪心,DP,二分等)或者数据结构(Trie树,线段树等)去优化程序

10.1 Hash算法

  Hash应该是字符串中最基础的算法了,主要是用来比较两个串是否相等。Hash一般的思路是将字符串转化为一个数字,通过数字之间的比较来快速完成两个串之间的比较。
  Hash一般采用字符集大小作为Base,然后使用一个大质数,例如109+7,108+7等作为模数

void Hash() 
{ 
    len[0]=1; 
    for(int i=1;i<=n;i++) len[i]=len[i-1]*Siz%M; 
    for(int i=1;i<=n;i++) hash[i]=(hash[i-1]*Siz+s[i])%M; 
    }
int getHash(Rint l,Rint r) 
{ 
    int tmp=(hash[r]-hash[l-1]*len[r-l+1])%M; 
    return (tmp+M)%M;
}

  Hash算法可以应用在很多的字符串题目中,例如用二分+Hash求最长公共子串等

#include<algorithm>
#include<iostream>
#include<cstdlib>
#include<cstdio>
using namespace std;
const int INF=0x3f3f3f3f;
const int N=1010;
struct node
{
    int u,v,w;
    bool operator < (const node &a) const
    {
        return w<a.w;
    }
}t[N];
int f[N];
int n,m,ans;
int find(int x)
{
    return x==f[x] ? f[x] : f[x]=find( f[x] ) ;
}
int main()
{
    int u,v,w;
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)   f[i]=i;
    for(int i=1;i<=m;i++)
    {
        scanf("%d%d%d",&u,&v,&w);
        t[i]=(node){ u,v,w };
    }
    sort( t+1,t+m+1 );
    for(int i=1,j=0;i<=m;i++)
    {
        u=t[i].u,v=t[i].v;
        u=find( u ),v=find( v );
        if( u==v )   continue ;
        f[u]=v;
        ans+=t[i].w;
        if( ++j==n-1 )   break ;
    }
    printf("%d\n",ans);
    return 0;
}

10.2 KMP算法

  KMP算法是用来解决字符串匹配的问题,一般的字符串匹配算法需要O(n*m)的复杂度,但是利用KMP可以做到O(n+m)
  Next的定义
  假定pre【i】表示T的前缀T1……i,那么next【i】表示:pre【i】的后缀B,满足B=pre【|B|】,此时B的最大长度就是next【i】

#include<iostream>
#include<cstdlib>
#include<cstring>
#include<cstdio>
using namespace std;
const int N=1000010;
char c[N],c1[N];//c1 模式串 c原串 
int Next[N];
void getnext()
{
    Next[0]=0;
    int i,j,len;
    len=strlen( c1 )-1;
    i=1,j=0;
    while( i<=len )
    {
        while( j && c1[j]!=c1[i] )   j=Next[j-1];
        if( c1[j]==c1[i] )   j=j+1;
        Next[i]=j,i=i+1;
    }
}
void KMP()
{
    int tot=0,len,Len;
    len=strlen( c )-1;
    Len=strlen( c1 );
    for(int i=0,j=0;i<=len;i++)
    {
        while( j && c1[j]!=c[i] )   j=Next[j-1];
        if( c1[j]==c[i] )   j=j+1;
        if( j==Len )
            tot++,j=Next[j-1];
    }
    printf("%d\n",tot);
}
int main()
{
    scanf("%s",c);
    scanf("%s",c1);
    getnext(),KMP();
    return 0;
}

10.3 Trie树

  字典树,顾名思义,你可以把它想象成一棵树。树根是最开始的字母,而节点就是后续的字母。也叫单词查找树,Trie树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来节约存储空间,最大限度地减少无谓的字符串比较,查询效率比哈希表高

struct dictionary 
{ 
    int conut;//记录通过数 
    int son[27];//记录孩子编号 
}T[MAXN];
`````` 
void build(int dp,int k)//分别表示未放入的长度和当前节点编号 
{ 
    if(dp<1) return ; 
    if(T[k].son[s[deep-dp]-'A'+1]==0) 
    { 
        tot++;
        T[k].son[s[deep-dp]-'A'+1]=tot;//孩子编号 
    } 
    k=T[k].son[s[deep-dp]-'A'+1];
    build(dp-1,k); 
}//deep为字符串s的长度 
int search(int dp,int k)//分别表示当前深度和当前节点编号
{ 
    if( dp==deep ) return T[k].conut;
    return search(dp+1,T[k].son[s[dp]-'a'+1]); 
} 
int main() 
{
 `````` 
    build(deep,1 ); 
    search(0,1); 
    return 0;
}

11.数据结构

11.1队列

  队列具有先进先出的特性,一般是作为BFS/spfa的算法的工具,有时候队列也可以单独解题。
  C++中有系统队列,并且还有优先队列,十分方便,但是在没开O2的情况下可能会比较慢,所以可以手写队列来优化。
  手写的普通队列就是一个数组+两个指针(头指针和尾指针),然后让指针一路往后推即可(注意不要越界),手写的优先队列就需要其他数据结构进行辅助,在后文中会讲到
  系统队列头文件为queue,优先队列定义声明为:priority_queue

11.2栈

  栈具有先进后出,后进先出,后来居上的特性,一般是作为DFS的工具。
  C++中同样有系统栈,头文件为stack,缺点也和队列一样,就是速度可能很慢,而手写栈就是一个数组+一个栈顶指针

11.3堆

  堆是一棵树,每一个点都有权值。根据堆的特性可以将堆分为大根堆以及小根堆:大根堆是指父亲节点的权值总是大于儿子节点的权值,而小根堆则相反
  常见的堆一般有二叉堆,斐波切纳堆等,而使用最多的就是二叉堆,二叉堆还可以用来优化Dijkstra算法
  除此之外,还有可并堆,左偏树等具有堆性质的数据结构

11.4并查集

  并查集也是一种树形结构,一般用来处理不相交集合的合并问题和查询,在图论问题中使用较多
  并查集可以同时维护集合的多种信息,例如siz,sum,maxw等等。并查集的主要数组是f数组,主要思想是路径压缩

11.5树状数组

  树状数组(BIT)主要是用来处理区间和等问题并进行单点的修改。树状数组其实是二进制的一种应用,其核心函数有:lowbit函数,modify函数,get函数

11.6线段树

  线段树应该是各种基础数据中应用最多的一个,而且线段树还有很多变形,例如主席树,二维线段树,权值线段树等等
  线段树基于二分的思想,将一个[l,r]的区间分为[l,mid]以及[mid+1,r]两个部分,分别维护子区间的信息,然后向上更新父区间的信息
  线段树可以支持区间加减法,区间求和,单点查询,单点修改,区间位运算,区间开方,区间取模,区间平方,区间立方等多种操作
  线段树在进行区间修改操作时,如果直接更新到每一个叶子节点,那么复杂度还是len*logn级别的,显然无法承受,所以线段树中有一个重要思想:懒操作。懒操作每一次只更新完全包含的区间,对于这些区间的子区间就不在继续更新,并且记录一个更新值,直到查询的时候,如果需要进入该区间的子区间,就将这个更新值下放到子区间,这样的操作可以显著减小时间复杂度

12.差分

  对于一个给定的序列A,将它差分后得到一个差分数组B,这个差分序列中的一段区间和对应原序列中相应位置两个数的差
  需要牢记的是:区间加法单点查询操作一定可以用差分数组实现,考试时不可忘记这个简单的做法

版权声明:本文为qq_38064038原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_38064038/article/details/78388803