@(ACM模板)[图论]
必须会:
- 次短路&&路径数
- 生成树(看里面的过程)
- 最大流(主要是模板)
- 割(论文:最小割模型在信息竞赛里的应用)
- 二分图
- 树的分治,(点分治如重心分解,边分治如树链剖分)
了解,会套模板即可:
- K短路
- 度限制MST~end
- ZKW数组模拟
- 一般图匹配(NP,套模板)
- 各种回路
- 了解经典问题
略
typedef long long LL;
const int maxn = ?;
const int maxm = ?;
int n, m;
int head[maxn], next[maxm];
struct Edge
{
int to;
int dis;//LL
Edge(int to = 0, int dis = 0):to(to),dis(dis) {}
}edges[maxm];//无向图size为2*maxm
void build_graph()
{
cin >> n >> m;
memset(head, 0xff, sizeof head);
int u, v, w;
for(int i = 0; i < m; ++i)
{
scanf("%d%d%d", &u, &v, &w);
edges[i] = Edge(u, v, w);
//无向图要加两条
next[i] = head[u];
head[u] = i;
}
}
遍历u的所有边
for(int e = first[u]; ~e; e = next[e])
{
//...
}
思想&步骤:
1. 初始化:d[s] = 0, 其他d为INF, 确定点集里只有s
2. 从未被确定的点的集合里找d最小的,加入确定的点集,并更新其他d
3. 重复2直到所有点都确定
注意:
- 不能处理负权。原因:Dijkstra贪心的找未确定点集里d最小的,若有负权可能之后找到d更小的。
- 此为使用队列优化的版本,复杂度 O(mlog(n)) ,其中 n 为点数, m 为边数
- 基础版执行n次,每次遍历所有点,复杂度 O(n2)
基础版即使在 n2<mlog(n) 时也往往比队列版本慢,因为队列版本执行push操作的前提是能进行松弛操作,若这个式子不常常成立,则push操作会很少。
代码
注意:
- 若最短路是long long 的,下面的代码需要在相应注释的地方更改
- 若需记录路径,需要启用pa[]数组
- 注意点的下标是否从0开始
#include
using namespace std;
typedef long long LL;
const int maxn = 1e5+5;//顶点数
const int maxm = 1e5+5;//边数
struct Dijstra//d[t] < inf代表有解
{
const int inf = 0x3f3f3f3f;
//const LL inf = 0x3f3f3f3f3f3f3f3f;
int n, m;
bitset done;
int d[maxn];//LL
int head[maxn];
//int par[maxn];//记录路径
struct Edge
{
int to, nxt;
int dis;//LL
}e[maxm];
struct Node
{
int u;//结点编号
int dis;//LL
bool operator<(const Node &rhs) const
{
return dis > rhs.dis;
}
};
void init(int nn)
{
n = nn;
m = 0;
memset(head, 0xff, sizeof head);
}
void addEdge(int from, int to, int dis)//LL dis
{
e[m].to = to;
e[m].dis = dis;
e[m].nxt = head[from];
head[from] = m++;
}
void finda(int s)
{
priority_queue q;
for(int i = 0; i < n; ++i) d[i] = inf;//0~n-1,注意下标从哪里开始
d[s] = 0;
done.reset();
q.push((Node){s, 0});
while(!q.empty())
{
int u = q.top().u;
q.pop();
if(done[u] == true) continue;
done[u] = true;
for(int i = head[u]; ~i; i = e[i].nxt)
{
int v = e[i].to;
int dis = e[i].dis;//LL
if(d[v] > d[u] + dis)
{
d[v] = d[u] + dis;
q.push((Node){v, d[v]});
}
}
}
}
};
思想&步骤
1. 初始化 所有顶点 d[i] = INF, 令d[s] = 0
2. 枚举每条边进行松弛,不能松弛时算法结束
3. 重复2,使2进行n-1次
memset(d, 0x3f, sizeof d);
d[s] = 0;
for(int times = 1; times < n; ++times)
{
for(int i = 0; i < m; ++i)
{
int x = u[i], y = v[i];
if(d[y] < d[x] + w[i]) d[y] = d[x] + w[i];
}
}
思想&步骤:
“松弛操作的连锁反应”
用queue存可用来松弛的点,每次用队首点进行松弛,然后将松弛到的&&不在queue中的点加入queue。
注意:
- 此为Bellman-Ford算法的队列优化版本
- 可以判断负环。因为任意一条最短路,所经过的点不会超过n,那么任何一点不可能超过(n-1)次被松弛。
- 最坏情况复杂度 O(nm) ,但实际中往往很快
#include
using namespace std;
typedef long long LL;
const int maxm = 1e5 + 5;
const int maxn = 1e5 + 5;
struct SPFA
{
const int inf = 0x3f3f3f3f;
//const LL inf = 0x3f3f3f3f3f3f3f3f;
bool vis[maxn];
int c[maxn], head[maxn];//c为入队次数
int d[maxn];//LL
int n, m;
struct Edge
{
int to, nxt;
int dis;//LL
}e[maxm];
void init(int nn)
{
n = nn;
m = 0;
memset(head, 0xff, sizeof head);
}
void addEdge(int from, int to, int dis)//LL dis
{
e[m].to = to;
e[m].dis = dis;
e[m].nxt = head[from];
head[from] = m++;
}
queue<int> q;
bool finda(int s)//若存在负环返回true
{
memset(d, 0x3f, sizeof d);
d[s] = 0;
memset(vis, 0, sizeof vis);
memset(c, 0, sizeof c);
while(!q.empty()) q.pop();
q.push(s);
vis[s] = true;
c[s] = 1;
while(!q.empty())
{
int x = q.front();
q.pop();
vis[x] = false;
for(int i = head[x]; ~i; i = e[i].nxt)
{
int y = e[i].to;
int dis = e[i].dis;
if(d[y] > d[x] + dis)
{
d[y] = d[x] + dis;
if(!vis[y])
{
vis[y] = true;
++c[y];
q.push(y);
if(c[y] > n) return true;
}
}
}
}
return false;
}
};
for(int k = 0; k < n; ++k)
for(int i = 0; i < n; ++i)
for(int j = 0; j < n; ++j)
if(d[i][j] < INF && d[k][j] < INF)
d[i][j] = d[i][k]+d[k][j];
若距离数组为bool,代表是否联通,则得到的结果成为有向图的传递闭包
对Dijkstra算法进行修改。增加一个用于记录次短路的数组d2。
在进行松弛操作时
- 若可更新最短路,则更新最短路,原最短路变为次短路
- 若可更新次短路,则更新次短路
#include
using namespace std;
typedef long long LL;
const int maxn = 1e5 + 5;//顶点数
const int maxm = 1e5 + 5;//边数
const int inf = 0x3f3f3f3f;
//const LL inf = 0x3f3f3f3f3f3f3f3f;
struct Dijstra//d2[t] < inf代表有解
{
int n, m;
int d[maxn], d2[maxn];//LL,d记录最短路,d2记录次短路
int head[maxn];
//int par[maxn];//记录路径
struct Edge
{
int to, nxt;
int dis;//LL
}e[maxm];
struct Node
{
int u;//结点编号
int dis;//LL
bool operator<(const Node &rhs) const
{
return dis > rhs.dis;
}
};
void init(int nn)
{
n = nn;
m = 0;
memset(head, 0xff, sizeof head);
}
void addEdge(int from, int to, int dis)//LL dis
{
e[m].to = to;
e[m].dis = dis;
e[m].nxt = head[from];
head[from] = m++;
}
void finda(int s)
{
priority_queue q;
for(int i = 0; i < n; ++i) d[i] = inf;//0~n-1,注意下标从哪里开始
for(int i = 0; i < n; ++i) d2[i] = inf;//0~n-1,注意下标从哪里开始
d[s] = 0;
q.push((Node){s, 0});
while(!q.empty())
{
Node x = q.top(); q.pop();
int u = x.u;
int dis = x.dis;
if(d2[u] < dis) continue;
for(int i = head[u]; ~i; i = e[i].nxt)
{
int v = e[i].to;
int dd = dis + e[i].dis;//LL
if(d[v] > dd)
{
swap(d[v], dd);
q.push((Node){v, d[v]});
}
if(dd > d[v] && dd < d2[v])
{
d2[v] = dd;
q.push((Node){v, d2[v]});
}
}
}
}
};
在7的基础上加个cnt数组,将松弛处的代码略加改动即可
#include
using namespace std;
typedef long long LL;
const int maxn = 1e5 + 5;//顶点数
const int maxm = 1e5 + 5;//边数
const int inf = 0x3f3f3f3f;
//const LL inf = 0x3f3f3f3f3f3f3f3f;
struct Dijstra//d2[t] < inf代表有解
{
int n, m;
int d[2][maxn];//LL,d[0]记录最短路,d[1]记录次短路
int cnt[2][maxn];//cnt[0]记录最短路条数,cnt[1]记录次短路径条数
int head[maxn];
//int par[maxn];//记录路径
struct Edge
{
int to, nxt;
int dis;//LL
}e[maxm];
struct Node
{
int f;//0代表最短路,1代表次短路
int u;//结点编号
int dis;//LL
bool operator<(const Node &rhs) const
{
return dis > rhs.dis;
}
};
void init(int nn)
{
n = nn;
m = 0;
memset(head, 0xff, sizeof head);
}
void addEdge(int from, int to, int dis)//LL dis
{
e[m].to = to;
e[m].dis = dis;
e[m].nxt = head[from];
head[from] = m++;
}
void finda(int s)
{
priority_queue q;
for(int i = 0; i < n; ++i) d[0][i] = d[1][i] = inf;//0~n-1,注意下标从哪里开始
d[0][s] = 0;
for(int i = 0; i < n; ++i) cnt[0][i] = cnt[1][i] = 0;
cnt[0][s] = 1;
q.push((Node){0, s, 0});
while(!q.empty())
{
Node x = q.top(); q.pop();
int u = x.u;
int dis = x.dis;
int f = x.f;
if(d[f][u] < dis) continue;
for(int i = head[u]; ~i; i = e[i].nxt)
{
int v = e[i].to;
int dd = dis + e[i].dis;//LL
if(d[0][v] > dd)//能更新最短路就先更新最短路
{
if(d[0][v] < inf)//之前的最短路变为次短路
{
d[1][v] = d[0][v];
cnt[1][v] = cnt[0][v];
q.push((Node){1, v, d[1][v]});
}
//更新最短路
d[0][v] = dd;
cnt[0][v] = cnt[f][u];
q.push((Node){0, v, d[0][v]});
}
else if(d[0][v] == dd)//更新最短路条数
{
cnt[0][v] += cnt[f][u];
}
else if(d[1][v] > dd)//不能更新最短路,只能更新次短路
{
d[1][v] = dd;
cnt[1][v] = cnt[f][u];
q.push((Node){1, v, d[1][v]});
}
else if(d[1][v] == dd)//更新次短路条数
{
cnt[1][v] += cnt[f][u];
}
}
}
}
};
在DAG中,可以按照点的拓扑序进行松弛操作,求得最短路。
复杂度 O(V+E)
二分图的一个等价定义是:不含有「含奇数条边的环」的图
注意:
- 二分图最大匹配可以dfs,但也可bfs,dfs好写
- 为了加快速度,一般用某种贪心先求出某个匹配,然后在此基础上继续求增广路
匹配点:匹配边集的端点
未盖点:不是匹配点的点
交替路:由“未盖点-非匹配边-匹配边–非匹配边-匹配边…”形成的路
增广路:终点是未盖点的交替路
增广路定理:任意图(不限于二分图)中,一个匹配为最大匹配的充要条件为:不存在增广路
增广路算法:
通过反复求增广路来求最大匹配。
最终会扩展出一个匈牙利树
此版本中,若二分图左边有x∈X,右边有y∈Y,且(x,y)为一条边,则所存的图中x有一条边y,不存储Y中点的边
当题目输入数据中未分好二分图可用此模板需除以2
多组数据vector要clear
const int maxn1 = 2000+5;
const int maxn2 = 2000+5;
vector<int> G[maxn1];
typedef vector<int>::iterator it;
int n, nx, ny;
int match[maxn2];
bool vis[maxn2];
bool dfs(int cur)
{
for(int i = 0; i < (int)G[cur].size(); i++)
{
int v = G[cur][i];
if(!vis[v])
{
vis[v] = true;
if(match[v] == -1 || dfs(match[v]))
{
match[v] = cur;
return true;
}
}
}
return false;
}
int hungary()
{
int res = 0;
memset(match, -1, sizeof match);
for(int i = 0; i < nx; i++)
{
memset(vis, 0, sizeof vis);
res += dfs(i);
}
return res;
}
此版本中,若二分图左边有x∈X,右边有y∈Y,且(x,y)为一条边,则所存的图中x有一条边y,y有一条边x
当题目输入数据中未分好二分图可用此模板
多组数据vector要clear
const int maxn = 2000+5;
vector<int> G[maxn];
typedef vector<int>::iterator it;
int n;
int match[maxn];
bool vis[maxn];
bool dfs(int cur)
{
for(int i = 0; i < (int)G[cur].size(); i++)
{
int v = G[cur][i];
if(!vis[v])
{
vis[v] = true;
if(match[v] == -1 || dfs(match[v]))
{
match[cur] = v;
match[v] = cur;
return true;
}
}
}
return false;
}
int hungary()
{
int res = 0;
memset(match, -1, sizeof match);
for(int i = 0; i < n; i++)
{
if(match[i] == -1)
{
memset(vis, 0, sizeof vis);
res += dfs(i);
}
}
return res;
}
若去掉某边,图的最大匹配变小,则该边为必须边
去掉某边重复计算匈牙利即可
int res = 0;
int mx = hungarian();
for(int u = 0; u < nl; u++)
{
int left = G[u].size();
while(left--)
{
it be = G[u].begin();
int v = *be;
G[u].erase(be);
if(hungarian() != mx) res++;
G[u].push_back(v);
}
}
需要用到并查集,如下:
const int maxn = 105;
const int maxe = maxn*maxn/2;
int n;
int par[maxn], rk[maxn];//par记录祖先,rk记录其在所在树中的深度
void init()
{
for(int i = 0; i < n; i++)
par[i] = i, rk[i] = 0;
}
int finda(int x)
{
if(par[x] == x) return x;
return par[x] = finda(par[x]);
}
bool unite(int x, int y)
{
x = finda(x);
y = finda(y);
if(x == y) return false;
if(rk[x] < rk[y]) par[x] = y;
else
{
par[y] = x;
if(rk[x] == rk[y]) rk[x]++;
}
return true;
}
struct Edge
{
int x,y,v;
friend bool operator< (const Edge& x, const Edge& y)
{
return x.v < y.v;
}
}e[maxe];
int m;
int kruskal()
{
int res = 0;
sort(e, e+m);
int left = n-1;//还差几条边
for(int i = 0; i < m; i++)
{
int x = finda(e[i].x);
int y = finda(e[i].y);
if(!unite(x, y)) continue;
res += e[i].v;
left--;
if(left == 0) break;
}
return res;
}