搜索与图论
第三章 搜索与图论
深搜与广搜
深搜:不断往前走,不撞南墙不回头
广搜:优先扩散
– | 数据结构 | 空间 | – |
---|---|---|---|
DFS | stack |
O ( h ) O(h) O ( h ) |
– |
BFS | queue |
O ( 2 h ) O(2^h) O ( 2 h ) |
具备最短路径 |
深搜
-
从解空间的搜索树进行理解,最重要的就是考虑一个能够
全遍历的合理顺序
-
需要注意
回溯
,要完全恢复现场 -
很多时候都是递归的形式
本题突出一个
剪枝
,就是判断有一条路肯定是走不通了,那就果断放弃
第一种做法:按行进行深搜,这是因为我们事先知道每一行只能放一个:
O
(
N
!
)
O(N!)
O
(
N
!)
#include <bits/stdc++.h>
using namespace std;
const int N = 2*10;
int n;
char g[N][N];
bool col[N],dg[N],udg[N];//用于标记是否能走的布尔数组,分别是列,对角线,副对角线
void dfs(int u){//参数里需要放一个表示“当前搜索进行到了哪一步”的标度,比如这里是“枚举第u行”
if(u == n){
for(int i = 0;i<n;i++) puts(g[i]);
puts("");
return ;
}
for(int i = 0;i<n;i++){//枚举第u行的每一列
if(!col[i] && !dg[u+i] && !udg[n-u+i]){//剪枝
//如果选择在这个点放皇后呢
g[u][i] = 'Q';
col[i] = dg[u+i] = udg[n-u+i] = true;//标明哪些列和对角线已经被占用了
dfs(u+1);//继续向前走
col[i] = dg[u+i] = udg[n-u+i] = false;//回溯
g[u][i] = '.';
}
}
}
int main(){
cin >> n;
for(int i = 0;i<n;i++){
for(int j = 0;j < n;j++){
g[i][j] = '.';
}
}
dfs(0);
return 0;
}
更原始的枚举顺序:一个格子一个格子进行枚举:
O
(
2
n
2
)
O(2^{n^2})
O
(
2
n
2
)
#include <bits/stdc++.h>
using namespace std;
const int N = 2*10;
int n;
char g[N][N];
bool col[N],dg[N],udg[N],row[N];//用于标记是否能走的布尔数组,分别是列,对角线,副对角线,行
void dfs(int x,int y,int s){//这里的标度就很复杂了,分别代表我们位于哪一行哪一列,共放了几个皇后
if(y == n) y = 0,x++;//表示已经到了行的最后一位
if(x == n){//表示搜索已结束
if(s == n){//如果皇后放够了,表示找到了一个解
for(int i = 0;i < n;i++) puts(g[i]);
puts("");
}
return ;
}
//不放皇后
dfs(x,y+1,s);//直接走
//放皇后
if(!row[x] && !col[y] && !dg[x+y] && !udg[x-y+n]){
g[x][y] = 'Q';
row[x] = col[y] = dg[x+y] = udg[x-y+n] = true;
dfs(x,y+1,s+1);
//回溯
row[x] = col[y] = dg[x+y] = udg[x-y+n] = false;
g[x][y] = '.';
}
}
int main(){
cin >> n;
for(int i = 0;i<n;i++){
for(int j = 0;j < n;j++){
g[i][j] = '.';
}
}
dfs(0,0,0);
return 0;
}
宽搜
-
一层一层搜,所以可以找所谓的最短路径
-
通常需要用到队列
模板:
//记录当前到达层数的宽搜
level = 0
初始值入队
while queue 不空:
size = queue.size()
while (size --) {
cur = queue.pop()
for 节点 in cur的所有相邻节点:
if 该节点有效且未被访问过:
queue.push(该节点)
}
level ++;
//给出的更一般的宽搜模板
初始值入队
while queue 不空{
t = 队头
“拓展”t //比如寻找相邻节点啥的
}
例题:
#include <cstdio>
#include <cstring>
#include <queue>
#include <utility>
using namespace std;
const int N = 1010;
typedef pair<int, int> PII;
int g[N][N];
bool flag[N][N]; //不回头条件,需要开一布尔数组来存哪些点已经到过了
int dx[4] = {0, 0, -1, 1}, dy[4] = {1, -1, 0, 0}; //用向量来表示向上下左右给
int n, m, k;
PII st, ed;
int bfs() {
st.first -= 1;
st.second -= 1;
ed.first -= 1;
ed.second -= 1;
//队列内储存坐标
queue<PII> q;
q.push(st); //起点进队
flag[st.first][st.second] = true;
int sec = 0; //记录层数
while (q.size()) { //当队列非空
int sz = q.size();
for (int i = 0; i < sz; i++) { //对当前这一层的每一个节点进行扩充
auto cur = q.front(); //取出要处理的节点
q.pop();
for (int o = 0; o < 4; o++) { //遍历上下左右四个方向
for (int t = 1; t <= k; t++) { //遍历所有步数
int x = cur.first + t * dx[o], y = cur.second + t * dy[o];
if (x < 0 || y < 0 || x >= m || y >= n || g[x][y]) {
break;
}
if (flag[x][y]) {
continue;
}
flag[x][y] = true;
PII ne = make_pair(x,y);
if(ne==ed){ //如果到达了终点
return ++sec;
}
q.push(ne);
}
}
}
sec++; //“层数”加一
}
return -1;
}
int main() {
scanf("%d %d %d", &n, &m, &k);
getchar();
memset(g, 0, sizeof(g));
memset(flag, 0, sizeof(flag));
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
switch (getchar()) {
case '.':
g[j][i] = 0;
break;
case '#':
g[j][i] = 1;
break;
}
}
getchar();
}
scanf("%d %d %d %d", &(st.second), &(st.first), &(ed.second), &(ed.first));
printf("%d\n", bfs());
}
如果需要记录一下路径,就开一个数组,专门记录当前点是由哪个点拓展来,增加后代码
int g[N][N];
bool flag[N][N]; //不回头条件,需要开一布尔数组来存哪些点已经到过了
int dx[4] = {0, 0, -1, 1}, dy[4] = {1, -1, 0, 0}; //用向量来表示向上下左右给
int n, m, k;
PII st, ed,pre[N][N];//pre数组记录从哪来的
int bfs() {
st.first -= 1;
st.second -= 1;
ed.first -= 1;
ed.second -= 1;
//队列内储存坐标
queue<PII> q;
q.push(st); //起点进队
flag[st.first][st.second] = true;
int sec = 0; //记录层数
while (q.size()) { //当队列非空
int sz = q.size();
for (int i = 0; i < sz; i++) { //对当前这一层的每一个节点进行扩充
auto cur = q.front(); //取出要处理的节点
q.pop();
for (int o = 0; o < 4; o++) { //遍历上下左右四个方向
for (int t = 1; t <= k; t++) { //遍历所有步数
int x = cur.first + t * dx[o], y = cur.second + t * dy[o];
if (x < 0 || y < 0 || x >= m || y >= n || g[x][y]) {
break;
}
if (flag[x][y]) {
continue;
}
flag[x][y] = true;
pre[x][y] = cur;//表cur走到了(x,y)处
PII ne = make_pair(x,y);
if(ne==ed){ //如果到达了终点
return ++sec;
}
q.push(ne);
}
}
}
sec++; //“层数”加一
}
return -1;
}
树和图的存储
- 无向图就存储为双向边的有向图
-
邻接矩阵:用二维数组
g[
a
]
[
b
]
g[a][b]
g
[
a
]
[
b
]
存储
a到
b
a到b
a
到
b
这条边的信息,适合存储稠密的图 - 邻接表:类似拉链法的哈希表,每个节点拉出一个单链表,用于存储当前这个节点的所有邻居
下面都在讨论邻接表
代码实现,ACWING讲的是用数组模拟,也可以用stl的vector数组
vector<int> h[N]
进行模拟,甚至用
unordered_map<int,vector<int> > h
进行模拟
基本操作代码模板
const int N = 100010,M = N*2;
// 对于每个点k,开一个单链表,存储k所有可以走到的点。h[k]存储这个单链表的头结点
int h[N], e[M], ne[M], idx;
// 添加一条边a->b
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
// 初始化
idx = 0;
memset(h, -1, sizeof h);
补充:带权重的邻接表:
int n,m;
int h[N],e[N],ne[N],w[N],idx;//邻接表,多开一个w[]数组来存储边的权重
void add(int a,int b,int c){//带权重的add算法
e[idx] = b,w[idx] = c,ne[idx] = h[a],h[a] = idx++;
}
树和图的dfs
bool st[N];//记录已经遍历过的点
void dfs(int u){//从编号为u的点开始深搜
//对u进行一些处理
st[u] = true;//标记,已到达
for(int i = h[u];i != -1;i = ne[i]){
int j = e[j];
if(!st[j]) dfs(j);
}
}
例题
-
先分析一下“把每个点删除后连通块的最大值”:其实就是要求子树的大小
- 更换具体说,每个节点的子树大小可以由dfs自然求得
-
但是除了该节点的所有子树,还有一个连通块,就是拔掉以当前节点为根的树后,剩下的部分。这部分大小是
n−
Σ
s
o
n
i
n- \Sigma son_i
n
−
Σ
so
n
i
-
基本思路是对于每一个节点,都储存一下他的“最大模块数”。这样的话就需要用深搜来遍历整棵树
#include <bits/stdc++.h>
using namespace std;
const int N = 100010,M = N*2;
int n;
int h[N],e[M],ne[M],idx;
bool st[N];
int ans = N;//存储全局答案
void add(int a,int b){
e[idx] = b,ne[idx] = h[a],h[a] = idx++;
}
int dfs(int u){//从编号为u的点开始深搜,返回以u为根的子树大小
st[u] = true;//标记,已到达
int sum = 1,res = 0;//res是连通块最大数,sum是记数
for(int i = h[u];i != -1;i = ne[i]){
int j = e[j];
if(!st[j]){
int s = dfs(j);
res = max(res,s);
sum += s;//sum记录子树大小
}
}
res = max(res,n-sum);
ans = min(ans,res);
return sum;
}
int main(){
cin >>n;
memset(h,-1,sizeof(h));
for(int i = 0;i<n;i++){
int a,b;
cin >> a>> b;
add(a,b),add(b,a);//存储无向图
}
dfs(1);
cout << ans << endl;
}
树和图的bfs
套用模板
#include <bits/stdc++.h>
using namespace std;
int level = 0;
queue<int> q;
int bfs(){
level = 0;
q.push(1);
while(q.size()){
int size = q.size();
while (size --) {
int cur = q.front();
q.pop();
for(int i = h[cur];i!=-1;i=ne[i]){
int j = e[i];
//进行一些处理
if(!st[j]){
if(check(j)){
//满足终止条件,返回当前层数
return ++level;
}
st[j] = true;
q.push(j);//入队
}
}
}
level ++;
}
return -1;
}
例题
#include <bits/stdc++.h>
using namespace std;
const int N = 100010;
int n, m;
int h[N], e[N], ne[N], idx;
int d[N];
void add(int a, int b) {
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
int bfs() {
memset(d, -1, sizeof d);//d用来存距离
queue<int> q;
d[1] = 0;
q.push(1);
while (q.size()) {
int t = q.front();
q.pop();
for (int i = h[t]; i != -1; i = ne[i]) {
int j = e[i];
if (d[j] == -1) {
d[j] = d[t] + 1;
q.push(j);
}
}
}
return d[n];
}
int main() {
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
for (int i = 0; i < m; i ++ ) {
int a, b;
scanf("%d%d", &a, &b);
add(a, b);
}
cout << bfs() << endl;
return 0;
}
bfs应用:拓扑序
- 只有无环图有拓扑序
-
拓扑序列是这样的序列:在序列中
不
存在这样的对子
(a
,
b
)
s
.
t
.
在某条包含
a
,
b
的路径里,
b
位于起点而
a
位于终点
(a,b) \ s.t. \ 在某条包含a,b的路径里,b位于起点而a位于终点
(
a
,
b
)
s
.
t
.
在某条包含
a
,
b
的路径里,
b
位于起点而
a
位于终点
几个基本概念
- 某个点的度数:就是该点的边数
- 入度:箭头指向该点的边数
- 出度:箭头指出概念的边数
大致步骤
-
首先找到入度为0的点,入队
-
只要队不空,排出队头t,加入到拓扑序内
-
枚举t的所有出边t—>j
-
删除该边,并更新节点j的入度
-
如果j的入度已经是0了
- j入队
-
-
可以证明,如果图中有环,则环中不可能有入度为0的点,所以如果用以上操作后,图中有节点剩余,则不可能有拓扑序列
代码
int d[N];//d数组存放入度
queue<int> top;//该队列存储拓扑序的输出
bool topsort() {
queue<int> q;
for(int i = 1;i <= n;i++){
if(!d[i]) q.push(i);
}
while (q.size()) {
int t = q.front();
q.pop();
top.push(t);
for (int i = h[t]; i != -1; i = ne[i]) {
int j = e[i];
d[j]--;
if(!d[j]) q.push(j);
}
}
return (q.size() == n-1);
}
最短路问题
分类
-
单源最短路:一个起点
-
无负权边
-
朴素Dijkstra算法
O(
n
2
)
O(n^2)
O
(
n
2
)
适合稠密图 -
堆优化Dijkstra算法
O(
m
l
o
g
n
)
O(mlogn)
O
(
m
l
o
g
n
)
适合稀疏图
-
朴素Dijkstra算法
-
有负权边
-
Bellman-Ford
O(
n
m
)
O(nm)
O
(
nm
)
-
SPFA
一般O
(
m
)
,最坏
O
(
n
m
)
一般O(m),最坏O(nm)
一般
O
(
m
)
,最坏
O
(
nm
)
-
Bellman-Ford
-
无负权边
-
多源汇最短路:Floyd
O(
n
3
)
O(n^3)
O
(
n
3
)
难点:
建图
朴素Dijkstra算法
大致步骤
-
初始化距离 :以起点为一号点,初始化:
ds
t
[
1
]
=
0
;
d
i
s
t
[
.
.
.
]
=
+
∞
dst[1] = 0 \ ;dist[…] = + \infty
d
s
t
[
1
]
=
0
;
d
i
s
t
[
…
]
=
+
∞
-
初始化集合
ss
s
:s是所有当前已经确定最短距离的点 -
循环:
fo
r
i
i
n
r
a
n
g
e
(
1…
n
)
for \ i \ in \ range \ (1…n)
f
or
i
in
r
an
g
e
(
1…
n
)
就是循环n次-
拿出
tt
t
:不在集合
ss
s
中的
ds
t
[
t
]
dst[t]
d
s
t
[
t
]
值最小的点 -
把
tt
t
加入到
ss
s
之中 -
用
tt
t
更新别的点的距离-
对于所有
tt
t
的出边连接的节点
xx
x
-
if
d
s
t
[
x
]
>
d
i
s
t
[
t
]
+
w
if \ dst[x]>dist[t]+w
i
f
d
s
t
[
x
]
>
d
i
s
t
[
t
]
+
w
;其中
ww
w
是
t−
>
x
t->x
t
−
>
x
边的权重: -
更新
di
s
t
[
x
]
dist[x]
d
i
s
t
[
x
]
为更小值
-
-
对于所有
-
拿出
数据结构:由于适合稠密图,所以一般用邻接矩阵来存储
代码
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 510;
int n,m;
int g[N][N];//邻接矩阵
int dist[N];//记录距离
bool st[N];//表示哪些点还未计算过最短路
int dijkstra(){
//初始化距离
memset(dist,0x3f,sizeof(dist));
dist[1] = 0;
for(int i = 0;i<n;i++){//迭代n次,以确保遍历所有的点
int t = -1;
for(int j = 1;j <= n;j++){
if(!st[j] && (t == -1 || dist[t] > dist[j])){//如果还没有最短距离
t = j;
}
}
st[t] = true;//入集合
for(int j = 1;j <= n;j++){
dist[j] = min(dist[j],dist[j] + g[t][j]);
}
}
if(dist[n] == 0x3f3f3f3f) return -1;//距离仍是正无穷,说明不连通
return dist[n];
}
int main(){
scanf("%d%d",&n,&m);
memset(g,0x3f,sizeof(g));//初始化邻接矩阵全为正无穷,为什么初始化成正无穷,则与算法的实现有关
while(m--){//读入图
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
g[a][b] = min(g[a][b],c);//面对重边 和 自环 的处理方式:每两点只保留最短的边
}
int t = dijkstra();
printf("%d\n",t);
return 0;
}
堆优化的Dijkstra算法
大致步骤
-
初始化距离 :以起点为一号点,初始化:
ds
t
[
1
]
=
0
;
d
i
s
t
[
.
.
.
]
=
+
∞
dst[1] = 0 \ ;dist[…] = + \infty
d
s
t
[
1
]
=
0
;
d
i
s
t
[
…
]
=
+
∞
-
初始化集合
ss
s
:s是所有当前已经确定最短距离的点 -
循环:
fo
r
i
i
n
r
a
n
g
e
(
1…
n
)
for \ i \ in \ range \ (1…n)
f
or
i
in
r
an
g
e
(
1…
n
)
-
拿出
tt
t
:不在集合
ss
s
中的
ds
t
[
t
]
dst[t]
d
s
t
[
t
]
值最小的点
在这里用小根堆优化,则减少了时间
-
把
tt
t
加入到
ss
s
之中 -
用
tt
t
更新别的点的距离-
对于所有
tt
t
的出边连接的节点
xx
x
-
if
d
s
t
[
x
]
>
d
i
s
t
[
t
]
+
w
if \ dst[x]>dist[t]+w
i
f
d
s
t
[
x
]
>
d
i
s
t
[
t
]
+
w
;其中
ww
w
是
t−
>
x
t->x
t
−
>
x
边的权重: -
更新
di
s
t
[
x
]
dist[x]
d
i
s
t
[
x
]
为更小值
-
-
对于所有
-
拿出
数据结构
- 稀疏矩阵:邻接表
-
堆:用STL里的优先队列
- 由于优先队列不存在修改的操作,所以修改时就是往堆里插入一个新元素,但是这就可能造成冗余
-
冗余的处理方式就是去查询
bo
o
l
s
t
[
]
bool \ st[]
b
oo
l
s
t
[
]
来判断是不是已经得到最小值了
代码
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
using namespace std;
typedef pair<int ,int> PII;//由于既需要存距离,又需要存编号,所以搞一个有序对
//由于有序对的比大小函数是先比第一个再比第二个,所以第一个放距离
const int N = 10010;
int n,m;
int h[N],e[N],ne[N],w[N],idx;//邻接表,多开一个w[]数组来存储边的权重
int dist[N];//记录距离
bool st[N];//表示哪些点还未计算过最短路
void add(int a,int b,int c){//带权重的add算法
e[idx] = b,w[idx] = c,ne[idx] = h[a],h[a] = idx++;
}
int dijkstra(){
//初始化距离
memset(dist,0x3f,sizeof(dist));
dist[1] = 0;
priority_queue<PII, vector<PII>,greater<PII> > heap;//用优先队列,但是优先队列是大根堆,先调整一下
heap.push({0,1});//放入起点
while(heap.size()){
auto t = heap.top();
heap.pop();
int ver = t.second, distance = t.first;
if(st[ver]) continue;//发现冗余,跳过
st[ver] = true;//进入集合
for(int i = h[ver];i != -1;i = ne[i]){
int j = e[i];
if(dist[j] > distance + w[i]){//更新距离
dist[j] = distance + w[i];
heap.push({dist[j],j});
}
}
}
if(dist[n] == 0x3f3f3f3f) return -1;//距离仍是正无穷,说明不连通
return dist[n];
}
int main(){
scanf("%d%d",&n,&m);
memset(h,-1,sizeof(h));//初始化邻接表
while(m--){//读入图
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
add(a,b,c);
}
int t = dijkstra();
printf("%d\n",t);
return 0;
}
Bellman-Ford 算法
大体步骤:
-
循环 n 次
-
fo
r
所有边
(
a
,
b
,
w
)
for \ 所有边 (a,b,w)
f
or
所有边
(
a
,
b
,
w
)
(意思是从a走向b权重为w)-
更新距离
di
s
t
[
b
]
=
m
i
n
(
d
i
s
t
(
n
o
w
)
[
b
]
,
d
i
s
t
(
p
r
e
v
i
o
u
s
)
[
a
]
+
w
)
dist[b] = min(dist^{(now)}[b],dist^{(previous)}[a]+w)
d
i
s
t
[
b
]
=
min
(
d
i
s
t
(
n
o
w
)
[
b
]
,
d
i
s
t
(
p
re
v
i
o
u
s
)
[
a
]
+
w
)
思路上其实差不多
-
更新距离
-
注:
-
可以想到,如果图中存在
负权环
,则最短距离不存在(因为可以一直绕着负权环走) - 循环第k次的意义:从起点开始,经过不超过k条边的时候,最短距离是多少
-
注意:
更新距离的时候用的是上一次迭代的结果,而非当前
- 这是为了保持”循环k次“的意义,防止边之间”串联 “
进一步解释:比如给出这样的图
第一次迭代后,如果是一边遍历一边更新,则效果是
d
i
s
t
[
1
]
=
0
,
d
i
s
t
[
2
]
=
1
,
d
i
s
t
[
3
]
=
2
dist[1] = 0,dist[2] = 1,dist[3] = 2
d
i
s
t
[
1
]
=
0
,
d
i
s
t
[
2
]
=
1
,
d
i
s
t
[
3
]
=
2
但是这不是我们期望的结果,当改成用上一次的迭代结果,则效果是
d
i
s
t
[
1
]
=
0
,
d
i
s
t
[
2
]
=
1
,
d
i
s
t
[
3
]
=
3
dist[1] = 0,dist[2] = 1,dist[3] = 3
d
i
s
t
[
1
]
=
0
,
d
i
s
t
[
2
]
=
1
,
d
i
s
t
[
3
]
=
3
数据结构
-
直接开个结构,三元组,存
(a
,
b
,
w
)
(a,b,w)
(
a
,
b
,
w
)
- 然后开个数组,把边存下来就可
代码
本题是限制边数的,正好与算法中的循环次数k相对应
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 510,M = 10010;
int n,m,k;//点数,边数,路径长度限制
int dist[N],backup[N];//距离,备份数组
struct Edge{
int a,b,w;
}edges[M];
int bellman_ford(){
memset(dist,0x3f,sizeof dist);
dist[1] = 0;
memset(backup,0x3f,sizeof backup);
backup[1] = 0;
for(int i = 0;i<k;i++){
memcpy(backup,dist,sizeof dist);
for(int j = 0;j<m;j++){
int a = edges[j].a,b = edges[j].b,w = edges[j].w;
dist[b] = min(dist[b],backup[a]+w);//只用上次迭代的结果进行备份
}
}
if(dist[n] > 0x3f3f3f3f/2) return -1;//判断大于一个比较大的数,这是因为负权边的存在,会导致无穷大被更新
return dist[n];
}
int main(){
scanf("%d%d%d",&n,&m,&k);
for(int i = 0;i<m;i++){
int a,b,w;
scanf("%d%d%d",&a,&b,&w);
edges[i] = {a,b,w};
}
int t = bellman_ford();
if(t == -1) puts("impossible");
else printf("%d\n",t);
}
SPFA算法
用队列对Bellman-Ford算法优化
-
观察我们用于更新的式子
di
s
t
[
b
]
=
m
i
n
(
d
i
s
t
(
n
o
w
)
[
b
]
,
d
i
s
t
(
p
r
e
v
i
o
u
s
)
[
a
]
+
w
)
dist[b] = min(dist^{(now)}[b],dist^{(previous)}[a]+w)
d
i
s
t
[
b
]
=
min
(
d
i
s
t
(
n
o
w
)
[
b
]
,
d
i
s
t
(
p
re
v
i
o
u
s
)
[
a
]
+
w
)
-
我们发现,事实上,在k,k+1次迭代之间(而非第k次迭代内),
di
s
t
k
+
1
[
b
]
dist^{k+1}[b]
d
i
s
t
k
+
1
[
b
]
变小的唯一可行能性是
di
s
t
i
[
a
]
dist^{i}[a]
d
i
s
t
i
[
a
]
变小了 - 这样追溯下去,实际上只需要自己变小的节点去更新其他节点
大致思路:
- 先把起点入队
-
循环:直到队列为空
- 取出队头
-
用队头更新所有队头的出边
- 如果更新成功,则把更新成功的节点加入队列
#include <cstring>
#include <iostream>
#include <algorithm>
#include <queue>
using namespace std;
typedef pair<int,int> PII;
const int N = 100010;
int n,m;
int h[N],w[N],e[N],ne[N],idx;//邻接表
int dist[N];
bool st[N];//防止有重复的边放入
void add(int a,int b,int ww){
e[idx] = b,ne[idx] = h[a],w[idx] = ww,h[a] = idx++;
}
int spfa(){
memset(dist,0x3f,sizeof dist);
dist[1] = 0;
queue<int> q;
q.push(1);
st[1] = true;
while(q.size()){
int t = q.front();
q.pop();
st[t] = false;
for(int i = h[i];i!=-1;i = ne[i]){
int j = e[i];
if(dist[j]>dist[t]+w[i]){
dist[j] = dist[t]+w[i];
if(!st[j]){//防止重复放入
q.push(j);
st[j] = true;
}
}
}
}
if(dist[n] == 0x3f3f3f3f) return -1;
return dist[n];
}
int main(){
scanf("%d%d",&n,&m);
memset(h,-1,sizeof h);
while(m--){
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
add(a,b,c);
}
int t = spfa();
if(t == -1) puts("impossible");
else printf("%d\n",t);
}
spfa判断负环:
- 维护一个cnt数组,表示从起点到当前点走了几条边
- cnt数组和dist数组同步更新
- 为了寻找起点可能在任意处的负环,需要在一开始把所有点都放入队列
int cnt[N];
bool spfa(){
queue<int> q;
for(int i = 0;i<n;i++){
q.push(i);
st[i] = true;
}
while(q.size()){
int t = q.front();
q.pop();
st[t] = false;
for(int i = h[i];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;//同步更新cnt数组
if(cnt[j] >= n) return true;//抽屉原理
if(!st[j]){//防止重复放入
q.push(j);
st[j] = true;
}
}
}
}
return false;
}
Floyd算法
数据结构
- 邻接矩阵
大体思路:
-
fo
r
k
i
n
r
a
n
g
e
(
1…
n
)
for \ k \ in \ range(1…n)
f
or
k
in
r
an
g
e
(
1…
n
)
-
fo
r
i
i
n
r
a
n
g
e
(
1…
n
)
for \ i \ in \ range(1…n)
f
or
i
in
r
an
g
e
(
1…
n
)
-
fo
r
j
i
n
r
a
n
g
e
(
1…
n
)
for \ j \ in \ range(1…n)
f
or
j
in
r
an
g
e
(
1…
n
)
-
d[
i
,
j
]
=
m
i
n
(
d
[
i
,
j
]
,
d
[
i
,
k
]
+
d
[
k
,
j
]
)
d[i,j] = min(d[i,j],d[i,k]+d[k,j])
d
[
i
,
j
]
=
min
(
d
[
i
,
j
]
,
d
[
i
,
k
]
+
d
[
k
,
j
])
-
-
-
原理:动态规划
-
dp
(
i
,
j
,
k
)
dp(i,j,k)
d
p
(
i
,
j
,
k
)
表示从
ii
i
开始,只经过
1..k
1..k
1..
k
这些点,到达
jj
j
的最短路径
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 210,INF = 1e9;
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(){
scanf("%d%d%d",&n,&m,&Q);
for(int i = 1;i <= n;i++){
for(int j = 1;j <= m;j++){
if(i == j) d[i][j] = 0;
else d[i][j] = INF;
}
}
while(m--){
int a,b,w;
scanf("%d%d%d",&a,&b,&w);
d[a][b] = min(d[a][b],w);//别忘记处理重边
}
floyd();
while(Q--){
int a,b;
scanf("%d%d",&a,&b);
if(d[a][b] > INF/2) printf("%d\n",d[a][b]);
else puts("impossible");
}
return 0;
}
小总结:关于一些细节的判定
- 用邻接矩阵注意处理重边
- 遇到负权的图,最判断是否有路径是判断一个较大的数,不是直接与正无穷比较
最小生成树
最主要是无向图
-
Prim算法:与Dijkstra算法很像
-
朴素版Prim算法:
O(
n
2
)
O(n^2)
O
(
n
2
)
:稠密图 -
堆优化版Prim算法:
O(
m
l
o
g
n
)
O(mlogn)
O
(
m
l
o
g
n
)
:稀疏图(不常用)
-
朴素版Prim算法:
-
Kruskal算法:
O(
m
l
o
g
m
)
O(mlogm)
O
(
m
l
o
g
m
)
:稀疏图(常用)
朴素版Prim算法
一个贪心算法
- 把所有点到集合的距离初始化为正无穷,初始化一个集合,表示当前在连通块内的点
-
n次迭代
- 找到集合 外 距离最近的点t
- 把t加入到集合之中,并把最短距离对应的边,加入到生成树
-
用t更新其他点到
集合
的距离
基本上和Dijkstra一样
到集合距离的定义:
-
就是某点 到 集合内 所有边中,长度最小的边,的长度
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 510;
int n,m;
int g[N][N];
int dist[N];
bool st[N];
int prim(){
memset(dist,0x3f,sizeof dist);
int res = 0;
for(int i = 0;i < n;i++){
int t = -1;
for(int j = 1;j<=n;j++){
if(!st[i] && (t == -1 || dist[t] > dist[j])){
t = j;
}
}//选点
st[t] = true;//入集合
//在这里判断第是否是第一个点,其实意思是默认第一个点一开始就在集合中
if(i && dist[t] == 0x3f3f3f3f) return 0x3f3f3f3f;
if(i) res += dist[t];//先累加
for(int j = 1;j <= n;j++) dist[j] = min(dist[j],g[t][j]);//再更新,这是防止负权自环的扰乱
}
}
int main(){
scanf("%d%d",&n,&m);
memset(g,0x3f,sizeof g);
while(m--){
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
g[a][b] = g[b][a] = min(g[a][b],c);
}
int t = prim();
if(t == 0x3f3f3f3f) puts("impossible");
else printf("%d\n",t);
return 0;
}
堆优化版的Prim算法(见堆优化的Dijkstra算法)
Kruskal算法
大致步骤
- 将所有边按权重从小到大排序
-
枚举所有边
(a
,
b
,
c
)
(a,b,c)
(
a
,
b
,
c
)
-
如果a,b不在同一个连通块内
-
将这条边加入集合
-
将这条边加入集合
-
如果a,b不在同一个连通块内
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010;
int n,m;
int p[N];//并查集
struct Edge{
int a,b,w;
bool operator< (const Edge &W)const {//重载运算符:按照边值大小排序
return w<W.w;
}
}edges[2*N];
int find(int x){//并查集功能:实现祖宗追溯
if(p[x] != x) p[x] = find(p[x]);
return p[x];
}
int kruskal(){
//事先准备:排序边,构建并查集
sort(edges,edges+m);
for(int i = 1;i <= n;i++) p[i] = i;//全部孤立点
int res = 0,cnt = 0;//分别是最小生成树的权值和,以及树中节点个数
for(int i = 0;i < m;i++){
int a = edges[i].a,b = edges[i].b,w = edges[i].w;
a = find(a),b = find(b);//寻找祖宗
if(a!=b){//ab尚未在一个连通块内
p[a] = b;//加入同一个连通块
res += w;
cnt++;
}
}
if(cnt < n-1) return -1;
else return res;
}
int main(){
scanf("%d%d",&n,&m);
for(int i = 0;i < m;i++){
int a,b,w;
scanf("%d%d%d",&a,&b,&w);
edges[i] = {a,b,w};
}
int t = kruskal();
if(t == -1) puts("impossible");
else printf("%d\n",t);
}
二分图
什么叫二分图?如下图,把节点划分为两个集合,且所有边都在集合之间
-
染色法:dfs
O(
m
+
n
)
O(m+n)
O
(
m
+
n
)
-
匈牙利算法:最坏
O(
m
n
)
O(mn)
O
(
mn
)
,实际运行时间远小于理论最坏
染色法
-
一个图是 二分图:当且仅当图中不含有奇数环
- 必要性:在奇数环中,任意选取一点作为起点,让其属于集合1.由于二分的要求,则其后继一定属于集合2。一直走下去,由于走过了奇数个点,得到起点必须属于集合2。矛盾。
-
充分性:进行一个构造,就是
染色
,给点染色,保证
一条边两侧的颜色不同
。具体步骤就是,对点进行遍历,如果遇到没有被染色的点,将其染成白色,再将其所有的邻居染成黑色。这就变成了,证明:只要图中不含有奇数环,则染色过程不会出现矛盾- 反证:结论的否命题引入:染色过程中出现了矛盾。这是因为,矛盾如果发生,则必须发生在一个点将要去染色一个已染色的邻居,这说明该点和即将下一个染色的点,一定处于一个环之中(易证)。这个环是奇数环是显然的,与前提矛盾。证毕
- 由以上论证,有 一个图是二分图,当且仅当其能被二染色
大致步骤:
-
遍历所有点
ii
i
-
if
ii
i
没有染色- 递归调用染色(dfs)
-
if
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100010,M = 200020;
int n,m;
int h[N],e[M],ne[M],idx;
int color[N];//存储染色
void add(int a,int b){
e[idx] = b,ne[idx] = h[a],h[a] = idx++;
}
bool 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 false;//递归调用,颜色1变2,2变1
}
else if(color[j] == c) return false;//如果被染色,且矛盾
}
return true;
}
bool dye(){//对一整个图进行染色
for(int i = 1;i <= n;i++){
if(!color[i]){
if(!dfs(i,1)) return false;
}
}
return true;
}
int main(){
scanf("%d%d",&n,&m);
memset(h,-1,sizeof h);
while(m--){
int a,b;
scanf("%d%d",&a,&b);
add(a,b),add(b,a);
}
bool flag = dye();
if(flag) puts("Yes");
else puts("No");
}
匈牙利算法
给定一个二分图,去找其最大匹配
匹配数量:选到边的数量
成功匹配:没有共用同一个点的边(不许搞三角关系)如下图,选择三条黑色的边,就是成功匹配
大体思路:贪心
-
从左1开始匹配
- 依次遍历出边,如果“出边”的点未被占用,则直接匹配,寻匹配左侧下一个节点
-
如果遍历该点所有出边,都被占用了,则进行协调:
- 找到第一个出边对应的点,该点被占用,则去找占用该点的左点去“协商”,看他能不能换一个
- 换不了?进行递归协商
-
如果都达到不了,放弃了
#include <bits/stdc++.h>
using namespace std;
const int N = 510,M = 1e5+10;
int n1,n2,m;//左点,右点,边数
int h[N],e[N],ne[N],idx;//邻接表
int match[N];//记录右侧点匹配了谁,即match[j] = i 表示右侧j匹配了i
bool st[N];//记录右侧点是否被占用
void add(int a,int b){
e[idx] = b;
ne[idx] = h[a];
h[a] = idx++;
}
//匈牙利算法部分,包括一个单点匹配函数和一个总体循环
bool find(int x){
for(int i = h[x];i!=-1;i=ne[i]){
int j = e[i];
if(!st[j]){//未被占用
st[j] = true;
if(match[j] == 0 || find(match[j])){//如果右侧点尚未匹配 或者 右侧匹配上的可以换人
match[j] = x;//直接匹配喵
return true;
}
}
}
return false;
}
int Xmacth(){
int res = 0;
for(int i = 1;i<=n1;i++){//遍历所有男生
memset(st,0,sizeof st);//初始化
if(find(i)) res++;
}
return res;
}
int main(){
scanf("%d%d%d",&n1,&n2,&m);
memset(h,-1,sizeof h);
while(m--){
int a,b;
scanf("%d%d",&a,&b);
add(a,b);
}
int t = Xmacth();
printf("%d\n",t);
}
证明见《算法导论》