学了忘,忘了学,学了还得忘
//欧拉路径:一条通过每条边一次且仅一次的路径
//欧拉回路:一条通过每条边一次且仅一次的回路
//无向图欧拉回路:所有顶点度数为偶数
//有向图欧拉回路:所有顶点入度等于出度
//无向图欧拉路径:除了起点与终点度为奇数,其它都是偶树
//有向图欧拉路径:起点出度比入度大一,终点入度比出度大一,其它入读等于出度
//递归(深度优先搜索)欧拉回路
void Euler(int u)
{
int v;
for(v=0;v<n;v++)
{
if(graph[u][v]==1)
{
graph[u][v]=graph[v][u]=0;
Euler(v);
path[pc++]=v;
}
}
}
//调用代码
int pc=0;Euler(s);path[pc++]=s;
for(int i=pc-1;i>=0;--i)
printf("%d ",path[i]+1);
在二分图里面找一个最大匹配
匹配:任意两个边没有公共点
最大匹配:边数最多
如果要在一般图中找最大匹配,需要开花算法(带花树)
//二分图:可以分为两部分的图
//性质:没有奇数顶点的环
//二分图最大匹配:匹配边数最多,每个匹配边连接来自不同部分的点,每个点只和一条匹配边连接
//匈牙利算法dfs版:
int linker[maxn];//存放右部点的匹配点,初始化为-1;
bool vis[maxn];
bool graph[maxn][maxn];//邻接矩阵存放图,顶点从0编号
bool dfs(int u)//u总是左部点
{
vis[u]=true;
for(int i=0;i<n;++i)
{
if(graph[u][v]&&!vis[v])
{
if(linker[v]==-1||dfs(linker[v]))
{
linker[v]=u;//最后只对右部匹配点存放了匹配信息
return true;
}
}
}
return false;
}
int hungary()//返回最大匹配边数
{
int cnt=0;
memset(linker,-1,sizeof(linker));
for(int i=0;i<n;++i)//对于每个左部点
{
memset(vis,false,sizeof(vis));
if(dfs(i))++cnt;//可以增广
}
return cnt;
}
以下求最短路的算法都采用前向星
const int maxn = 1e5 + 10;
struct node
{
int u, v, w, next;
node(int _u = 0, int _v = 0, int _w = 0, int _next = 0):u(_u), v(_v), w(_w), next(_next){};
}edges[maxn];
int cnt, head[maxn];
void addedge(int u, int v, int w)
{
edges[++cnt] = node(u, v, w, head[u]);
head[u] = cnt;
}
要求边权全部为正 O ( v l o g v + e ) O(vlogv + e) O(vlogv+e)
int d[maxn], vis[maxn];//vis数组可以优化掉
struct mynode
{
int first, second;
bool operator < (const mynode &a)const
{
return first > a.first;//!!!
}
};
void Dijkstra(int s, int n)//s是起点,n是顶点数
{
memset(vis, 0, sizeof(vis));
const int INF = INT_MAX;
for(int i = 1; i <= n; ++i)
{
d[i] = INF;
}
d[s] = 0;
priority_queue<mynode > q;//自己定义优先级,从小到大排
q.push({0, s});
while(!q.empty())
{
int dis = q.top().first, u = q.top().second;
q.pop();
if(vis[u])continue;//一个节点可能多次入队
d[u] = dis, vis[u] = 1;
for(int i = head[u]; i; i = edges[i].next)
{
int v = edges[i].v, w = edges[i].w;
if(d[v] > dis + w)
{
q.push({dis + w, v});
}
}
}
}
O ( e v ) O(ev) O(ev),边权可以为负,可以检查出负环。
每次对全图进行一次松弛,因为最短路上最多只有n - 1条边,所以松弛n - 1次就能得到最短路。
松弛n - 1次后检查一下是否能继续松弛,如果可以的话,说明存在负权环。
int dis[maxn];
bool bellman_ford(int s)//不存在负权环返回true,存在返回false
{
const int INF = INT_MAX;
for(int i = 1; i <= n; ++i)dis[i] = INF;
dis[s] = 0;
for(int pc = 1;pc <= n - 1; ++pc)//n - 1次松弛操作
{
bool updated = false;//如果没有更新,就可以提前退出循环
for(int i = 1; i <= cnt; ++i)
{
if(dis[edges[i].u] != INF&&dis[edges[i].v] > dis[edges[i].u] + edges[i].w)
dis[edges[i].v] = dis[edges[i].u] + edges[i].w, updated = true;
}
if(!updated)return true;
}
for(int i = 1; i <= cnt; ++i)
{
if(dis[edges[i].v] > dis[edges[i].u] + edges[i].w)
return false;
}
return true;
}
SPFA是基于Bellman-Ford的思想,采用先进先出(FIFO)队列进行优化的一个计算单源最短路的快速算法。利用一个先进先出的队列用来保存待松弛的结点,每次取出队首结点u,并且枚举从u出发的所有边(u, v),如果d[u] + w(u, v) < d[v],则更新d[v] = d[u] + w(u, v),然后判断v点在不在队列中,如果不在就将v点放入队尾。这样不断从队列中取出结点来进行松弛操作,直至队列空为止。
只要最短路径存在,SPFA算法必定能求出最小值
如果存在负权圈,并且起点可以通过一些顶点到达负权圈,那么利用SPFA算法会进入一个死循环,因为d值会越来越小,并且没有下限,使得最短路不存在。那么我们假设不存在负权圈,则任何最短路上的点必定小于等于n个(没有圈),换言之,用一个数组c[i]来记录i这个点入队的次数,所有的c[i]必定都小于等于n,所以一旦有一个c[i] > n,则表明这个图中存在负权圈。
假设图中所有边的边权都为1,那么SPFA其实就是一个BFS(Breadth First Search,广度优先搜索)
使用双向队列,可以加速
int dis[maxn], inq[maxn], viscount[maxn];
bool SPFA(int s)
{
const int INF = INT_MAX;
for(int i = 1; i <= n; ++i)dis[i] = INF;
dis[s] = 0;
memset(inq, 0, sizeof(inq));memset(viscount, 0, sizeof(viscount));
deque<int> q;
q.push_front(s);
inq[s] = 1;
while(!q.empty())
{
int u = q.front(); q.pop_front();
inq[u] = 0;
if(++viscount[u] > n)return false;//有负环
for(int i = head[u]; i; i = edges[i].next)
{
int v = edges[i].v, w = edges[i].w;
if(dis[u] + w < dis[v])
{
dis[v] = dis[u] + w;
if(!inq[v])
{
inq[v] = 1;
if(q.empty()||dis[v] < dis[q.front()])q.push_front(v);
else q.push_back(v);
}
}
}
}
return true;
}
**问题描述:**给定一个又n个节点,m条有向边的图,问从 s s s到 t t t 的第 k k k短路的长度(无向图k短路没多大意义,可以拆边用这个方法)
**算法描述:**借鉴A*算法,设估价函数 f ( x ) = g ( x ) + h ( x ) f(x)=g(x)+h(x) f(x)=g(x)+h(x),其中 g ( x ) g(x) g(x) 是从初始状态(起点)到当前状态(当前节点)的实际代价,另 h ( x ) h(x) h(x) 为当前状态到结束状态的真实最小代价(当前节点到终点的最短路径),那么优先队列第 i i i 个出来状态 t t t 就是第 i i i 短路
**算法步骤:**用反向图求出所有点到终点的最短距离,A*搜索(当一个节点出队超过k次时,可以跳过这个节点的更新)
//代码来源网站:https://oi-wiki.org/graph/kth-path/
const int maxn = 5010;
const int maxm = 400010;
const int inf = 2e9;
int n, m, s, t, k, u, v, ww, H[maxn], cnt[maxn];
int cur, h[maxn], nxt[maxm], p[maxm], w[maxm];
int cur1, h1[maxn], nxt1[maxm], p1[maxm], w1[maxm];
bool tf[maxn];
void add_edge(int x, int y, double z) {
cur++;
nxt[cur] = h[x];
h[x] = cur;
p[cur] = y;
w[cur] = z;
}
void add_edge1(int x, int y, double z) {
cur1++;
nxt1[cur1] = h1[x];
h1[x] = cur1;
p1[cur1] = y;
w1[cur1] = z;
}
struct node {
int x, v;
bool operator<(node a) const { return v + H[x] > a.v + H[a.x]; }
};
priority_queue<node> q;
struct node2 {
int x, v;
bool operator<(node2 a) const { return v > a.v; }
} x;
priority_queue<node2> Q;
int main() {
scanf("%d%d%d%d%d", &n, &m, &s, &t, &k);
while (m--) {
scanf("%d%d%d", &u, &v, &ww);
add_edge(u, v, ww);
add_edge1(v, u, ww);
}
//反向图求最短路
for (int i = 1; i <= n; i++) H[i] = inf;
Q.push({t, 0});
while (!Q.empty()) {
x = Q.top();
Q.pop();
if (tf[x.x]) continue;
tf[x.x] = true;
H[x.x] = x.v;
for (int j = h1[x.x]; j; j = nxt1[j]) Q.push({p1[j], x.v + w1[j]});
}
q.push({s, 0});
while (!q.empty()) {
node x = q.top();
q.pop();
cnt[x.x]++;
if (x.x == t && cnt[x.x] == k) {
printf("%d\n", x.v);
return 0;
}
if (cnt[x.x] > k) continue;
for (int j = h[x.x]; j; j = nxt[j]) q.push({p[j], x.v + w[j]});
}
printf("-1\n");
return 0;
}
给出一个图,问其中的有 个节点构成的边权和最小的环( n > = 3 n>=3 n>=3)是多大。
图的最小环也称围长。
基于 F l o y d Floyd Floyd,复杂度 O ( n 3 ) O(n^3) O(n3)
注意到 F l o y d Floyd Floyd 算法有一个性质:在最外层循环到点 k k k 时(实际未计算到 k k k),最短路 d i s dis dis 数组中, d i s u , v dis_{u,v} disu,v 表示 u u u到 v v v且只经过$ [1,k) $点的最短路。
对每个环,假设我们要更新这个最小环的答案到贡献中。假定这个环中编号最大的顶点是 w w w ,环上与 w w w 相邻的两个点是 i , j i,j i,j ,那么在外层循环到 w w w 时,可以计算出该环边权和 = d i s [ i ] [ j ] + v a l [ i ] [ w ] + v a l [ j ] [ w ] =dis[i][j] + val[i][w] + val[j][w] =dis[i][j]+val[i][w]+val[j][w]
int val[maxn + 1][maxn + 1]; // 原图的邻接矩阵
inline int floyd(const int &n) {
static int dis[maxn + 1][maxn + 1]; // 最短路矩阵
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j) dis[i][j] = val[i][j]; // 初始化最短路矩阵
int ans = inf;
for (int k = 1; k <= n; ++k) {
for (int i = 1; i < k; ++i)
for (int j = 1; j < i; ++j)
ans = std::min(ans, dis[i][j] + val[i][k] + val[k][j]); // 更新答案
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j)
dis[i][j] = std::min(
dis[i][j], dis[i][k] + dis[k][j]); // 正常的 floyd 更新最短路矩阵
}
return ans;
}
方法二:(适用于边权为1的图) 对图上每个点 v v v,可以从点 v v v开始 b f s bfs bfs, 记录下访问的深度,如果重复访问,可以计算环的大小。该方法不能单独求v所在环边数,可以用来bfs图上每个点求全局最小环。
int ans = INT_MAX;
int d[maxn], vis[maxn];
void bfs(int u, int f)
{
vis[u] = 1, d[u] = 1;
queue<pair<int, int> > q;//传入节点和父亲(避免用父亲更新到ans = min(ans, d[it] + d[v] -1))
q.push({u, 0});
while (!q.empty())
{
int v = q.front().first, f = q.front().second; q.pop();
for (auto it : graph[v])
{
if (it == f)continue;
if (!vis[it])
{
d[it] = d[v] + 1;
vis[it] = 1;
q.push({it, v});
}
//如果u不在it和v的环上,d[it] + d[v] - 1不是真实环大小,比真实环大
//所以该方法可以用于全局求解,而不能单独求过点u的最小环
else ans = min(ans, d[it] + d[v] - 1);
}
}
if(ans == INT_MAX)ans = -1;
}
删除一条边 < u , v > <u,v> ,计算 u , v u,v u,v 间的最短路径,如果可达,那么 < u , v > <u,v> 边所在环的边权和等于 d i s u , v + w u , v dis_{u,v}+w_{u,v} disu,v+wu,v (注意重边的影响)
n个未知量( x 0 , x 1 x . . . x n − 1 x_0,x_1x_...x_{n-1} x0,x1x...xn−1),m个约束关系,即m个形如 x i − x j < = c k x_i-x_j<=c_k xi−xj<=ck 的方程组,问 x t − x 0 x_t-x_0 xt−x0 的最大值是多少?
建立有向图,如果 x i − x j < = c k x_i-x_j<=c_k xi−xj<=ck 则建立一条 j j j 到 i i i 权为 c k c_k ck 的边
以0为起点跑单源最短路,同时判断是否存在负环,负环说明找不到满足约束关系的一组解, x t − x 0 x_t-x_0 xt−x0的最大值是 d i s [ t ] dis[t] dis[t]
Kosaraju 算法依靠两次简单的 DFS 实现。
第一次 DFS,选取任意顶点作为起点,遍历所有未访问过的顶点,并在回溯之前给顶点编号,也就是后序遍历。
第二次 DFS,对于反向后的图,以标号最大的顶点作为起点开始 DFS。这样遍历到的顶点集合就是一个强连通分量。对于所有未访问过的结点,选取标号最大的,重复上述过程。
两次 DFS 结束后,强连通分量就找出来了,Kosaraju 算法的时间复杂度为 O ( n + m ) O(n+m) O(n+m) 。
//未测试
const int maxn = 10000 + 10;
int n;
//g原图,gv反向图
vector<int> g[maxn], gv[maxn];
//所处连通分量编号
int color[maxn],dpc = 0, vis[maxn];
stack<int> q;
void dfs(int u)
{
for (auto v : g[u])
if (!vis[v])dfs(v);
q.push(u);//入队的点,dfs后序从小到大排列
}
void dfs2(int u, int c)
{
color[u] = c;
for (auto v : gv[u])
if (!color[v])dfs2(v, c);
}
void Kosaraju()
{
for (int i = 1; i <= n; ++i)if (!vis[i])dfs(i);
int cpc = 0;
while (!q.empty())
{
if (!color[q.top()])dfs2(q.top(), ++cpc);
q.pop();
}
}
给定一个带权有向图G和其中一个节点u,找出一个以u为根节点的,边权和最小的有向生成树。有向生成树(directed spanning tree)也叫做树形图(arborescence),是指一个类似树的有向图,满足以下条件。
求解最小有向生成树,可以使用朱-刘算法。主要分为预处理和主循环两步
预处理:去除自环边,并判断根节点是否对其它任意节点可达,如不可达,无解,终止算法
主循环:
- 对根节点外每个节点找出权值最小的入边共n-1条
- 如果选出的边不形成环,可以证明,这n-1条边构成最小有向生成树。否则,将环缩成点,重复1,2步。
那树形图的边权和怎么计算呢?
首先加入选中的每条入边的贡献,包括环上的边
显然,一个环上实际有一条边不能选,那么如果缩点后选中的入边对应的实际边是u到v,那么环上v的入边的贡献就应该减去。
在实际算法实现中,应该当舍弃的边的负贡献更新到缩点后的边权上,然后继续循环选最小的入边,此句话可能难以理解,详见代码。
struct node//边的权和顶点
{
int u, v;
type w;
}edge[MAXN * MAXN];
int pre[MAXN], id[MAXN], vis[MAXN], n, m, pos;
type in[MAXN];//存最小入边权,pre[v]为该边的起点
type Directed_MST(int root, int V, int E)
{
type ret = 0;//存最小树形图总权值
while (true)
{
int i;
//1.找每个节点的最小入边
for (i = 0; i < V; i++)
in[i] = INF;//初始化为无穷大
for (i = 0; i < E; i++)//遍历每条边
{
int u = edge[i].u;
int v = edge[i].v;
if (edge[i].w < in[v] && u != v)//说明顶点v有条权值较小的入边 记录之
{
pre[v] = u;//节点u指向v
in[v] = edge[i].w;//最小入边
if (u == root)//这个点就是实际的起点
pos = i;
}
}
for (i = 0; i < V; i++)//判断是否存在最小树形图
{
if (i == root)
continue;
if (in[i] == INF)
return -1;//除了根以外有点没有入边,则根无法到达它 说明它是独立的点 一定不能构成树形图
}
//2.找环
int cnt = 0;//记录环数
memset(id, -1, sizeof(id));
memset(vis, -1, sizeof(vis));
in[root] = 0;
for (i = 0; i < V; i++) //标记每个环
{
ret += in[i];//权值加入答案
int v = i;
while (vis[v] != i && id[v] == -1 && v != root)//如果能遍历到root则v不在环上,否则当遍历到此轮遍历过的点时停止,点在环上
{
vis[v] = i;
v = pre[v];
}
if (v != root && id[v] == -1)//没有遍历到root,这是一个环
{
for (int u = pre[v]; u != v; u = pre[u])
id[u] = cnt;//标记节点u为第几个环
id[v] = cnt++;
}
}
if (cnt == 0)
break; //无环 找到答案break
for (i = 0; i < V; i++)
if (id[i] == -1)
id[i] = cnt++;
//3.建立新图 缩点,重新标记
for (i = 0; i < E; i++)
{
int u = edge[i].u;
int v = edge[i].v;
edge[i].u = id[u];
edge[i].v = id[v];
if (id[u] != id[v])//如果后边这条边被选中,则in[v]代表的边要被删除,将其贡献从答案中减去
edge[i].w -= in[v];
}
V = cnt;
root = id[root];
}
return ret;
}