@

# 一、最短路

## 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);
}
}
}
}
}


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() {
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求最短路的优化

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;
}
void init() {
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$$第二次被从堆中取出时，$$d[s]$$就是经过$$s$$的最小环长度

## 3.Floyd求多源最短路/传递闭包/倍增floyd

### 3.3 倍增floyd

$$f[i][j][t]$$表示从i到j走2^t步的最短路
$$f[i][j][t] = min\{f[i][k][t-1]+f[k][j][t-1]\}$$

### 3.4 无向图最小环

$$f[i][j]$$表示经过编号不超过k-1的结点从i到j 或者从i先到k 再到j

#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);
}


# 二、生成树

## 1.性质与kruskal代码

### 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];
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++) {
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;
}
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.增量最小生成树

sol：加入一条边<u,v>后图中恰好包含一个环 根据回路性质删掉加边前树上u到v唯一路径上的最大边即可 O(nm)

## 6.次小生成树

lyd的书上有O(MlogN)做法：用倍增预处理出各点到根路径上的最大边权与严格次大边权 最终得到路径上最大和次大边权

## 7.最小有向生成树（最小树形图）&&朱-刘算法

• 恰好有一个入度为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 dp求树的直径

$$d(x)$$为从结点x出发走向以x为根的子树，能够达到的最远点的距离
$$f(x)$$为经过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);
}
}


## 2.树链剖分

### 2.1 套数据结构维护树上信息

#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];
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].v = u;
}
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() {
for (int i = 1; i <= n; i++) {
}
for (int i = 1; i < n; i++) {
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--) {
if (d == 1) {
solve1(x, y, z);
}
else if (d == 2) {
solve2(x, y);
}
else if (d == 3) {
solve3(x, z);
}
else {
solve4(x);
}
}
getchar();
return 0;
}


# 四、搜索相关

## 1.欧拉回路

### 1.2无向图欧拉路径（套圈法）

• 若该点剩余度数为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$$的子结点$$v$$,满足：$$dfn[u]<=low[v]$$
$$u$$是搜索树的根结点，则$$u$$是割点仅当搜索树上存在至少两个子结点$$v1,v2$$满足上述条件。

#include<bits/stdc++.h>
using namespace std;
const int maxn = 100007;
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++) {
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;
}
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求无向图的割边（桥）

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() {
eid = 0;
}
void insert(int u, int v) {
e[eid].v = v;
}
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 定义

1. 图的顶点数不超过2
2. 图中任意两点都同时包含在至少一个简单环中。其中“简单环”指不自交的环

### 3.3 点双连通分量的求法与缩点

#### 3.3.1vdcc的求法

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]);
}
}


## 4.有向图的强连通分量，缩点与tarjan算法

### 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;
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++) {
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) {
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++) {
if (c[u] != 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.有向图的必经点和必经边

1. 在原图中按拓扑序dp，求出点S到图中每个点的路径条数fs[x]。
2. 在反图上再按拓扑序dp，求出每个点x到T的路径条数ft[x]

1. 对于有向边（x,y) 若fs[x]*ft[y]==fs[T] 则(x,y)是从S到T的必经边
2. 对于点x，若fs[x]*ft[x]==fs[T] 则点x是从S到T的必经点
路径条数太多时->hash乱搞

# 五、2-SAT（2-可满足性问题）

## 2.判定

1. 建立2N个点的有向图，每个变量$$A_{i}$$对应两个结点，一般设为$$i$$$$i+N$$
2. 考虑每个条件，形如“若变量$$A_i$$赋值为$$A_{i,p}$$,则变量$$A_j$$必须赋值为$$A_{j,q}$$,$$p,q∈\{0,1\}$$",从i+pN到j+qN连一条有向边.
上述条件蕴含着它的逆否命题”若变量$$A_j$$必须赋值为$$A_{j,1-q}$$,则变量$$A_i$$必须赋值为$$A_{i,1-p}$$。如果在给出的M个限制条件中，原命题与逆否命题不一定成对出现，应该从$$j+(1-q)*N$$$$i+(1-p)*N$$连一条有向边
根据原命题和逆否命题的对称性，2-SAT建出的有向图一定能画成一侧是结点$$1 - N$$，一侧是结点$$N+1-2N$$
3. 用tarjan求有向图中所有SCC
4. 若存在$$i∈[1,N]$$，满足$$i和i+N$$属于同一个scc，表明无解

# 六、二分图匹配&相关模型

## 2.二分图最大匹配

### 2.1匈牙利算法

#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++;
}
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;
{
return true;
}
}
}
return false;
}
int hungary() {
int res = 0;
for (int i = 1; i <= n; i++)
{
memset(vis, false, sizeof(vis));
res += dfs(i);
}
return res;
}
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++) {
if (v <= m) {
ins(u, v + n);
}
}
cout << hungary() << endl;
//getchar();
return 0;
}


### 2.2二分图带权匹配

#### 2.2.1 KM解法

#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费用流解法

#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() {
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的顶点总数-拆点后最大匹配

# 七.网络流与最小割

## 7.1 最大流的dinic

#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);
}
printf("%d",dinic());
return 0;
}


## 7.4 费用流

#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;
bool inq[maxn];
struct edge{
int v,c,w,nxt;
}e[maxm<<1];
void init(){
eid=0;
}
void insert(int u,int v,int c,int w){
}
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;
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;
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--){
}
cost_flow();

printf("%d %d",maxf,minw);
return 0;
}


## 7.5 混合图欧拉路

$$I_i$$表示第$$i$$个点的入度，$$O_i$$表示第i个点的出度。如果存在一个点k，$$|O_k-I_k|\ mod \ 2 = 1$$ 那么G必没有欧拉回路

# 总结

posted @ 2023-07-12 19:06  yjmstr  阅读(8)  评论(0编辑  收藏  举报