搜索与图论
1.深度优先搜索
- 用的是栈
- 空间是O(h)
- 不具有最短路性质
回溯:恢复现场
剪枝:不是最优解,或者不是可行解,就直接剪枝(不搜索)
2.广度优先搜索
- 一层一层搜索
- 用的是队列
- 空间是O( 2 h 2^h 2h),指数级别
- 具有最短路性质(第一次搜到的符合条件的点,一定是最近的点)
- 边权都是 1 的时候,才能用 B F S BFS BFS解决最短路问题
queue = 初始化
while queue 不空
{
t = 队头
拓展 t
}
3.图的存储
- 树是一种无环连通图
邻接矩阵
- 空间复杂度高
- 适合稠密图
邻接表
- 其实就是哈希表的拉链法
还可以用结构体存边(边的两端点及边的权重)
4.图的遍历
- 时间复杂度 O ( n + m ) O(n+m) O(n+m) , n n n 表示点数, m m m 表示边数
深度优先遍历
- 基本代码框架
int dfs(int k)
{
st[k]=1;
for(int i = h[k];i!=-1;i=ne[i])
{
int j = e[i];
if(!st[j])
dfs(j);
}
}
广度优先遍历
- 基本代码框架
queue<int> q;
q.push(1);
st[1]=1;
while(!q.empty())
{
int t = q.front();
q.pop();
for(int i = h[t];i!=-1;i=ne[i])
{
int j = e[i];
if(!st[j])
{
st[j]=1;
q.push(j);
}
}
}
5.拓扑排序
-
必须是有向图
-
按照拓扑序排好之后,所有的边都是从前指向后的
-
并不是所有图都有拓扑序(有环)(有向无环图一定存在拓扑序列)
-
时间复杂度是 O ( n + m ) O(n+m) O(n+m)
int topsort()
{
for(int i = 1;i<=n;i++)
{
if(!d[i]) //d[i] 表示 点 i 的入度
q[++tt]=i; //即把入度为 0 的点存入队列
}
while(hh<=tt)
{
int t = q[hh++];
for(int i = h[t];i!=-1;i=ne[i])
{
int j = e[i]; //遍历新的结点
d[j]--; //该节点的入度--
if(!d[j]) //如果该结点的入度变为0,就是存入队列
{
q[++tt] = j;
}
}
}
if(tt == n-1) //如果队列里面放了n-1个,即前n-1个都满足条件,所以最后一个也满足条件,所以符合拓扑排序
return 1;
return 0;
}
6.最短路问题
难点在建图
单源最短路
所有边权都是正数
朴素迪杰斯特拉算法( d j i k s t r a djikstra djikstra)
- 时间复杂度 O ( n 2 ) O(n^2) O(n2),与边数没有关系,适合稠密图
思路:
-
设置
dist[i]
数组 代表第 i i i 个点到起点的最短距离 -
设置
f[i]
数组 代表已经确定第 i i i个点是 1 1 1~ n n n的最短路上的点 -
首先初始化
dist[N]
数组为正无穷,并令dist[1] = 0
-
然后遍历 n − 1 n-1 n−1 遍,(因为剩下的最后一个点没有必要作为基点更新最短路了)
-
每次遍历都要先找到满足条件的点,然后用来更新
找到满足条件的点:没有被访问过并且距离可以变小
int t = 0;
for (int j = 1; j <= n; j++)
{
if (!f[j] && dist[t] > dist[j])
t = j;
}
更新
for (int j = 1; j <= n; j++)
{
dist[j] = min(dist[j], dist[t] + g[t][j]);
}
f[t] = 1;
代码:
#include<bits/stdc++.h>
#define N 1010
using namespace std;
int n, m;
int g[N][N];
int dist[N];
bool f[N];
int dijkstra()
{
dist[1] = 0;
for (int i = 1; i < n; i++)
{
int t = 0;
for (int j = 1; j <= n; j++)
{
if (!f[j] && dist[t] > dist[j])
t = j;
}
for (int j = 1; j <= n; j++)
{
dist[j] = min(dist[j], dist[t] + g[t][j]);
}
f[t] = 1;
}
if (dist[n] > 0x3f3f3f3f/2)
return -1;
return dist[n];
}
int main()
{
cin >> n >> m;
memset(g, 0x3f, sizeof g);
memset(dist, 0x3f, sizeof dist);
for (int i = 1; i <= m; i++)
{
int a, b, c;
cin >> a >> b >> c;
g[a][b] = min(g[a][b], c);
}
cout << dijkstra() << "\n";
return 0;
}
堆优化版的迪杰斯特拉算法
-
时间复杂度是 O ( m l o g n ) O(mlogn) O(mlogn)
-
适用于稀疏图
怎么优化?
- 把遍历求最小值的操作,变为优先队列的操作
代码:
#include<bits/stdc++.h>
#define N 100010
using namespace std;
typedef pair<int, int> PII;
int n, m;
int h[N], e[N], ne[N], w[N], idx;
int dist[N];
bool f[N];
priority_queue<PII, vector<PII>, greater<PII>> q;
int dijkstra()
{
q.push({ 0,1 }); //到1号点的距离是0,注意这里的顺序不能变!!!(因为pair<int,int> 的排序是先根据first,再根据second)
dist[1] = 0;
while (!q.empty())
{
PII t = q.top();
q.pop();
int b = t.second;
if (f[b]) //如果b这个点被访问过了吗,可以直接continue
continue;
f[b] = 1;
for (int i = h[b]; i != -1; i = ne[i])
{
int j = e[i];
//更新
if (dist[j] > dist[b] + w[i])
{
dist[j] = dist[b] + w[i];
q.push({ dist[j],j });
}
}
}
if (dist[n] > 0x3f3f3f3f / 2)
return -1;
return dist[n];
}
void add(int a, int b, int c)
{
e[idx] = b;
w[idx] = c;
ne[idx] = h[a];
h[a] = idx++;
}
int main()
{
cin >> n >> m;
memset(h, -1, sizeof h);
memset(dist, 0x3f, sizeof dist);
for (int i = 1; i <= m; i++)
{
int a, b, c;
cin >> a >> b >> c;
add(a, b, c);
}
cout << dijkstra() << "\n";
return 0;
}
存在负权边
B e l l m a n − F o r d Bellman-Ford Bellman−Ford 时间复杂度: O ( n m ) O(nm) O(nm)
-
该算法可求得:从 1 − n 1-n 1−n号点,经过 k k k 条边的最短距离
-
如果有负环,则最短路 可能 不存在
-
可以判断是否存在负环(但是一般用 s p f a spfa spfa来判断)
思路:
- 遍历 k k k 次,每次找到一条边
- 每次遍历所有边,找到可以更新的
- 注意更新时要用到
backup[]
!!!
#define _CRT_SECURE_NO_WARNINGS 1
#include<bits/stdc++.h>
#define N 100010
using namespace std;
int n, m, k;
int backup[N];
int dist[N];
bool f[N];
typedef struct edge
{
int a, b, w;
}edge;
edge e[N];
int bellman_fold()
{
dist[1] = 0;
for (int i = 1; i <= k; i++)
{
//要用 backup[]来更新!!!
memcpy(backup, dist, sizeof dist);
for (int j = 1; j <= m; j++)
{
int a = e[j].a, b = e[j].b, w = e[j].w;
//关键代码!!!
dist[b] = min(dist[b], backup[a] + w);
}
}
if (dist[n] > 0x3f3f3f3f / 2)
return -0x3f;
return dist[n];
}
int main()
{
cin >> n >> m >> k;
memset(dist, 0x3f, sizeof dist);
for (int i = 1; i <= m; i++)
{
int a, b, c;
cin >> a >> b >> c;
e[i] = { a,b,c };
}
int t = bellman_fold();
if(t==-0x3f)
cout<<"impossible";
else
cout<<t;
return 0;
}
S P F A SPFA SPFA 一般: O ( m ) O(m) O(m),最坏: O ( n m ) O(nm) O(nm)
- 用队列优化 B e l l m a n − f o l d Bellman-fold Bellman−fold
- 我们发现 B e l l m a n − f o l d Bellman-fold Bellman−fold 遍历的所有边中,并不是每个边都用得到,所以我们可以用队列存下来有用的新边,遍历这些边即可
f[i]=1
代表队列中存在1号结点
#include<bits/stdc++.h>
#define N 1000100
using namespace std;
typedef pair<int, int> PII;
int n, m;
int h[N], e[N], ne[N], w[N], idx;
int dist[N];
bool f[N];
queue<int> q;
int spfa()
{
dist[1] = 0;
q.push(1);
f[1] = 1;//代表队列中有1号结点
while (!q.empty())
{
int t = q.front();
q.pop();
f[t] = 0;
for (int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
if (dist[j] > dist[t] + w[i])
{
dist[j] = dist[t] + w[i];
if (!f[j])
{
q.push(j); f[j] = 1;
}
}
}
}
return dist[n];
}
void add(int a, int b, int c)
{
e[idx] = b;
w[idx] = c;
ne[idx] = h[a];
h[a] = idx++;
}
int main()
{
cin >> n >> m;
memset(h, -1, sizeof h);
memset(dist, 0x3f, sizeof dist);
for (int i = 1; i <= m; i++)
{
int a, b, c;
cin >> a >> b >> c;
add(a, b, c);
}
int t = spfa();
if (t == -1)
cout << "impossible";
else
cout << t;
return 0;
}
求负环
-
用
cnt[]
数组表示到达第 i i i个点的最短路,需要经过的边数 -
所以每次更新
dist[]
,cnt[]
都需要 + 1 +1 +1 -
cnt[x]>=n
,即存在负环 -
关键代码
if (dist[j] > dist[t] + w[i])
{
dist[j] = dist[t] + w[i];
cnt[j] = cnt[t] + 1;
}
#define _CRT_SECURE_NO_WARNINGS 1
#include<bits/stdc++.h>
#define N 1000100
using namespace std;
typedef pair<int, int> PII;
int n, m;
int h[N], e[N], ne[N], w[N], idx;
int dist[N]; int cnt[N];
bool f[N];
queue<int> q;
int spfa()
{
for (int i = 1; i <= n; i++)
{
f[i] = 1;
q.push(i);
}
while (!q.empty())
{
int t = q.front();
q.pop();
f[t] = 0;
for (int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
if (dist[j] > dist[t] + w[i])
{
dist[j] = dist[t] + w[i];
cnt[j] = cnt[t] + 1;
if (cnt[j] >= n)
return 1;
if (!f[j])
{
q.push(j); f[j] = 1;
}
}
}
}
return 0;
}
void add(int a, int b, int c)
{
e[idx] = b;
w[idx] = c;
ne[idx] = h[a];
h[a] = idx++;
}
int main()
{
cin >> n >> m;
memset(h, -1, sizeof h);
// memset(dist, 0x3f, sizeof dist);
for (int i = 1; i <= m; i++)
{
int a, b, c;
cin >> a >> b >> c;
add(a, b, c);
}
if (spfa())
cout << "Yes";
else
cout << "No";
return 0;
}
多源汇最短路
F l o y d Floyd Floyd 时间复杂度: O ( n 3 ) O(n^3) O(n3)
关键代码
void floyd()
{
for (int k = 1; k <= n; k++)
{
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= n; j++)
{
d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}
}
}
}
#define _CRT_SECURE_NO_WARNINGS 1
#include<bits/stdc++.h>
#define N 1010
#define INF 1000000000
using namespace std;
int n, m, Q;
int d[N][N];
void floyd()
{
for (int k = 1; k <= n; k++)
{
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= n; j++)
{
d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}
}
}
}
int main()
{
cin >> n >> m >> Q;
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= n; j++)
{
if (i == j)
d[i][j] = 0;
else
d[i][j] = INF;
}
}
while (m--)
{
int a, b, c;
cin >> a >> b >> c;
d[a][b] = min(d[a][b], c);
}
floyd();
while (Q--)
{
int a, b;
cin >> a >> b;
int t = d[a][b];
if (t > INF / 2)
cout << "impossible\n";
else
cout << t<<'\n';
}
return 0;
}
7.最小生成树
普利姆算法( P r i m Prim Prim)
朴素版 P r i m Prim Prim 时间复杂度: O ( n 2 ) O(n^2) O(n2)
适合于稠密图,思路跟 d i j k s t r a dijkstra dijkstra类似
- 注意与
d
i
j
k
s
t
r
a
dijkstra
dijkstra不同的是,每次更新
dist[]
数组时,需要更新 t t t 点到集合中所有点的距离,而不只是到起点
#include<bits/stdc++.h>
#define N 1010
#define INF 0x3f3f3f3f
using namespace std;
int g[N][N];
int n, m;
int dist[N];
bool st[N];
int prim()
{
dist[1] = 0;
int res = 0;
for (int i = 1; i <= n; i++)
{
int t = 0;
for (int j = 1; j <= n; j++)
{
if (!st[j] && dist[t] > dist[j])
{
t = j;
}
}
st[t] = 1;
if (i != 1 && dist[t] > INF / 2)
return -INF;
if (i != 1)
res += dist[t];
//关键代码!!!
for (int j = 1; j <= n; j++)
{
dist[j] = min(dist[j], g[t][j]);
}
}
return res;
}
int main()
{
cin >> n >> m;
memset(dist, 0x3f, sizeof dist);
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= n; j++)
{
if (i == j)
g[i][j] = 0;
else
g[i][j] = INF;
}
}
for (int i = 1; i <= m; i++)
{
int a, b, c;
cin >> a >> b >> c;
g[a][b] = g[b][a] = min(g[a][b], c);
}
int t = prim();
if (t == -INF)
cout << "impossible\n";
else
cout << t;
return 0;
}
堆优化版 P r i m Prim Prim O ( n 2 ) O(n^2) O(n2)
不常用
克鲁斯卡尔算法( K r u s k a l Kruskal Kruskal) 时间复杂度: O ( m l o g n ) O(mlogn) O(mlogn)
适用于稀疏图
- 将所有边按照权值大小排序
- 枚举每条边以及它的权重
- 如果边的两顶点不连通 就把这条边并入到集合中 (并查集!!!)
#include<bits/stdc++.h>
#define N 200010
#define INF 0x3f3f3f3f
using namespace std;
int n, m;
typedef struct node
{
int a, b, w;
}edge;
edge e[N];
int p[N];
int ans; int cnt;
bool cmp(node& x, node& y)
{
return x.w < y.w;
}
int find(int x)
{
if (p[x] != x)
p[x] = find(p[x]);
return p[x];
}
int main()
{
cin >> n >> m;
for (int i = 1; i <= m; i++)
{
int a, b, c;
cin >> a >> b >> c;
e[i] = { a,b,c };
}
sort(e + 1, e + 1 + n, cmp);
for (int i = 1; i <= n; i++)
p[i] = i;
for (int i = 1; i <= m; i++)
{
int a = e[i].a; int b = e[i].b; int w = e[i].w;
//关键代码!!!
a = find(a), b = find(b);
if (a != b)
{
p[a] = b;
ans += w;
cnt++;
}
}
if (cnt < n - 1)
cout << "impossible\n";
else
cout << ans;
return 0;
}
8.二分图
判别是否为二分图:
染色法 O ( m + n ) O(m+n) O(m+n)
关键代码
int dfs(int u, int c)
{
color[u] = c;
for (int i = h[u]; i != -1; i = ne[i])
{
int j = e[i];
if (!color[j])
{
if (!dfs(j, 3 - c))
return 0;
}
else if (color[j] == c)
return 0;
}
return 1;
}
for (int i = 1; i <= n; i++)
{
if (!color[i]) //color[i]==0,表示未染色
{
if (!dfs(i, 1)) //dfs()返回0,表示找不到符合条件的染色方案
{
f = 0; break;
}
}
}
代码表示
#include<bits/stdc++.h>
#define N 1000010
#define M 1000010
using namespace std;
int n, m;
int h[N], e[N], ne[N], idx;
int color[N]; bool f = 1;
void add(int a, int b)
{
e[idx] = b;
ne[idx] = h[a];
h[a] = idx++;
}
int dfs(int u, int c)
{
color[u] = c;
for (int i = h[u]; i != -1; i = ne[i])
{
int j = e[i];
if (!color[j])
{
if (!dfs(j, 3 - c))
return 0;
}
else if (color[j] == c)
return 0;
}
return 1;
}
int main()
{
cin >> n >> m;
memset(h, -1, sizeof h);
for (int i = 1; i <= m; i++)
{
int a, b;
cin >> a >> b;
add(a, b);add(b,a);
}
for (int i = 1; i <= n; i++)
{
if (!color[i])
{
if (!dfs(i, 1))
{
f = 0; break;
}
}
}
if (!f)
cout << "No\n";
else
cout << "Yes\n";
return 0;
}
给定一个二分图,要求最大匹配: 匈牙利算法
最坏: O ( m n ) O(mn) O(mn) 实际一般远小于
成功匹配:不存在两条边公用一个点的(没有一个点有两条边连着)
- 可以形象的表示为找对象 ( d o g e ) (doge) (doge) ,具体表示就不赘述了,下面开始看代码
关键代码
//为男生x找对象
int find(int x)
{
for(int i = h[x];i!=-1;i=ne[i])
{
int j = e[i];
if(!st[j]) //如果第j个女生没有被男生x找过
{
st[j]=1;
if(!match[j] || find(match[j])) //如果j已经是之前某个男生的对象了,或者j之前的对象还有别的选择
{
match[j]=x;
return 1;
}
}
}
return 0;
}
for(int i = 1;i<=n1;i++)
{
memset(st,0,sizeof st); //st[j]==0, 意思是 为每个男生i找对象时,还没有找到第j个女生
if(find(i))
res++;
}
代码:
#include<bits/stdc++.h>
#define N 100010
#define M 100010
using namespace std;
int n1, n2, m;
int h[N], e[N], ne[N], idx;
bool st[N]; int res;
int match[N];
int find(int x)
{
for(int i = h[x];i!=-1;i=ne[i])
{
int j = e[i];
if(!st[j])
{
st[j]=1;
if(!match[j] || find(match[j]))
{
match[j]=x;
return 1;
}
}
}
return 0;
}
void add(int a, int b)
{
e[idx] = b;
ne[idx] = h[a];
h[a] = idx++;
}
int main()
{
cin >> n1 >> n2 >> m;
memset(h, -1, sizeof h);
for (int i = 1; i <= m; i++)
{
int a, b;
cin >> a >> b;
add(a, b);
}
for(int i = 1;i<=n1;i++)
{
memset(st,0,sizeof st);
if(find(i))
res++;
}
cout << res;
return 0;
}