- 图的存储
-
邻接矩阵
map[i][j] 表示第 i 个点与第 j 个点连有一条边,如有权值就把矩阵内元素的值赋成权值。空间复杂度O(n2),时间复杂度O(n2)
void Adjacency matrix() {
for(int i=1;i<=m;i++) {
int u,v,c;
scanf("%d%d%d",&u,&v,&c);
map[u][v]=c;
}
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++) {
}
}
-
邻接表
使用vector存储,开n个vector,每个代表一个节点,它后面的数就是这个节点能连向哪个节点。空间复杂度会因为不会扫到没有用的边而降到O(m)
void Adjacency list() {
vector <int> ve[MAXN];
for(int i=1;i<=m;i++) {
int u,v,c;
scanf("%d%d%d",&u,&v,&c);
ve[u].push_back(v);
}
for(int i=1;i<=n;i++)
for(int j=1;j<=ve[i].size();j++) {
}
}
-
边表(前向星)
边表是图的一种存储结构,用来描述图上的每一个点。对图的每个边进行编号,对图的每个顶点建立一个链表(n个顶点建立n个链表),第i个容器中的结点包含以顶点Vi为起点的所有边的编号。
边表与邻接表的区别:边表存储了以点为起点的边的信息,邻接表存储了以点为出发点的点的信息。
struct Edge {
int nxt,to,cost;
}e[MAXN];
void insert(int u,int v,int w) {
num++;
e[num].to=v;
e[num].cost=w;
e[num].nxt=head[u];
head[u]=num;
}
int main() {
for(int i=1;i<=m;i++) {
int u,v,w;
scanf("%d%d%d",&u,&v,&w);
insert(u,v,w);
}
for(int i=head[u];i;i=e[i].nxt) {
}
}
- 最短路问题
-
Dijkstra
迪杰斯特拉算法是由荷兰计算机科学家Dijkstra于1959 年提出的,因此又叫Dijkstra算法。是从一个顶点到其余各顶点的最短路径算法,解决的是有向图中最短路径问题。迪杰斯特拉算法主要特点是以起始点为中心向外层层扩展,直到扩展到终点为止。
时间复杂度为O(n2),不能处理存在负边权的情况,求单源最短路。 -
算法实现:
每次找到一个能到达的最近的点,来更新到达其他点的距离。如图:
设起点为s,dis[v]表示从s到v的最短路径,pre[v]为v的前驱节点,用来输出路径。
初始化:dis[v]=∞(v≠s); dis[s]=0; pre[s]=0;
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;
}
算法结束:dis[v]为s到v的最短距离;pre[v]为v的前驱节点,用来输出路径。
-
代码实现:
const int MAXINT = 32767;
const int MAXNUM = 10;
int dist[MAXNUM];
int prev[MAXNUM];
int A[MAXUNM][MAXNUM];
void Dijkstra(int v0) {
bool S[MAXNUM]; // 判断是否已存入该点到S集合中
int n=MAXNUM;
for(int i=1; i<=n; ++i) {
dist[i] = A[v0][i];
S[i] = false; // 初始都未用过该点
if(dist[i] == MAXINT)
prev[i] = -1;
else
prev[i] = v0;
}
dist[v0] = 0;
S[v0] = true;
for(int i=2; i<=n; i++) {
int mindist = MAXINT;
int u = v0;
for(int j=1; j<=n; ++j) // 找出当前未使用的点j的dist[j]最小值
if((!S[j]) && dist[j]<mindist) {
u = j; // u保存当前邻接点中距离最小的点的号码
mindist = dist[j];
}
S[u] = true;
for(int j=1; j<=n; j++)
if((!S[j]) && A[u][j]<MAXINT) {
if(dist[u] + A[u][j] < dist[j]) { //在通过新加入的u点路径找到离v0点更短的路径
dist[j] = dist[u] + A[u][j]; //更新dist
prev[j] = u; //记录前驱顶点
}
}
}
}
-
Floyd
Floyd算法又称为插点法,是一种利用动态规划的思想寻找给定的加权图中多源点之间最短路径的算法,与Dijkstra算法类似。该算法名称以创始人之一、1978年图灵奖获得者、斯坦福大学计算机科学系教授Robert W·Floyd命名。
时间复杂度O(n3),可以处理所有图,多源最短路。
算法实现:
假设有三个点 i,j,k,i连向 j,i 连向 k,k 连向 j,i 到 j 的最短路就是 i 到 j 的路径和 i 到 k 的路径加 k 到 j 的路径和较短的那个。
代码实现:
void floyd() {
memset(dist,0x3f,sizeof(dist));
memset(map,0x3f,sizeof(map));
for(int k=1;k<=n;k++)
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++) {
dist[i][j]=min(map[i][j],map[i][k]+map[k][j]);
}
}
-
SPFA
SPFA(Shortest Path Faster Algorithm)(队列优化)算法是求单源最短路径的一种算法,它还有一个重要的功能是判负环(在差分约束系统中会得以体现),在Bellman-ford算法的基础上加上一个队列优化,减少了冗余的松弛操作,是一种高效的最短路算法。
期望时间复杂度O(ME)其中M为所有顶点进队的平均次数,可以证明M一般小于等于2n: “算法编程后实际运算情况表明m一般没有超过2n.事实上顶点入队次数M是一个不容易事先分析出来的数,但它确是一个随图的不同而略有不同的常数.所谓常数,就是与E无关,与n也无关,仅与边的权值分布有关.一旦图确定,权值确定,原点确定,E就是一个确定的常数.所以SPFA算法复杂度为O(E).证毕"(SPFA的论文)。 不过这个证明好像是错误的,所以SPFA的复杂度纯粹看脸,一般还是比较快的。出题人我给你糖吃。
单源最短路,不过可以处理有负边权的情况,但是无法处理负环图,可判断负环。(如果某个点进入队列的次数超过N次则存在负环,一般用dfs判断)
-
算法实现:
我们采取的方法是动态逼近法:设立一个先进先出的队列用来保存待优化的结点,优化时每次取出队首结点u,并且用u点当前的最短路径估计值对离开u点所指向的结点v进行松弛操作,如果v点的最短路径估计值有所调整,且v点不在当前的队列中,就将v点放入队尾。这样不断从队列中取出结点来进行松弛操作,直至队列空为止。
优化:
SPFA算法有两个优化策略SLF和LLL——SLF:Small Label First 策略,设要加入的节点是j,队首元素为i,若dist(j)<dist(i),则将j插入队首,否则插入队尾; LLL:Large Label Last 策略,设队首元素为i,队列中所有dist值的平均值为x,若dist(i)>x则将i插入到队尾,查找下一元素,直到找到某一i使得dist(i)<=x,则将i出队进行松弛操作。 SLF 可使速度提高 15 ~ 20%;SLF + LLL 可提高约 50%。
-
代码实现:
- 判断负环:
int spfa_dfs(int u) {
vis[u]=1;
for(int k=f[u]; k!=0; k=e[k].next) {
int v=e[k].v,w=e[k].w;
if(dis[u]+w<dis[v]) {
dis[v]=dis[u]+w;
if(!vis[v]) {
if(spfa_dfs(v))
return 1;
} else
return 1;
}
}
vis[u]=0;
return 0;
}
- 一般SPFA:
void spfa(int s) {
for(int i=0;i<=10010;i++)dist[i]=2147483647;
queue<int> q;
dist[s]=0;
vis[s]=1;
q.push(s);
while(!q.empty()) {
int u=q.front();
q.pop();
vis[u]=0;
for(int i=head[u];i;i=e[i].next) {
int to=e[i].to;
if(dist[to]>dist[u]+e[i].cost) {
dist[to]=dist[u]+e[i].cost;
if(!vis[to]) {
vis[to]=1;
q.push(to);
}
}
}
}
}
- SLF优化:
void spfa(int s) {
fill(dis,dis+n+1,2147483647);
deque <int> q;
dis[s]=0;
vis[s]=1;
q.push_back(s);
while(!q.empty()) {
int u=q.front();
q.pop_front();
vis[u]=0;
for(int i=head[u];i;i=e[i].nxt) {
int to=e[i].to;
if(dis[to]>dis[u]+e[i].cost) {
dis[to]=dis[u]+e[i].cost;
if(!vis[to]) {
vis[to]=1;
if(!q.empty() && dis[to]<dis[q.front()]) q.push_front(to);
else q.push_back(to);
}
}
}
}
}
- SLF + LLL优化:
void spfa(int s) {
fill(dis,dis+n+1,2147483647);
deque <int> q;
int tot=1,sum=0;
dis[s]=0;
vis[s]=1;
q.push_back(s);
while(!q.empty()) {
int u=q.front();
q.pop_front();
if(dis[u]*tot>sum) {
q.push_back(u);
continue;
}
tot--;
sum-=dis[u];
vis[u]=0;
for(int i=head[u];i;i=e[i].nxt) {
int to=e[i].to;
if(dis[to]>dis[u]+e[i].cost) {
dis[to]=dis[u]+e[i].cost;
if(!vis[to]) {
vis[to]=1;
tot++;
sum+=dis[to];
if(!q.empty() && dis[to]<dis[q.front()]) q.push_front(to);
else q.push_back(to);
}
}
}
}
}
- 拓扑排序
如果存在一个排列a1,a2,…,an,使得在该图中不存在ai到aj的路径(i>j),我们称这个排列为这个图的拓扑序列。我们每次寻找入度为 0 的点加入序列中。并将当前点连接的所有边均删除,更新其它点的度数。由于每条边至多被删除一次。因此这个时间复杂度是 O(|E|) 的。
bool topsort() {
queue<int>q;
for(int i=1;i<=n;i++) if(du[i]==0) q.push(i);
while(!q.empty()) {
now=q.front();
q.pop();
k++;
for(int i=p[now]; i!=-1; i=e[i].next) {
go=e[i].to;
du[go]--;
if(du[go]==0)
q.push(go);
}
}
if(k==n) return true;
else return false;
}
- 并查集
字面意思,可供合并和查找的集合。常用于判断两个点是否联通。
int find(int x) {
if(fa[x]!=x) return fa[x]=find(fa[x]);
}
void merge(int x,int y) {
fa[find(x)]=fa[find(y)];
}
- 二分图
如果一个无向图G中V能分成两个点集A与B,且位于A中的顶点互相之间没有边,位于B中的顶点互相之间没有边,则称这个图为二分图。
-
二分图染色(判断是否为二分图)
vector <int> v[N];
void dfs(int x,int y)
{
col[x]=y;
for (int i=0; i<v[x].size(); i++)
{
if (!col[v[x][i]]) dfs(v[x][i],3-y);
if (col[v[x][i]]==col[x]) FLAG=true;
}
}
void color() {
for (i=1; i<=n; i++) col[i]=0;
for (i=1; i<=n; i++) if (!col[i]) dfs(i,1);
if (FLAG) cout<<"NO"; else cout<<"YES";
}
col[i]=1 i这个点染成黑色 col[i]=2 i这个点染成白色 col[i]=0 i这个点还未被染色
-
裂点
首先明确:二分图不存在奇环。
将每个点A裂成两个点A与A’。
若A与B之间存在边,则连一条A与B’的边,A’与B的边。
若此时A与A’连通,或者B与B’连通,则该图不是二分图。(若连通则必然出现了奇环)
int find(int k) {
return f[k]==k?f[k]:f[k]=find(f[k]);
}
for (i=1; i<=n; i++) p[i][0]=++cnt,p[i][1]=++cnt;
for (i=1; i<=cnt; i++) f[i]=i;
for (i=1; i<=m; i++) {
// u-v
f[find(p[u][0])]=find(p[v][1]);
f[find(p[u][1])]=find(p[v][0]);
if (find(p[u][0])==find(p[u][1]) || find(p[v][0])==find(p[v][1])) FLAG=true;
}
if (FLAG) puts("NO");
else puts("YES");
-
二分图最大匹配
-
匈牙利算法
顾名思义,在一个二分图中选择最多的边,使得没有任何一个点有连接它的两条边被选择到。
有四个男生,四个女生,每个男生都对几个女生有好感,我们的目标是满足尽量多的男生的要求。
先给1号男生找女生,找到了女1号。
给2号男生找女生,找到了女2号。
给3号男生找女生,找到女1号,先抢了试试。
1号男生没了女生,找下一个,找到女2号,抢了!
2号男生没了女生,找到了女3号!
给4号男生找女生,发现无论如何也做不到不拆散别人的情况下给他分配一个女生了。
用递归实现上述过程。
bool dfs(int now) {
for (int a=1; a<=m; a++)
if (match[now][a] && !use[a]) {
use[a]=true;
if (!result[a] || dfs(result[a])) {
result[a]=now;
return true;
}
}
return false;
}
void xiongyali() {
int ans=0;
for (int a=1; a<=n; a++) {
memset(use,false,sizeof(use));
if (dfs(a)) ans++;
}
}
如果让你求一个答案答案一定在 ans , n - ans , m - ans , n + m - ans 之中。
- 最小生成树
一个有 n 个结点的连通图的生成树是原图的极小连通子图,且包含原图中的所有 n 个结点,并且有保持图连通的最少的边。
-
Kruskal
思想:贪心的去寻找最短的边,然后用并查集维护。
稀疏图Kruskal跑的飞快,时间复杂度大约O(nlogn)
int comp(E a,E b) {
return a.v<b.v;
}
int find(int x) {
if(fa[x]!=x) return fa[x]=find(fa[x]);
}
void merge(int x,int y) {
fa[find(x)]=fa[find(y)];
}
void Kruskal() {
for(int i=1;i<=n;i++) fa[i]=i;
sort(e+1,e+1+m,comp);
for(int i=1,k=0;i<=m;i++) {
if(find(e[i].x) != find(e[i].y)) {
merge(e[i].x,e[i].y);
k++;
tot+=e[i].v;
}
if(k == n-1) {
cout<<tot;
return ;
}
}
cout<<"orz";
}
- 强连通分量
简单来说强联通分量就是同一个图中能够互相联通的几个点。可以实现图的缩点。
-
Tarjan
思想:以DFS为基础,我们定义DFN[x]为搜索到x时的时间戳(即搜索到的时间)。LOW[x]为搜索树中x以及它的子孙可以访问到的最早祖先。有LOW[x]=min(DFN[x],DFN[j],LOW[k]),其中存在边(x,j),(x,k),j为x的祖先,k为x的子孙。令v[i]表示i是否已被搜索过以及是否找到了极大强连通分量。若i已经找到了极大强连通分量或者还未被搜索过,则v[i]=0,否则v[i]=1。
①找到一个未被搜索过的节点u,若不存在,退出,否则以这个节点为根开始建立搜索树。
②给该节点建立时间戳,将x压入栈中,枚举该节点的所有儿子x,若其儿子没被访问过,则搜索其儿子(重复②),之后更新LOW[u]=min(LOW[u],LOW[x]),否则若v[x]=1(表示x是u的祖先),则更新LOW[u]=min(LOW[u],DFN[x])。
③对u搜索完毕后,若LOW[u]=DFN[u],则找到了一个极大强连通分量,栈顶到u都为一个极大强连通分量。更新v与栈。
-
代码实现:
void dfs(int x) {
DFN[x]=++Time;
LOW[x]=DFN[x];
st[++r]=x;
int R=r;
V[x]=true;
for (int i=0; i<v[x].size(); i++)
if (v[x][i]!=fa[x]) {
fa[v[x][i]]=x;
if (!DFN[v[x][i]]) {
dfs(v[x][i]);
LOW[x]=min(LOW[x],LOW[v[x][i]]);
} else if (V[v[x][i]]) LOW[x]=min(LOW[x],DFN[v[x][i]]);
}
if (LOW[x]==DFN[x]) {
cnt++;
for (int i=R; i<=r; i++) {
p[st[i]]=cnt;
V[st[i]]=false;
}
r=R-1;
}
}
cnt 第cnt个极大强联通分量 p[i] i所处的是哪个极大强联通分量
LOW[x]==DFN[x] 已出现一个极大强联通分量
缩点:
for (i=1; i<=m; i++)
if (p[u[i]]!=p[v[i]])
merge(p[u[i]],p[v[i]]);
- 最近公共祖先(LCA)
Lowest Common Ancestors,对于有根树T的两个结点u、v,最近公共祖先LCA(T,u,v)表示一个结点x,满足x是u、v的祖先且x的深度尽可能大。
-
离线算法 Tarjan
没写过。
在一次遍历中把所有询问一次性解决,时间复杂度是O(n+q)。
算法流程:
1.任选一个点为根节点,从根节点开始。
2.遍历该点u所有子节点v,并标记这些子节点v已被访问过。
3.若是v还有子节点,返回2,否则下一步。
4.合并v到u上。
5.寻找与当前点u有询问关系的点v。
6.若是v已经被访问过了,则可以确认u和v的最近公共祖先为v被合并到的父亲节点a。
遍历的话需要用到dfs来遍历,至于合并,最优化的方式就是利用并查集来合并两个节点。
void tarjan(int x) {
for(int i=0;i<tree[x].size();++i) {
tarjan(tree[x][i]);
merge(x,tree[x][i]);
anc[find(x)] = x;
}
vis[x] = true;
for(int i=0;i<query[x].size();++i) {
if(vis[query[x][i]])
printf("the LCA for %d and %d is %d\n", x, query[x][i], anc[find(query[x][i])]);
}
}
-
在线算法 倍增
找最近公共祖先这问题很容易想到让两节点一起往上走最后相遇,但这样的dfs显然很慢,于是就需要倍增。就是用二进制的思维,以1,2,4,8等2的阶层步长接近答案,比一步一步向上要快很多。
所以要dfs出来点的2^k的父亲节点与该节点的深度。
找lca时先将下面的点升到与另一点同一深度,再用往上倍增找lca。
有两种大同小异的方法:一种是以上一步2倍长的步伐向上试,不行再缩减,找到一个离lca能达到的最近点。另一种是先求出最大深度是2的几次方,再以当前最大步伐向上走。
int lca(int x,int y) {
if(deep[x]>deep[y]) swap(x,y);
for(int i=20; i>=0; i--) {
if(deep[fa[y][i]]>=deep[x]) y=fa[y][i];
}
if(x==y)return x;
for(int i=20; i>=0; i--) {
if(fa[x][i]!=fa[y][i]) {
x=fa[x][i],y=fa[y][i];
}
}
return fa[x][0];
}
void dfs(int x) {
deep[x]=deep[fa[x][0]]+1;
for(int i=0; fa[x][i]; i++)
fa[x][i+1]=fa[fa[x][i]][i];
for(int i=0; i<vec[x].size(); i++)
if(!deep[vec[x][i]]) {
fa[vec[x][i]][0]=x;
dfs(vec[x][i]);
}
}
-
树剖
size[x]为以x为结点的子树的结点的个数 每个结点和它的儿子之间只有一条重边,这个重边连向它的儿子中size最大的那个儿子。
如果结点u和它的儿子v之间是一条轻边,那么size[v]2<size[u]。
top[i]记录i结点所在链的链头,如果top[u]!=top[v]说明u v不在一条链上 将 u=fa[u],重复上述操作;当它们在一条链上时,深度较小的那个点为他们的lca。*
void dfs(int x) {
size[x]=1;
deep[x]=deep[fa[x]]+1;
for(int i=0; i<vec[x].size(); i++) {
if(fa[x]!=vec[x][i]) {
fa[vec[x][i]]=x;
dfs(vec[x][i]);
size[x]+=size[vec[x][i]];
}
}
}
void Get_heavy_edge(int x) {
int s=0;
if(!top[x]) top[x]=x;
for(int i=0; i<vec[x].size(); i++) {
if(fa[x]!=vec[x][i]&&size[vec[x][i]]>size[s])s=vec[x][i];
}
if(s) {
top[s]=top[x];
Get_heavy_edge(s);
}
for(int i=0; i<vec[x].size(); i++) {
if(fa[x]!=vec[x][i]&&vec[x][i]!=s) Get_heavy_edge(vec[x][i]);
}
}
int lca(int x,int y) {
for(; top[x]!=top[y];) {
if(deep[top[x]]<deep[top[y]]) swap(x,y);
x=fa[top[x]];
}
if(deep[x]>deep[y]) swap(x,y);
return x;
}