清北提高组基础班Day5

一、图论

1、图的定义

 

一个图由点的集合与边的集合构成。

一条连接u,v的边用(u,v)表示,当u=v时存在自环。

在有向图中所有边都是有向的,也就是说(u,v)≠(v,u)。在无向图中所有边都是无向的,也就是说(u,v)=(v,u)。

每条边可以有边权也可以没有边权

2、图的表示方式

邻接矩阵。
对于一个有n个点的无边权的图。
令a[i][j]表示是否存在一条(i,j)的边。

若存在,则a[i][j]=1,否则a[i][j]=0。

邻接矩阵的优势与劣势?
方便!快捷!
需要n^2的空间,访问一个点的所有边时时间复杂度为O(n)。
两个点之间只能存最多一条边。

适用于完全图或者稠密图中

cin>>n>>m; // n代表点的个数,m代表边的个数
while (m--)
{
	cin>>u>>v>>z; (u->v 有一条长度为z的边)
	a[u][v]=z;
}

当m很小的时候,这个矩阵大多数地方0,很浪费空间;

cin>>n>>m;
vector <int> E[100005];  //n<=100000  E[i]表示左栏编号为i的那一行
while (m--)
{
	cin>>u>>v; 
	//假设有向图  u->v
	//相当于左边这栏编号为u的那一行对应的右边需要增加编号为v的点。
	E[u].push_back(v);
}

vector基本操作

(1)头文件#include<vector>.

(2)创建vector对象,vector<int> vec(1维)/vec[-](二维);

(3)尾部插入数字:vec[].push_back(a);

for (i=1; i<=n; i++) 
{
  len=E[i].size();
  for (j=0; j<len; j++) cout<<i<<' '<<E[i][j]<<endl;
}  //邻接表

3、拓扑排序

如果存在一个排列a1,a2,…,an,使得在该图中不存在ai到aj的路径(i>j),我们称这个排列为这个图的拓扑序列。

显然只有有向图才有拓扑序列。

思想:每次找没有其它点能到达它的点,找到后,删除与该点连接的所有边

for (i=1; i<=n; i++)  //总共要找n个点
{
  for (j=1; j<=n; j++)  //v表示这个点是否已经出现在拓扑序中
  if (!v[j])
  {
    FLAG=true;  //尝试是否有点连接到它
    for (k=1; k<=n; k++)
	  if (a[k][j]) FLAG=false;
	if (FLAG)
	{
	  cout<<j<<' ';
	  for (k=1; k<=n; k++) a[j][k]=0;
	  v[j]=true;
	  break;
	}
  }
}

优化O(n^2 + m):

思路:定义编号为i的点的入度  为  能直接连接到它的点的个数,每次找没有其它点能到达它的点  本质上  就是找入度为0的点

删除一条边(u,v),本质上是减小了v的入度

for (i=1; i<=n; i++)  //总共要找n个点
{
  for (j=1; j<=n; j++)  //v表示这个点是否已经出现在拓扑序中
  if (!v[j] && du[i]==0)  // 对于每个不同的点,至多只有1次会进入if里面
  {
	  cout<<j<<' ';
	  for (k=0; k<E[j].size(); k++) du[E[j][k]]--;
	  v[j]=true;
	  break;
  }
}

队列优化O(n+m):

l=1; r=0;
for (i=1; i<=n; i++)
	if (du[i]==0) s[++r]=i; // s是队列,存的是入度为0的点
for (i=1; i<=n; i++)  //总共找n个点
{
	//不需要枚举找哪个点入度为0,直接找队列里的元素就可以了
	cout<<s[l]<<' ';
	if (l>r) {cout<<"存在环";}
	for (k=0; k<E[s[l]].size(); k++) 
	{
		du[E[s[l]][k]]--;
		if (du[E[s[l]][k]]==0) s[++r]=E[s[l]][k];
	}
	l++;
}

求拓扑序 -> 找入度为0的点
一个图有拓扑序  DAG  在DAG上是可以进行dp的
一个图存在拓扑序 当且仅当 任意时刻 都存在入度为0的点     ->   有拓扑序 <==> 没有环

如果把一个题目的模型转化成了  怎么求一个图的拓扑序的方案总数  ->  模型转化错了

4、最短路

给定一张带边权的图与两个点u,v,询问u到v的所有路径中最短的那条是多少。

(1)dijkstra O(n^2)

