图论算法模板,随缘不定期更新
用来保存图论的模板方便备赛,算法的解析看心情写
代码中读入函数scanf和scanf_s与编译器有关,没有太大差别
帮我把点赞数的最后一位取反,谢谢
—Ninght
搜索(更新于2021/1/11)
DFS(深度优先搜索)和BFS(广度优先搜索)是图轮中常用的两种搜索算法,在不加任何优化的情况下,两种算法的时间复杂度相同。
DFS
BFS
并查集(更新于2021/3/20—10:54)
概念及作用
我对于并查集的理解是她是一种用于处理数据之间关系的数据结构,由一个整型数组、一个查询函数和一个合并函数构成。
举一个最简单的例子:现在有n名从1到n标号的人,并且我们知道他们之间的一些关系如1与2是朋友关系,2与3是朋友关系,3与4是敌对关系等,我们规定朋友的朋友是朋友,朋友的敌人是敌人切不存在既是朋友又是敌人的情况,那么对于给定的两个人x,y(小于n),我们要如何根据已知的关系推断出两人是朋友还是敌人?
并查集是用于解决类似上述问题的一种数据结构。
原理及实现
并查集的原理可以看成是给离散的n个点建立无向图来维护给定的关系。这里我们假设n=5并且给出关系(1,2)、(2,3)、(4,5)((x,y)表示x,y为朋友,没有给出则为敌人关系)。
那么我们首先由n=5可以得到五个离散的点
这里我们令关系(x,y)表示连接结点x和y的一条无向边,那么我们可以得到一个无向图
不难看出1、2、3在一个连通分量中,4、5在一个连通分量中,即1,2,3是朋友关系,4,5 是朋友关系,他们(1,2,3和4,5)之间任意两人为敌对关系。如果我们用连通分量中的一个点来表示这个连通分量中所有的点,那么在查找给定两人x,y的关系时只需判断两人是否属于同一个连通分量即可。
因此这里引入一个整型数组a[MAX]来记录每个点所在的连通分量(a[i]=j表示点i所在的连通分量为j即点i和点j在同一个连通分量中,换句话说i和j是朋友关系),显然,在最开始时所有的点均离散,所以每个点构成了一个连通分量,数组a要初始化为a[x]=x。
接下来需要根据给出的关系来维护这个数组a,所以定义函数Hebin(x,y)表示将x与y合并放在同一个连通分量中,那么对于数组a的操作应为a[x所在的连通分量]=y所在的连通分量(意义为x所在的连通分量与y所在的连通分量是同一个连通分量)。
要实现这个操作,我们需要知道x(或y)所在的连通分量是哪一个,这就需要用到数组a中的值,前面提到,数组a初始化为a[x]=x,因此我们知道当查询一个点x在数组a中对应的值a[x]就是它本身时,点x所在的连通分量就是x,那么若还存在点y、z使得a[y]=x、a[z]=y,我们也可以知道y所在的连通分量是x,z所在的连通分量与y相同也是x。所以查询点x所在连通分量的函数Chaxun(x)可以通过简单递归来实现。
int Chaxun(int x)
{
return (a[x] == x) ? x : a[x] = Chaxun(a[x]);
}
根据Chaxun(x)这个函数我们就可以实现上面的Hebin(x,y)函数(a[x所在的连通分量]=y所在的连通分量)
void Hebin(int x, int y)
{
a[Chaxun(x)] = Chaxun(y);
}
完整代码
下面的代码中主函数用于解决洛谷中的模板题P3367
#include <iostream>
#include<cstdio>
using namespace std;
const int maxn = 10002;
int n, m, a[maxn];
int Chaxun(int x)
{
return (a[x] == x) ? x : a[x] = Chaxun(a[x]);
}
void Hebin(int x, int y)
{
a[Chaxun(x)] = Chaxun(y);
}
int main()
{
int i, x, y, z;
scanf("%d%d", &n, &m);
for (i = 1; i <= n; i++)a[i] = i;
for (i = 0; i < m; i++)
{
scanf("%d%d%d", &z, &x, &y);
if (z == 1)Hebin(x, y);
if (z == 2)
{
if (Chaxun(x) == Chaxun(y))printf("Y\n");
else printf("N\n");
}
}
return 0;
}
启发式合并
void join(int x, int y)
{
int xx = find(x), yy = find(y); //xx,yy 分别表示x与y的祖先节点
if (xx == yy) return;
if (rank[xx] > rank[yy]) //rank[] 表示树的高度
fa[yy] = xx; //将yy接到xx上去
//由于xx的高度比yy的大,所以无需更新高度 rank[xx]>=rank[yy]+1
else
{
fa[xx] = yy;//将xx接到yy上去
if (rank[xx] == rank[yy])
rank[yy]++;
}
}
并查集是一个非常实用而且我很早之前就想整理详解的数据结构,结果拖了将近一年才写,原因是汇编语言实在是看不下去了,果然复习的时候除了复习觉得什么事都挺有意思的hhh,现在距离汇编考试还有9个小时,本来以为挺好写的,结果还是写了将近两个小时,组织语言太难了hhh。还是不想复习,回去睡觉了。。。考试加油
—Ninght
网络流
最大流
dinic(更新于2021/4/19—16:16)
//输入顶点数,边数,源点,汇点及各有向边的起始点和权值,输出汇点最大流
#include<algorithm>
#include<cstring>
#include<cstdio>
#include<queue>
#define int long long
using std::queue;
struct node {
int to;//终点
int next;//与该边同起点的上一条边的序号
int w;//权值
}qxx[200005];//边集链式前向星
int h[200005];//起点为x的一条边的序号
int cnt;//计数器
int n, m, st, en;//顶点数,边数,源点,汇点
int x, y, z;//始点,终点,权值
void add(int x, int y, int z) {
qxx[++cnt] = node{ y,h[x],z };
h[x] = cnt;
}//添加边(始点,终点,权值)
void ad(int x, int y, int z) {
add(x, y, z);//正向
add(y, x, 0);//反向
}//添加边
int d[200005];
bool bfs() {
memset(d, 0, sizeof d);
queue<int>q;
while (!q.empty())q.pop();
d[st] = 1;
q.push(st);
while (!q.empty()) {
int x = q.front();
q.pop();
for (int i = h[x]; i; i = qxx[i].next) {
int v = qxx[i].to;
if (!d[v] && qxx[i].w) {
d[v] = d[x] + 1;
q.push(v);
if (v == en)return true;
}
}
}
return false;
}
int dfs(int u, int flow) {
if (u == en)return flow;
int rest = flow;
for (int i = h[u]; i; i = qxx[i].next) {
int v = qxx[i].to;
if (d[v] == d[u] + 1 && qxx[i].w) {
int tmp = dfs(v, std::min(qxx[i].w, rest));
if (!tmp)d[v] = 0;
rest -= tmp;
qxx[i].w -= tmp;
qxx[i ^ 1].w += tmp;
if (!rest)break;
}
}
return flow - rest;
}
int ans, sth;
signed main() {
scanf_s("%lld%lld%lld%lld", &n, &m, &st, &en);
for (int i = 1; i <= m; i++) {
scanf_s("%lld%lld%lld", &x, &y, &z);
ad(x, y, z);
}
while (bfs())while (sth = dfs(st, 1e9))ans += sth;
printf("%lld", ans);
return 0;
}
洛谷p1646(费用流最小割)
#include <bits/stdc++.h>
#define xuhao(i,j) ((i-1)*m+j)
using namespace std;
const int maxn = 4e4 + 9e3 + 6e2 + 2, maxm = 2e5 + 7e4 + 7e3 + 6e2 + 1, inf = 0x7fffffff;
int n, m, a, s, t, tot = 1, head[maxn], dep[maxn], ans, sum, cnt;
struct edge
{
int to, next, w;
}e[maxm];
void addedge(int x, int y, int w)
{
e[++tot].to = y; e[tot].w = w; e[tot].next = head[x]; head[x] = tot;
e[++tot].to = x; e[tot].w = 0; e[tot].next = head[y]; head[y] = tot;
}
bool bfs()
{
memset(dep, 0, sizeof dep);
queue<int>q;
q.push(s); dep[s] = 1;
while (!q.empty())
{
int x = q.front(); q.pop();
for (int i = head[x]; i; i = e[i].next)
{
int y = e[i].to, w = e[i].w;
if (w && !dep[y])
{
dep[y] = dep[x] + 1;
q.push(y);
}
}
}
return dep[t];
}
int dfs(int u, int flow) {
if (u == t) return flow;
int ans = 0;
for (int i = head[u]; i && ans < flow; i = e[i].next) {
int v = e[i].to;
if (e[i].w && dep[v] == dep[u] + 1) {
int x = dfs(v, min(e[i].w, flow - ans));
if (x) e[i].w -= x, e[i ^ 1].w += x, ans += x;
}
}
if (ans < flow) dep[u] = -1;
return ans;
}
int main()
{
ios::sync_with_stdio(0);
scanf("%d %d", &n, &m);
s = 0, t = n * m + 2 * (n - 1) * m + 2 * n * (m - 1) + 1;
for (int i = 1; i <= n; i++)for (int j = 1; j <= m; j++)scanf("%d", &a), sum += a, addedge(s, xuhao(i, j), a);
for (int i = 1; i <= n; i++)for (int j = 1; j <= m; j++)scanf("%d", &a), sum += a, addedge(xuhao(i, j), t, a);
cnt = n * m;
if (n - 1 != 0)
{
for (int i = 1; i <= n - 1; i++)
for (int j = 1; j <= m; j++)
{
++cnt;
scanf("%d", &a), sum += a, addedge(s, cnt, a);
addedge(cnt, xuhao(i, j), inf);
addedge(cnt, xuhao(i + 1, j), inf);
}
for (int i = 1; i <= n - 1; i++)
for (int j = 1; j <= m; j++)
{
++cnt;
scanf("%d", &a), sum += a, addedge(cnt, t, a);
addedge(xuhao(i, j), cnt, inf);
addedge(xuhao(i + 1, j), cnt, inf);
}
}
if (m - 1 != 0)
{
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m - 1; j++)
{
++cnt;
scanf("%d", &a), sum += a, addedge(s, cnt, a);
addedge(cnt, xuhao(i, j), inf);
addedge(cnt, xuhao(i, j + 1), inf);
}
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m - 1; j++)
{
++cnt;
scanf("%d", &a), sum += a, addedge(cnt, t, a);
addedge(xuhao(i, j), cnt, inf);
addedge(xuhao(i, j + 1), cnt, inf);
}
}
while (bfs())ans += dfs(s, inf);
printf("%d\n", sum - ans);
return 0;
}
hlpp(更新于2021/1/6)
#include<cstdio>
#include<cstring>
#include<cmath>
#include<cstdlib>
#include<climits>
#include<ctime>
#include<algorithm>
#include<complex>
#include<iostream>
#include<map>
#include<queue>
#include<vector>
#define ll long long
#define inf 0x3f3f3f3f
#define re register
#define il inline
using namespace std;
struct edge
{
int to, next;
int flow;
}a[2000020];//链式前向星
int head[10010];
int gap[10010];//存储同层结点数
int h[10010];
int e[10010];//e[i]表示第i号节点的超额流
int vis[10010];
int cnt(0);
int n, m, st, ed;//顶点数,边数,源点,汇点
struct cmp
{
il bool operator ()(int xi, int yi)const
{
return h[xi] < h[yi];
}
};
priority_queue<int, vector<int>, cmp> pq;//以层数为优先级队列
queue<int> q;
il void addedge(int xi, int yi, int fi)
{
a[cnt].to = yi;
a[cnt].next = head[xi];
a[cnt].flow = fi;
head[xi] = cnt++;
}
il bool bfs()
{
re int i;
memset(h + 1, inf, sizeof(int) * n);//将所有的节点高度均设为inf,表示不在网络内
h[ed] = 0;
q.push(ed);
while (!q.empty())
{
int t = q.front();
q.pop();
for (i = head[t]; i != -1; i = a[i].next)
{
int v = a[i].to;
if (a[i ^ 1].flow && h[v] > h[t] + 1)
{
h[v] = h[t] + 1;//更新节点高度
q.push(v);
}
}
}
return h[st] != inf;
}
il void push(int u)
{
re int i;
for (i = head[u]; i != -1; i = a[i].next)//遍历当前点的邻点
{
int v = a[i].to;
if ((a[i].flow) && (h[v] + 1 == h[u]))//将该店的超额流传入层数小1的邻点
{
int df = min(e[u], a[i].flow);
a[i].flow -= df;
a[i ^ 1].flow += df;
e[u] -= df;
e[v] += df;
if ((v != st) && (v != ed) && (!vis[v]))
{
pq.push(v);
vis[v] = 1;
}
if (!e[u])break;
}
}
}//只将处源点和汇点以外的点送入队列
il void relabel(int u)
{
re int i;
h[u] = inf;
for (i = head[u]; i != -1; i = a[i].next)
{
int v = a[i].to;
if ((a[i].flow) && (h[v] + 1 < h[u]))h[u] = h[v] + 1;
}
}
inline int hlpp()
{
re int i;
if (!bfs())return 0;
h[st] = n;
memset(gap, 0, sizeof(int) * (n << 1));
for (i = 1; i <= n; i++)if (h[i] != inf)gap[h[i]]++;
for (i = head[st]; i != -1; i = a[i].next)
{
int v = a[i].to;
if (int f = a[i].flow)
{
a[i].flow -= f; a[i ^ 1].flow += f;
e[st] -= f; e[v] += f;
if (v != st && v != ed && !vis[v])
{
¬¬pq.push(v);
vis[v] = 1;
}
}
}
while (!pq.empty())
{
int t = pq.top(); pq.pop();
vis[t] = 0; push(t);
if (e[t])
{
gap[h[t]]--;
if (!gap[h[t]])
{
for (re int v = 1; v <= n; v++)
{
if (v != st && v != ed && h[v] > h[t] && h[v] < n + 1)
{
h[v] = n + 1;
}
}
}
relabel(t); gap[h[t]]++;
pq.push(t); vis[t] = 1;
}
}
return e[ed];
}
signed main()
{
re int i;
memset(head, -1, sizeof(head));
scanf_s("%d%d%d%d", &n, &m, &st, &ed);
for (i = 1; i <= m; i++)
{
int x, y;
ll f;
scanf_s("%d%d%lld", &x, &y, &f);
addedge(x, y, f);
addedge(y, x, 0);
}
ll maxf = hlpp();
printf("%lld", maxf);
return 0;
}
最小费用最大流(更新于2021/1/6)
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;
const int maxn = 100010;
bool vis[maxn];
int n, m, s, t, x, y, z, f, dis[maxn], pre[maxn], last[maxn], flow[maxn], maxflow, mincost;
//dis最小花费;pre每个点的前驱;last每个点的所连的前一条边;flow源点到此处的流量
struct Edge {
int to, next, flow, dis;//flow流量 dis花费
}edge[maxn];
int head[maxn], num_edge;
queue <int> q;
void add_edge(int from, int to, int flow, int dis)
{
edge[++num_edge].next = head[from];
edge[num_edge].to = to;
edge[num_edge].flow = flow;
edge[num_edge].dis = dis;
head[from] = num_edge;
}
bool spfa(int s, int t)
{
memset(dis, 0x7f, sizeof(dis));
memset(flow, 0x7f, sizeof(flow));
memset(vis, 0, sizeof(vis));
q.push(s); vis[s] = 1; dis[s] = 0; pre[t] = -1;
while (!q.empty())
{
int now = q.front();
q.pop();
vis[now] = 0;
for (int i = head[now]; i != -1; i = edge[i].next)
{
if (edge[i].flow > 0 && dis[edge[i].to] > dis[now] + edge[i].dis)//正边
{
dis[edge[i].to] = dis[now] + edge[i].dis;
pre[edge[i].to] = now;
last[edge[i].to] = i;
flow[edge[i].to] = min(flow[now], edge[i].flow);//
if (!vis[edge[i].to])
{
vis[edge[i].to] = 1;
q.push(edge[i].to);
}
}
}
}
return pre[t] != -1;
}
void MCMF()
{
while (spfa(s, t))
{
int now = t;
maxflow += flow[t];
mincost += flow[t] * dis[t];
while (now != s)
{//从源点一直回溯到汇点
edge[last[now]].flow -= flow[t];//flow和dis容易搞混
edge[last[now] ^ 1].flow += flow[t];
now = pre[now];
}
}
}
int main()
{
memset(head, -1, sizeof(head)); num_edge = -1;//初始化
scanf("%d%d%d%d", &n, &m, &s, &t);
for (int i = 1; i <= m; i++)
{
scanf("%d%d%d%d", &x, &y, &z, &f);
add_edge(x, y, z, f); add_edge(y, x, 0, -f);
//反边的流量为0,花费是相反数
}
MCMF();
printf("%d %d", maxflow, mincost);
return 0;
}
无源汇有上下界可行流(更新于2021/1/6)
#include<queue>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define N 100005
#define M 1000005
#define inf 1ll<<31ll-1
using namespace std;
int n, m, s, t, ss, tt, num = 1;
int v[M], w[M], next1[M];
int d[N], f[N], sum[N], first[N];
bool can[N];
void add(int x, int y, int f)
{
num++;
next1[num] = first[x];
first[x] = num;
v[num] = y;
w[num] = f;
}
bool bfs(int start, int end)
{
int x, y, i, j;
memset(d, -1, sizeof(d));
memcpy(f, first, sizeof(f));
queue<int>q;
q.push(start);
d[start] = 0;
while (!q.empty())
{
x = q.front();
q.pop();
for (i = first[x]; i; i = next1[i])
{
y = v[i];
if (w[i] && d[y] == -1 && !can[y])
{
d[y] = d[x] + 1;
if (y == end)
return true;
q.push(y);
}
}
}
return false;
}
int dinic(int now, int end, int flow)
{
if (now == end)
return flow;
int x, delta, ans = 0;
for (int& i = f[now]; i; i = next1[i])
{
x = v[i];
if (w[i] && d[x] == d[now] + 1 && !can[x])
{
delta = dinic(x, end, min(flow, w[i]));
w[i] -= delta;
w[i ^ 1] += delta;
flow -= delta;
ans += delta;
if (!flow) break;
}
}
if (flow) d[now] = -1;
return ans;
}
int main()
{
int x, y, i, j, l, r;
int ans = 0, maxflow = 0;
scanf("%d%d%d%d", &n, &m, &s, &t);
ss = 0, tt = n + 1;
for (i = 1; i <= m; ++i)
{
scanf("%d%d%d%d", &x, &y, &l, &r);
sum[x] -= l, sum[y] += l;
add(x, y, r - l), add(y, x, 0);
}
for (i = 1; i <= n; ++i)
{
if (sum[i] > 0) add(ss, i, sum[i]), add(i, ss, 0), ans += sum[i];
if (sum[i] < 0) add(i, tt, -sum[i]), add(tt, i, 0);
}
add(t, s, inf), add(s, t, 0);
while (bfs(ss, tt))
maxflow += dinic(ss, tt, inf);
can[ss] = false;
can[tt] = false;
if (maxflow != ans)
{
printf("please go home to sleep");
return 0;
}
int minflow = 0;
add(t, s, -inf), add(s, t, 0);
while (bfs(t, s))
minflow -= dinic(t, s, inf);
printf("%d", minflow);
return 0;
}
有源汇上下界最大流(更新于2021/1/6)
#include<queue>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define N 100005
#define M 1000005
#define inf 1ll<<31ll-1
using namespace std;
int n, m, s, t, ss, tt, num = 1;
int v[M], w[M], next1[M];
int d[N], f[N], sum[N], first[N];
bool can[N];
void add(int x, int y, int f)
{
num++;
next1[num] = first[x];
first[x] = num;
v[num] = y;
w[num] = f;
}
bool bfs(int start, int end)
{
int x, y, i, j;
memset(d, -1, sizeof(d));
memcpy(f, first, sizeof(f));
queue<int>q;
q.push(start);
d[start] = 0;
while (!q.empty())
{
x = q.front();
q.pop();
for (i = first[x]; i; i = next1[i])
{
y = v[i];
if (w[i] && d[y] == -1 && !can[y])
{
d[y] = d[x] + 1;
if (y == end)
return true;
q.push(y);
}
}
}
return false;
}
int dinic(int now, int end, int flow)
{
if (now == end)
return flow;
int x, delta, ans = 0;
for (int& i = f[now]; i; i = next1[i])
{
x = v[i];
if (w[i] && d[x] == d[now] + 1 && !can[x])
{
delta = dinic(x, end, min(flow, w[i]));
w[i] -= delta;
w[i ^ 1] += delta;
flow -= delta;
ans += delta;
if (!flow) break;
}
}
if (flow) d[now] = -1;
return ans;
}
int main()
{
int x, y, i, j, l, r;
int ans = 0, maxflow = 0;
scanf("%d%d%d%d", &n, &m, &s, &t);
ss = 0, tt = n + 1;
for (i = 1; i <= m; ++i)
{
scanf("%d%d%d%d", &x, &y, &l, &r);
sum[x] -= l, sum[y] += l;
add(x, y, r - l), add(y, x, 0);
}
for (i = 1; i <= n; ++i)
{
if (sum[i] > 0) add(ss, i, sum[i]), add(i, ss, 0), ans += sum[i];
if (sum[i] < 0) add(i, tt, -sum[i]), add(tt, i, 0);
}
add(t, s, inf), add(s, t, 0);
while (bfs(ss, tt))
maxflow += dinic(ss, tt, inf);
can[ss] = false;
can[tt] = false;
if (maxflow != ans)
{
printf("please go home to sleep");
return 0;
}
maxflow = 0;
while (bfs(s, t))
maxflow += dinic(s, t, inf);
printf("%d", maxflow);
return 0;
}
最短路径
floyd(更新于2021/1/24—09:40)
算法实现
Floyd算法是典型的动态规划算法,时间复杂度为o(n3),空间复杂度为o(n2)
可通过维护一个二维数组d来实现(d[i][j]表示从点i到点j的最短距离,注意在初始化时应将数组中的值赋为INF表示不存在路径)
考虑点i和点j,其最短距离存在两种情况
- 点i到点j的一条边即为最短距离
- 存在点k使得点i到点k的距离加上点k到点j的距离是最短距离
对于点i和点k以及点k和点j进行相同的上述判断即可得到i到j的最短距离
因此转移方程为
if (d[i][j] > d[i][k] + d[k][j])
d[i][j] = d[i][k] + d[k][j];
在三层循环中维护二维数组d,当循环结束,d[i][j]即为点i到j的最短距离
完整代码
输入格式
第一行两个整数n m表示n个点,m条边
后m行u v w 表示一条连接点u和点v且权重为w的边
最后一行两个整数s t表示查询点s到点t的最短路径长
输出格式
一行一个整数ans表示点s到点t的最短路径长度
#include<iostream>
#include<cstdio>
#define MAX 1000
using namespace std;
int d[MAX][MAX];
int p[MAX][MAX];
int n, m;
int s, t;
int u, v, w;
int main()
{
memset(d, 0x7f, sizeof(d));
memset(p, 0, sizeof(p));
scanf_s("%d%d", &n, &m);
for (int i = 1; i <= m; i++)
{
scanf_s("%d%d%d", &u, &v, &w);
d[u][v] = w;
d[v][u] = w;
}
scanf_s("%d%d", &s, &t);
for (int k = 1; k <= n; k++)
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
if (d[i][k] != 2139062143 && d[k][j] != 2139062143)
if (d[i][j] > d[i][k] + d[k][j])
{
d[i][j] = d[i][k] + d[k][j];
}
if (d[s][t] != 2139062143)
printf("%d", d[s][t]);
else printf("-1");
return 0;
}
输入样例
7 12
1 2 12
1 6 16
1 7 14
2 3 10
2 6 7
3 4 3
3 5 5
3 6 6
4 5 4
5 6 2
5 7 8
6 7 9
1 7
输出样例
14
代码运行截图
Floyd算法是很简单的最短路算法,通过动态规划实现,因为时间复杂度太高所以我不是很常用(毕竟三次方的算法随便就爆掉了)在回家的高铁上实在太无聊所以写了Floyd算法的解析。冬天真的到了呢,换上了厚衣服,虽然是南方但还是期待一场大雪,期待过年吃一顿火锅,还期待很多很多。
—Ninght
dijkstra(更新于2021/1/6)
算法实现
dijkstra的算法实现过程类似寻找最小生成树,以一维数组d来维护点到源点的最短距离
从起点开始寻找与其直接相连且距离最短的邻点(注意这里的距离最短是指已确定的距离加上未加的边权之和最短,比如生成树中有n个点,那么我们需要寻找使d[i]+w(i,j)最小的点,其中d[i]表示生成树中已经确定的最短距离,w(i,j)表示连接点i和j的边权),修改数组d的值(数组开始时初始化为INF,d[s]=0)并将连接两个点的边加入生成树中
后对生成树中的所有的点,在分别与各点直接相连且不在树中的边中选出最小的边加入生成树中得到一颗新的生成树,修改d的值并重复操作直至生成树中包含所有的点
此时数组d中存储的信息即为起点s到各点的最短距离(d[i]=j表示起点s到点i的最短距离为j)。
以下图为例(起点s=0)
首先我们初始化数组d[0]=0,d[1]=0x7fffffff,d[2]=0x7fffffff,d[3]=0x7fffffff
从顶点0寻找与其直接相连的最近的点,即点1(下图)
后更新d[1]=d[0]+w(0,1)=1
d[0]=0,d[1]=1,d[2]=0x7fffffff,d[3]=0x7fffffff
然后从0,1两点开始寻找直接相连的最近的点,即点2(下图)
后更新d[2]=d[0]+w(0,2)
d[0]=0,d[1]=1,d[2]=2,d[3]=0x7fffffff
然后从0,1,2三个点寻找最近的点,即点3(下图)
后更新d[3]=d[1]+w(1,3)
d[0]=0,d[1]=1,d[2]=2,d[3]=5
此时算法结束,数组d中的数据即为各点到0点的最短距离
完整代码
#include<cstdio>
#include<iostream>
#include<queue>
#define MAX 200020
#define inf 0x7fffffff
using namespace std;
int n, m,vis[MAX];
long long dis[MAX];
struct edgenode
{
int to;
int next;
int weight;
}edge[MAX];
int cnt, head[MAX];
int u, v, w;
int s;
struct node
{
long long dis;
int pos;
bool operator <(const node& x)const
{
return x.dis < dis;
}
};
std::priority_queue<node> q;//优先队列
void add_edge(int u, int v, int w);
void dijkstra();
int main()
{
memset(dis, -1, sizeof(dis));
scanf_s("%d%d%d", &n, &m, &s);
for (int i = 1; i <= n; i++)dis[i] = inf;
for (int i = 0; i < m; i++)
{
scanf_s("%d%d%d", &u, &v, &w);
add_edge(u, v, w);
}
dijkstra();
for (int i = 1; i <= n; i++)printf("%lld ", dis[i]);
}
void add_edge(int u, int v, int w)
{
edge[++cnt] = edgenode{ v,head[u],w };
head[u] = cnt;
}
void dijkstra()
{
dis[s] = 0;
q.push(node { 0, s });
while (!q.empty())
{
node tmp = q.top();
q.pop();
int x = tmp.pos, d = tmp.dis;
if (vis[x])
continue;
vis[x] = 1;
for (int i = head[x]; i; i = edge[i].next)
{
int y = edge[i].to;
if (dis[y] > dis[x] + edge[i].weight)
{
dis[y] = dis[x] + edge[i].weight;
if (!vis[y])
{
q.push(node { dis[y], y });
}
}
}
}
}
spfa(更新于2021/1/6)
#include<cstdio>
#include<iostream>
#include<queue>
#define MAX 200010
#define inf 2147483647
using namespace std;
int n, m, s;//点数,边数,出发点
int vis[MAX];
long long dis[MAX];
int head[MAX], cnt, u, v, w;
struct node
{
int to;
int next;
int weight;
}edge[MAX];
void add_edge(int u, int v, int w);
void spfa();
int main()
{
memset(head, -1, sizeof(head));
scanf_s("%d%d%d", &n, &m, &s);
for (int i = 0; i < m; i++)
{
scanf_s("%d%d%d", &u, &v, &w);
add_edge(u, v, w);
}
spfa();
for (int i = 1; i <= n; i++)
if (i == s)printf("0 ");
else printf("%lld ", dis[i]);
}
void add_edge(int u, int v, int w)
{
edge[++cnt] = node{ v,head[u],w };
head[u] = cnt;
}
void spfa()
{
queue<int> q; //spfa用队列,这里用了STL的标准队列
for (int i = 1; i <= n; i++)
{
dis[i] = inf; //带权图初始化
vis[i] = 0; //记录点i是否在队列中,同dijkstra算法中的visited数组
}
q.push(s); dis[s] = 0; vis[s] = 1; //第一个顶点入队,进行标记
while (!q.empty())
{
int u = q.front(); //取出队首
q.pop(); vis[u] = 0; //出队标记
for (int i = head[u]; i!=-1; i = edge[i].next) //邻接表遍历7
{
int v = edge[i].to;
if (dis[v] > dis[u] + edge[i].weight) //如果有最短路就更改
{
dis[v] = dis[u] + edge[i].weight;
if (vis[v] == 0) //未入队则入队
{
vis[v] = 1; //标记入队
q.push(v);
}
}
}
}
}
最小生成树
kruskal(更新于2021/1/12—00:34)
算法实现
对于n个点m条边的联通无向图,首先根据n个点得到不含边的初始图并将所有的边根据边权由小到大排序,后依次选择边权最小的边进行判断,若边所连的两顶点不在同一个联通分量中,则将该边加入图中,当所有的点都在同一联通分量中时,得到的树即为原图的最小生成树。
以下图为例
首先我们得到一个不含任何边的初始图
同时将边(u,v,w)按照边权排序可得到
{(1,2,2),(2,4,3),(1,4,4),(3,4,5),(1,3,6),(2,3,7)}
然后根据这个顺序由边权最小的边开始向图中加边
首先是(1,2,2),显然点1和点2不在同一个联通分量,因此边(1,2,2)满足条件
然后是(2,4,3),同理这条边也满足条件
然后到边(1,4,4),这时我们发现点1和点4已经在同一个联通分量中了,因此这条边不满足条件直接跳过
考虑边(3,4,5),点3和点4不在同一个联通分量中,因此该边满足条件,将其加入图中
此时所有的点均在同一个联通分量中,算法结束,这时的图即为原图的最小生成树
完整代码
代码使用并查集来实现对于顶点所在联通分量的判定
输入格式为
第一行n m表示n个点m条边
接下来m行u,v,w表示点u和点v之间存在一条权重为w的边
输出格式为
第一行输出
The edges chosen are :
接下来n-1行为u,v,w表示所选择的边
The weight of MST is :
接下来输出最小生成树的权重
#include<stdio.h>
#include<stdlib.h>
#include<iostream>
using namespace std;
#define MAXN 11 //顶点个数的最大值
#define MAXM 20 //边的个数的最大值
struct edge //边
{
int u, v, w;
}edges[MAXM]; //边的数组
int parent[MAXN]; //parent[i]为顶点i所在集合对应的树中的根结点
int n, m; //顶点个数、边的个数
int i, j; //循环变量
void UFset() //初始化
{
for (i = 1; i <= n; i++) parent[i] = -1;
}
int Find(int x) //查找并返回结点x所属集合的根结点
{
int s; //查找位置
for (s = x; parent[s] >= 0; s = parent[s]);
while (s != x) //优化方案——压缩路径,使后续的查找操作加速
{
int tmp = parent[x];
parent[x] = s;
x = tmp;
}
return s;
}
//运用并查集,将两个不同集合的元素进行合并,使两个集合中任意两个元素都连通
void Union(int R1, int R2)
{
int r1 = Find(R1), r2 = Find(R2); //r1和r2分别为R1和R2的根结点
int tmp = parent[r1] + parent[r2]; //两个集合结点数之和(负数)
//如果R2所在树结点个数 > R1所在树结点个数(注意parent[r1]是负数)
if (parent[r1] > parent[r2])
{
parent[r1] = r2;
parent[r2] = tmp;
}
else
{
parent[r2] = r1;
parent[r1] = tmp;
}
}
int cmp(const void* a, const void* b) //实现从小到大的比较函数
{
edge aa = *(const edge*)a, bb = *(const edge*)b;
return aa.w - bb.w;
}
void Kruskal()
{
int sumweight = 0; //生成树的权值
int num = 0; //已选用的边的数目
UFset(); //初始化parent数组
for (i = 0; i < m; i++)
{
if (Find(edges[i].u) != Find(edges[i].v))
{
printf("%d %d %d\n", edges[i].u, edges[i].v, edges[i].w);
sumweight += edges[i].w; num++;
Union(edges[i].u, edges[i].v);
}
if (num >= n - 1) break;
}
printf("The weight of MST is : %d\n", sumweight);
}
void main()
{
scanf("%d%d", &n, &m); //读入顶点个数和边数
for (int i = 0; i < m; i++)
scanf("%d%d%d", &edges[i].u, &edges[i].v, &edges[i].w); //读入边的起点和终点
printf("The edges chosen are :\n");
qsort(edges, m, sizeof(edges[0]), cmp); //对边按权值从小到大排序
Kruskal();
}
样例输入
4 6
1 2 2
2 3 7
3 4 5
1 3 6
1 4 4
2 4 3
样例输出
The edges chosen are :
1 2 2
2 4 3
3 4 5
The weight of MST is : 10
代码运行截图
kruskal算法是基于边的最小生成树算法,代码以并查集为基础实现,因为并查集好写所以也是我比较习惯用的算法(老懒狗了hhh),在军训第三天的晚上写完这个算法的解析,有一说一冬天在深圳军训还是挺冷的。
—Ninght
prim(更新于2021/1/12—23:48)
算法实现
prim算法是一个典型的贪心算法,通过维护一棵树并不断给这棵树加边来实现。
算法共涉及到两个集合Edge和Vertex,其中Edge用于储存生成树的边,Vertex用于记录顶点
算法开始时选择任意一个顶点加入Vertex中,后进行循环,不断选择所连两点中有且仅有一个在集合Vertex中的边加入集合Edge中,当Vertex涵盖了所有顶点时算法结束,此时Edge中所包含的边所形成的树即为原图的最小生成树。
以下图为例
不妨开始时选择点1加入Vertex集合,此时Edge:{}为空集,Vertex:{1}
后我们不难发现在连接集合Vertex和集合外的点的边中,边(1,2,2)的权值最小,因此我们将其加入Edge中并将点2加入Vertex
此时Edge:{(1,2,2)},Vertex:{1,2}
这时连接集合Vertex和集合外点的边有(1,4,4)、(1,3,6)、(2,4,3)、(2,3,7),权值最小的边为(2,4,3)因此将(2,4,3)加入Edge中,将点4加入集合Vertex中
此时Edge:{(1,2,2),(2,4,3)},Vertex:{1,2,4}
循环操作后可以得到原图的最小生成树
Edge:{(1,2,2),(2,4,3),(3,4,5)},Vertex:{1,2,3,4}
计算可得最小生成树的权值为10
完整代码
以下代码针对的是完全图,不是完全图需在读入时修改edgenum的值
代码中用visit数组来实现Vertex的功能
输入格式为
第一行n表示n个点
接下来(n-1)*n/2行u v w表示点u和点v之间存在一条权重为w的边
输出格式为
一行输出最小生成树的权重
#include<iostream>
#include<cstdio>
#include<cstring>
#include<string>
#include<algorithm>
using namespace std;
#define INF 0x3f3f3f3f
#define MAXN 110
int map[MAXN][MAXN], lowcost[MAXN];
bool visit[MAXN];
int nodenum, sum;
void prim()
{
int temp, k;
sum = 0;
memset(visit, false, sizeof(visit)); //初始化visit
visit[1] = true;
for (int i = 1; i <= nodenum; ++i) //初始化lowcost[i]
lowcost[i] = map[1][i];
for (int i = 1; i <= nodenum; ++i)//找生成树集合点集相连最小权值的边
{
temp = INF;
for (int j = 1; j <= nodenum; ++j)
if (!visit[j] && temp > lowcost[j])
temp = lowcost[k = j];
if (temp == INF) break;
visit[k] = true; //加入最小生成树集合
sum += temp;//记录权值之和
for (int j = 1; j <= nodenum; ++j) //更新lowcost数组
if (!visit[j] && lowcost[j] > map[k][j])
lowcost[j] = map[k][j];
}
}
int main()
{
int a, b, cost, edgenum;
while (scanf("%d", &nodenum) && nodenum)//结点数
{
memset(map, INF, sizeof(map));
edgenum = nodenum * (nodenum - 1) / 2;
for (int i = 1; i <= edgenum; ++i) //输入边的信息
{
scanf("%d%d%d", &a, &b, &cost);
if (cost < map[a][b])
map[a][b] = map[b][a] = cost;
}
prim();
printf("%d\n", sum); //最小生成树权值之和
}
return 0;
}
样例输入
4
1 2 2
2 3 7
3 4 5
1 3 6
1 4 4
2 4 3
样例输出
10
代码运行截图
因为习惯用kruskal算法来求最小生成树,所以prim算法只整理了时间复杂度为o(n^2)的代码,其实通过优先队列来优化最短距离可以将时间复杂度降到o(nlogn),和kruskal算法的代码实现类似(不过既然有kruskal为什么还要用prim。。我基本不用prim是因为二维数组没有安全感)。晚上回宿舍发现队已经排到食堂门口了,不想排队所以直接来T2写了prim的解析。只能说深圳的冬天,懂得都懂。
—Ninght
欧拉通路与欧拉回路(更新于2021/1/11)
在无向图中
存在欧拉路径充要条件:所有点度数均为偶数或恰好有两个点度数为奇数
存在欧拉回路充要条件:所有点的度数为偶数
在有向图中
存在欧拉路径充要条件:所有点的入度等于该点的出度或恰好一点入度比出度大一,一点出度比入度大一且其它点入度与出度相同
存在欧拉回路充要条件:所有点的入度等于该点的出度
判断存在欧拉回路(更新于2021/1/6)
#include <stdio.h>
int arr[1000];
int father[1000];
int rand_deep[1000];
int findSet(int x) {
int px = x, i;
while (px != father[px])
px = father[px];
//路径压缩,加快查找速度
while (x != px) {
i = father[x];
father[x] = px;
x = i;
}
return px;
}
void unionSet(int x, int y) {
x = findSet(x);
y = findSet(y);
if (rand_deep[x] > rand_deep[y])
father[y] = x;
else {
father[x] = y;
if (rand_deep[x] == rand_deep[y])rand_deep[y]++;
}
}
//并查集
int main() {
int N, M;
while (scanf("%d", &N) != EOF && N) {
scanf("%d", &M);
int i;
int flag = 1;
for (i = 1; i <= N; i++) {
father[i] = i;
rand_deep[i] = 0;
arr[i] = 0;
}
for (i = 0; i < M; i++) {
int x, y;
scanf("%d %d", &x, &y);
arr[x] ++;
arr[y] ++;
unionSet(x, y);
}
int father;
for (i = 1; i <= N; i++) {
if (i == 1)
father = findSet(1);
else {
if (father != findSet(i)) {
flag = 0;
break;
}
}
if (arr[i] == 0 || arr[i] % 2 != 0) {
flag = 0;
break;
}
}
//判断是否在同一连通分量中
printf("%d\n", flag);
}
return 0;
}
寻找欧拉路径(更新于2021/1/6)
#include<iostream>
#include<cstdio>
#include<cstring>
#include<string.h>
#include<algorithm>
#include<vector>
using namespace std;
const int N = 1005;
int n, m, flag, top, sum, du[N], ans[5005], map[N][N];
void dfs(int x)
{
ans[++top] = x;
for (int i = 1; i <= n; i++)
{
if (map[x][i] >= 1)
{
map[x][i]--;
map[i][x]--;
dfs(i);
break;
}
}
}
void fleury(int x)
{
top = 1;
ans[top] = x;
while (top > 0)
{
int k = 0;
for (int i = 1; i <= n; i++)//判断是否可扩展
{
if (map[ans[top]][i] >= 1)//若存在一条从ans[top]出发的边 那么就是可扩展
{
k = 1; break;
}
}
if (k == 0)//该点x没有其他的边可以先走了(即不可扩展), 那么就输出它
{
printf("%d ", ans[top]);
top--;
}
else if (k == 1)//如可扩展, 则dfs可扩展的哪条路线
{
top--;//这需要注意
dfs(ans[top + 1]);
}
}
}
int main()
{
while (scanf("%d%d", &n, &m) != EOF)
{
memset(du, 0, sizeof(du));
memset(map, 0, sizeof(map));
for (int i = 1; i <= m; i++)
{
int x, y;
scanf("%d%d", &x, &y);
map[x][y]++; //记录边, 因为是无向图所以加两条边, 两个点之间可能有多条边
map[y][x]++;
du[x]++;
du[y]++;
}
flag = 1; // flag标记开始点。 如果所有点度数全为偶数那就从1开始搜
sum = 0;
for (int i = 1; i <= n; i++)
{
if (du[i] % 2 == 1)
{
sum++;
flag = i;// 若有奇数边, 从奇数边开始搜
}
}
if (sum == 0 || sum == 2)//度数均为偶数或仅有两个奇度定点则开始算法
fleury(flag);
}
return 0;
}
prufer编码
prufer编码是描述无根树的一种编码方式,对于给定的prufer编码可以唯一确定一颗无根树
树转prufer编码(更新于2021/1/27—10:33)
算法实现
在给定的一颗无根树中,不断选定编号最小的度为1的节点删除,并记录下其相邻节点编号,当只剩下两个节点时算法停止,得到的数列即为该树的prufer编码
以图中的树为例
首先删去编号最小的度为1的节点(1)并记录其相邻节点(2)
后删除节点3并记录其相邻节点(2)
重复操作至仅剩下两个节点时算法停止,此时得到的数列即为原树的prufer编码(2 2 2 6 6)
prufer编码转树(更新于2021/1/27—10:33)
算法实现
参考prufer编码的得到过程可以很容易实现prufer转树的算法
将prufer编码存入一个队列中,通过不断出队读取一个节点并将计数器与其建立一条边,最后得到一颗树
完整代码
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<queue>
using namespace std;
#define MAX 20//顶点数最大值
typedef struct arc
{
int u;
int v;
}arcnode;
queue<int> q1;
int flag[MAX];
int n, m, edgenum;
int min();
int main()
{
arcnode edge[MAX * (MAX - 1) / 2];//边集
scanf_s("%d", &n);//顶点数
for (int i = 0; i < n - 2; i++)
{
scanf_s("%d", &m);
q1.push(m);
flag[m]++;
}
q1.push(n);
flag[n]++;
while(!q1.empty())
{
int u = min();
flag[u]++;
int v = q1.front();
q1.pop();
flag[v]--;
edge[edgenum].u = u;
edge[edgenum].v = v;
edgenum++;
}
for (int i = 0; i < edgenum; i++)
{
printf("(%d,%d)\n", edge[i].u, edge[i].v);
}
return 0;
}
int min()//求不在队列内的点编号最小值
{
for (int i = 1; i <= n; i++)
{
if (flag[i] == 0)return i;
}
}
prufer编码在我看来是一个很简单的编码方式,第一次见到就被这种新奇的思想吸引(当然也是我太菜见识短浅)所以就花了点时间研究 了一下,但是从来都没有用到过,可能是见的题还不够吧。在老家写的这个解析,要过年了,等一场雪。
—Ninght
LCA(更新于2021/1/6)
#include<cstdio>
#include<iostream>
#define MAX 600000
using namespace std;
int n, m, s;//树节点个数,询问个数,根节点序号
int x, y;//查询点序号
int father[MAX][20], log_2[MAX], depth[MAX],lca[MAX];//father[i][j]表示节点i的第2^j级祖先
struct node
{
int to;
int next;
}edge[2*MAX];
int cnt, u, v, head[MAX];
void add_edge(int u, int v);
void dfs(int present_node, int father_node);
int LCA(int x, int y);//求点x和y的最近公共祖先
int main()
{
memset(head, -1, sizeof(head));
scanf_s("%d%d%d", &n, &m, &s);//树节点个数,询问个数,根节点序号
for (int i = 0; i < n - 1; i++)
{
scanf_s("%d%d", &u, &v);
add_edge(u, v); add_edge(v, u);
}
for (int i = 1; i <= n; ++i) //预先算出log_2(i)+1的值
log_2[i] = log_2[i - 1] + (1 << log_2[i - 1] == i);
dfs(s, 0);
for (int i = 0; i < m; i++)
{
scanf_s("%d%d", &x, &y);
lca[i] = LCA(x, y);
}
for (int i = 0; i < m;i++)printf("%d\n", LCA(x, y));
return 0;
}
void add_edge(int u, int v)
{
edge[++cnt] = node{ v,head[u] };
head[u] = cnt;
}
void dfs(int present_node, int father_node)
{
father[present_node][0] = father_node;
depth[present_node] = depth[father_node] + 1;
for (int i = 1; i <= log_2[depth[present_node]]; i++)
{
father[present_node][i] = father[father[present_node][i - 1]][i - 1];
}//当前节点的2^i祖先等于当前节点的2^(i-1)级祖先的2^(i-1)级祖先即2^i=2^(i+1)+2^(i+1)
for (int i = head[present_node]; i; i = edge[i].next)
{
if (edge[i].to != father_node)
dfs(edge[i].to, present_node);
}
}
int LCA(int x, int y)
{
if (depth[x] < depth[y])
swap(x, y);//不妨x的深度大于y的深度
while (depth[x] > depth[y])
x = father[x][log_2[depth[x] - depth[y]] - 1];//将x跳到与y同深度
if (x == y)return x;
for (int k = log_2[depth[x]] - 1; k >= 0; --k) //不断向上跳
{
if (father[x][k] != father[y][k]) //因为我们要跳到它们LCA的下面一层,所以它们肯定不相等,如果不相等就跳过去。
x = father[x][k], y = father[y][k];
}
return father[x][0]; //返回父节点
}
tarjan求割点(更新于2021/1/6)
#include<bits/stdc++.h>
#include<cstdio>
#include<iostream>
#include<algorithm>
#define MAX 20100
using namespace std;
int n, m, index1, cutnum;
int LOW[MAX], cut_vex[MAX], DFN[MAX];
struct node
{
int to;
int next;
}edge[MAX * 10];
int u, v;
int head[MAX], cnt;
void add_edge(int u, int v);
void tarjan(int u, int fa);
int main()
{
memset(DFN, 0, sizeof(DFN));
memset(head, -1, sizeof(head));
scanf("%d%d", &n, &m);
for (int i = 0; i < m; i++)
{
scanf("%d%d", &u, &v);
add_edge(u, v);
add_edge(v, u);
}
for (int i = 1; i <= n; i++)
{
if (DFN[i] == 0)
tarjan(i, i);
}
for (int i = 1; i <= n; i++)
{
if (cut_vex[i])
cutnum++;
}
printf("%d\n", cutnum);
for (int i = 1; i <= n; i++)
{
if (cut_vex[i])
printf("%d ", i);
}
}
void add_edge(int u, int v)
{
edge[++cnt].to = v;
edge[cnt].next = head[u];
head[u] = cnt;
}
void tarjan(int u, int fa)//当前节点及其父节点
{
index1++;
DFN[u] = index1;
LOW[u] = index1;//index记录遍历的点的个数,当前节点的DFN和LOW均为查询顺序
int child = 0;
for (int i = head[u]; i != -1; i = edge[i].next)//遍历当前节点的所有子节点
{
int nx = edge[i].to;
if (!DFN[nx])//若该子节点未访问则求出其所在连通分量
{
tarjan(nx, fa);
LOW[u] = min(LOW[u], LOW[nx]);//更新当前节点的最近根节点
if (LOW[nx] >= DFN[u] && u != fa)//
cut_vex[u] = 1;
if (u == fa)
child++;
}
LOW[u] = min(LOW[u], DFN[nx]);
}
if (child >= 2 && u == fa)
cut_vex[u] = 1;
}