YJMSTR的算法竞赛模板
目录
文章目录
- YJMSTR的算法竞赛模板
- 目录
- 图论
- 一、最短路
- 1.spfa与负环、最短路
- 1.1 bfs-spfa找负环:
- 1.2 dfs-spfa找负环
- 1.3 spfa求最短路的优化
- 2.dijkstra模板(set模拟二叉堆堆优化)
- 2.1有向图最小环
- 3.Floyd求多源最短路/传递闭包/倍增floyd
- 3.1 最短路代码略
- 3.2 传递闭包
- 3.3 倍增floyd
- 3.4 无向图最小环
- 4.Johnson全源最短路算法
- 5.差分约束
- 二、生成树
- 1.性质与kruskal代码
- 1.1回路性质:
- 1.2切割性质
- 1.3 kruskal代码
- 2.最小直径生成树:
- 3.增量最小生成树
- 4.最小瓶颈生成树
- 5.最小瓶颈路
- 5.1 一对结点
- 5.2 每对结点间的最小瓶颈路
- 6.次小生成树
- 7.最小有向生成树(最小树形图)&&朱-刘算法
- 三、树上问题
- 1.树的直径
- 1.1 dp求树的直径
- 1.2 两次bfs求树直径
- 2.树链剖分
- 2.1 套数据结构维护树上信息
- 四、搜索相关
- 1.欧拉回路
- 1.1定义
- 1.2无向图欧拉路径(套圈法)
- 1.3有向图欧拉路径(套圈法)
- 2.求无向图割点/桥(割边)
- 2.1求无向图的割点
- 2.2求无向图的割边(桥)
- 3.无向图的双连通分量
- 3.1 定义
- 3.2 边双连通分量(e-DCC)的求法与缩点
- 3.3 点双连通分量的求法与缩点
- 3.3.1vdcc的求法
- 3.3.2 v-DCC的缩点
- 4.有向图的强连通分量,缩点与tarjan算法
- 4.1 定义
- 4.2 tarjan强连通分量缩点模板(洛谷2812)
- 5.有向图的必经点和必经边
- 五、2-SAT(2-可满足性问题)
- 1. 定义
- 2.判定
- 3.求可行解(咕)
- 六、二分图匹配&相关模型
- 1.二分图判定
- 2.二分图最大匹配
- 2.1匈牙利算法
- 2.2二分图带权匹配
- 2.2.1 KM解法
- 2.2.2费用流解法
- 3.二分图模型
- 4.二分图最大匹配的必须边和可行边
- 七.网络流与最小割
- 7.1 最大流的dinic
- 7.2 最小割
- 7.3 最大权闭合图
- 7.4 费用流
- 7.5 混合图欧拉路
- 总结
图论
一、最短路
1.spfa与负环、最短路
bool inq[maxn];
int d[maxn];
void spfa(int s) {
memset(inq, 0, sizeof(inq));
memset(d, 0x3f, sizeof(d));
d[s] = 0; inq[s] = true;
queue<int> q; q.push(s);
d[s] = 0;
while (!q.empty()) {
int u = q.front(); q.pop(); inq[u] = false;
for (int i = head[u]; ~i; i = e[i].nxt) {
int v = e[i].v;
if (d[u] + e[i].w < d[v]) {
d[v] = d[u] + e[i].w;
if (!inq[v]) {
inq[v] = true;
q.push(v);
}
}
}
}
}
判断负环:记录最短路上的点数 点数大于n即为负环
或是判断入队次数 一个点入队超过n次 说明它是环上的点 存在负环
SPFA 有 BFS 和 DFS 两种实现方式,如果仅仅要判断是否存在负环,DFS-SPFA 要比 BFS-SPFA 快上很多。但是在没有负环时要求出解,DFS-SPFA 会比 BFS-SPFA 慢很多
1.1 bfs-spfa找负环:
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxn = 10007, inf = 0x3f3f3f3f;
int T, n, m;
struct edge {
int v, w, nxt;
}e[maxn];
int head[maxn], eid, dis[maxn], cnt[maxn], q[maxn], qh, qt;
bool inq[maxn];
void init() {
memset(head, -1, sizeof(head));
eid = 0;
}
void insert(int u, int v, int w) {
e[eid].v = v; e[eid].nxt = head[u]; e[eid].w = w; head[u] = eid++;
}
bool spfa(){
qh = qt = 0;
memset(dis, 0x3f, sizeof(dis));
memset(inq, false, sizeof(inq));
q[qt++] = 1; //记录最短路上的点数 点数大于n即为负环
cnt[1] = 0;
inq[1] = true;
dis[1] = 0;
while (qh ^ qt) {
int u = q[qh++];
inq[u] = false;
if (qh >= maxn)
qh = 0;
for (int i = head[u]; ~i; i = e[i].nxt) {
int v = e[i].v;
if (dis[v] > dis[u] + e[i].w) {
dis[v] = dis[u] + e[i].w;
cnt[v] = cnt[u] + 1;
if (cnt[v] >= n)
return false;
if (!inq[v]) {
q[qt++] = v;
if (qt >= maxn)
qt = 0;
inq[v] = true;
}
}
}
}
return true;
}
int rd() {
int s = 0, f = 1;
char c = getchar();
while (c > '9' || c < '0') {
if (c == '-')
f = -1;
c = getchar();
}
while (c >= '0' && c <= '9') {
s = s * 10 + c - '0';
c = getchar();
}
return s * f;
}
int main() {
T = rd();
while (T--) {
init();
n = rd(), m = rd();
for (int i = 1, u, v, w; i <= m; i++) {
u = rd(), v = rd(), w = rd();
if (w < 0)
insert(u, v, w);
else
insert(u, v, w), insert(v, u, w);
}
if (spfa()) {
puts("N0");
} else
puts("YE5");
}
return 0;
}
1.2 dfs-spfa找负环
bool dfs_spfa(int u) {
vis[u] = true;
for (int i = head[u]; ~i; i = e[i].nxt) {
int v = e[i].v;
if (dis[v] > dis[u] + e[i].w) {
dis[v] = dis[u] + e[i].w;
if (vis[v] || !dfs_spfa(v)) return false;
}
}
vis[u] = false;
return true;
}
1.3 spfa求最短路的优化
堆优化:将队列换成堆,与 Dijkstra 的区别是允许一个点多次入队。在有负权边的图可能被卡成指数级复杂度。
栈优化:将队列换成栈(即将原来的 BFS 过程变成 DFS),在寻找负环时可能具有更高效率,但最坏时间复杂度仍然为指数级。
SLF 优化:将普通队列换成双端队列,每次将入队结点距离和队首比较,如果更大则插入至队尾,否则插入队首。LLL 优化:将普通队列换成双端队列,每次将入队结点距离和队内距离平均值比较,如果更大则插入至队尾,否则插入队首。
2.dijkstra模板(set模拟二叉堆堆优化)
#include<bits/stdc++.h>
using namespace std;
const int maxn = 2e5 + 7;
typedef pair<int, int> pii;
set <pii, less<pii> > min_heap;
struct edge {
int v, w, nxt;
}e[maxn];
int head[maxn], eid, dis[maxn], n, m, s;
void insert(int u, int v, int w) {
e[eid].v = v;
e[eid].w = w;
e[eid].nxt = head[u];
head[u] = eid++;
}
void init() {
memset(head, -1, sizeof(head));
eid = 0;
}
void dij(int s) {
memset(dis, 0x3f, sizeof(dis));
dis[s] = 0;
min_heap.insert(make_pair(0,s));
while (!min_heap.empty()) {
set<pii, less<pii> > :: iterator it = min_heap.begin();
int u = it->second;
min_heap.erase(*it);
for (int i = head[u]; ~i; i = e[i].nxt) {
int v = e[i].v;
if (dis[v] > dis[u] + e[i].w) {
min_heap.erase(make_pair(dis[v], v));
dis[v] = dis[u] + e[i].w;
min_heap.insert(make_pair(dis[v], v));
}
}
}
}
int main() {
cin >> n >> m >> s;
init();
for (int i = 1; i <= m; i++) {
int u, v, w;
cin >> u >> v >> w;
insert(u, v, w);
}
dij(s);
for (int i = 1; i <= n; i++)
cout << dis[i] << " ";
return 0;
}
2.1有向图最小环
枚举起点
s
s
s 跑
n
n
n遍堆优化的
d
i
j
k
s
t
r
a
dijkstra
dijkstra求解
s
s
s一定是第一个被从堆中取出的结点
扫一遍
s
s
s的所有出边 拓展完后 令
d
[
s
]
=
+
∞
d[s]=+∞
d[s]=+∞ 然后继续求解
当
s
s
s第二次被从堆中取出时,
d
[
s
]
d[s]
d[s]就是经过
s
s
s的最小环长度
3.Floyd求多源最短路/传递闭包/倍增floyd
3.1 最短路代码略
3.2 传递闭包
注意关系是否有向
3.3 倍增floyd
每次floyd可以把步长翻倍,这样可以凑出限制步数条件下的答案
设
f
[
i
]
[
j
]
[
t
]
f[i][j][t]
f[i][j][t]表示从i到j走2^t步的最短路
f
[
i
]
[
j
]
[
t
]
=
m
i
n
{
f
[
i
]
[
k
]
[
t
−
1
]
+
f
[
k
]
[
j
]
[
t
−
1
]
}
f[i][j][t] = min\{f[i][k][t-1]+f[k][j][t-1]\}
f[i][j][t]=min{f[i][k][t−1]+f[k][j][t−1]}
其实相当于对于广义矩阵做快速幂
3.4 无向图最小环
求无向图中一个至少包含3个点的环 环上的结点不重复 并且环上边的边权之和最小
考虑floyd
外层循环到k时
f
[
i
]
[
j
]
f[i][j]
f[i][j]表示经过编号不超过k-1的结点从i到j 或者从i先到k 再到j
所以
m
i
n
1
<
=
i
<
j
<
k
{
f
[
i
]
[
j
]
+
a
[
i
]
[
k
]
+
a
[
k
]
[
j
]
}
min_{1<=i<j<k}\{f[i][j]+a[i][k]+a[k][j]\}
min1<=i<j<k{f[i][j]+a[i][k]+a[k][j]}就是由编号不超过k的结点构成的经过结点k的最小环长度
代码源自lyd的书 要输出方案直接看path
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int maxn = 107, inf = 0x3f3f3f3f;
vector<int> path;
ll a[maxn][maxn], f[maxn][maxn], n, m, ans, pre[maxn][maxn];
void get_path(int u, int v) {
if (pre[u][v] == 0) return;
get_path(u, pre[u][v]);
path.push_back(pre[u][v]);
get_path(pre[u][v], v);
}
int main() {
cin >> n >> m;
for (int i = 0; i < maxn; i++) {
for (int j = 0; j < maxn; j++) {
a[i][j] = f[i][j] = 9999999999999ll;
}
}
ans = inf;
for (int i = 1; i <= n; i++) a[i][i] = 0;
for (int i = 1; i <= m; i++) {
ll u, v, w;
cin >> u >> v >> w;
a[u][v] = a[v][u] = min(a[u][v], w);
}
memcpy(f, a, sizeof(a));
for (int k = 1; k <= n; k++) {
for (int i = 1; i < k; i++) {
for (int j = i + 1; j < k; j++) {
if (f[i][j] + a[i][k] + a[k][j] < ans) {
ans = f[i][j] + a[i][k] + a[k][j];
path.clear();
path.push_back(i);
get_path(i, j);
path.push_back(j);
path.push_back(k);
}
}
}
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++) {
if (f[i][j] > f[i][k] + f[k][j]) {
f[i][j] = f[i][k] + f[k][j];
pre[i][j] = k;
}
}
}
if (ans == inf) {
puts("No solution.");
return 0;
} else printf("%lld\n", ans);
}
4.Johnson全源最短路算法
新建一个虚拟节点0号点 向其他所有点连一条边权为0的边
然后spfa求0号点到其他所有点的最短路 记为
h
i
h_i
hi
假如存在一条边
(
u
,
v
,
w
)
(u, v, w)
(u,v,w) 那么将该边边权重新设为
w
+
h
u
−
h
v
w+h_u-h_v
w+hu−hv
接下来以每个点为起点 跑n轮dijkstra即可求出含负权边的多源最短路了
时间复杂度
O
(
n
m
l
o
g
m
)
O(nmlogm)
O(nmlogm)
5.差分约束
若限制条件是相除 可以取log使其变为相减的形式
二、生成树
1.性质与kruskal代码
最小生成树性质
1.1回路性质:
假定所有边权都不相同 设C是图G中的任意回路 e是C上权值最大的边 则图G的所有生成树不包括e
1.2切割性质
假定所有边权均不相同 设S为既非空集也非全集的V的子集 边e是满足一个端点在S内
另外一个端点在S外的所有边中权值最小的一个 则图G的所有生成树均包含e
1.3 kruskal代码
struct edge {
int u, v, w;
}e[maxm];
int fa[maxn], n, m, ans;
bool operator < (edge a, edge b) {
return a.w < b.w;
}
int get(int x) {return x == fa[x] ? x : fa[x] = get(fa[x]);}
void kruskal() {
sort(e+1, e+m+1);
for (int i = 1; i <= n; i++) fa[i] = i;
for (int i = 1; i <= m; i++) {
int u = get(e[i].u);
int v = get(e[i].v);
if (x ^ y) {
fa[x] = y;
ans += e[i].w;
}
}
cout << ans << endl;
}
2.最小直径生成树:
#include<bits/stdc++.h>
using namespace std;
const int maxn = 507;
#define ll long long
const ll inf = 1ll << 61;
int n, m;
struct edge {
int u, v;
ll w;
}e[maxn * maxn];
ll d[maxn][maxn], g[maxn][maxn];
vector<int> adj[maxn];
int pre[maxn];
int rk[maxn][maxn]; //rk[i][j]表示距离点i第j近的点
ll tmp[maxn], ans, dis[maxn], X;
bool cmp(int a, int b) { return tmp[a] < tmp[b];}
int rd() {
int s = 0, f = 1; char c = getchar();
while (c < '0' || c > '9') {if (c == '-') f = -1; c = getchar();}
while (c >= '0' && c <= '9') {s = s * 10 + c - '0'; c = getchar();}
return s * f;
}
typedef pair<int, int> pii;
set<pii, less<pii> > min_heap, res;
#define mk make_pair
void dij(int s1, int s2) {
for (int i = 1; i <= n; i++) dis[i] = inf;
dis[s1] = X; dis[s2] = g[s1][s2] - X;
min_heap.insert(mk(dis[s1], s1));
min_heap.insert(mk(dis[s2], s2));
if (s1 != s2) {
pre[s2] = s1;
}
while(!min_heap.empty()) {
auto it = min_heap.begin();
int u = it->second;
min_heap.erase(*it);
for (int i = 0; i < adj[u].size(); i++) {
int v = adj[u][i];
if (dis[v] > dis[u] + g[u][v]) {
min_heap.erase(mk(dis[v], v));
dis[v] = dis[u] + g[u][v];
min_heap.insert(mk(dis[v], v));
res.erase(mk(pre[v], v));
pre[v] = u;
res.insert(mk(pre[v], v));
}
}
}
}
int main() {
n = rd(); m = rd();
for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) d[i][j] = inf;
for (int i = 1; i <= n; i++) d[i][i] = 0;
for (int i = 1; i <= m; i++) {
int u, v, w;
u = e[i].u = rd(); v = e[i].v = rd(); w = e[i].w = g[u][v] = g[v][u] = rd()*2ll;
d[u][v] = d[v][u] = w;
adj[u].push_back(v); adj[v].push_back(u);
}
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]);
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
rk[i][j] = j; tmp[j] = d[i][j];
}
sort(rk[i] + 1, rk[i] + n + 1, cmp);
}
ans = inf;//直径
int s1, s2;
for (int k = 1; k <= m; k++) {
int u = e[k].u, v = e[k].v;
if (d[u][rk[u][n]] == d[u][rk[u][n-1]] && d[u][rk[u][n]] * 2ll < ans) {
ans = d[u][rk[u][n]] * 2ll;//u点作为中心
s1 = s2 = u;
}
if (d[v][rk[v][n]] == d[v][rk[v][n-1]] && d[v][rk[v][n]] * 2ll < ans) {
ans = d[v][rk[v][n]] * 2ll;
s1 = s2 = v;
}
int lst, cur;
for (cur = n-1, lst = n; cur >= 1; cur--) {//d[u][i]减小 合法的d[v][i]应对应增加
if (d[v][rk[u][lst]] < d[v][rk[u][cur]]) {
if (ans > d[u][rk[u][cur]] + d[v][rk[u][lst]] + e[k].w) {
ans = d[u][rk[u][cur]] + d[v][rk[u][lst]] + e[k].w;
X = ans / 2 - d[u][rk[u][cur]];
s1 = u, s2 = v;
}
lst = cur;
}
}
}
cout << ans / 2ll << endl;
dij(s1, s2);
for (auto it = res.begin(); it != res.end(); ++it) {
printf("%d %d\n", it->first, it->second);
}
if (s1 != s2) printf("%d %d\n",s1, s2);
return 0;
}
3.增量最小生成树
从包含n个点的空图开始 依次加入m条带权边。每加入一条边,输出当前图中的最小生成树权值(不连通无解)
sol:加入一条边<u,v>后图中恰好包含一个环 根据回路性质删掉加边前树上u到v唯一路径上的最大边即可 O(nm)
4.最小瓶颈生成树
给出加权无向图,求一棵生成树,使得最大边权尽量小
原图的最小生成树就是一棵最小瓶颈生成树。
5.最小瓶颈路
5.1 一对结点
给定加权无向图的两个结点u和v,求出u到v的一条路径,求出从u到v的一条路径,使得路径上的最长边尽量短。
直接找最小生成树上u到v的唯一路径即可。
5.2 每对结点间的最小瓶颈路
求每对结点的最小瓶颈路上最大边长f(u,v)
先求mst 然后dfs转成有根树 并计算f(u,v):当访问一个新结点u时,考虑所有已经访问过的结点x
更新f(u,x)=max(w(u,fa[u]),f(fa[u],x)) 这样每个f可以在常数时间内算出 总时间复杂度为
O
(
n
2
)
O(n^2)
O(n2)
6.次小生成树
如果最小生成树不唯一 次小生成树权值和一棵最小生成树相同
枚举最小生成树中不在次小生成树中出现的边 求n-1棵最小生成树 里面权值最小的就是原图的次小生成树
另外一种方法是枚举加入的新边 根据回路性质 我们会删掉形成的唯一环上的最大边来得到新的生成树
用每对结点的最小瓶颈路的方法可以求出原图最小生成树上任意两点路径间的最大边
枚举m-(n-1)条边与原最小生成树上的边交换 时间复杂度
O
(
n
2
)
O(n^2)
O(n2)
lyd的书上有O(MlogN)做法:用倍增预处理出各点到根路径上的最大边权与严格次大边权 最终得到路径上最大和次大边权
7.最小有向生成树(最小树形图)&&朱-刘算法
给定一个有向带权图G和其中一个结点u,找出一个以u为根结点,权和最小的有向生成树。有向生成树也叫树形图,满足以下条件:
- 恰好有一个入度为0的点,称为根结点。
- 其他结点入度均为1
- 可以从根结点到达其他所有结点
固定根的最小树形图可用朱-刘算法解决
不固定根要新加入一个虚点 向所有点连权值相当于所有边权和+1的边
朱-刘算法流程如下:
首先预处理,删除自环并判断根结点是否可以到达其他结点
然后给所有非根结点选择一条权最小的入边,如果选出来的n-1条边不构成圈,则这些边就形成了一个最小树形图,否则把每个圈缩成一个点,继续上述过程。
缩圈知后每个圈的入弧的权值需要减去该弧终点的另外一条入弧的权值
根为r:
#include<bits/stdc++.h>
using namespace std;
const int maxn = 107, maxm = 1e4 + 7, inf = 0x3f3f3f3f;
struct edge {
int u, v, w;
}e[maxm];
int n, m, r;
int ine[maxn], pre[maxn], vis[maxn], id[maxn], tot;
int zhu_liu() {
int ans = 0;
while (1) {
for (int i = 1; i <= n; i++) ine[i] = inf;
for (int i = 1; i <= m; i++) {
int u = e[i].u, v = e[i].v;
if (u != v && e[i].w < ine[v]) {
ine[v] = e[i].w;
pre[v] = u;
}
}
for (int i = 1; i <= n; i++) {
if (i != r && ine[i] == inf) {
return -1;
}
}
tot = 0;
memset(vis, 0, sizeof(vis));
memset(id, 0, sizeof(id));
for (int i = 1; i <= n; i++) {
if (i == r) continue;
ans += ine[i];
int v = i;
while (vis[v] != i && !id[v] && v != r) {
vis[v] = i;
v = pre[v];
}
if (!id[v] && v != r) {
id[v] = ++tot;
for (int u = pre[v]; u != v; u = pre[u]) \
id[u] = tot;
}
}
if (tot == 0) break;
for (int i = 1; i <= n; i++) {
if (!id[i]) id[i] = ++tot;
}
for (int i = 1; i <= m; i++) {
int u = e[i].u, v = e[i].v;
e[i].u = id[u], e[i].v = id[v];
if (id[u] != id[v]) e[i].w -= ine[v];
}
r = id[r];
n = tot;
}
return ans;
}
int main() {
cin >> n >> m >> r;
for (int i = 1; i <= m; i++) {
int u, v, w;
cin >> u >> v >> w;
e[i].u = u, e[i].v = v, e[i].w = w;
}
printf("%d", zhu_liu());
return 0;
}
不固定根 :新建一个虚点 向所有点连权值为所有边权和+1的边 然后执行朱刘算法
三、树上问题
1.树的直径
1.1 dp求树的直径
记
d
(
x
)
d(x)
d(x)为从结点x出发走向以x为根的子树,能够达到的最远点的距离
f
(
x
)
f(x)
f(x)为经过x的最长链长度
则直径为
m
a
x
{
f
(
x
)
}
max\{f(x)\}
max{f(x)}
void dp(int u) {
vis[u] = 1;
for (int i = head[u]; ~i; i = e[i].nxt) {
int v = e[i].v;
if (vis[v]) continue;
dp(v);
ans = max(ans, d[u] + d[v] + e[i].w);
d[u] = max(d[u], d[v] + e[i].w);
}
}
1.2 两次bfs求树直径
第一次从任意点出发bfs/dfs找到最远点
然后再从最远点做一遍
代码略
2.树链剖分
2.1 套数据结构维护树上信息
以线段树为例
洛谷p3384
#include<cstdio>
#include<cstring>
#include<algorithm>
#define ls p<<1
#define rs p<<1|1
#define Swap(_A,_B) _A^=_B^=_A^=_B
using namespace std;
const int maxn = 100007;
int n, m, root, mod;
struct edge {
int v, nxt;
}e[maxn << 1];
int head[maxn], eid = 0, tot = 0, dep[maxn], top[maxn], dfl[maxn], siz[maxn],
son[maxn], fa[maxn], sum[maxn << 2], tag[maxn << 2], w[maxn];
inline int read() {
int s = 0, f = 1; char c = getchar(); while (c<'0' || c>'9') { if (c == '-') f = -1; c = getchar(); }
while (c >= '0'&&c <= '9') { s = s * 10 + c - '0'; c = getchar(); }
return s*f;
}
void ins(int u, int v) {
e[++eid].v = v;
e[eid].nxt = head[u];
head[u] = eid;
e[++eid].v = u;
e[eid].nxt = head[v];
head[v] = eid;
}
void dfs1(int u) {
siz[u] = 1;
int i, v;
for (i = head[u]; i; i = e[i].nxt) {
v = e[i].v;
if (v != fa[u]) {
dep[v] = dep[u] + 1;
fa[v] = u;
dfs1(v);
siz[u] += siz[v];
if (siz[son[u]] < siz[v])
son[u] = v;
}
}
}
void dfs2(int u, int t) {
dfl[u] = ++tot;
top[u] = t;
if (son[u]) {
dfs2(son[u], t);
int i, v;
for (i = head[u]; i; i = e[i].nxt) {
v = e[i].v;
if (v != fa[u] && v != son[u]) {
dfs2(v, v);
}
}
}
}
void pushup(int p) {
sum[p] = sum[ls] + sum[rs];
if (sum[p] >= mod)
sum[p] %= mod;
}
void pushdown(int p, int l, int r) {
if (!tag[p]) return;
int mid = (l + r) >> 1;
tag[ls] += tag[p];
tag[rs] += tag[p];
sum[ls] += tag[p] * (mid - l + 1);
sum[rs] += tag[p] * (r - mid);
tag[p] = 0;
if (tag[ls] >= mod) tag[ls] %= mod;
if (tag[rs] >= mod) tag[rs] %= mod;
if (sum[ls] >= mod) sum[ls] %= mod;
if (sum[rs] >= mod) sum[rs] %= mod;
}
void modify(int p, int l, int r, int x, int y, int v) {
if (x <= l&&r <= y) {
sum[p] += v*(r - l + 1);
tag[p] += v;
if (sum[p] >= mod) sum[p] %= mod;
if (tag[p] >= mod) tag[p] %= mod;
return;
}
pushdown(p, l, r);
int mid = (l + r) >> 1;
if (x <= mid) modify(ls, l, mid, x, y, v);
if (y > mid) modify(rs, mid + 1, r, x, y, v);
pushup(p);
}
int query(int p, int l, int r, int x, int y) {
if (x <= l&&r <= y) {
return sum[p] >= mod ? sum[p]=sum[p] % mod : sum[p];
}
pushdown(p, l, r);
int mid = (l + r) >> 1, ans = 0;
if (x <= mid) ans += query(ls, l, mid, x, y);
if (y > mid) ans += query(rs, mid + 1, r, x, y);
return ans >= mod ? ans%mod : ans;
}
void solve1(int x, int y, int z) {
while (top[x] ^ top[y]) {
if (dep[top[x]] < dep[top[y]]) Swap(x, y);
modify(1, 1, n, dfl[top[x]], dfl[x], z);
x = fa[top[x]];
}
if (dep[x] > dep[y]) Swap(x, y);
modify(1, 1, n, dfl[x], dfl[y], z);
}
void solve2(int x, int y) {
int ans = 0;
while (top[x] ^ top[y]) {
if (dep[top[x]] < dep[top[y]]) Swap(x, y);
ans += query(1, 1, n, dfl[top[x]], dfl[x]);
if (ans >= mod) ans %= mod;
x = fa[top[x]];
}
if (dep[x] > dep[y]) Swap(x, y);
ans += query(1, 1, n, dfl[x], dfl[y]);
if (ans >= mod) ans %= mod;
printf("%d\n", ans);
}
void solve3(int x, int z) {
modify(1, 1, n, dfl[x], dfl[x] + siz[x] - 1, z);
}
void solve4(int x) {
int ans = 0;
ans += query(1, 1, n, dfl[x], dfl[x] + siz[x] - 1);
if (ans >= mod) ans %= mod;
printf("%d\n", ans);
}
int x, y, d, z;
int main() {
n = read(); m = read(); root = read(); mod = read();
for (int i = 1; i <= n; i++) {
w[i] = read();
}
for (int i = 1; i < n; i++) {
x = read(); y = read();
ins(x, y);
}
dfs1(root);
dfs2(root, root);
for (int i = 1; i <= n; i++)
modify(1, 1, n, dfl[i], dfl[i], w[i]);
while (m--) {
d = read();
if (d == 1) {
x = read(); y = read(); z = read();
solve1(x, y, z);
}
else if (d == 2) {
x = read(); y = read();
solve2(x, y);
}
else if (d == 3) {
x = read(); z = read();
solve3(x, z);
}
else {
x = read();
solve4(x);
}
}
getchar();
return 0;
}
四、搜索相关
1.欧拉回路
1.1定义
恰好经过每条边一次->欧拉路径
欧拉路径是一个环->欧拉回路
具有欧拉回路的图称为欧拉图
具有欧拉路径但不具有欧拉回路的图称为半欧拉图
1.2无向图欧拉路径(套圈法)
从一个合适的点开始dfs,对于当前点u
- 若该点剩余度数为0,那么就将u加入答案并回溯
- 否则,对于与u相连的所有点v,删除边(u,v)并继续搜索点v。将所有和点u相邻的顶点遍历完以后,将u加入路径中。
#include<bits/stdc++.h>
using namespace std;
const int maxn = 107;
int g[maxn][maxn], n;
int deg[maxn]; // 剩余的度数
void solve(int u) {
if (deg[u]) {
for (int i = 1; i <= n; i++) {
if (g[u][i]) {
g[u][i]--;
g[i][u]--;
solve(i);
}
}
}
printf("visiting %d\n", u);
}
1.3有向图欧拉路径(套圈法)
和之前不同的是 要将输出顺序倒一下
#include<bits/stdc++.h>
using namespace std;
const int maxn = 107;
int g[maxn][maxn], n;
int deg[maxn]; // 剩余的度数
int stk[maxn], top ;
void solve(int u) {
if (deg[u]) {
for (int i = 1; i <= n; i++) {
if (g[u][i]) {
g[u][i]--;
g[i][u]--;
solve(i);
}
}
}
stk[++top] = u;
}
2.求无向图割点/桥(割边)
2.1求无向图的割点
若
u
u
u不是搜索树的根结点,则
u
u
u是割点当且仅当搜索树上存在一个
u
u
u的子结点
v
v
v,满足:
d
f
n
[
u
]
<
=
l
o
w
[
v
]
dfn[u]<=low[v]
dfn[u]<=low[v]
若
u
u
u是搜索树的根结点,则
u
u
u是割点仅当搜索树上存在至少两个子结点
v
1
,
v
2
v1,v2
v1,v2满足上述条件。
#include<bits/stdc++.h>
using namespace std;
const int maxn = 100007;
vector<int> adj[maxn];
int dfn[maxn], low[maxn], tot, n, m, num, root, ans;
bool cut[maxn];
void tarjan(int u) {
dfn[u] = low[u] = ++tot;
int flag = 0;
for (int i = 0; i < adj[u].size(); i++) {
int v = adj[u][i];
if (!dfn[v]) {
tarjan(v);
low[u] = min(low[u], low[v]);
if (low[v] >= dfn[u]) {
flag ++;
if (u != root || flag > 1) {
cut[u] = true; //此处点u可能被判多次是割点,不能在这里统计割点数量
}
}
} else low[u] = min(low[u], dfn[v]);
}
}
int main() {
cin >> n >> m;
for (int i = 1; i <= m; i++) {
int u, v;
cin >> u >> v;
if (u == v) continue;
adj[u].push_back(v);
adj[v].push_back(u);
}
for (int i = 1; i <= n; i++) {
if (!dfn[i]) {
root = i;
tarjan(i);
}
}
for (int i = 1; i <= n; i++)
if (cut[i]) ans++;
cout << ans << endl;
for (int i = 1; i <= n; i++)
if (cut[i]) cout << i << " ";
}
2.2求无向图的割边(桥)
无向边
(
u
,
v
)
(u,v)
(u,v)是桥仅当搜索树上存在
u
u
u的一个子结点
v
v
v
满足
d
f
n
[
u
]
<
l
o
w
[
v
]
dfn[u]<low[v]
dfn[u]<low[v]
const int maxn = 114514, maxm = 1919810;
struct edge {
int v, nxt;
}e[maxn];
int head[maxn], eid, dfn[maxn], tot, low[maxn], n, m;
bool bridge[maxm];
void init() {
memset(head, -1, sizeof(head));
eid = 0;
}
void insert(int u, int v) {
e[eid].v = v;
e[eid].nxt = head[u];
head[u] = eid++;
}
void tarjan(int u, int in_edge) {
dfn[u] = low[u] = ++tot;
for (int i = head[u]; i; i = e[i].nxt) {
int v = e[i].v;
if (!dfn[v]) {
tarjan(v, i);
low[u] = min(low[u], low[v]);
if (low[v] > dfn[u])
bridge[i] = bridge[i^1] = true;
}
else if (i != (in_edge^1))
low[u] = min(low[u], dfn[v]);
}
}
int main() {
init();
eid = 2;//把0空出来做根的入边标记(没有入边)
cin >> n >> m;
for (int i = 1; i <= m; i++) {
int u, v;
cin >> u >> v;
insert(u, v);
insert(v, u);
}
for (int i = 1; i <= n; i++)
if (!dfn[i]) tarjan(i, 0);
for (int i = 2; i < eid; i += 2)
if (bridge[i])
printf("%d %d\n", e[i].v, e[i^1].v);
}
3.无向图的双连通分量
3.1 定义
点双连通图:不存在割点
边双连通图:不存在割边
无向图的极大点双连通子图:点双连通分量(v-DCC)
无向图的极大边双连通子图:边双连通分量(e-DCC)
点双连通图的判定定理:
一张无向连通图是点双连通图 当且仅当满足以下两个条件之一
- 图的顶点数不超过2
- 图中任意两点都同时包含在至少一个简单环中。其中“简单环”指不自交的环
一张无向连通图是边双连通图,当且仅当任意一条边都包含在至少一个简单环中
3.2 边双连通分量(e-DCC)的求法与缩点
求出无向图中所有的桥,删除他们后,剩下的每个连通块就是一个边双连通分量
把每个e-DCC看作一个结点,把桥边(u,v)看作连接两个结点的无向边,会产生一棵树/森林
3.3 点双连通分量的求法与缩点
孤立点自身是一个v-DCC。
割点可能属于多个v-DCC
3.3.1vdcc的求法
求v-DCC的时候要在tarjan算法过程中维护一个栈,并按照如下方法求出栈内元素
1.第一次访问某个点 把该结点入栈
2.当割点判定法则中条件dfn[u]<=low[v]成立时,无论u是否为根,都:
- 从栈顶不断弹出结点 直到v被弹出
- 刚才弹出的所有结点和u一起构成一个v-DCC
- dcc[i]保存编号为i的v-dcc中所有结点
void tarjan_vdcc(int u) {
dfn[u] = low[u] = ++tot;
stk[++top] = u;
if (u == root && head[x] == -1) {//孤立点
dcc[++cnt].push_back(u);
return;
}
int flag = 0;
for (int i = head[u]; ~i; i = e[i].nxt) {
int v = e[i].v;
if (!dfn[v]) {
tarjan_vdcc(v);
low[u] = min(low[u], low[v]);
if (low[v] >= dfn[u]) {
flag++;
if (u != root || flag > 1) cut[x] = true;
cnt++;
int x;
do {
x = stk[top--];
dcc[cnt].push_back(x);
} while (x ^ v);
dcc[cnt].push_back(u);
}
}
else low[u] = min(low[u], dfn[v]);
}
}
3.3.2 v-DCC的缩点
设图中有p个割点和t个v-DCC,我们新建一个含p+t个结点的新图,把每个v-DCC和每个割点都作为新图中的结点,并在每个割点和包含它的所有v-DCC之间连边。存在另外一个邻接表中
4.有向图的强连通分量,缩点与tarjan算法
4.1 定义
强连通图指有向图中任意点对均互相可达的图
有向图的极大强连通子图为强连通分量,简称scc
若点u回溯前 有low[u]==dfn[u]成立 则从栈中u到栈顶的所有结点构成一个强连通分量
4.2 tarjan强连通分量缩点模板(洛谷2812)
#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 7;
int n, m, dfn[maxn], low[maxn], tot, c[maxn], stk[maxn], top, istk[maxn], indeg[maxn], outdeg[maxn], ans1, ans2, cnt;
vector<int> adj[maxn], adj2[maxn], scc[maxn];
int rd() {
int s = 0, f = 1; char c = getchar();
while (c < '0' || c > '9') { if (c == '-') f = -1; c = getchar(); }
while (c >= '0' && c <= '9') { s = s * 10 + c - '0'; c = getchar(); }
return s * f;
}
void tarjan(int u) {
dfn[u] = low[u] = ++tot;
stk[++top] = u;
istk[u] = 1;
for (int i = 0; i < adj[u].size(); i++) {
int v = adj[u][i];
if (!dfn[v]) {
tarjan(v);
low[u] = min(low[u], low[v]);
} else if (istk[v])
low[u] = min(low[u], dfn[v]);
}
if (dfn[u] == low[u]) {
cnt++; int x;
do {
x = stk[top--], istk[x] = 0;
c[x] = cnt, scc[cnt].push_back(x);
} while (u != x);
}
}
int main() {
n = rd();
for (int i = 1; i <= n; i++) {
int v = rd();
while (v != 0) {
adj[i].push_back(v);
v = rd();
}
}
for (int i = 1; i <= n; i++) {
if (!dfn[i]) tarjan(i);
}
for (int u = 1; u <= n; u++) {
for (int j = 0; j < adj[u].size(); j++) {
int v = adj[u][j];
if (c[u] != c[v]) {
adj2[c[u]].push_back(c[v]);
indeg[c[v]]++; outdeg[c[u]]++;
}
}
}
for (int i = 1; i <= cnt; i++) {
if (indeg[i] == 0) ans1++;
if (outdeg[i] == 0) ans2++;
}
if (cnt == 1) {
puts("1");
puts("0");
return 0;
}
cout << ans1 << "\n" << max(ans1, ans2) << endl;
}
5.有向图的必经点和必经边
给定一张有向图,起点为S,终点为T。若从S到T的每条路径都经过一个点x,则称点x为有向图中从S到T的必经点。
若从S到T的每条路径都经过一条边(x,y),则称这条边是有向图中从S到T的必经边或"桥"。
图中若有环 sol:支配树(待填坑)
对于有向无环图的必经点和必经边:
- 在原图中按拓扑序dp,求出点S到图中每个点的路径条数fs[x]。
- 在反图上再按拓扑序dp,求出每个点x到T的路径条数ft[x]
根据乘法原理
- 对于有向边(x,y) 若fs[x]*ft[y]==fs[T] 则(x,y)是从S到T的必经边
- 对于点x,若fs[x]*ft[x]==fs[T] 则点x是从S到T的必经点
路径条数太多时->hash乱搞
五、2-SAT(2-可满足性问题)
1. 定义
有N个变量 每个变量只有两种可能的取值,再给定M个条件,每个条件都是对两个变量的取值限制。求是否存在对N个变量的合法赋值,使M个条件均得到满足。
设变量
A
A
A的两种取值分别是
A
i
,
0
A_{i,0}
Ai,0和
A
i
,
1
A_{i,1}
Ai,1。在2-SAT问题中,M个条件都可以转化为统一的形式:若变量
A
i
A_i
Ai赋值为
A
i
,
p
A_{i,p}
Ai,p,则变量
A
j
A_j
Aj必须赋值为
A
j
,
q
A_{j,q}
Aj,q,
p
,
q
∈
{
0
,
1
}
p,q∈\{0,1\}
p,q∈{0,1}
2.判定
判定方法如下:
- 建立2N个点的有向图,每个变量 A i A_{i} Ai对应两个结点,一般设为 i i i和 i + N i+N i+N。
- 考虑每个条件,形如“若变量
A
i
A_i
Ai赋值为
A
i
,
p
A_{i,p}
Ai,p,则变量
A
j
A_j
Aj必须赋值为
A
j
,
q
A_{j,q}
Aj,q,
p
,
q
∈
{
0
,
1
}
p,q∈\{0,1\}
p,q∈{0,1}",从i+pN到j+qN连一条有向边.
上述条件蕴含着它的逆否命题”若变量 A j A_j Aj必须赋值为 A j , 1 − q A_{j,1-q} Aj,1−q,则变量 A i A_i Ai必须赋值为 A i , 1 − p A_{i,1-p} Ai,1−p。如果在给出的M个限制条件中,原命题与逆否命题不一定成对出现,应该从 j + ( 1 − q ) ∗ N j+(1-q)*N j+(1−q)∗N向 i + ( 1 − p ) ∗ N i+(1-p)*N i+(1−p)∗N连一条有向边
根据原命题和逆否命题的对称性,2-SAT建出的有向图一定能画成一侧是结点 1 − N 1 - N 1−N,一侧是结点 N + 1 − 2 N N+1-2N N+1−2N - 用tarjan求有向图中所有SCC
- 若存在 i ∈ [ 1 , N ] i∈[1,N] i∈[1,N],满足 i 和 i + N i和i+N i和i+N属于同一个scc,表明无解
3.求可行解(咕)
六、二分图匹配&相关模型
1.二分图判定
无向图是二分图,当且仅当图中不存在奇环。
执行一遍dfs或bfs进行黑白染色,若相邻点有相同的颜色,则不是二分图
2.二分图最大匹配
2.1匈牙利算法
复杂度 O ( N M ) O(NM) O(NM)
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
int n, m, E;
struct edge {
int v, next;
}e[10000000];
int p[2007], eid = 0;
inline void ins(int u, int v) {
e[eid].v = v;
e[eid].next = p[u];
p[u] = eid++;
}
int link[2007];
bool vis[2007];
inline void init() {
memset(p, -1, sizeof(p));
eid = 0;
}
bool dfs(int u) {
int v;
for (int i = p[u]; ~i; i = e[i].next) {
v = e[i].v;
if (!vis[v]) {
vis[v] = true;
if (link[v] == -1 || dfs(link[v]))
{
link[v] = u;
return true;
}
}
}
return false;
}
int hungary() {
int res = 0;
memset(link, -1, sizeof(link));
for (int i = 1; i <= n; i++)
{
memset(vis, false, sizeof(vis));
res += dfs(i);
}
return res;
}
inline int read() {
int s = 0, f = 1; char c = getchar(); while (c<'0' || c>'9') { if (c == '-') f = -1; c = getchar(); }
while (c >= '0'&&c <= '9') { s = s * 10 + c - '0'; c = getchar(); }
return s*f;
}
int main() {
cin >> n >> m >> E;
init();
int u, v;
for (int i = 1; i <= E; i++) {
u = read(); v = read();
if (v <= m) {
ins(u, v + n);
}
}
cout << hungary() << endl;
//getchar();
return 0;
}
2.2二分图带权匹配
匹配数最大,然后再最大化匹配边的权值
2.2.1 KM解法
只能在满足“带权最大匹配一定是完备匹配”的图中正确求解 复杂度 O ( n 4 ) O(n^4) O(n4)巨大常数 洛谷上只有30分
#include<bits/stdc++.h>
using namespace std;
const int maxn = 1007;
#define ll long long
ll w[maxn][maxn], delta;
int n, m;
ll la[maxn], lb[maxn]; //左右顶点的顶标
bool va[maxn], vb[maxn]; // 是否在交错树中
int match[maxn];//右部点的匹配点
int L[maxn], R[maxn];
bool dfs(int u) {
va[u] = 1;
for (int v = 1; v <= n; v++) {
if (!vb[v]) {
if (la[u] + lb[v] - w[u][v] == 0) { //相等子图
vb[v] = 1;
if (!match[v] || dfs(match[v])) {
match[v] = u;
return true;
}
}
else delta = min(delta, la[u] + lb[v] - w[u][v]);
}
}
return false;
}
ll km() {
for (int i = 1; i <= n; i++) {
la[i] = -99999999999999ll;
lb[i] = 0;
for (int j = 1; j <= n; j++)
la[i] = max(la[i], w[i][j]);
}
for (int i = 1; i <= n; i++) {
while (1) {
memset(va, 0, sizeof(va));
memset(vb, 0, sizeof(vb));
delta = 99999999999999ll;
if (dfs(i)) break;
for (int j = 1; j <= n; j++) {
if (va[j]) la[j] -= delta;
if (vb[j]) lb[j] += delta;
}
}
}
ll ans = 0;
for (int i = 1; i <= n; i++) ans += w[match[i]][i];
return ans;
}
ll rd() {
ll s = 0, f = 1; char c = getchar();
while (c < '0' || c > '9') {if (c == '-') f = -1; c = getchar();}
while (c >= '0' && c <= '9') {s = s * 10 + c - '0'; c = getchar();}
return s * f;
}
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
w[i][j] = -99999999999999ll;
for (int i = 1; i <= m; i++) {
L[i] = rd(), R[i] = rd();
w[L[i]][R[i]] = rd();
}
cout << km() << endl;
for (int i = 1; i <= n; i++) {
printf("%d ", match[R[i]]);
}
}
2.2.2费用流解法
还是30分 复杂度 O ( n 2 m ) O(n^2m) O(n2m) 第四个点是wa 怀疑费用流有问题
#include<bits/stdc++.h>
using namespace std;
const int maxn = 1070, maxm = 500007;
#define ll long long
struct edge {
int v, nxt;
ll w, c;
}e[maxm<<1];
int head[maxn], eid, pre[maxn], s, t, a[maxn], b[maxn], n, m;
ll w[maxn][maxn], d[maxn], match[maxn];
bool inq[maxn], isa[maxn];
void init() {
memset(head, -1, sizeof(head));
eid = 0;
}
void insert(int u, int v, ll c, ll w) {
e[eid].v = v; e[eid].w = w; e[eid].c = c ; e[eid].nxt = head[u]; head[u] = eid++;
e[eid].v = u; e[eid].nxt = head[v]; e[eid].w = -w; e[eid].c = 0; head[v] = eid++;
}
bool spfa() {
memset(inq, 0, sizeof(inq));
for (int i = 0; i <= 1069; i++) {
d[i] = -9999999999999ll;
pre[i] = -1;
}
queue<int> q;
q.push(s);
d[s] = 0; inq[s] = true;
while (!q.empty()) {
int u = q.front(); q.pop();
inq[u] = false;
for (int i = head[u]; ~i; i = e[i].nxt) {
if (e[i].c) {
int v = e[i].v;
if (d[u] + e[i].w > d[v]) {
d[v] = d[u] + e[i].w;
pre[v] = i;
if (isa[u]) match[v] = u;
if (!inq[v]) {
inq[v] = true;
q.push(v);
}
}
}
}
}
return pre[t] != -1;
}
ll costflow() {
ll res = 0;
while (spfa()) {
ll flow = 9999999999999ll;
for (int i = t; i != s; i = e[pre[i]^1].v) {
flow = min(flow, e[pre[i]].c);
}
for (int i = t; i != s; i = e[pre[i] ^ 1].v) {
e[pre[i]^1].c += flow;
e[pre[i]].c -= flow;
res += e[pre[i]].w * flow;
}
}
return res;
}
int rd() {
int s = 0, f = 1; char c = getchar();
while (c < '0' || c > '9') { if (c == '-') f = -1; c = getchar(); }
while (c >= '0' && c <= '9') {s = s * 10 + c - '0'; c = getchar();}
return s * f;
}
int main() {
n = rd(), m = rd();
init();
for (int i = 1; i <= m; i++) {
a[i] = rd(), b[i] = rd()+n;
w[a[i]][b[i]] = rd();
isa[a[i]] = 1;
insert(a[i], b[i], 1, w[a[i]][b[i]]);
}
s = 2*n + 1; t = s + 1;
for (int i = 1; i <= n; i++) insert(s, a[i], 1, 0), insert(b[i], t, 1, 0);\
cout << costflow() << endl;
for (int k = 1; k <= n; k++) {
int u = a[k];
for (int i = head[u]; ~i; i = e[i].nxt) {
if (e[i].c == 0) {
match[e[i].v] = u;
}
}
}
for (int i = 1; i <= n; i++) printf("%d ", match[b[i]]);
}
3.二分图模型
二分图的最小点覆盖(任意一条边都含有点覆盖集中的点)等于二分图最大匹配包含的边数。
二分图的最大点独立集(任意两个顶点不相邻)= 最小边覆盖(边盖住所有顶点)= 顶点总数-最大匹配
DAG的最小路径覆盖(路径覆盖:在有向图中,找到若干条路径,使之覆盖了图中的所有顶点,并且任何一个顶点只在一条路径中) = dag的顶点总数-拆点后最大匹配
要先把dag的每个点拆成两个点
二分图的最大边独立集 = 最大匹配
4.二分图最大匹配的必须边和可行边
二分图所有最大匹配中都包含的边称为必须边
至少属于一个最大匹配的边称为可行边
考虑以下情况:二分图最大匹配是完备匹配
必须边的判定:把二分图的非匹配边看作从左到右的有向边,匹配边看作从右向左的有向边,构成新图G1,若当前边是原图的匹配边,且在新图中属于不同的scc,则当前边是必须边;若当前边是原图的匹配边,或在新图中属于相同的scc,则当前边是可行边。
七.网络流与最小割
7.1 最大流的dinic
复杂度 O ( n 2 m ) O(n^2m) O(n2m) 求最大匹配的话是 O ( m n ) O(m\sqrt{n}) O(mn) 实际表现更快
#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
using namespace std;
const int INF=10000000;
const int maxn=207;
const int maxm=207;
int N,M;
struct edge{
int v,nxt,cap;
}e[maxn<<1];
//源点即农田区 汇点为小溪
int p[maxm],eid=0;
inline void ins(int u,int v,int cap){
e[++eid].v=v;e[eid].nxt=p[u];e[eid].cap=cap;p[u]=eid;
}
inline void add(int u,int v,int cap){
ins(u,v,cap);
ins(v,u,0); //插入当前容量为0的反平行边
}
int S,T;//源点和汇点
int d[maxm];
bool bfs(){ //bfs构建层次网络
memset(d,-1,sizeof(d)); //当前层次网络中
queue<int> q;
q.push(S);
d[S]=0;
while(!q.empty()){
int u=q.front();q.pop();
for(int i=p[u];i;i=e[i].nxt){
int v=e[i].v;
if(e[i].cap>0&&d[v]==-1){ //d[v]==-1表示未访问过(未加入层次网络中
//省去一个vst数组 前向弧必须是非饱和弧 若e[i].cap==0 则f==0 饱和了
q.push(v);d[v]=d[u]+1; //加入层次网络中
}
}
}
return (d[T]!=-1); //汇点仍在层次网络中 就继续增广
}
int dfs(int u,int flow){ //flow表示当前搜索分支流量上限
if(u==T) return flow; //找到一条流量为flow的增广路
int res=0;
for(int i=p[u];i;i=e[i].nxt){
int v=e[i].v;
if(e[i].cap>0&&d[u]+1==d[v]){
int tmp=dfs(v,min(flow,e[i].cap)); //用c(u,v)更新当前流量上限
//找到一条增广路 修改反向边的流量 这次dfs的res=0的时候tmp为0 不会改变反平行边
flow-=tmp;
e[i].cap-=tmp; //回退时修改容量
res+=tmp;
e[i^1].cap+=tmp; //修改反平行边的容量
if(flow==0) break; //流量达到上限 不用搜了
}
}
if(res==0) d[u]=-1;
return res;
}
int dinic(){
int res=0;
while(bfs()){
res+=dfs(S,INF); //初始流量上限为INF
}
return res;
}
int main(){
cin>>N>>M;
T=M;S=1;
for(int i=0;i<N;i++)
{
int s,e,c;
scanf("%d%d%d",&s,&e,&c);
add(s,e,c);
}
printf("%d",dinic());
return 0;
}
7.2 最小割
最大流最小割定理:在一个网络中,最大流的流量等于最小割的容量
例题:求最小割的同时使边数最少
如果边数最大为M,那么把容量设为
(
M
+
1
)
c
+
1
(M+1)c+1
(M+1)c+1 最后最小割答案若为ans,那么对应代价总和为ans/(M+1)向下取整,边数为ans mod (M+1)
模型: 二者选其一 方案不同有额外开销
对两个集合分别设置源点,汇点,选择方案的开销为顶点连向对应源/汇的代价 某两个点在同一集合中的额外开销为这两个点之间双向弧的容量
7.3 最大权闭合图
闭合图:某有向图的一个子图,这个子图中所有顶点的所有出边都指向这个子图中的点
给每个顶点分配一个权值,最大权闭合图是一个点权总和最大的闭合图。
最大权闭合图的计算是最小割的经典应用
在原图的基础上增加源汇点,将原图中的每一条有向边替换为容量为无穷的弧;增加源点到每个正权顶点i的弧,容量为点权;增加每个负权点到汇点的弧,弧容量为点权的相反数
最终,最大权闭合图的值等于所有正权点权总和减去最小割容量
求方案:在剩余网络中进行dfs,所有经过的点都是最大权闭合图上的顶点
7.4 费用流
洛谷p3381
#include<cstdio>
#include<cstring>
#include<queue>
using namespace std;
#define Min(_A,_B) (_A>_B?_B:_A)
const int maxn=5007,maxm=50007,inf=0x3f3f3f3f;
int n,m,s,t,pre[maxn],d[maxn],head[maxn],eid=0,incf[maxn];
bool inq[maxn];
struct edge{
int v,c,w,nxt;
}e[maxm<<1];
void init(){
memset(head,-1,sizeof(head));
eid=0;
}
void insert(int u,int v,int c,int w){
e[eid].v=v;e[eid].c=c;e[eid].w=w;e[eid].nxt=head[u];head[u]=eid++;
}
void addedge(int u,int v,int c,int w){
insert(u,v,c,w);insert(v,u,0,-w);
}
bool spfa(){
fill(d,d+n+1,inf);
memset(inq,false,sizeof(inq));
fill(incf,incf+n+1,inf);
queue<int>q;q.push(s);inq[s]=true;d[s]=0;
incf[s]=inf;
while(!q.empty()){
int u=q.front();q.pop();
inq[u]=false;
for(int i=head[u];~i;i=e[i].nxt){
if(e[i].c>0){
int v=e[i].v;
if(d[v]>d[u]+e[i].w){
d[v]=d[u]+e[i].w;
pre[v]=i;
incf[v]=Min(incf[u],e[i].c);
if(!inq[v]){
inq[v]=true;
q.push(v);
}
}
}
}
}
return d[t]!=inf;
}
int maxf=0,minw=0;
void cost_flow(){
while(spfa()){
for(int i=t;i!=s;i=e[pre[i]^1].v){
e[pre[i]].c-=incf[t];
e[pre[i]^1].c+=incf[t];
}
minw+=incf[t]*d[t];
maxf+=incf[t];
}
}
int u,v,w,f;
inline int read(){
int s=0,f=1;char c=getchar();while(c<'0'||c>'9') {if(c=='-') f=-1;c=getchar();}
while(c>='0'&&c<='9') {s=s*10+c-'0';c=getchar();}
return s*f;
}
int main(){
scanf("%d%d%d%d",&n,&m,&s,&t);
init();
while(m--){
u=read();v=read();w=read();f=read();
addedge(u,v,w,f);
}
cost_flow();
printf("%d %d",maxf,minw);
return 0;
}
7.5 混合图欧拉路
在给定的一个即包含有向边又包含无向边的图中,判断其中是否有欧拉回路。
做法如下:给无向边随机定向,造个新图。
用
I
i
I_i
Ii表示第
i
i
i个点的入度,
O
i
O_i
Oi表示第i个点的出度。如果存在一个点k,
∣
O
k
−
I
k
∣
m
o
d
2
=
1
|O_k-I_k|\ mod \ 2 = 1
∣Ok−Ik∣ mod 2=1 那么G必没有欧拉回路
否则 对于所有
I
i
>
O
i
I_i>O_i
Ii>Oi的点 从源点连一条容量为
I
i
−
O
i
2
\frac {I_i-O_i} {2}
2Ii−Oi的弧
对于所有
O
i
>
I
i
O_i>I_i
Oi>Ii的点,从
i
i
i向汇点连一条容量为
O
i
−
I
i
2
\frac {O_i-I_i}{2}
2Oi−Ii的弧
如果是无向边 在端点之间建立容量为1的双向弧。
若此网络的最大流等于
∑
I
i
>
O
i
I
i
−
O
i
2
{\sum_{I_i>O_i}} \frac{I_i-O_i}2
∑Ii>Oi2Ii−Oi
那么混合图G就存在欧拉回路
总结
我好菜啊