令dis[i]表示当前u到i的最短路是多少。
①将dis[u]=0,dis[i]=inf(i!=u)。
②寻找最小的dis[x]且x曾经没被找到过。
③若x=v,输出答案并退出。
④枚举x的所有边,用dis[x]去更新其余dis[],回到步骤②。
时间复杂度为n^2。

使用范围:不存在负权边。

 

for (i=1; i<=n; i++) dis[i]=INF;
dis[u]=0;
while (1) //最多执行n次
{
	MIN=INF;
	for (i=1; i<=n; i++)
		if (dis[i]<MIN && !V[i]) // V表示之前是否已经用来扩展过
		{
			MIN=dis[i];
			X=i;
		}
	//X表示最小的dis并且之前没被访问过
	if (X==v) {cout<<dis[v]; break;} //找到终点
	for (i=1; i<=n; i++)  //用X来扩展得到其它点的最短路
		if (dis[X]+a[X][i]<dis[i])
			dis[i]=dis[X]+a[X][i];
	V[X]=true;
}

(2)dijkstra 堆优化 O(mlgn)

m边的条数  n是点的个数  堆来进行优化   用堆来维护最小的dis值,

每次扩展x时需要枚举x的所有出边,再维护整个堆(小根堆)

for (i=1; i<=n; i++) dis[i]=INF;
dis[u]=0;
int cmp(node i,node j) {return i.x>j.x;}
for (i=1; i<=n; i++)
{
	t[i].x=dis[i];
	t[i].y=i;  // 编号为t[].y的点的dis值是t[].x
	push_heap(t+1,t+i+1,cmp);
}
cnt=n;
while (1) //最多执行n次
{
	while (V[t[1].y]) {pop_heap(t+1,t+cnt+1,cmp); cnt--;}
	X=t[1].y;
	if (X==v) {cout<<dis[v]; break;} //找到终点
	for (i=0; i<E[X].size(); i++) // G表示边权
		if (dis[E[X][i]]>dis[X]+G[X][i])
		{
			dis[E[X][i]]=dis[X]+G[X][i];
			t[++cnt].x=dis[E[X][i]];
			t[cnt].y=E[X][i];
			push_heap(t+1,t+cnt+1,cmp);
		}
	V[X]=true;
}

SPFA和dijk
dijk:每次是找确定是最短路的点去更新其它点

SPFA:每次随便找一个可能能更新其它点的点来更新

(3)SPFA(nm)可以处理负边权

令dis[i]表示当前u到i的最短路是多少。
①将dis[u]=0,dis[i]=inf(i!=u),并将u加入队列中。
②设当前队首为x。
③枚举x的所有边,用dis[x]去更新其余dis[],若dis[i]此时被更新且i当前不在队列中,将其加入队列。

④将x弹出队列,若此时队列为空,结束,否则返回步骤②。

l=1; r=1; s[1]=u; 
for (i=1; i<=n; i++) dis[i]=INF; dis[u]=0;
for (i=1; i<=n; i++) V[i]=false; V[u]=true; // V来表示是否在队列
//这个队列存储的是有可能能更新其它点的点
while (l<=r)
{
	x=s[l]; l++;
	for (i=0; i<E[x].size(); i++)
		if (dis[E[x][i]]>dis[x]+G[x][i])
		{
			dis[E[x][i]]=dis[x]+G[x][i]; //如果更新成功,说明E[x][i]还有可能去更新其它点
			if (!V[E[x][i]]) {r++; s[r]=E[x][i]; V[E[x][i]]=true;}
		}
	V[x]=false;
}

例:校外的树

学校门口有n个点(1~n),要种一堆树。种m次,每次在li~ri种一棵树。求最终由多少点没种上树。每次种完树后求有多少点还没种上树。n,m<=1000000。

思路:令f[i]表示右端(包括自己)最近的没种上树的位置。每次操作时,都向右找最近没种上树的位置并种上一棵树,更新f即可。
f的维护方法和并查集一模一样。

并查集简易代码:

f[i] : i是根  f[i]=i   i不是根:  f[i]是i的父亲
int getf(int k) {return f[k]==k?k:f[k]=getf(f[k]);}  getf(i)  i的祖先
for (i=1; i<=n; i++) f[i]=i;

cin>>A>>B;
f[getf(A)]=getf(B);


