1.图
图(graph):顶点(vertex)和边(edge)组成的集合。
边:点对(v,w),如果点对有序,则称该图为有向图(digraph),否则叫无向图。
邻接(adjacent):点v和点w邻接当且仅当(v,w)是边。
权(weigh)或值(cost):边的属性。
路径(path):的顶点序列,其中()是边,这样一条路径的长(length)是该路径的边数N-1,不包含边的路径长度为0,路径(v,v)叫做环。
简单路径:路径上的所有顶点互异,但路径的起点和终点可以相同。
回路(cycle):满足且长至少为1的一条路径,如果回路是简单路径,则称之为简单回路。
如果一个有向图没有回路,则称其为有向无环图(DAG),有向无环图一定没有双向边。
对于一个无向图,如果任取它的两个顶点,总存在一条路径,则称这个无向图为连通图(connected)。
对于一个有向图,如果任取它的两个顶点,总存在一条路径,则称这个有向图为强连通图(strongly connected)。
如果一个有向图不是强连通的,但是它的基础图(underlying graph),即对应的无向图连通,则它是弱连通图(weakly connected)。
完全图(complete graph):每一对顶点之间都存在一条边的图。
2.图的表示
邻接表:对每个顶点,用一个表存放所有邻接的顶点。
实现:开个vector<int> a[size], size为点的个数。
如果要保存顶点的名字或者权,需要用map<struct XX,struct XX> a[size]
3.拓扑排序
对于有向无环图,如果有一条v到w的路径,则把顶点v排在w的前面。
顶点v的入度(indegree):边(u,v)的条数。
排序方法:先遍历邻接表,让所有入度为0的顶点进入队列,让队首元素出队并将它和它的边一起删除,即它指向的顶点入度都减1,重复以上步骤直到队列为空,出队的顺序即为拓扑排序顺序。
4.最短路径算法
加权路径长:从到的路径中,所有边的权的加和。
无权路径长:从到的路径边数,即。
单源最短路问题:给定一个加权图G和一个顶点s,找出从s到G中的其他顶点的最短加权路径。这里先假定权都是正的,否则反复走负边可能会使加权路径长无限小。从s到s的最短路径为0。
无权最短路:
直接BFS:从s开始,将s记为0,把所有与s邻接的顶点记为1,然后不断扩展邻接的顶点的最短路径为原来顶点加1,直到所有顶点都算过了。
实现:
第一行输入m,n为顶点数和边数。
以下的n行各有两个整数,为有向边。
输入一个整数x。
输出第x个顶点到每个顶点的最短路径长以及最短路径,如果到不了,输出-1.
#include<iostream>
#include<vector>
#include<queue>
#include<string>
#include<sstream>
using namespace std;
int m, n;
struct vertex {
int num;
int dist;
int from;
}v[100];
vector<int> edge[1000];
queue<vertex> q;
int a, b, x;
int len;
int num;
int main() {
cin >> m >> n;
for (int i = 1; i <= n; i++) {
cin >> a >> b;
edge[a].push_back(b);
}
cin >> x;
for (int i = 1; i <= m; i++) {
v[i].num = i;
v[i].dist = -1;
}
v[x].dist = 0;
v[x].from = -1;
q.push(v[x]);
while (!q.empty()) {
vertex now = q.front();
q.pop();
num = now.num;
len = edge[num].size();
for (int i = 0; i < len; i++) {
if (v[edge[num][i]].dist == -1) {
v[edge[num][i]].dist = v[num].dist + 1;
v[edge[num][i]].from = num;
q.push(v[edge[num][i]]);
}
}
}
int temp;
stringstream ss;
string out;
for (int i = 1; i <= m; i++) {
cout << v[i].dist << endl;
if (v[i].dist == -1) continue;
cout << i;
temp = i;
ss.clear();
out.clear();
while (v[temp].from != -1) {
ss << "<-" << v[temp].from;
temp = v[temp].from;
}
ss >> out;
cout << out << endl;
}
return 0;
}
时间复杂度:
有权最短路(Djikstra算法)
需要标记数组bool known[n],来标记到该顶点的最短路径是否确定。
初始化:把所有顶点都置成unknown,最短距离都置成。
s到s最短距离置为0,s设置为known。
对于和s邻接的所有顶点,最短距离记为s到它们的带权距离。
找出这些顶点中距离s最近的那个,它到s的最短距离就是它直接到s的距离。(反证:如果能从别的地方过来最短,由于权非负,它一定不是离s最近的顶点。)设这个顶点为s1,并把它设成known。
对于和s1邻接的所有顶点,最短距离记为s1到它们的带权距离。
再找出unknown顶点中s到它的距离最短的那个,设这个顶点为s2,并把它设成known。
对于和s2邻接的所有顶点,更新它们的最短距离(如果加上s2的距离比现有的距离短,就更新)。
重复以上操作直到所有的顶点都被标记成known。
对于边数很多()的图(稠密图),直接遍历所有unknown顶点,找出离s最短的顶点即可,时间复杂度。
实现:
第一行输入m,n为顶点数和边数。
以下的n行各有三个整数,为有向边和它的权。
输入一个整数x。
输出第x个顶点到每个顶点的带权最短路径长以及最短路径,如果到不了,输出-1.
#include<fstream>
#include<iostream>
#include<vector>
#define INF 1000000
using namespace std;
int m, n;
int a;
int x;
struct edge {
int to;
int w;
};
struct vertex {
int num;
vector<edge> list;
bool known;
int dist;
int from = -1;
}v[1000];
void printpath(vertex now) {
if (now.from != -1) {
printpath(v[now.from]);
cout << "->";
}
cout << now.num;
}
edge temp;
int main() {
ifstream fin("input.txt");
fin >> m >> n;
for (int i = 1; i <= n; i++) {
fin >> a >> temp.to >> temp.w;
v[a].list.push_back(temp);
}
for (int i = 1; i <= m; i++) {
v[i].num = i;
v[i].known = false;
v[i].dist = INF;
}
fin >> x;
v[x].dist = 0;
int minnum;
int mindist;
int len;
int num;
while (1) {
minnum = -1;
mindist = INF;
for (int i = 1; i <= m; i++) {
if (v[i].known) continue;
if (v[i].dist < mindist) {
minnum = i;
mindist = v[i].dist;
}
}
if (minnum == -1) break;
v[minnum].known = true;
len = v[minnum].list.size();
for (int i = 0; i < len; i++) {
num = v[minnum].list[i].to;
if (v[num].known) continue;
if (v[minnum].dist + v[minnum].list[i].w < v[num].dist) {
v[num].dist = v[minnum].dist + v[minnum].list[i].w;
v[num].from = minnum;
}
}
}
for (int i = 1; i <= m; i++) {
cout << i << ":" << endl << v[i].dist << endl;
printpath(v[i]);
cout << endl;
cout << endl;
}
return 0;
}
相比上面的无权最短路:用了文件输入便于测试,将邻接表放进了vertex类里,输出路径改为了正向输出(用递归实现)。
优化:用优先队列(堆)返回距离s最近的结点,能把时间复杂度降到
先补充一种数据结构:链式前向星。
相当于一个多叉树。
int top;
int head[10010];
struct Edge {
int to;
int w;
int next;
}edge[200010];
void add(int u, int v, int w) {
edge[++top].to = v;
edge[top].w = w;
edge[top].next = head[u];
head[u] = top;
}
https://www.luogu.org/problem/P4779
Djikstra(堆优化版):
#include<iostream>
#include<queue>
#define INF 2147483647
using namespace std;
int n, m, s;
int head[200010], vis[100010], dist[100010];
int top;
struct Edge {
int to;
int next;
int w;
}edge[200010];
struct vertex {
int num;
int dist;
bool operator < (const vertex &x) const {//STL的优先队列是最大堆,要重载成最小堆
if (x.dist < dist) return true;
else return false;
}
};
void add(int u, int v, int w) {
edge[++top].to = v;
edge[top].w = w;
edge[top].next = head[u];
head[u] = top;
}
vertex temp;
void Djikstra() {
for (int i = 1; i <= n; i++) dist[i] = INF;
priority_queue<vertex> q;
dist[s] = 0;
temp.num = s;
temp.dist = 0;
q.push(temp);
while (!q.empty()) {
temp = q.top();
q.pop();
int u = temp.num;
if (vis[u]) continue;
vis[u] = 1;
for (int i = head[u]; i; i = edge[i].next) {
int d = edge[i].to;
if (dist[d] > dist[u] + edge[i].w) {
dist[d] = dist[u] + edge[i].w;
if (!vis[d]) {//只用给用到的结点编号
temp.num = d;
temp.dist = dist[d];
q.push(temp);
}
}
}
}
}
int main() {
int u, v, w;
cin >> n >> m >> s;
for (int i = 1; i <= m; i++) {
cin >> u >> v >> w;
add(u, v, w);
}
Djikstra();
for (int i = 1; i <= n; i++) cout << dist[i] << ' ';
return 0;
}
负权最短路(不含负环)
https://www.luogu.org/problem/P3371
对于非负权图,Djikstra最优,但是对于负权图就不行了,只能用SPFA。
SPFA:用BFS遍历每一个结点u,如果u指向的结点v原来的最短路比现在u的最短路加上u到v的距离长,则更新v的最短路,同时由于v之后结点的最短路也会改变,要将v进入队列,同时要设一个标记数组以防止重复进队。
这种算法的时间复杂度比Djikstra的优化版本大:。
#include<iostream>
#include<queue>
const long long inf = 2147483647;
const int maxn = 10005;
const int maxm = 500005;
using namespace std;
int n, m, s, top = 1;
int dist[maxn], inque[maxn], head[maxm];
struct Edge {
int to;
int w;
int next;
}edge[maxm];
void add(int u, int v, int w) {
edge[++top].to = v;
edge[top].w = w;
edge[top].next = head[u];
head[u] = top;
}
void spfa() {
queue<int> q;
for (int i = 1; i <= n; i++) {
dist[i] = inf;
inque[i] = 0;
}
q.push(s);
dist[s] = 0;
inque[s] = 1;
while (!q.empty()) {
int u = q.front();
q.pop();
inque[u] = 0;
for (int i = head[u]; i > 0; i = edge[i].next) {
int d = edge[i].to;
if (dist[d] > dist[u] + edge[i].w) {
dist[d] = dist[u] + edge[i].w;
if (inque[d] == 0) {
inque[d] = 1;
q.push(d);
}
}
}
}
}
int main() {
cin >> n >> m >> s;
for (int i = 1; i <= m; i++ ) {
int u, v, w;
cin >> u >> v >> w;
add(u, v, w);
}
spfa();
for (int i = 1; i <= n; i++) cout << dist[i] << ' ';
return 0;
}
这种算法要保证图中不含负环(权值和为负的环),否则会无限循环。
负环问题
在SPFA中,如果一个结点被访问n次,说明它所在环越加越小,故存在负环,可以设置一个cnt数组保存访问的次数。
例题:https://www.luogu.org/problem/P3385
注意每一次读数据都要清空cnt和head……
代码:https://www.luogu.org/record/22610301
5.网络流问题
https://baijiahao.baidu.com/s?id=1607147031919790970&wfr=spider&for=pc
最大流问题:
对于一个有权图G,选定一个点S为起点,点T为终点,电流从S流向T,要求对于所有既不是S又不是T的顶点,流入的电流等于流出的电流,且流经
每一条边的电流都不超过该边的权,求最多有多少电流能从S流到T?
增广路:从S到T的一条路径,若选取这条路径,从S到T的流可以增加。
如果一个状态不存在增广路,则此状态确定的流为最大流。
一个朴素的想法:从S开始BFS,当找到一条到T的通路时,就把此通路所有边的权都减去这条通路上所有边的最小权,直到没有S到T的通路为止。
但是这样不对,因为可以构造反例使得这样计算的结果与选择通路的顺序有关。
EK算法:一开始给原来的图G的每条边都补上权为0的反向边,每次某一条边进行减法运算时,它的反向边同时进行加法运算,这样结果就与选择通路的顺序无关了。原因是反向边相当于可以把之前走过来的人再撵回去,从而得到更好的走法。
模板题:https://www.luogu.org/problem/P3376
本题数据规模很大,不能用vector(会超时),而且也不好访问反向边,也不能用二维数组(会超空间),可以用链式前向星。
#include<iostream>
#include<queue>
#include<algorithm>
#include<memory.h>
#define INF 1<<30
using namespace std;
int n, m, s, t;
int top = 1, head[100010]; //top表示栈顶,head[u]表示上一个起点为u的边。
bool isused[100010]; //标记某个结点是否用过
queue<int> q;
struct Edge {
int to; //终点编号
int w; //权
int next; //前一个与它共起点的边
}edge[200010];
struct Pre {
int v;
int e;
}pre[100010]; //找到增广路后,某一个顶点的前一个顶点以及以它为终点的边
void add(int u, int v, int w) { //添加边
edge[++top].to = v;
edge[top].w = w;
edge[top].next = head[u];
head[u] = top;//每新加进一条边,就让它的next指向与它共起点的前一条边;对于第一条边,让它的next=0
}
int x;
int to;
bool bfs() {//广搜找增广路
while (!q.empty()) q.pop(); //清空队列
memset(pre, -1, sizeof(pre)); //初始化标记数组
memset(isused, 0, sizeof(isused));
q.push(s); //起点进队
isused[s] = 1; //别忘了把起点设置为isused,因为有双向边
while (!q.empty()) { //BFS
x = q.front();
q.pop();//取队首元素并让其出队
for (int i = head[x]; i > 0; i = edge[i].next){ //遍历所有以x为起点的边
to = edge[i].to;
if (!isused[to] && edge[i].w > 0) { //如果终点已经用过,就不要走这条路了
pre[to].v = x; //to的前置顶点为x
pre[to].e = i; //to的前置边为i
if (to == t) return 1; //终点是t,说明找到了一条增广路
isused[to] = 1;
q.push(to);
}
}
}
return 0;
}
int EK() {
int res = 0;
while (bfs()) {
int mi = INF;
for (int i = t; i != s; i = pre[i].v) {
mi = min(mi, edge[pre[i].e].w); //找到增广路上的最小边
}
for (int i = t; i != s; i = pre[i].v) {
edge[pre[i].e].w -= mi; //增广路上的正向边减去mi
edge[pre[i].e ^ 1].w += mi;
//由于top从1开始,正向边为2,4,6...,反向边都是3,5,7....,所以它们仅有最后一个二进制位不同
//用异或可以实现访问反向边
}
res += mi;
}
return res;
}
int main() {
cin >> n >> m >> s >> t;
int u, v, w;
for (int i = 1; i <= m; i++) {
cin >> u >> v >> w;
add(u, v, w); //读入正向边
add(v, u, 0); //读入反向边
}
cout << EK();
return 0;
}
时间复杂度:
二分图匹配
已知A={,,...,};B={,,...,},对于每个,都有若干与之匹配,求最多能匹配多少组?
此问题可以转化为最大流问题:如果能和匹配,则它们之间存在价值为1的单向边,再新增两个顶点s和t,让s指向所有a,所有b指向t,求s到t的最大流即可。
当然可以用EK算法,但是求最大流有更优的Dinic算法:
先求所有顶点到s的最短距离(BFS),然后用dfs求最短距离递增的增广路,这样可以一次找到多条增广路。
#include<iostream>
#include<algorithm>
#include<queue>
#include<memory.h>
#define INF 1<<30
using namespace std;
int dep[10010], head[10010]; //dep储存s到每个结点的最短路径长
int top = 1;
int maxflow; //最大流
int n, m, s, t;
struct Edge {
int to;
int w;
int next;
}edge[200010];
void add(int u, int v, int w) {
edge[++top].to = v;
edge[top].w = w;
edge[top].next = head[u];
head[u] = top;
}
bool bfs() {
memset(dep, 0x3f, sizeof(dep)); //memset的技巧:不能直接设置大数,但可以设置0x3f,此时每个元素的值为0x3f3f3f3f
dep[s] = 0; //s结点为0层
queue<int> q;
q.push(s);
while (!q.empty()) { //BFS求最短路径长
int u = q.front();
q.pop();
for (int i = head[u]; i > 0; i = edge[i].next) { //遍历以u为起点的边
int d = edge[i].to;
if (dep[d] > dep[u] + 1 && edge[i].w > 0) { //保证u直接能到d结点比u的层数深1,注意要判断w>0防双向边
dep[d] = dep[u] + 1;
q.push(d);
}
}
}
if (dep[t] != 0x3f3f3f3f) return 1; //能到达t,说明能分层
return 0;
}
int dfs(int u,int flow) { //u:当前的结点,flow:从s到当前结点的最大流,返回值:从当前结点到t的最大流
int rlow = 0; //余量:从当前结点到t的最大流
if (u == t) return flow; //已经到了t,最大流不再改变
for (int i = head[u]; i > 0; i = edge[i].next) { //遍历以u为起点的边
int d = edge[i].to;
if (edge[i].w > 0 && dep[d] == dep[u] + 1) { //判断终点是否比起点的层数多1
rlow = dfs(d, min(flow, edge[i].w)); //递归地搜索能到达t的最大流,如果到达不了,rlow=0,继续搜索别的顶点
if (rlow > 0) { //如果能到达,沿途的正向边减rlow,负向边加rlow
edge[i].w -= rlow;
edge[i ^ 1].w += rlow;
return rlow;
}
}
}
return 0;//如果搜不到rlow>0,说明按照当前层数排序已经没有增广路了,需要改变层序
}
int Dinic() {//返回最大流
int lowflow;
while (bfs()) { //如果能分层
while (1) {
lowflow = dfs(s, INF); //返回一条增广路的最大流
if (lowflow > 0) maxflow += lowflow;
else break; //如果找不到增广路,break出去改变层序
}
}
return maxflow;
}
int main() {
cin >> n >> m >> s >> t;
int u, v, w;
for (int i = 1; i <= m; i++) {
cin >> u >> v >> w;
add(u, v, w);
add(v, u, 0);
}
cout << Dinic();
return 0;
}
时间复杂度:,适用于边数较多的图。
特别地,在二分图匹配问题中为
例题:https://www.luogu.org/problem/P2756
只需要把上面的代码改一下就行,设一个不是结点的s和t,让它们分别指向外籍飞行员和被英国飞行员指向,再用res数组保存u指向d的关系(注意它的位置,必须是能走到终点的情况),输出即可。
完整代码:https://www.luogu.org/record/22582768
二分图匹配还可以用匈牙利算法,这里先欠着……
最小费用最大流
https://www.luogu.org/problem/P3381
对于每条边有两个权,分别是流和流过单位流量的费用,要求在最大流的前提下,求最小费用。
把EK算法中的BFS改成SPFA即可,注意SPFA的对象是费用不是流,对于反向边,费用要变成负的。
https://www.luogu.org/record/22610301
6.最小生成树
最小生成树:对于一个无向图G,包含G的所有顶点的树中,权值和最低的树叫做G的最小生成树。
只有连通图有最小生成树,且最小生成树不包含环,即它的边数比顶点数少1。
求最小生成树:https://www.luogu.org/problem/P3366
Prim
适用于稠密图。遍历所有起点u已经在树上的边(u,v),找出权值最小的边,把它加到最小生成树上。重复以上步骤直到所有顶点都在树上。
#include<iostream>
#include<algorithm>
using namespace std;
const int maxn = 5010;
const int maxm = 200010;
const int INF = 1 << 30;
int n, m, u, v, w;
int top, head[maxn], dist[maxn]; //dist:到现在在树上的点的最小距离
int num, now;
int res;
bool vis[maxn];
struct Edge {
int to;
int next;
int w;
}edge[maxm*2];//无向图,所有的边都是双向边
void add(int u, int v, int w) {
edge[++top].to = v;
edge[top].w = w;
edge[top].next = head[u];
head[u] = top;
}
void prim() {
for (int i = 1; i <= n; i++) dist[i] = INF;
now = 1;
for (int i = head[1]; i; i = edge[i].next) {
dist[edge[i].to] = min(dist[edge[i].to], edge[i].w);
} //从第一个点开始设置,用min解决重边问题
while (++num < n) { //到n结束,此时加了n-1条边
int minn = INF;
vis[now] = 1;
for (int i = 1; i <= n; i++) {//找到最短的边,并把它的终点加到树上
if (!vis[i] && minn > dist[i]) {
minn = dist[i];
now = i;
}
}
res += minn;
for (int i = head[now]; i; i = edge[i].next) { //更新dist
int d = edge[i].to;
if (dist[d] > edge[i].w && !vis[d]) dist[d] = edge[i].w;
}
}
}
int main() {
cin >> n >> m;
for (int i = 1; i <= m; i++) {
cin >> u >> v >> w;
add(u, v, w);
add(v, u, w);
}
prim();
cout << res;
return 0;
}
时间复杂度:
同理Dijkstra,堆优化后时间复杂度降到。
以下的模板用了pair模板类,同上面的Dijkstra模板相比要精炼许多,因为pair重载了小于运算符(见前面的某篇笔记),而且可以用make_pair构造函数。
#include<iostream>
#include<utility>
#include<queue>
using namespace std;
const int maxn = 5010;
const int maxm = 200010;
const int inf = 1 << 30;
struct Edge {
int to;
int w;
int next;
}edge[maxm*2];
int top, head[maxn], dist[maxn], vis[maxn];
int n, m, u, v, w;
int cnt, res;
typedef pair<int, int> p;//因为pair类的排序是字典序,所以first是dist,second是编号
priority_queue<p, vector<p>, greater<p> > q;//最小堆
void add(int u, int v, int w) {
edge[++top].to = v;
edge[top].w = w;
edge[top].next = head[u];
head[u] = top;
}
void prim() {
dist[1] = 0;
for (int i = 2; i <= n; i++) dist[i] = inf;
q.push(make_pair(0, 1));//第一个顶点进堆
while (!q.empty() && cnt < n) {
int minn = q.top().first;
int u = q.top().second;
q.pop();
if (vis[u]) continue;
cnt++;
res += minn;
vis[u] = 1;
for (int i = head[u]; i; i = edge[i].next) {//更新顶点距离
int d = edge[i].to;
if (!vis[d]&&edge[i].w < dist[d]) {
dist[d] = edge[i].w;
q.push(make_pair(dist[d], d));
}
}
}
}
int main() {
cin >> n >> m;
for (int i = 1; i <= m; i++) {
cin >> u >> v >> w;
add(u, v, w);
add(v, u, w);
}
prim();
if (cnt == n) cout << res;
else cout<<"orz";
return 0;
}
Kruskal
适用于稀疏图。将边按照权从小到大排序,从权值最小的边开始选取,如果出现环则跳过(用并查集判断),直到边数比总点数少1。
#include<iostream>
#include<algorithm>
using namespace std;
const int maxn = 5010;
const int maxm = 200010;
struct Edge {//不用链式前向星了,只用存每个边的权
int u;
int v;
int w;
bool operator < (const Edge& x) const {
return w < x.w;
}
}edge[maxm];
int fa[5010];
int n, m, res, u, v, cnt;
int find(int x) {//并查集模板
if (x == fa[x]) return x;
return fa[x] = find(fa[x]);
}
void kruskal() {
sort(edge, edge + m); //按边权从小到大排序
for (int i = 0; i < m; i++) {
u = find(edge[i].u);
v = find(edge[i].v);
if (u == v) continue; //如果成环(起点和终点已经连通)则跳过
res += edge[i].w;
fa[v] = u;//将起点和终点设为连通
if (++cnt == n - 1) break;
}
}
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) fa[i] = i;//初始化并查集
for (int i = 0; i < m; i++) cin >> edge[i].u >> edge[i].v >> edge[i].w;
kruskal();
if(cnt == n-1)cout << res;
else cout << "orz";
return 0;
}