f[i]表示i个这个点向右(包括i),最近的没被种的树在哪里

for (i=1; i<=n; i++) f[i]=i;
ans=n;
cin>>m;
while (m--)
{
	cin>>L>>R;
	for (i=getf(L); i<=R; i=getf(i+1))
	{
		//意味着i这个点种了树
		f[i]=getf(i+1);
		ans--;
	}
	cout<<ans<<endl;
}

5、最小生成树

给定一个无向带非负整数边权的图。选择其中若干条边,使得这张图联通,并且要求这些边的边权之和最小。

选出n-1条边

(1)PRIM算法

①任选一个点作为一个子图。
②在原来图中选择一条最短的边,使得这条边一端在当前子图中,另一端不在当前子图中,加入这条边与这个点。重复n-1次。

得到最小生成树

(2)KURSKAL算法

每次选择一条边权最小且两个端点不连通的边。将其加入进最小生成树中。重复n-1次。判断是否连通用并查集实现。

for (i=1; i<=m; i++)
{
	cin>>u>>v>>z;  // u->v长为z的边
	t[i].x=u; t[i].y=v; t[i].z=z;
}
int cmp(node i,node j) {return i.z<j.z;}
int getf(int k) {return f[k]==k?k:f[k]=getf(f[k]);}  getf(i)  i的祖先

sort(t+1,t+n+1,cmp);
for (i=1; i<=n; i++) f[i]=i;
for (i=1; i<=m; i++)
	if (getf(t[i].x)!=getf(t[i].y))
	{
		ans+=t[i].z;
		f[getf(t[i].x)]=getf(t[i].y);
	}

例:打水

一个村庄有n个点,(i,j)之间都可以花费a[i][j]的代价造一条路径(有a[i][j]=a[j][i]),村民们想在这n个点选择若干点挖水井,第i个点挖水井的代价为w[i],构造一个方案使得代价最小的情况下村民们都有水喝。

思路:选择若干条边,变成若干联通块,每个联通块都需要有一口井。构造一个虚拟点,向每个点i连一条长度为w[i]的边,跑最小生成树即可;两个有井的点同时与虚拟点相连 -> 所有点都是联通的

6、搜索树

从某一点开始进行深度优先搜索。搜索到的边构成的树称为搜索树。在这棵树上的边称为树边,其余边称为非树边。性质:对无向图求搜索树时,非树边连接的两个端点在搜索树中一定是其中一个点是另一个点的祖先。

非树边在图中一定是祖孙 -> Tarjan里有用

7、二分图

如果一个无向图G中V能分成两个点集A与B,且位于A中的顶点互相之间没有边,位于B中的顶点互相之间没有边,则称这个图为二分图。

一个图是二分图 <==> 不存在奇环
如果出现奇环 -> 不是二分图
如果没有奇环 ->   是二分图

直接深度优先搜索,一旦出现矛盾:不是二分图,没出现矛盾:是二分图

二分图的判别方法:利用DFS将这个二分图进行染色,若染色成功,则这个图为二分图,否则不是二分图。将每个点A裂成两个点A与A’。若A与B之间存在边,则连一条A与B’的边,A’与B的边。若此时A与A’连通,或者B与B’连通,则该图不是二分图。(若连通则必然出现了奇环),利用并查集实现即可。

假设这个图是联通
cin>>n>>m;
vector <int> E[100005];  //n<=100000  E[i]表示左栏编号为i的那一行
while (m--)
{
	cin>>u>>v; 
	E[u].push_back(v);
	E[v].push_back(u);
}
void dfs(int x,int y)
{
	for (int i=0; i<E[x].size(); i++)
	{
		if (!v[E[x][i]])
		{
			v[E[x][i]]=true;
			col[E[x][i]]=1-y;
			dfs(E[x][i],1-y);
		}
		else 
			if (col[x]==col[E[x][i]]) {puts("不是二分图"); }
	}
}
for (i=1; i<=n; i++) v[i]=false; v[1]=true;
col[1]=0;
dfs(1,0);

例:关押罪犯

有n个人,存在m对敌对关系,即Ai与Bi之间敌对值Ci。将这些人分为两批,使得在同一批中敌对值最大的那个值最小。

思路:枚举在同一批这个敌对值最大的数是多少,假设是x;也就是说敌对值超过x的那些人,不能被分配到一起了。而敌对值<=x的那些人,是可以被分配到一起。也就是说敌对值超过x的那些人,连一条边,判断能否构成二分图,如果能构成二分图,说明x是可行,  如果构不成二分图,说明x是不行的
 

8、LCA

 

LCA(Least Common Ancestors),即最近公共祖先,是指在有根树中,找出某两个结点u和v最近的公共祖先。 ———来自百度百科

https://blog.csdn.net/jeryjeryjery/article/details/52853017

例如:

此处输入图片的描述

在这棵树中 17 和 8 的LCA就是 3 。9 和 7 的LCA就是 7 。

明白了LCA后,就下来我们就要探讨探讨LCA怎么求了 qwq

  • 暴力算法 以 17 和 18 为例,既然要求LCA,那么我们就让他们一个一个向上爬(我要一步一步往上爬 —— 《蜗牛》),直到相遇为止。,第一次相遇即是他们的LCA。 模拟一下就是: 17->14->10->7->3 18->16->12->8->5->3 最终结果就是 3。 当然这个算法妥妥的会T飞掉,那么我们就要进行优化,于是就有了用倍增来加速的倍增LCA,这也是我们今天介绍的重点。
  • 倍增算法 所谓倍增,就是按2的倍数来增大,也就是跳 1、2、4 、8 、16、32 …… 不过在这我们不是按从小到大跳,而是从大向小跳,即按……、32、16、8、4、2、 1、如果大的跳不过去,再把它调小。这是因为从小开始跳,可能会出现“悔棋”的现象。拿 5 为例,从小向大跳,5≠1+2+4,所以我们还要回溯一步,然后才能得出5=1+4;而从大向小跳,直接可以得出5=4+1。这也可以拿二进制为例,5(101),从高位向低位填很简单,如果填了这位之后比原数大了,那我就不填,这个过程是很好操作的。 还是以 17 和 18 为例: 17->3 18->8->3 可以看出向上跳的次数大大减小了。这个算法的时间复杂度为O(NlogN),已经很不错,可以满足大部分的需求了。 想要实现这个算法,首先我们要记录各个点的深度和他们2<sup>i</sup>级的的祖先,用数组deepth表示每个节点的深度,fa[i][j]表示节点i的2<sup>j</sup>级祖先。

给定n个数,有Q次询问,每次询问一段区间的最小值。

倍增。线段树。分块。整体二分。

简单做法:

void dfs(int x,int y) // 节点x深度为y
{
	dep[x]=y;
	for (int i=0; i<E[x].size(); i++) dfs(E[x][i],y+1);
}
求出每个点的深度
dfs(1,1);
cin>>m;
while (m--)
{
	cin>>A>B;
	while (A!=B)
	{
			if (dep[A]>dep[B]) A=fa[A]; 
			else B=fa[B];
	}
	printf("%d\n",A);
}

每次询问的复杂度  是  O(树的深度)  这么多

倍增是个什么东西?我们考虑记录a[x]~a[x+2^k-1]的最小值,令其为f[x][k]。这可以在nlgn的时间内求出。

对于每次询问L~R,令k=log[R-L+1]。有ans=min(f[L][k],f[R-2^k+1][k])。(即进行O(1)询问)

类似st表

f[j][i] 表示 j这个节点向上走2^i步后会到达哪里

为了简化问题,我们假设x,y在同一高度。令f[i][j]表示i向上跳2^j步的父亲是啥。将k从logn枚举到0,若f[x][k]与f[y][k]不同,则将x跳至f[x][k],将y跳至f[y][k]。最后若x=y,则LCA=x,否则LCA=f[x][0]。

for (i=1; i<=n; i++) f[i][0]=fa[i];
lg=int(log(n)/log(2));
for (i=1; i<=lg; i++)
	for (j=1; j<=n; j++) f[j][i]=f[f[j][i-1]][i-1];
通过f数组求出两个点的最近公共祖先。

先让两个点跳到同一层,  然后再一起向上跳,直到遇到公共祖先为止。

cin>>A>>B; // 假设dep[A]>dep[B]  A的深度比B要高
if (dep[A]<dep[B]) swap(A,B);
cha = dep[A]-dep[B];
for (i=lg; i>=0; i--) 
	if (cha>=(1<<i))
	{
		A=f[A][i];
		cha-=(1<<i);
	}

最终cha=0  也就意味着 dep[A]=dep[B]

for (i=lg; i>=0; i--)
	if (f[A][i]==f[B][i]); else 
      A=f[A][i],B=f[B][i];  //让A和B尽可能的逼近最近公共祖先,但就是不到达它
if (A==B) cout<<A; else cout<<f[A][0];
它们一开始就相等   它们一开始不相等

假设A与LCA距离差x,把(x-1)写作二进制,i在枚举的过程遇到二进制中的1,才会往上跳。跳完后,相当于走了x-1 步,所以再走一步就是LCA了

 

链上最大值问题:

f[j][i] j这个点向上跳2^i步会到哪儿       g[j][i] j这个点向上跳2^i步 经过的最长边是多少

通过求LCA,问题转化为

要求 x~y的链的最长边  y是x的祖先

同样的令f[i][j]表示i向上跳2^j能跳到哪儿。也令g[i][j]表示i向上跳2^j的过程中遇到的最长的边是多少。现在我们先求出两个点x,y的LCA。用倍增的方法求出x到LCA的最长边,y到LCA的最长边就可以了。

for (i=1; i<=n; i++) f[i][0]=fa[i],g[i][0]=b[i]; // b[i]表示i与fa[i]这一条边的边权
lg=int(log(n)/log(2));
for (i=1; i<=lg; i++)
	for (j=1; j<=n; j++) 
	{
		f[j][i]=f[f[j][i-1]][i-1];
		g[j][i]=max(g[j][i-1],g[f[j][i-1]][i-1]);
	}
cha=dep[x]-dep[y]
for (i=lg; i>=0; i--)
	if (cha>=(1<<i))
	{
		MAX=max(MAX,g[x][i]);
		cha-=(1<<i);
	}

例:reward

老板给员工发工资,工资由初始工资与奖金组成。初始工资为888元,奖金为任意非负整数。有些员工比较奇怪,认为自己的工资需要比别人的工资高(因为他来自新东方),老板要在满足每个人的前提下分出的钱最少。保证有解。求至少多少钱。n,m<=50000

思路:d[i]  i要比d[i]个人工资要高
分0块钱的那些人,d值一定等于0,把这些人扔掉,并且把相应的d值减去
分1块钱的那些人,d值一定等于0。对其进行拓扑排序。一个点的工资为它指向的所有点的工资最大值+1。累加答案。

例:货车运输

给定一个n个点m条带权边的图。定义一条路径的值为这条路径上经过的边的最小值。有Q组询问,每组询问形如x到y所有路径的最大值。n,m,Q<=30000

路径显然在最大生成树上。
建立最大生成树。问题转化为链上最小值问题。
这个问题是可以通过树上倍增来实现的。

假定 kruskal 是正确的,值最大的路径  一定在  最大生成树上   反证法
求出最大生成树
用倍增求链上最小值
在一棵树上,任意两个点x,y,恰好有一条简单路径(不经过一个点两次)
要求任意两点的最短路之和:
如何求一个图的最短路:
枚举每个点,跑BFS,就能直接算出其它点到这个点的最短路。 直到了n对点对的最短路的值
n * m 
加起来这个点对答案的贡献。
每个点对答案的贡献之和就是图的最短路。
先枚举j作为根节点,得到宽度优先搜索树,再枚举所有边i,如果是树边则f[i][j]=1(表示会产生影响),否则f[i][j]=0
f[i][j] 表示  删除i这条边  对j为根的宽度优先搜索树是否会产生影响
枚举每条被删除的边i,再枚举所有点j作为根节点,如果f[i][j]=0 ,则直接更新答案,否则重新以j作为根跑bfs,然后再更新答案

 

例:图的最短路

我们定义一张图的最短路为任意两点的最短路之和。给定一个无权无向图,求每条边被删除时的图的最短路。n<=100,m<=3000。

对于每个点求出最短路树,这棵树一定有n-1条边,对于每条边被炸毁,若在最短路树上,则重新求一次最短路,否则直接统计答案。最短路可以用BFS代替。因此总复杂度为n^2m。

void get_tree(int x)
{
	for (int i=1; i<=n; i++) v[i]=false; //v[i]表示i是否被宽搜过
	v[x]=true; l=1; r=1; s[1]=x; // s是一个队列
	while (l<=r)
	{
		now=s[l]; l++;
		for (int i=0; i<E[now].size(); i++)
			if (!v[E[now][i]])
			{
				v[E[now][i]]=true;
				r++; s[r]=E[now][i];
				tree[ID[now][i]]=1;  //ID表示这条边的编号
			}
	}
}

for (j=1; j<=n; j++)
{
	get_tree(j); 
	sum[j]=get(j); //sum[j]=j到其它点的最短路的值之和
	for (i=1; i<=m; i++)
		if (tree[i]) f[i][j]=1; else f[i][j]=0;
}
for (i=1; i<=m; i++)
{
	ans=0;
	for (j=1; j<=n; j++)
		if (f[i][j]==0) ans+=sum[j]; else 
		{
			delete(i); //屏蔽掉i这条边
			ans+=get(j);
			insert(i); //加入这条边
		}
	printf("%d\n",ans);
}

9、前缀和优化
 

(1)n个数,每次查询一段区间的和。利用前缀和优化做到O(1)查询。

(2)n个数,一开始都是0,总共有m次操作,每次操作一段区间的值,操作方法是所有数+k。求最终这n个数会变成什么

5
3
1 3 2   //[1,3] +=2
2 4 3   //[2,4] +=3
4 5 4   //[4,5] +=4                    输出:2 5 5 7 4
要求一个O(n+m)的做法

//s[i]=a[i]-a[i-1]
cin>>m;
while (m--)
{
cin>>L>>R>>k;
s[L]+=k; s[R+1]-=k;
}
//将每次操作都在O(1)时间完成了。

差值加速
求最终这n个数会变成什么,但是我们现在只知道最终这n个数两两之间的差值是什么
s[1]=a[1]                        s[1]=a[1]-a[0]  a[0]=0  -> s[1]=a[1]
s[2]=a[2]-s[1]                   s[2]=a[2]-a[1]  a[1]=s[1] -> s[2]=a[2]-s[1]
s[3]=a[3]-s[2]-s[1]
s[4]=a[4]-s[3]-s[2]-s[1]   ->

a[1]=s[1]
a[2]=s[1]+s[2]
a[3]=s[1]+s[2]+s[3]
a[4]=s[1]+s[2]+s[3]+s[4]
a就是s的前缀和

for (i=1; i<=n; i++) a[i]=a[i-1]+s[i];

(3)给定一棵n个点带边权的树,初始所有边权都为0,共有m次操作,每次操作将一条链上的所有边边权增加k。求最终,所有边的边权会是多少。

要求一个复杂度低于nm的做法。

s[i]表示i与它父亲的边的边权 - sum{i与它儿子的边的边权}
对树进行搜索,就能通过s来还原得到a
(x,y)这条链所有边增加边权k
s[x]+=k  s[y]+=k   s[LCA(x,y)]-=2*k

时间复杂度是求LCA的时间复杂度,总时间复杂度是n+mlgn

拓展:二分图最大匹配的匈牙利算法;Tarjan求强连通分量

NOIP
一些模板  
对于一道题目  转化好模型  直接套模板

1、a[20][100000]  速度会快
a[100000][20]
 

2、

for (i=1; i<=n; i++)      速度会快

for (j=1; j<=m; j++)
a[i][j]=i+j;
O(nm)
for (j=1; j<=m; j++)
    for (i=1; i<=n; i++)
a[i][j]=i+j;
O(nm)  5W 1秒
循环展开
假设m是8的倍数
for (i=1; i<=n; i++)
for (j=1; j<=m; j+=8)
{
a[i][j]=i+j;
a[i][j+1]=i+j+1;
a[i][j+2]=i+j+2;
a[i][j+3]=i+j+3;
a[i][j+4]=i+j+4;
a[i][j+5]=i+j+5;
a[i][j+6]=i+j+6;
a[i][j+7]=i+j+7;
}

 

3、

利用的好常数  
n<=100000
n^2 可以过

4、
对n个数相加,然后对MOD取模
for (i=1; i<=n; i++)
sum=(sum+a[i])%MOD;
for (i=1; i<=n; i++)  快很多
{
sum+=a[i];
if (sum>=MOD) sum-=MOD;
}
 

5、

n*2

n<<1  快很多
(l+r)/2

(l+r)>>1 快很多

10、二分图,匈牙利算法

https://www.cnblogs.com/shenben/p/5573788.html
https://blog.csdn.net/cillyb/article/details/55511666

11、强连通分量

https://blog.csdn.net/qq_16234613/article/details/77431043

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值